org.auraframework.http.AppCacheResourcesUITest.java Source code

Java tutorial

Introduction

Here is the source code for org.auraframework.http.AppCacheResourcesUITest.java

Source

/*
 * Copyright (C) 2013 salesforce.com, inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.auraframework.http;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;

import org.auraframework.Aura;
import org.auraframework.def.ApplicationDef;
import org.auraframework.def.ComponentDef;
import org.auraframework.def.ControllerDef;
import org.auraframework.def.DefDescriptor;
import org.auraframework.def.Definition;
import org.auraframework.def.NamespaceDef;
import org.auraframework.def.StyleDef;
import org.auraframework.service.ContextService;
import org.auraframework.system.AuraContext;
import org.auraframework.system.AuraContext.Authentication;
import org.auraframework.system.AuraContext.Format;
import org.auraframework.system.AuraContext.Mode;
import org.auraframework.system.Source;
import org.auraframework.test.WebDriverTestCase;
import org.auraframework.test.WebDriverUtil.BrowserType;
import org.auraframework.test.annotation.FreshBrowserInstance;
import org.auraframework.test.annotation.ThreadHostileTest;
import org.auraframework.test.controller.TestLoggingAdapterController;
import org.auraframework.util.AuraTextUtil;
import org.openqa.selenium.By;
import org.openqa.selenium.Cookie;
import org.openqa.selenium.StaleElementReferenceException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.ExpectedCondition;

import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;

/**
 * Tests for AppCache functionality by watching the requests received at the server and verifying that the updated
 * content is being used by the browser. AppCache only works for WebKit browsers.
 * 
 * @since 0.0.224
 */
@FreshBrowserInstance
@ThreadHostileTest("TestLoggingAdapter not thread-safe")
public class AppCacheResourcesUITest extends WebDriverTestCase {
    private final boolean debug = false;

    private final static String COOKIE_NAME = "%s_%s_%s_lm";
    private final static String TOKEN = "@@@TOKEN@@@";

    private static final String AURA = "aura";

    private enum Status {
        UNCACHED, IDLE, CHECKING, DOWNLOADING, UPDATEREADY, OBSOLETE;
    }

    private String appName;
    private String namespace;
    private String cmpName;

    public AppCacheResourcesUITest(String name) {
        super(name);
        timeoutInSecs = 60;
    }

    @Override
    public void setUp() throws Exception {
        super.setUp();
        namespace = "appCacheResourcesUITest" + getAuraTestingUtil().getNonce();
        appName = "cacheapplication";
        cmpName = "cachecomponent";

        DefDescriptor<ComponentDef> cmpDesc = createDef(ComponentDef.class,
                String.format("%s:%s", namespace, cmpName),
                "<aura:component>" + "<aura:attribute name='output' type='String'/>"
                        + "<div class='clickableme' onclick='{!c.cssalert}'>@@@TOKEN@@@</div>"
                        + "<div class='attroutput'>{!v.output}</div>" + "</aura:component>");

        createDef(ControllerDef.class,
                String.format("%s://%s.%s", DefDescriptor.JAVASCRIPT_PREFIX, namespace, cmpName),
                "{ cssalert:function(c){" + "function getStyle(elem, style){" + "var val = '';"
                        + "if(document.defaultView && document.defaultView.getComputedStyle){"
                        + "val = document.defaultView.getComputedStyle(elem, '').getPropertyValue(style);"
                        + "} else if(elem.currentStyle){" + "style = style.replace(/\\-(\\w)/g, function (s, ch){"
                        + "return ch.toUpperCase();" + "});" + "val = elem.currentStyle[style];" + "}"
                        + "return val;" + "};" + "var style = getStyle(c.getElement(),'background-image');"
                        + "c.set('v.output','@@@TOKEN@@@' + style.substring(style.lastIndexOf('?')+1,style.lastIndexOf(')'))"
                        + "+ ($A.test ? $A.test.dummyFunction() : '@@@TOKEN@@@'));" + "}}");

        createDef(ApplicationDef.class, String.format("%s:%s", namespace, appName), String.format(
                "<aura:application useAppcache='true' render='client'>" + "<%s:%s/>" + "</aura:application>",
                namespace, cmpDesc.getName()));
    }

    /**
     * Opening cached app will only query server for the manifest and the component load.
     * BrowserType.SAFARI is disabled : W-2367702
     */
    @TargetBrowsers({ BrowserType.GOOGLECHROME, BrowserType.IPAD, BrowserType.IPHONE })
    public void testNoChanges() throws Exception {
        List<Request> logs = loadMonitorAndValidateApp(TOKEN, TOKEN, "", TOKEN);
        assertRequests(getExpectedInitialRequests(), logs);
        assertAppCacheStatus(Status.IDLE);

        // only expect a fetch for the manifest and the initAsync component load
        logs = loadMonitorAndValidateApp(TOKEN, TOKEN, "", TOKEN);
        List<Request> expected = Lists.newArrayList(new Request("/auraResource", null, null, "manifest", 200));
        assertRequests(expected, logs);
        assertAppCacheStatus(Status.IDLE);
    }

    /**
     * Opening cached app that had a prior cache error will reload the app.
     * BrowserType.SAFARI is disabled : W-2367702
     */
    @TargetBrowsers({ BrowserType.GOOGLECHROME, BrowserType.IPAD, BrowserType.IPHONE })
    public void testCacheError() throws Exception {
        List<Request> logs = loadMonitorAndValidateApp(TOKEN, TOKEN, "", TOKEN);
        assertRequests(getExpectedInitialRequests(), logs);
        assertAppCacheStatus(Status.IDLE);

        Date expiry = new Date(System.currentTimeMillis() + 60000);
        String cookieName = getManifestCookieName();
        updateCookie(cookieName, "error", expiry, "/");

        logs = loadMonitorAndValidateApp(TOKEN, TOKEN, "", TOKEN);
        List<Request> expectedChange = Lists.newArrayList();
        expectedChange.add(new Request("/auraResource", null, null, "manifest", 404)); // reset
        expectedChange.add(new Request(getUrl(), null, null, null, 302)); // hard refresh
        switch (getBrowserType()) {
        case GOOGLECHROME:
            expectedChange.add(new Request(3, "/auraResource", null, null, "manifest", 200));
            expectedChange.add(new Request(2, getUrl(), null, null, null, 200));
            break;
        default:
            expectedChange.add(new Request("/auraResource", null, null, "manifest", 200));
            expectedChange.add(new Request(getUrl(), null, null, null, 200));
            expectedChange.add(new Request("/auraResource", null, null, "css", 200));
            expectedChange.add(new Request("/auraResource", null, null, "js", 200));
        }
        assertRequests(expectedChange, logs);
        assertAppCacheStatus(Status.IDLE);
        // There may be a varying number of requests, depending on when the initial manifest response is received.
        Cookie cookie = getDriver().manage().getCookieNamed(cookieName);
        assertFalse("Manifest cookie was not changed " + cookie.getValue(), "error".equals(cookie.getValue()));
    }

    /**
     * Opening uncached app that had a prior cache error will have limited caching.
     * BrowserType.SAFARI is disabled : W-2367702
     */
    @TargetBrowsers({ BrowserType.GOOGLECHROME, BrowserType.IPAD, BrowserType.IPHONE })
    public void testCacheErrorWithEmptyCache() throws Exception {
        openNoAura("/aura/application.app"); // just need a domain page to set cookie from

        Date expiry = new Date(System.currentTimeMillis() + 60000);
        String cookieName = getManifestCookieName();
        updateCookie(cookieName, "error", expiry, "/");

        List<Request> logs = loadMonitorAndValidateApp(TOKEN, TOKEN, "", TOKEN);
        List<Request> expectedChange = Lists.newArrayList();
        expectedChange.add(new Request("/auraResource", null, null, "manifest", 404)); // reset
        expectedChange.add(new Request("/auraResource", null, null, "css", 200));
        expectedChange.add(new Request("/auraResource", null, null, "js", 200));
        switch (getBrowserType()) {
        case GOOGLECHROME:
            expectedChange.add(new Request(1, getUrl(), null, null, null, 200));
            break;
        default:
            expectedChange.add(new Request(getUrl(), null, null, null, 200));
        }
        assertRequests(expectedChange, logs);
        assertAppCacheStatus(Status.UNCACHED);
        // There may be a varying number of requests, depending on when the initial manifest response is received.
        Cookie cookie = getDriver().manage().getCookieNamed(cookieName);
        assertNull("No manifest cookie should be present", cookie);
    }

    /**
     * Manifest request limit exceeded for the time period should result in reset.
     * BrowserType.SAFARI is disabled : W-2367702
     */
    @TargetBrowsers({ BrowserType.GOOGLECHROME, BrowserType.IPAD, BrowserType.IPHONE })
    public void testManifestRequestLimitExceeded() throws Exception {
        List<Request> logs = loadMonitorAndValidateApp(TOKEN, TOKEN, "", TOKEN);
        assertRequests(getExpectedInitialRequests(), logs);
        assertAppCacheStatus(Status.IDLE);

        Date expiry = new Date(System.currentTimeMillis() + 60000);
        String cookieName = getManifestCookieName();
        Cookie cookie = getDriver().manage().getCookieNamed(cookieName);
        String timeVal = cookie.getValue().split(":")[1];
        updateCookie(cookieName, "8:" + timeVal, expiry, "/");
        logs = loadMonitorAndValidateApp(TOKEN, TOKEN, "", TOKEN);
        List<Request> expectedChange = Lists.newArrayList();

        expectedChange.add(new Request("/auraResource", null, null, "manifest", 404)); // reset
        expectedChange.add(new Request(getUrl(), null, null, null, 302)); // hard refresh
        switch (getBrowserType()) {
        case GOOGLECHROME:
            expectedChange.add(new Request(3, "/auraResource", null, null, "manifest", 200));
            expectedChange.add(new Request(2, getUrl(), null, null, null, 200));
            break;
        default:
            expectedChange.add(new Request("/auraResource", null, null, "manifest", 200));
            expectedChange.add(new Request(getUrl(), null, null, null, 200));
            expectedChange.add(new Request("/auraResource", null, null, "css", 200));
            expectedChange.add(new Request("/auraResource", null, null, "js", 200));
        }
        assertRequests(expectedChange, logs);
        assertAppCacheStatus(Status.IDLE);
    }

    /**
     * Opening cached app after namespace style change will trigger cache update.
     */
    @ThreadHostileTest("NamespaceDef modification affects namespace")
    @TargetBrowsers({ BrowserType.GOOGLECHROME, BrowserType.SAFARI, BrowserType.IPAD, BrowserType.IPHONE })
    // W-2359835 - disabled due to extra 302 being detected
    public void _testComponentCssChange() throws Exception {
        createDef(NamespaceDef.class, String.format("%s://%s", DefDescriptor.MARKUP_PREFIX, namespace),
                "<aura:namespace></aura:namespace>");

        createDef(StyleDef.class, String.format("%s://%s.%s", DefDescriptor.CSS_PREFIX, namespace, cmpName),
                ".THIS {background-image: url(/auraFW/resources/qa/images/s.gif?@@@TOKEN@@@);}");

        List<Request> logs = loadMonitorAndValidateApp(TOKEN, TOKEN, TOKEN, TOKEN);
        assertRequests(getExpectedInitialRequests(), logs);
        assertAppCacheStatus(Status.IDLE);

        // update a component's css file
        String replacement = getName() + System.currentTimeMillis();

        replaceToken(getTargetComponent().getStyleDescriptor(), replacement);

        logs = loadMonitorAndValidateApp(TOKEN, TOKEN, replacement, TOKEN);
        assertRequests(getExpectedChangeRequests(), logs);
        assertAppCacheStatus(Status.IDLE);

        logs = loadMonitorAndValidateApp(TOKEN, TOKEN, replacement, TOKEN);
        List<Request> expected = Lists.newArrayList(new Request("/auraResource", null, null, "manifest", 200));
        assertRequests(expected, logs);
        assertAppCacheStatus(Status.IDLE);
    }

    /**
     * Opening cached app after namespace controller change will trigger cache update.
     */
    @TargetBrowsers({ BrowserType.GOOGLECHROME, BrowserType.SAFARI, BrowserType.IPAD, BrowserType.IPHONE })
    // W-2359835 - disabled due to extra 302 being detected
    public void _testComponentJsChange() throws Exception {
        List<Request> logs = loadMonitorAndValidateApp(TOKEN, TOKEN, "", TOKEN);
        assertRequests(getExpectedInitialRequests(), logs);
        assertAppCacheStatus(Status.IDLE);
        // update a component's js controller file
        String replacement = getName() + System.currentTimeMillis();
        DefDescriptor<?> desc = null;
        for (DefDescriptor<?> cd : getTargetComponent().getControllerDefDescriptors()) {
            if ("js".equals(cd.getPrefix())) {
                desc = cd;
                break;
            }
        }
        replaceToken(desc, replacement);
        logs = loadMonitorAndValidateApp(TOKEN, replacement, "", TOKEN);
        assertRequests(getExpectedChangeRequests(), logs);
        assertAppCacheStatus(Status.IDLE);
        logs = loadMonitorAndValidateApp(TOKEN, replacement, "", TOKEN);
        List<Request> expected = Lists.newArrayList(new Request("/auraResource", null, null, "manifest", 200));
        assertRequests(expected, logs);
        assertAppCacheStatus(Status.IDLE);
    }

    /**
     * Opening cached app after component markup change will trigger cache update.
     */
    @TargetBrowsers({ BrowserType.GOOGLECHROME, BrowserType.SAFARI, BrowserType.IPAD, BrowserType.IPHONE })
    // W-2359835 - disabled due to extra 302 being detected
    public void _testComponentMarkupChange() throws Exception {
        List<Request> logs = loadMonitorAndValidateApp(TOKEN, TOKEN, "", TOKEN);
        assertRequests(getExpectedInitialRequests(), logs);
        assertAppCacheStatus(Status.IDLE);
        // update markup of namespaced component used by app
        String replacement = getName() + System.currentTimeMillis();
        replaceToken(getTargetComponent().getDescriptor(), replacement);
        logs = loadMonitorAndValidateApp(replacement, TOKEN, "", TOKEN);
        assertRequests(getExpectedChangeRequests(), logs);
        assertAppCacheStatus(Status.IDLE);
        logs = loadMonitorAndValidateApp(replacement, TOKEN, "", TOKEN);
        List<Request> expected = Lists.newArrayList(new Request("/auraResource", null, null, "manifest", 200));
        assertRequests(expected, logs);
        assertAppCacheStatus(Status.IDLE);
    }

    private <T extends Definition> DefDescriptor<T> createDef(Class<T> defClass, String qualifiedName,
            String content) {
        DefDescriptor<T> desc = Aura.getDefinitionService().getDefDescriptor(qualifiedName, defClass);
        addSourceAutoCleanup(desc, content);
        return desc;
    }

    private String getManifestCookieName() {
        return String.format(COOKIE_NAME, getAuraModeForCurrentBrowser().toString(), namespace, appName);
    }

    private void assertAppCacheStatus(final Status status) {
        auraUITestingUtil.waitUntil(new Function<WebDriver, Boolean>() {
            @Override
            public Boolean apply(WebDriver input) {
                return status.name()
                        .equals(Status.values()[Integer.parseInt(
                                auraUITestingUtil.getEval("return window.applicationCache.status;").toString())]
                                        .name());
            }
        }, "applicationCache.status was not " + status.name());
    }

    // provide a test component with TOKENs for replacement to trigger lastMod updates
    private ComponentDef getTargetComponent() throws Exception {
        ContextService service = Aura.getContextService();
        AuraContext context = service.getCurrentContext();
        if (context == null) {
            context = service.startContext(Mode.SELENIUM, Format.HTML, Authentication.AUTHENTICATED);
        }
        return Aura.getDefinitionService().getDefinition(String.format("%s:%s", namespace, cmpName),
                ComponentDef.class);
    }

    /**
     * this function will check each request in actual list against expected list. fudge is the number this request
     * suppose to show up. we remove the request from expected list once it has been visited #fudge times. any missing
     * request will be added to missingRequests list.
     * 
     * @param expected : list of expected request
     * @param actual : list of actual request captured by log
     * @throws Exception
     */
    private void assertRequests(List<Request> expected, List<Request> actual) throws Exception {
        boolean failed;

        List<Request> unexpectedRequests = Lists.newArrayList();
        List<Request> expectedRequests = Lists.newArrayList(expected);
        List<Request> missingRequests = Lists.newArrayList();
        for (Request r : actual) {
            int idx = expectedRequests.indexOf(r);
            if (idx != -1) {
                if (expectedRequests.get(idx).mark()) {
                    expectedRequests.remove(idx);
                }
            } else {
                unexpectedRequests.add(r);
            }
        }
        for (Request r : expectedRequests) {
            if (!r.passed()) {// return fudge > 0 && count > 0;
                missingRequests.add(r);
            }
        }

        failed = unexpectedRequests.size() > 0 || missingRequests.size() > 0;

        if (debug) {
            System.out.println(">>> assertRequests: ");
            System.out.println("EXPECTED:");
            for (Request r : expected) {
                System.out.println("E: " + r + ",fudge:" + r.fudge);
            }
            System.out.println("ACTUAL:");
            for (Request r : actual) {
                r.setShowExtras(failed);
                System.out.println("A: " + r);
            }
        }
        if (failed) {
            StringBuffer sb = new StringBuffer();
            String separator = "";

            if (unexpectedRequests.size() > 0) {
                sb.append("Unexpected requests:\n");
                sb.append(unexpectedRequests);
                separator = "\n";
            }
            if (missingRequests.size() > 0) {
                sb.append(separator);
                sb.append("Missing Requests:\n");
                sb.append(missingRequests);
            }
            fail(sb.toString());
        }
    }

    /**
     * Load and get all the log lines for the app load. Some sanity checks that our simple test app is functional after
     * cache resolutions.
     * <ul>
     * <li>updated markup text is rendered (markupToken)</li>
     * <li>updated client actions functional (jsToken)</li>
     * <li>updated styling applied (cssToken)</li>
     * <li>updated framework called (fwToken)</li>
     * </ul>
     * 
     * @param markupToken The text to be found in the markup.
     * @param jsToken The text to be found from js
     * @param cssToken The text to be found from css.
     * @param Token The text to be found from the framework.
     */
    private List<Request> loadMonitorAndValidateApp(final String markupToken, String jsToken, String cssToken,
            String fwToken) throws Exception {
        TestLoggingAdapterController.beginCapture();

        // Opening a page through WebDriverTestCase adds a nonce to ensure fresh resources. In this case we want to see
        // what's cached, so build our URL and call WebDriver.get() directly.
        String url = getUrl();
        Map<String, String> params = new HashMap<>();
        params.put("aura.mode", getAuraModeForCurrentBrowser().toString());
        url = addUrlParams(url, params);
        getDriver().get(getAbsoluteURI(url).toString());

        auraUITestingUtil.waitUntilWithCallback(new Function<WebDriver, Integer>() {
            @Override
            public Integer apply(WebDriver input) {
                Integer appCacheStatus = Integer
                        .parseInt(auraUITestingUtil.getEval("return window.applicationCache.status;").toString());
                if (appCacheStatus != 3 && appCacheStatus != 2) {
                    return appCacheStatus;
                } else {
                    return null;
                }
            }
        }, new ExpectedCondition<String>() {
            @Override
            public String apply(WebDriver d) {
                Object ret = auraUITestingUtil.getRawEval("return window.applicationCache.status");
                return "Current AppCache status is "
                        + auraUITestingUtil.appCacheStatusIntToString(((Long) ret).intValue());
            }
        }, 10, "fail waiting on application cache not to be Downloading or Checking before clicking on 'clickableme'");

        auraUITestingUtil.waitUntil(new Function<WebDriver, WebElement>() {
            @Override
            public WebElement apply(WebDriver input) {
                try {
                    WebElement find = findDomElement(By.cssSelector(".clickableme"));
                    if (markupToken.equals(find.getText())) {
                        return find;
                    }
                } catch (StaleElementReferenceException e) {
                    // slight chance of happening between the findDomElement and getText
                }
                return null;
            }
        }, "fail to load clickableme");
        Thread.sleep(200);
        List<Request> logs = endMonitoring();

        String output = auraUITestingUtil.waitUntil(new Function<WebDriver, String>() {
            @Override
            public String apply(WebDriver input) {
                try {
                    WebElement find = findDomElement(By.cssSelector(".clickableme"));
                    find.click();
                    WebElement output = findDomElement(By.cssSelector("div.attroutput"));
                    return output.getText();
                } catch (StaleElementReferenceException e) {
                    // could happen before the click or if output is
                    // rerendering
                }
                return null;
            }
        }, "fail to click on clickableme or couldn't locate output value");

        assertEquals("Unexpected alert text", String.format("%s%s%s", jsToken, cssToken, fwToken), output);

        return logs;
    }

    private String getUrl() {
        return String.format("/%s/%s.app", namespace, appName);
    }

    // replaces TOKEN found in the source file with the provided replacement
    private void replaceToken(DefDescriptor<?> descriptor, String replacement) throws Exception {
        assertNotNull("Missing descriptor for source replacement!", descriptor);
        ContextService service = Aura.getContextService();
        AuraContext context = service.getCurrentContext();
        if (context == null) {
            context = service.startContext(Mode.SELENIUM, Format.HTML, Authentication.AUTHENTICATED);
        }
        Source<?> source = context.getDefRegistry().getSource(descriptor);
        String originalContent = source.getContents();
        assert originalContent.contains(TOKEN);
        source.addOrUpdate(originalContent.replace(TOKEN, replacement));
    }

    private List<Request> endMonitoring() {
        List<Request> logs = Lists.newLinkedList();
        for (Map<String, Object> log : TestLoggingAdapterController.endCapture()) {
            if (!"GET".equals(log.get("requestMethod"))) {
                if (debug) {
                    // Log ignored lines so that we can monitor what happens. The line above had nulls as requestMethod,
                    // so this catches randomness.
                    System.out.println("IGNORED: " + log);
                }
                continue;
            }
            int status = -1;

            if (log.get("httpStatus") != null) {
                try {
                    status = Integer.parseInt((String) log.get("httpStatus"));
                } catch (NumberFormatException nfe) {
                }
            }
            Request toAdd = new Request(log.get("auraRequestURI").toString(), null, null, null, status);
            for (String part : AuraTextUtil.urldecode(log.get("auraRequestQuery").toString()).split("&")) {
                String[] parts = part.split("=", 2);
                String key = parts[0].substring(AURA.length() + 1);
                String v = parts[1];
                toAdd.put(key, (v != null && !v.isEmpty()) ? v : null);
            }
            logs.add(toAdd);
        }
        return logs;
    }

    /**
     * Get the set of expected requests on change. These are the requests that we expect for filling the app cache.
     * 
     * @return the list of request objects, not necessarily in order.
     */
    private List<Request> getExpectedChangeRequests() {
        switch (getBrowserType()) {
        case GOOGLECHROME:
            /*
             * For Chrome Get the set of expected requests on change. These are the requests that we expect for filling
             * the app cache. The explanation is as follows. <ul> <li>The manifest is pulled</li> <li>The browser now
             * gets all three components, initial, css, and js</li> <li>Finally, the browser re-fetches the manifest to
             * check contents</li> <ul> The primary difference between this and the initial requests is that we don't
             * get the initial page twice, and we get the manifest three times... odd that. we usually only get js and
             * css only once, but it's not stable, do see some test get them twice sometimes.
             */
            return ImmutableList.of(new Request(getUrl(), null, null, null, 302), // hard refresh
                    new Request("/auraResource", null, null, "manifest", 404), // manifest out of date
                    new Request(3, "/auraResource", null, null, "manifest", 200),
                    new Request(2, getUrl(), null, null, null, 200), // rest are cache updates
                    new Request(2, "/auraResource", null, null, "css", 200),
                    new Request(2, "/auraResource", null, null, "js", 200));
        default:
            /*
             * For iOS Get the set of expected requests on change. These are the requests that we expect for filling the
             * app cache. The explanation is as follows. <ul> <li>The manifest is pulled</li> <li>The browser now gets
             * all three components, initial, css, and js</li> <li>Finally, the browser re-fetches the manifest to check
             * contents</li> <ul> The primary difference between this and the initial requests is that we get the
             * initial page twice
             */
            return ImmutableList.of(new Request(getUrl(), null, null, null, 302), // hard refresh
                    new Request("/auraResource", null, null, "manifest", 404), // manifest out of date
                    new Request("/auraResource", null, null, "manifest", 200),
                    new Request(2, getUrl(), null, null, null, 200), // rest are cache updates
                    new Request(2, "/auraResource", null, null, "css", 200),
                    new Request(2, "/auraResource", null, null, "js", 200));
        }
    }

    /**
     * Get the set of expected requests on change. These are the requests that we expect for filling the app cache.
     * 
     * @return the list of request objects, not necessarily in order.
     */
    private List<Request> getExpectedInitialRequests() {
        switch (getBrowserType()) {
        case GOOGLECHROME:
            /*
             * For Chrome Get the set of expected initial requests. These are the requests that we expect for filling
             * the app cache. The explanation is as follows. <ul> <li>The browser requests the initial page from the
             * server</li> <li>The manifest is pulled</li> <li>The browser now gets all three components, initial, css,
             * and js</li> <li>Finally, the browser re-fetches the manifest to check contents</li> <ul> Note that there
             * are two requests for the initial page, one as the first request, and one to fill the app cache (odd, but
             * true). There are also two manifest requests.
             */
            return ImmutableList.of(new Request(2, getUrl(), null, null, null, 200),
                    new Request(2, "/auraResource", null, null, "manifest", 200),
                    new Request("/auraResource", null, null, "css", 200),
                    new Request(2, "/auraResource", null, null, "js", 200));
        default:
            /*
             * For iOS Get the set of expected requests on change. These are the requests that we expect for filling the
             * app cache. The explanation is as follows. <ul> <li>The manifest is pulled</li> <li>The browser now gets
             * all three components, initial, css, and js</li> <li>Finally, the browser re-fetches the manifest to check
             * contents</li> <ul> Note that there are also two css and js request.
             */
            return ImmutableList.of(new Request(1, getUrl(), null, null, null, 200),
                    new Request(1, "/auraResource", null, null, "manifest", 200),
                    new Request(2, "/auraResource", null, null, "css", 200),
                    new Request(2, "/auraResource", null, null, "js", 200));
        }
    }

    /**
     * A request object, which can either be an 'expected' request, or an 'actual' request. Expected requests can also
     * have a fudge factor allowing multiple requests for the resource. This is very helpful for different browsers
     * doing diferent things with the manifest. We allow multiple fetches of both the manifest and initial page in both
     * the initial request and the requests on change of resource.
     */
    static class Request extends HashMap<String, String> {
        private static final long serialVersionUID = 4149738936658714181L;
        private static final ImmutableSet<String> validKeys = ImmutableSet.of("URI", "tag", "namespaces", "format",
                "httpStatus");

        private final int fudge;
        private int count = 0;
        private Map<String, String> extras = null;
        private boolean showExtras = false;

        Request(int fudge, String URI, String tag, String namespaces, String format, int status) {
            super();
            this.fudge = fudge;
            put("URI", URI);
            put("tag", tag);
            put("namespaces", namespaces);
            put("format", format);
            if (status != -1) {
                put("httpStatus", String.valueOf(status));
            }
        }

        Request(String URI, String tag, String namespaces, String format, int status) {
            super();
            this.fudge = 0;
            put("URI", URI);
            put("tag", tag);
            put("namespaces", namespaces);
            put("format", format);
            if (status != -1) {
                put("httpStatus", String.valueOf(status));
            }
        }

        @Override
        public String put(String k, String v) {
            if (validKeys.contains(k)) {
                return super.put(k, v);
            } else {
                if (extras == null) {
                    extras = new HashMap<>();
                }
                extras.put(k, v);
            }
            return null;
        }

        /**
         * We passed the test for this request.
         * 
         * @return true if we got the request. each request from expected list must show up at least once in the actual
         *         list.
         */
        public boolean passed() {
            return fudge > 0 && count > 0;
        }

        /**
         * Mark the request as found.
         * 
         * @return true if it should be removed.count > fudge: browsers don't behave consistently. better have a loose
         *         bound here. we are comparing two requests list: actual list and expected list. count start at 0, we
         *         are expecting 1,2,..,fudge, or fudge+1 request. once we have some request X that show up fudge+1 in
         *         actual list, X get removed from expected list. then if we receive another X again, it will be added
         *         to unexpected requestes list and error out.
         */
        public boolean mark() {
            if (fudge == 0) {
                return true;
            } else {
                count += 1;
                return count > fudge;
            }
        }

        public void setShowExtras(boolean value) {
            this.showExtras = value;
        }

        @Override
        public String toString() {
            if (extras == null || !showExtras) {
                return super.toString();
            } else {
                return super.toString() + String.valueOf(extras);
            }
        }
    }

    private void updateCookie(String name, String value, Date expiry, String path) {
        SimpleDateFormat sd = new SimpleDateFormat();
        sd.setTimeZone(TimeZone.getTimeZone("GMT"));
        String expiryFormatted = sd.format(expiry);
        String command = "document.cookie = '" + name + "=" + value + "; expires=" + expiryFormatted + "; path="
                + path + "';";
        auraUITestingUtil.getEval(command);
    }
}