diff options
Diffstat (limited to 'dom/serviceworkers/test')
388 files changed, 20645 insertions, 0 deletions
diff --git a/dom/serviceworkers/test/ForceRefreshChild.sys.mjs b/dom/serviceworkers/test/ForceRefreshChild.sys.mjs new file mode 100644 index 0000000000..b2b965be9e --- /dev/null +++ b/dom/serviceworkers/test/ForceRefreshChild.sys.mjs @@ -0,0 +1,12 @@ +export class ForceRefreshChild extends JSWindowActorChild { + constructor() { + super(); + } + + handleEvent(evt) { + this.sendAsyncMessage("test:event", { + type: evt.type, + detail: evt.details, + }); + } +} diff --git a/dom/serviceworkers/test/ForceRefreshParent.sys.mjs b/dom/serviceworkers/test/ForceRefreshParent.sys.mjs new file mode 100644 index 0000000000..cb2d4809e9 --- /dev/null +++ b/dom/serviceworkers/test/ForceRefreshParent.sys.mjs @@ -0,0 +1,77 @@ +var maxCacheLoadCount = 3; +var cachedLoadCount = 0; +var baseLoadCount = 0; +var done = false; + +export class ForceRefreshParent extends JSWindowActorParent { + constructor() { + super(); + } + + receiveMessage(msg) { + // if done is called, ignore the msg. + if (done) { + return; + } + if (msg.data.type === "base-load") { + baseLoadCount += 1; + if (cachedLoadCount === maxCacheLoadCount) { + ForceRefreshParent.SimpleTest.is( + baseLoadCount, + 2, + "cached load should occur before second base load" + ); + done = true; + return ForceRefreshParent.done(); + } + if (baseLoadCount !== 1) { + ForceRefreshParent.SimpleTest.ok( + false, + "base load without cached load should only occur once" + ); + done = true; + return ForceRefreshParent.done(); + } + } else if (msg.data.type === "base-register") { + ForceRefreshParent.SimpleTest.ok( + !cachedLoadCount, + "cached load should not occur before base register" + ); + ForceRefreshParent.SimpleTest.is( + baseLoadCount, + 1, + "register should occur after first base load" + ); + } else if (msg.data.type === "base-sw-ready") { + ForceRefreshParent.SimpleTest.ok( + !cachedLoadCount, + "cached load should not occur before base ready" + ); + ForceRefreshParent.SimpleTest.is( + baseLoadCount, + 1, + "ready should occur after first base load" + ); + ForceRefreshParent.refresh(); + } else if (msg.data.type === "cached-load") { + ForceRefreshParent.SimpleTest.ok( + cachedLoadCount < maxCacheLoadCount, + "cached load should not occur too many times" + ); + ForceRefreshParent.SimpleTest.is( + baseLoadCount, + 1, + "cache load occur after first base load" + ); + cachedLoadCount += 1; + if (cachedLoadCount < maxCacheLoadCount) { + return ForceRefreshParent.refresh(); + } + ForceRefreshParent.forceRefresh(); + } else if (msg.data.type === "cached-failure") { + ForceRefreshParent.SimpleTest.ok(false, "failure: " + msg.data.detail); + done = true; + ForceRefreshParent.done(); + } + } +} diff --git a/dom/serviceworkers/test/abrupt_completion_worker.js b/dom/serviceworkers/test/abrupt_completion_worker.js new file mode 100644 index 0000000000..7afebc6d45 --- /dev/null +++ b/dom/serviceworkers/test/abrupt_completion_worker.js @@ -0,0 +1,18 @@ +function setMessageHandler(response) { + onmessage = e => { + e.source.postMessage(response); + }; +} + +setMessageHandler("handler-before-throw"); + +// importScripts will throw when the ServiceWorker is past the "intalling" state. +importScripts(`empty.js?${Date.now()}`); + +// When importScripts throws an uncaught exception, these calls should never be +// made and the message handler should remain responding "handler-before-throw". +setMessageHandler("handler-after-throw"); + +// There needs to be a fetch handler to avoid the no-fetch optimizaiton, +// which will skip starting up this worker. +onfetch = e => e.respondWith(new Response("handler-after-throw")); diff --git a/dom/serviceworkers/test/activate_event_error_worker.js b/dom/serviceworkers/test/activate_event_error_worker.js new file mode 100644 index 0000000000..9f09cc5225 --- /dev/null +++ b/dom/serviceworkers/test/activate_event_error_worker.js @@ -0,0 +1,4 @@ +// Worker that errors on receiving an activate event. +onactivate = function(e) { + undefined.doSomething; +}; diff --git a/dom/serviceworkers/test/async_waituntil_worker.js b/dom/serviceworkers/test/async_waituntil_worker.js new file mode 100644 index 0000000000..94219e4879 --- /dev/null +++ b/dom/serviceworkers/test/async_waituntil_worker.js @@ -0,0 +1,53 @@ +var keepAlivePromise; +var resolvePromise; +var result = "Failed"; + +onactivate = function(event) { + event.waitUntil(clients.claim()); +}; + +onmessage = function(event) { + if (event.data === "Start") { + event.waitUntil(Promise.reject()); + + keepAlivePromise = new Promise(function(resolve, reject) { + resolvePromise = resolve; + }); + + result = "Success"; + event.waitUntil(keepAlivePromise); + event.source.postMessage("Started"); + } else if (event.data === "Result") { + event.source.postMessage(result); + if (resolvePromise !== undefined) { + resolvePromise(); + } + } +}; + +addEventListener("fetch", e => { + let respondWithPromise = new Promise(function(res, rej) { + setTimeout(() => { + res(new Response("ok")); + }, 0); + }); + e.respondWith(respondWithPromise); + // Test that waitUntil can be called in the promise handler of the existing + // lifetime extension promise. + respondWithPromise.then(() => { + e.waitUntil( + clients.matchAll().then(cls => { + dump(`matchAll returned ${cls.length} client(s) with URLs:\n`); + cls.forEach(cl => { + dump(`${cl.url}\n`); + }); + + if (cls.length != 1) { + dump("ERROR: no controlled clients.\n"); + } + client = cls[0]; + client.postMessage("Done"); + }) + ); + }); +}); diff --git a/dom/serviceworkers/test/blocking_install_event_worker.js b/dom/serviceworkers/test/blocking_install_event_worker.js new file mode 100644 index 0000000000..5cb9270b04 --- /dev/null +++ b/dom/serviceworkers/test/blocking_install_event_worker.js @@ -0,0 +1,22 @@ +function postMessageToTest(msg) { + return clients.matchAll({ includeUncontrolled: true }).then(list => { + for (var client of list) { + if (client.url.endsWith("test_install_event_gc.html")) { + client.postMessage(msg); + break; + } + } + }); +} + +addEventListener("install", evt => { + // This must be a simple promise to trigger the CC failure. + evt.waitUntil(new Promise(function() {})); + postMessageToTest({ type: "INSTALL_EVENT" }); +}); + +addEventListener("message", evt => { + if (evt.data.type === "ping") { + postMessageToTest({ type: "pong" }); + } +}); diff --git a/dom/serviceworkers/test/browser-common.ini b/dom/serviceworkers/test/browser-common.ini new file mode 100644 index 0000000000..e1c8cfbc48 --- /dev/null +++ b/dom/serviceworkers/test/browser-common.ini @@ -0,0 +1,52 @@ +[DEFAULT] +support-files = + browser_base_force_refresh.html + browser_cached_force_refresh.html + browser_head.js + download/window.html + download/worker.js + download_canceled/page_download_canceled.html + download_canceled/server-stream-download.sjs + download_canceled/sw_download_canceled.js + fetch.js + file_userContextId_openWindow.js + force_refresh_browser_worker.js + ForceRefreshChild.sys.mjs + ForceRefreshParent.sys.mjs + empty.html + empty_with_utils.html + empty.js + intercepted_channel_process_swap_worker.js + navigationPreload_page.html + network_with_utils.html + page_post_controlled.html + redirect.sjs + simple_fetch_worker.js + storage_recovery_worker.sjs + sw_respondwith_serviceworker.js + sw_with_navigationPreload.js + utils.js + +[browser_antitracking.js] +[browser_antitracking_subiframes.js] +[browser_devtools_serviceworker_interception.js] +skip-if = serviceworker_e10s +[browser_force_refresh.js] +skip-if = verify # Bug 1603340 +[browser_download.js] +[browser_download_canceled.js] +skip-if = verify +[browser_intercepted_channel_process_swap.js] +skip-if = !fission +[browser_intercepted_worker_script.js] +[browser_navigation_fetch_fault_handling.js] +skip-if = + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure +[browser_navigationPreload_read_after_respondWith.js] +[browser_remote_type_process_swap.js] +[browser_storage_permission.js] +skip-if = true # Crashes: @ mozilla::dom::ServiceWorkerManagerService::PropagateUnregister(unsigned long, mozilla::ipc::PrincipalInfo const&, nsTSubstring<char16_t> const&), #Bug 1578337 +[browser_storage_recovery.js] +[browser_unregister_with_containers.js] +[browser_userContextId_openWindow.js] +skip-if = true # See bug 1769437. diff --git a/dom/serviceworkers/test/browser-dFPI.ini b/dom/serviceworkers/test/browser-dFPI.ini new file mode 100644 index 0000000000..c327062f16 --- /dev/null +++ b/dom/serviceworkers/test/browser-dFPI.ini @@ -0,0 +1,7 @@ +[DEFAULT] +# Enable dFPI(cookieBehavior 5) for service worker tests. +prefs = + network.cookie.cookieBehavior=5 +dupe-manifest = true + +[include:browser-common.ini] diff --git a/dom/serviceworkers/test/browser.ini b/dom/serviceworkers/test/browser.ini new file mode 100644 index 0000000000..2a7162e9ed --- /dev/null +++ b/dom/serviceworkers/test/browser.ini @@ -0,0 +1,4 @@ +[DEFAULT] +dupe-manifest = true + +[include:browser-common.ini] diff --git a/dom/serviceworkers/test/browser_antitracking.js b/dom/serviceworkers/test/browser_antitracking.js new file mode 100644 index 0000000000..cb72f1c7e8 --- /dev/null +++ b/dom/serviceworkers/test/browser_antitracking.js @@ -0,0 +1,104 @@ +const BEHAVIOR_ACCEPT = Ci.nsICookieService.BEHAVIOR_ACCEPT; +const BEHAVIOR_REJECT_TRACKER = Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER; + +let { UrlClassifierTestUtils } = ChromeUtils.import( + "resource://testing-common/UrlClassifierTestUtils.jsm" +); + +const TOP_DOMAIN = "http://mochi.test:8888/"; +const SW_DOMAIN = "https://tracking.example.org/"; + +const TOP_TEST_ROOT = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + TOP_DOMAIN +); +const SW_TEST_ROOT = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + SW_DOMAIN +); + +const TOP_EMPTY_PAGE = `${TOP_TEST_ROOT}empty_with_utils.html`; +const SW_REGISTER_PAGE = `${SW_TEST_ROOT}empty_with_utils.html`; +const SW_IFRAME_PAGE = `${SW_TEST_ROOT}page_post_controlled.html`; +// An empty script suffices for our SW needs; it's by definition no-fetch. +const SW_REL_SW_SCRIPT = "empty.js"; + +/** + * Set up a no-fetch-optimized ServiceWorker on a domain that will be covered by + * tracking protection (but is not yet). Once the SW is installed, activate TP + * and create a tab that embeds that tracking-site in an iframe. + */ +add_task(async function() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.testing.enabled", true], + ["network.cookie.cookieBehavior", BEHAVIOR_ACCEPT], + ], + }); + + // Open the top-level page. + info("Opening a new tab: " + SW_REGISTER_PAGE); + let topTab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: SW_REGISTER_PAGE, + }); + + // ## Install SW + info("Installing SW"); + await SpecialPowers.spawn( + topTab.linkedBrowser, + [{ sw: SW_REL_SW_SCRIPT }], + async function({ sw }) { + // Waive the xray to use the content utils.js script functions. + await content.wrappedJSObject.registerAndWaitForActive(sw); + } + ); + + // Enable Anti-tracking. + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.trackingprotection.enabled", false], + ["privacy.trackingprotection.pbmode.enabled", false], + ["privacy.trackingprotection.annotate_channels", true], + ["network.cookie.cookieBehavior", BEHAVIOR_REJECT_TRACKER], + ], + }); + await UrlClassifierTestUtils.addTestTrackers(); + + // Open the top-level URL. + info("Loading a new top-level URL: " + TOP_EMPTY_PAGE); + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + topTab.linkedBrowser + ); + BrowserTestUtils.loadURI(topTab.linkedBrowser, TOP_EMPTY_PAGE); + await browserLoadedPromise; + + // Create Iframe in the top-level page and verify its state. + let { controlled } = await SpecialPowers.spawn( + topTab.linkedBrowser, + [{ url: SW_IFRAME_PAGE }], + async function({ url }) { + const payload = await content.wrappedJSObject.createIframeAndWaitForMessage( + url + ); + return payload; + } + ); + + ok(!controlled, "Should not be controlled!"); + + // ## Cleanup + info("Loading the SW unregister page: " + SW_REGISTER_PAGE); + browserLoadedPromise = BrowserTestUtils.browserLoaded(topTab.linkedBrowser); + BrowserTestUtils.loadURI(topTab.linkedBrowser, SW_REGISTER_PAGE); + await browserLoadedPromise; + + await SpecialPowers.spawn(topTab.linkedBrowser, [], async function() { + await content.wrappedJSObject.unregisterAll(); + }); + + // Close the testing tab. + BrowserTestUtils.removeTab(topTab); +}); diff --git a/dom/serviceworkers/test/browser_antitracking_subiframes.js b/dom/serviceworkers/test/browser_antitracking_subiframes.js new file mode 100644 index 0000000000..d2b7a97c96 --- /dev/null +++ b/dom/serviceworkers/test/browser_antitracking_subiframes.js @@ -0,0 +1,105 @@ +const BEHAVIOR_REJECT_TRACKER = Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER; + +const TOP_DOMAIN = "http://mochi.test:8888/"; +const SW_DOMAIN = "https://example.org/"; + +const TOP_TEST_ROOT = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + TOP_DOMAIN +); +const SW_TEST_ROOT = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + SW_DOMAIN +); + +const TOP_EMPTY_PAGE = `${TOP_TEST_ROOT}empty_with_utils.html`; +const SW_REGISTER_PAGE = `${SW_TEST_ROOT}empty_with_utils.html`; +const SW_IFRAME_PAGE = `${SW_TEST_ROOT}page_post_controlled.html`; +const SW_REL_SW_SCRIPT = "empty.js"; + +/** + * Set up a ServiceWorker on a domain that will be used as 3rd party iframe. + * That 3rd party frame should be controlled by the ServiceWorker. + * After that, we open a second iframe into the first one. That should not be + * controlled. + */ +add_task(async function() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.testing.enabled", true], + ["network.cookie.cookieBehavior", BEHAVIOR_REJECT_TRACKER], + ], + }); + + // Open the top-level page. + info("Opening a new tab: " + SW_REGISTER_PAGE); + let topTab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: SW_REGISTER_PAGE, + }); + + // Install SW + info("Registering a SW: " + SW_REL_SW_SCRIPT); + await SpecialPowers.spawn( + topTab.linkedBrowser, + [{ sw: SW_REL_SW_SCRIPT }], + async function({ sw }) { + // Waive the xray to use the content utils.js script functions. + await content.wrappedJSObject.registerAndWaitForActive(sw); + // User interaction + content.document.userInteractionForTesting(); + } + ); + + info("Loading a new top-level URL: " + TOP_EMPTY_PAGE); + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + topTab.linkedBrowser + ); + BrowserTestUtils.loadURI(topTab.linkedBrowser, TOP_EMPTY_PAGE); + await browserLoadedPromise; + + // Create Iframe in the top-level page and verify its state. + info("Creating iframe and checking if controlled"); + let { controlled } = await SpecialPowers.spawn( + topTab.linkedBrowser, + [{ url: SW_IFRAME_PAGE }], + async function({ url }) { + content.document.userInteractionForTesting(); + const payload = await content.wrappedJSObject.createIframeAndWaitForMessage( + url + ); + return payload; + } + ); + + ok(controlled, "Should be controlled!"); + + // Create a nested Iframe. + info("Creating nested-iframe and checking if controlled"); + let { nested_controlled } = await SpecialPowers.spawn( + topTab.linkedBrowser, + [{ url: SW_IFRAME_PAGE }], + async function({ url }) { + const payload = await content.wrappedJSObject.createNestedIframeAndWaitForMessage( + url + ); + return payload; + } + ); + + ok(!nested_controlled, "Should not be controlled!"); + + info("Loading the SW unregister page: " + SW_REGISTER_PAGE); + browserLoadedPromise = BrowserTestUtils.browserLoaded(topTab.linkedBrowser); + BrowserTestUtils.loadURI(topTab.linkedBrowser, SW_REGISTER_PAGE); + await browserLoadedPromise; + + await SpecialPowers.spawn(topTab.linkedBrowser, [], async function() { + await content.wrappedJSObject.unregisterAll(); + }); + + // Close the testing tab. + BrowserTestUtils.removeTab(topTab); +}); diff --git a/dom/serviceworkers/test/browser_base_force_refresh.html b/dom/serviceworkers/test/browser_base_force_refresh.html new file mode 100644 index 0000000000..1c3d02d42f --- /dev/null +++ b/dom/serviceworkers/test/browser_base_force_refresh.html @@ -0,0 +1,27 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> +</head> +<body> +<script type="text/javascript"> +addEventListener('load', function(event) { + navigator.serviceWorker.register('force_refresh_browser_worker.js').then(function(swr) { + if (!swr) { + return; + } + window.dispatchEvent(new Event("base-register", { bubbles: true })); + }); + + navigator.serviceWorker.ready.then(function() { + window.dispatchEvent(new Event("base-sw-ready", { bubbles: true })); + }); + + window.dispatchEvent(new Event("base-load", { bubbles: true })); +}); +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/browser_cached_force_refresh.html b/dom/serviceworkers/test/browser_cached_force_refresh.html new file mode 100644 index 0000000000..faf2ee7a83 --- /dev/null +++ b/dom/serviceworkers/test/browser_cached_force_refresh.html @@ -0,0 +1,59 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> +</head> +<body> +<script type="text/javascript"> +function ok(exp, msg) { + if (!exp) { + throw(msg); + } +} + +function is(actual, expected, msg) { + if (actual !== expected) { + throw('got "' + actual + '", but expected "' + expected + '" - ' + msg); + } +} + +function fail(err) { + window.dispatchEvent(new Event("cached-failure", { bubbles: true, detail: err })); +} + +function getUncontrolledClients(sw) { + return new Promise(function(resolve, reject) { + navigator.serviceWorker.addEventListener('message', function onMsg(evt) { + if (evt.data.type === 'CLIENTS') { + navigator.serviceWorker.removeEventListener('message', onMsg); + resolve(evt.data.detail); + } + }); + sw.postMessage({ type: 'GET_UNCONTROLLED_CLIENTS' }) + }); +} + +addEventListener('load', function(event) { + if (!navigator.serviceWorker.controller) { + return fail(window.location.href + ' is not controlled!'); + } + + getUncontrolledClients(navigator.serviceWorker.controller) + .then(function(clientList) { + is(clientList.length, 1, 'should only have one client'); + is(clientList[0].url, window.location.href, + 'client url should match current window'); + is(clientList[0].frameType, 'top-level', + 'client should be a top-level window'); + window.dispatchEvent(new Event('cached-load', { bubbles: true })); + }) + .catch(function(err) { + fail(err); + }); +}); +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/browser_devtools_serviceworker_interception.js b/dom/serviceworkers/test/browser_devtools_serviceworker_interception.js new file mode 100644 index 0000000000..fc31159116 --- /dev/null +++ b/dom/serviceworkers/test/browser_devtools_serviceworker_interception.js @@ -0,0 +1,264 @@ +"use strict"; + +const BASE_URI = "http://mochi.test:8888/browser/dom/serviceworkers/test/"; +const emptyDoc = BASE_URI + "empty.html"; +const fakeDoc = BASE_URI + "fake.html"; +const helloDoc = BASE_URI + "hello.html"; + +const CROSS_URI = "http://example.com/browser/dom/serviceworkers/test/"; +const crossRedirect = CROSS_URI + "redirect"; +const crossHelloDoc = CROSS_URI + "hello.html"; + +const sw = BASE_URI + "fetch.js"; + +async function checkObserver(aInput) { + let interceptedChannel = null; + + // We always get two channels which receive the "http-on-stop-request" + // notification if the service worker hijacks the request and respondWith an + // another fetch. One is for the "outer" window request when the other one is + // for the "inner" service worker request. Therefore, distinguish them by the + // order. + let waitForSecondOnStopRequest = aInput.intercepted; + + let promiseResolve; + + function observer(aSubject) { + let channel = aSubject.QueryInterface(Ci.nsIChannel); + // Since we cannot make sure that the network event triggered by the fetch() + // in this testcase is the very next event processed by ObserverService, we + // have to wait until we catch the one we want. + if (!channel.URI.spec.includes(aInput.expectedURL)) { + return; + } + + if (waitForSecondOnStopRequest) { + waitForSecondOnStopRequest = false; + return; + } + + // Wait for the service worker to intercept the request if it's expected to + // be intercepted + if (aInput.intercepted && interceptedChannel === null) { + return; + } else if (interceptedChannel) { + ok( + aInput.intercepted, + "Service worker intercepted the channel as expected" + ); + } else { + ok(!aInput.intercepted, "The channel doesn't be intercepted"); + } + + var tc = interceptedChannel + ? interceptedChannel.QueryInterface(Ci.nsITimedChannel) + : aSubject.QueryInterface(Ci.nsITimedChannel); + + // Check service worker related timings. + var serviceWorkerTimings = [ + { + start: tc.launchServiceWorkerStartTime, + end: tc.launchServiceWorkerEndTime, + }, + { + start: tc.dispatchFetchEventStartTime, + end: tc.dispatchFetchEventEndTime, + }, + { start: tc.handleFetchEventStartTime, end: tc.handleFetchEventEndTime }, + ]; + if (!aInput.swPresent) { + serviceWorkerTimings.forEach(aTimings => { + is(aTimings.start, 0, "SW timings should be 0."); + is(aTimings.end, 0, "SW timings should be 0."); + }); + } + + // Check network related timings. + var networkTimings = [ + tc.domainLookupStartTime, + tc.domainLookupEndTime, + tc.connectStartTime, + tc.connectEndTime, + tc.requestStartTime, + tc.responseStartTime, + tc.responseEndTime, + ]; + if (aInput.fetch) { + networkTimings.reduce((aPreviousTiming, aCurrentTiming) => { + ok(aPreviousTiming <= aCurrentTiming, "Checking network timings"); + return aCurrentTiming; + }); + } else { + networkTimings.forEach(aTiming => + is(aTiming, 0, "Network timings should be 0.") + ); + } + + interceptedChannel = null; + Services.obs.removeObserver(observer, topic); + promiseResolve(); + } + + function addInterceptedChannel(aSubject) { + let channel = aSubject.QueryInterface(Ci.nsIChannel); + if (!channel.URI.spec.includes(aInput.url)) { + return; + } + + // Hold the interceptedChannel until checking timing information. + // Note: It's a interceptedChannel in the type of httpChannel + interceptedChannel = channel; + Services.obs.removeObserver(addInterceptedChannel, topic_SW); + } + + const topic = "http-on-stop-request"; + const topic_SW = "service-worker-synthesized-response"; + + Services.obs.addObserver(observer, topic); + if (aInput.intercepted) { + Services.obs.addObserver(addInterceptedChannel, topic_SW); + } + + await new Promise(resolve => { + promiseResolve = resolve; + }); +} + +async function contentFetch(aURL) { + if (aURL.includes("redirect")) { + await content.window.fetch(aURL, { mode: "no-cors" }); + return; + } + await content.window.fetch(aURL); +} + +// The observer topics are fired in the parent process in parent-intercept +// and the content process in child-intercept. This function will handle running +// the check in the correct process. Note that it will block until the observers +// are notified. +async function fetchAndCheckObservers( + aFetchBrowser, + aObserverBrowser, + aTestCase +) { + let promise = null; + + promise = checkObserver(aTestCase); + + await SpecialPowers.spawn(aFetchBrowser, [aTestCase.url], contentFetch); + await promise; +} + +async function registerSWAndWaitForActive(aServiceWorker) { + let swr = await content.navigator.serviceWorker.register(aServiceWorker, { + scope: "empty.html", + }); + await new Promise(resolve => { + let worker = swr.installing || swr.waiting || swr.active; + if (worker.state === "activated") { + return resolve(); + } + + worker.addEventListener("statechange", () => { + if (worker.state === "activated") { + return resolve(); + } + }); + }); + + await new Promise(resolve => { + if (content.navigator.serviceWorker.controller) { + return resolve(); + } + + content.navigator.serviceWorker.addEventListener( + "controllerchange", + resolve, + { once: true } + ); + }); +} + +async function unregisterSW() { + let swr = await content.navigator.serviceWorker.getRegistration(); + swr.unregister(); +} + +add_task(async function test_serivce_worker_interception() { + info("Setting the prefs to having e10s enabled"); + await SpecialPowers.pushPrefEnv({ + set: [ + // Make sure observer and testing function run in the same process + ["dom.ipc.processCount", 1], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ], + }); + + waitForExplicitFinish(); + + info("Open the tab"); + let tab = BrowserTestUtils.addTab(gBrowser, emptyDoc); + let tabBrowser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(tabBrowser); + + info("Open the tab for observing"); + let tab_observer = BrowserTestUtils.addTab(gBrowser, emptyDoc); + let tabBrowser_observer = gBrowser.getBrowserForTab(tab_observer); + await BrowserTestUtils.browserLoaded(tabBrowser_observer); + + let testcases = [ + { + url: helloDoc, + expectedURL: helloDoc, + swPresent: false, + intercepted: false, + fetch: true, + }, + { + url: fakeDoc, + expectedURL: helloDoc, + swPresent: true, + intercepted: true, + fetch: false, // should use HTTP cache + }, + { + // Bypass http cache + url: helloDoc + "?ForBypassingHttpCache=" + Date.now(), + expectedURL: helloDoc, + swPresent: true, + intercepted: false, + fetch: true, + }, + { + // no-cors mode redirect to no-cors mode (trigger internal redirect) + url: crossRedirect + "?url=" + crossHelloDoc + "&mode=no-cors", + expectedURL: crossHelloDoc, + swPresent: true, + redirect: "hello.html", + intercepted: true, + fetch: true, + }, + ]; + + info("Test 1: Verify simple fetch"); + await fetchAndCheckObservers(tabBrowser, tabBrowser_observer, testcases[0]); + + info("Register a service worker"); + await SpecialPowers.spawn(tabBrowser, [sw], registerSWAndWaitForActive); + + info("Test 2: Verify simple hijack"); + await fetchAndCheckObservers(tabBrowser, tabBrowser_observer, testcases[1]); + + info("Test 3: Verify fetch without using http cache"); + await fetchAndCheckObservers(tabBrowser, tabBrowser_observer, testcases[2]); + + info("Test 4: make a internal redirect"); + await fetchAndCheckObservers(tabBrowser, tabBrowser_observer, testcases[3]); + + info("Clean up"); + await SpecialPowers.spawn(tabBrowser, [undefined], unregisterSW); + + gBrowser.removeTab(tab); + gBrowser.removeTab(tab_observer); +}); diff --git a/dom/serviceworkers/test/browser_download.js b/dom/serviceworkers/test/browser_download.js new file mode 100644 index 0000000000..271f217012 --- /dev/null +++ b/dom/serviceworkers/test/browser_download.js @@ -0,0 +1,97 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +var { Downloads } = ChromeUtils.importESModule( + "resource://gre/modules/Downloads.sys.mjs" +); + +var gTestRoot = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + "http://mochi.test:8888/" +); + +function getFile(aFilename) { + if (aFilename.startsWith("file:")) { + var url = NetUtil.newURI(aFilename).QueryInterface(Ci.nsIFileURL); + return url.file.clone(); + } + + var file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + file.initWithPath(aFilename); + return file; +} + +function windowObserver(win, topic) { + if (topic !== "domwindowopened") { + return; + } + + win.addEventListener( + "load", + function() { + if ( + win.document.documentURI === + "chrome://mozapps/content/downloads/unknownContentType.xhtml" + ) { + executeSoon(function() { + let dialog = win.document.getElementById("unknownContentType"); + let button = dialog.getButton("accept"); + button.disabled = false; + dialog.acceptDialog(); + }); + } + }, + { once: true } + ); +} + +function test() { + waitForExplicitFinish(); + + Services.ww.registerNotification(windowObserver); + + SpecialPowers.pushPrefEnv( + { + set: [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.testing.enabled", true], + ], + }, + function() { + var url = gTestRoot + "download/window.html"; + var tab = BrowserTestUtils.addTab(gBrowser); + gBrowser.selectedTab = tab; + + Downloads.getList(Downloads.ALL) + .then(function(downloadList) { + var downloadListener; + + function downloadVerifier(aDownload) { + if (aDownload.succeeded) { + var file = getFile(aDownload.target.path); + ok(file.exists(), "download completed"); + is(file.fileSize, 33, "downloaded file has correct size"); + file.remove(false); + downloadList.remove(aDownload).catch(console.error); + downloadList.removeView(downloadListener).catch(console.error); + gBrowser.removeTab(tab); + Services.ww.unregisterNotification(windowObserver); + + executeSoon(finish); + } + } + + downloadListener = { + onDownloadAdded: downloadVerifier, + onDownloadChanged: downloadVerifier, + }; + + return downloadList.addView(downloadListener); + }) + .then(function() { + BrowserTestUtils.loadURI(gBrowser, url); + }); + } + ); +} diff --git a/dom/serviceworkers/test/browser_download_canceled.js b/dom/serviceworkers/test/browser_download_canceled.js new file mode 100644 index 0000000000..836a581acf --- /dev/null +++ b/dom/serviceworkers/test/browser_download_canceled.js @@ -0,0 +1,176 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * Test cancellation of a download in order to test edge-cases related to + * channel diversion. Channel diversion occurs in cases of file (and PSM cert) + * downloads where we realize in the child that we really want to consume the + * channel data in the parent. For data "sourced" by the parent, like network + * data, data streaming to the child is suspended and the parent waits for the + * child to send back the data it already received, then the channel is resumed. + * For data generated by the child, such as (the current, to be mooted by + * parent-intercept) child-side intercept, the data (currently) stream is + * continually pumped up to the parent. + * + * In particular, we want to reproduce the circumstances of Bug 1418795 where + * the child-side input-stream pump attempts to send data to the parent process + * but the parent has canceled the channel and so the IPC Actor has been torn + * down. Diversion begins once the nsURILoader receives the OnStartRequest + * notification with the headers, so there are two ways to produce + */ + +const { Downloads } = ChromeUtils.importESModule( + "resource://gre/modules/Downloads.sys.mjs" +); + +/** + * Clear the downloads list so other tests don't see our byproducts. + */ +async function clearDownloads() { + const downloads = await Downloads.getList(Downloads.ALL); + downloads.removeFinished(); +} + +/** + * Returns a Promise that will be resolved once the download dialog shows up and + * we have clicked the given button. + */ +function promiseClickDownloadDialogButton(buttonAction) { + const uri = "chrome://mozapps/content/downloads/unknownContentType.xhtml"; + return BrowserTestUtils.promiseAlertDialogOpen(buttonAction, uri, { + async callback(win) { + // nsHelperAppDlg.js currently uses an eval-based setTimeout(0) to invoke + // its postShowCallback that results in a misleading error to the console + // if we close the dialog before it gets a chance to run. Just a + // setTimeout is not sufficient because it appears we get our "load" + // listener before the document's, so we use TestUtils.waitForTick() to + // defer until after its load handler runs, then use setTimeout(0) to end + // up after its eval. + await TestUtils.waitForTick(); + + await new Promise(resolve => setTimeout(resolve, 0)); + + const button = win.document + .getElementById("unknownContentType") + .getButton(buttonAction); + button.disabled = false; + info(`clicking ${buttonAction} button`); + button.click(); + }, + }); +} + +async function performCanceledDownload(tab, path) { + // If we're going to show a modal dialog for this download, then we should + // use it to cancel the download. If not, then we have to let the download + // start and then call into the downloads API ourselves to cancel it. + // We use this promise to signal the cancel being complete in either case. + let cancelledDownload; + + if ( + Services.prefs.getBoolPref( + "browser.download.always_ask_before_handling_new_types", + false + ) + ) { + // Start waiting for the download dialog before triggering the download. + cancelledDownload = promiseClickDownloadDialogButton("cancel"); + // Wait for the cancelation to have been triggered. + info("waiting for download popup"); + } else { + let downloadView; + cancelledDownload = new Promise(resolve => { + downloadView = { + onDownloadAdded(aDownload) { + aDownload.cancel(); + resolve(); + }, + }; + }); + const downloadList = await Downloads.getList(Downloads.ALL); + await downloadList.addView(downloadView); + } + + // Trigger the download. + info(`triggering download of "${path}"`); + /* eslint-disable no-shadow */ + await SpecialPowers.spawn(tab.linkedBrowser, [path], function(path) { + // Put a Promise in place that we can wait on for stream closure. + content.wrappedJSObject.trackStreamClosure(path); + // Create the link and trigger the download. + const link = content.document.createElement("a"); + link.href = path; + link.download = path; + content.document.body.appendChild(link); + link.click(); + }); + /* eslint-enable no-shadow */ + + // Wait for the download to cancel. + await cancelledDownload; + info("cancelled download"); + + // Wait for confirmation that the stream stopped. + info(`wait for the ${path} stream to close.`); + /* eslint-disable no-shadow */ + const why = await SpecialPowers.spawn(tab.linkedBrowser, [path], function( + path + ) { + return content.wrappedJSObject.streamClosed[path].promise; + }); + /* eslint-enable no-shadow */ + is(why.why, "canceled", "Ensure the stream canceled instead of timing out."); + // Note that for the "sw-stream-download" case, we end up with a bogus + // reason of "'close' may only be called on a stream in the 'readable' state." + // Since we aren't actually invoking close(), I'm assuming this is an + // implementation bug that will be corrected in the web platform tests. + info(`Cancellation reason: ${why.message} after ${why.ticks} ticks`); +} + +const gTestRoot = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + "http://mochi.test:8888/" +); + +const PAGE_URL = `${gTestRoot}download_canceled/page_download_canceled.html`; + +add_task(async function interruptedDownloads() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.testing.enabled", true], + ], + }); + + // Open the tab + const tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: PAGE_URL, + }); + + // Wait for it to become controlled. Check that it was a promise that + // resolved as expected rather than undefined by checking the return value. + const controlled = await SpecialPowers.spawn( + tab.linkedBrowser, + [], + function() { + // This is a promise set up by the page during load, and we are post-load. + return content.wrappedJSObject.controlled; + } + ); + is(controlled, "controlled", "page became controlled"); + + // Download a pass-through fetch stream. + await performCanceledDownload(tab, "sw-passthrough-download"); + + // Download a SW-generated stream + await performCanceledDownload(tab, "sw-stream-download"); + + // Cleanup + await SpecialPowers.spawn(tab.linkedBrowser, [], function() { + return content.wrappedJSObject.registration.unregister(); + }); + BrowserTestUtils.removeTab(tab); + await clearDownloads(); +}); diff --git a/dom/serviceworkers/test/browser_force_refresh.js b/dom/serviceworkers/test/browser_force_refresh.js new file mode 100644 index 0000000000..11c3c3f1f7 --- /dev/null +++ b/dom/serviceworkers/test/browser_force_refresh.js @@ -0,0 +1,87 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +var gTestRoot = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + "http://mochi.test:8888/" +); + +async function refresh() { + EventUtils.synthesizeKey("R", { accelKey: true }); +} + +async function forceRefresh() { + EventUtils.synthesizeKey("R", { accelKey: true, shiftKey: true }); +} + +async function done() { + // unregister window actors + ChromeUtils.unregisterWindowActor("ForceRefresh"); + let tab = gBrowser.selectedTab; + let tabBrowser = gBrowser.getBrowserForTab(tab); + await ContentTask.spawn(tabBrowser, null, async function() { + const swr = await content.navigator.serviceWorker.getRegistration(); + await swr.unregister(); + }); + + BrowserTestUtils.removeTab(tab); + executeSoon(finish); +} + +function test() { + waitForExplicitFinish(); + SpecialPowers.pushPrefEnv( + { + set: [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true], + ["browser.cache.disk.enable", false], + ["browser.cache.memory.enable", false], + ], + }, + async function() { + // create ForceRefreseh window actor + const { ForceRefreshParent } = ChromeUtils.importESModule( + getRootDirectory(gTestPath) + "ForceRefreshParent.sys.mjs" + ); + + // setup helper functions for ForceRefreshParent + ForceRefreshParent.SimpleTest = SimpleTest; + ForceRefreshParent.refresh = refresh; + ForceRefreshParent.forceRefresh = forceRefresh; + ForceRefreshParent.done = done; + + // setup window actor options + let windowActorOptions = { + parent: { + esModuleURI: + getRootDirectory(gTestPath) + "ForceRefreshParent.sys.mjs", + }, + child: { + esModuleURI: + getRootDirectory(gTestPath) + "ForceRefreshChild.sys.mjs", + events: { + "base-register": { capture: true, wantUntrusted: true }, + "base-sw-ready": { capture: true, wantUntrusted: true }, + "base-load": { capture: true, wantUntrusted: true }, + "cached-load": { capture: true, wantUntrusted: true }, + "cached-failure": { capture: true, wantUntrusted: true }, + }, + }, + allFrames: true, + }; + + // register ForceRefresh window actors + ChromeUtils.registerWindowActor("ForceRefresh", windowActorOptions); + + // create a new tab and load test url + var url = gTestRoot + "browser_base_force_refresh.html"; + var tab = BrowserTestUtils.addTab(gBrowser); + var tabBrowser = gBrowser.getBrowserForTab(tab); + gBrowser.selectedTab = tab; + BrowserTestUtils.loadURI(gBrowser, url); + } + ); +} diff --git a/dom/serviceworkers/test/browser_head.js b/dom/serviceworkers/test/browser_head.js new file mode 100644 index 0000000000..e1d8049308 --- /dev/null +++ b/dom/serviceworkers/test/browser_head.js @@ -0,0 +1,319 @@ +/** + * This file contains common functionality for ServiceWorker browser tests. + * + * Note that the normal auto-import mechanics for browser mochitests only + * handles "head.js", but we currently store all of our different varieties of + * mochitest in a single directory, which potentially results in a collision + * for similar heuristics for xpcshell. + * + * Many of the storage-related helpers in this file come from: + * https://searchfox.org/mozilla-central/source/dom/localstorage/test/unit/head.js + **/ + +// To use this file, explicitly import it via (including the eslint comment): +// +// /* import-globals-from browser_head.js */ +// Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/dom/serviceworkers/test/browser_head.js", this); + +// Find the current parent directory of the test context we're being loaded into +// such that one can do `${originNoTrailingSlash}/${DIR_PATH}/file_in_dir.foo`. +const DIR_PATH = getRootDirectory(gTestPath) + .replace("chrome://mochitests/content/", "") + .slice(0, -1); + +const SWM = Cc["@mozilla.org/serviceworkers/manager;1"].getService( + Ci.nsIServiceWorkerManager +); + +// The expected minimum usage for an origin that has any Cache API storage in +// use. Currently, the DB uses a page size of 4k and a minimum growth size of +// 32k and has enough tables/indices for this to round up to 64k. +const kMinimumOriginUsageBytes = 65536; + +function getPrincipal(url, attrs) { + const uri = Services.io.newURI(url); + if (!attrs) { + attrs = {}; + } + return Services.scriptSecurityManager.createContentPrincipal(uri, attrs); +} + +async function _qm_requestFinished(request) { + await new Promise(function(resolve) { + request.callback = function() { + resolve(); + }; + }); + + if (request.resultCode !== Cr.NS_OK) { + throw new RequestError(request.resultCode, request.resultName); + } + + return request.result; +} + +async function qm_reset_storage() { + return new Promise(resolve => { + let request = Services.qms.reset(); + request.callback = resolve; + }); +} + +async function get_qm_origin_usage(origin) { + return new Promise(resolve => { + const principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin( + origin + ); + Services.qms.getUsageForPrincipal(principal, request => + resolve(request.result.usage) + ); + }); +} + +/** + * Clear the group associated with the given origin via nsIClearDataService. We + * are using nsIClearDataService here because nsIQuotaManagerService doesn't + * (directly) provide a means of clearing a group. + */ +async function clear_qm_origin_group_via_clearData(origin) { + const uri = Services.io.newURI(origin); + const baseDomain = Services.eTLD.getBaseDomain(uri); + info(`Clearing storage on domain ${baseDomain} (from origin ${origin})`); + + // Initiate group clearing and wait for it. + await new Promise((resolve, reject) => { + Services.clearData.deleteDataFromBaseDomain( + baseDomain, + false, + Services.clearData.CLEAR_DOM_QUOTA, + failedFlags => { + if (failedFlags) { + reject(failedFlags); + } else { + resolve(); + } + } + ); + }); +} + +/** + * Look up the nsIServiceWorkerRegistrationInfo for a given SW descriptor. + */ +function swm_lookup_reg(swDesc) { + // Scopes always include the full origin. + const fullScope = `${swDesc.origin}/${DIR_PATH}/${swDesc.scope}`; + const principal = getPrincipal(fullScope); + + const reg = SWM.getRegistrationByPrincipal(principal, fullScope); + + return reg; +} + +/** + * Install a ServiceWorker according to the provided descriptor by opening a + * fresh tab that will be closed when we are done. Returns the + * `nsIServiceWorkerRegistrationInfo` corresponding to the registration. + * + * The descriptor may have the following properties: + * - scope: Optional. + * - script: The script, which usually just wants to be a relative path. + * - origin: Requred, the origin (which should not include a trailing slash). + */ +async function install_sw(swDesc) { + info( + `Installing ServiceWorker ${swDesc.script} at ${swDesc.scope} on origin ${swDesc.origin}` + ); + const pageUrlStr = `${swDesc.origin}/${DIR_PATH}/empty_with_utils.html`; + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: pageUrlStr, + }, + async browser => { + await SpecialPowers.spawn( + browser, + [{ swScript: swDesc.script, swScope: swDesc.scope }], + async function({ swScript, swScope }) { + await content.wrappedJSObject.registerAndWaitForActive( + swScript, + swScope + ); + } + ); + } + ); + info(`ServiceWorker installed`); + + return swm_lookup_reg(swDesc); +} + +/** + * Consume storage in the given origin by storing randomly generated Blobs into + * Cache API storage and IndexedDB storage. We use both APIs in order to + * ensure that data clearing wipes both QM clients. + * + * Randomly generated Blobs means Blobs with literally random content. This is + * done to compensate for the Cache API using snappy for compression. + */ +async function consume_storage(origin, storageDesc) { + info(`Consuming storage on origin ${origin}`); + const pageUrlStr = `${origin}/${DIR_PATH}/empty_with_utils.html`; + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: pageUrlStr, + }, + async browser => { + await SpecialPowers.spawn(browser, [storageDesc], async function({ + cacheBytes, + idbBytes, + }) { + await content.wrappedJSObject.fillStorage(cacheBytes, idbBytes); + }); + } + ); +} + +// Check if the origin is effectively empty, but allowing for the minimum size +// Cache API database to be present. +function is_minimum_origin_usage(originUsageBytes) { + return originUsageBytes <= kMinimumOriginUsageBytes; +} + +/** + * Perform a navigation, waiting until the navigation stops, then returning + * the `textContent` of the body node. The expectation is this will be used + * with ServiceWorkers that return a body that indicates the ServiceWorker + * provided the result (possibly derived from the request) versus if + * interception didn't happen. + */ +async function navigate_and_get_body(swDesc, debugTag) { + let pageUrlStr = `${swDesc.origin}/${DIR_PATH}/${swDesc.scope}`; + if (debugTag) { + pageUrlStr += "?" + debugTag; + } + info(`Navigating to ${pageUrlStr}`); + + const tabResult = await BrowserTestUtils.withNewTab( + { + gBrowser, + url: pageUrlStr, + // In the event of an aborted navigation, the load event will never + // happen... + waitForLoad: false, + // ...but the stop will. + waitForStateStop: true, + }, + async browser => { + info(` Tab opened, querying body content.`); + const spawnResult = await SpecialPowers.spawn(browser, [], function() { + const controlled = !!content.navigator.serviceWorker.controller; + // Special-case about: URL's. + let loc = content.document.documentURI; + if (loc.startsWith("about:")) { + // about:neterror is parameterized by query string, so truncate that + // off because our tests just care if we're seeing the neterror page. + const idxQuestion = loc.indexOf("?"); + if (idxQuestion !== -1) { + loc = loc.substring(0, idxQuestion); + } + return { controlled, body: loc }; + } + return { + controlled, + body: content.document?.body?.textContent?.trim(), + }; + }); + + return spawnResult; + } + ); + + return tabResult; +} + +function waitForIframeLoad(iframe) { + return new Promise(function(resolve) { + iframe.onload = resolve; + }); +} + +function waitForRegister(scope, callback) { + return new Promise(function(resolve) { + let listener = { + onRegister(registration) { + if (registration.scope !== scope) { + return; + } + SWM.removeListener(listener); + resolve(callback ? callback(registration) : registration); + }, + }; + SWM.addListener(listener); + }); +} + +function waitForUnregister(scope) { + return new Promise(function(resolve) { + let listener = { + onUnregister(registration) { + if (registration.scope !== scope) { + return; + } + SWM.removeListener(listener); + resolve(registration); + }, + }; + SWM.addListener(listener); + }); +} + +// Be careful using this helper function, please make sure QuotaUsageCheck must +// happen, otherwise test would be stucked in this function. +function waitForQuotaUsageCheckFinish(scope) { + return new Promise(function(resolve) { + let listener = { + onQuotaUsageCheckFinish(registration) { + if (registration.scope !== scope) { + return; + } + SWM.removeListener(listener); + resolve(registration); + }, + }; + SWM.addListener(listener); + }); +} + +function waitForServiceWorkerRegistrationChange(registration, callback) { + return new Promise(function(resolve) { + let listener = { + onChange() { + registration.removeListener(listener); + if (callback) { + callback(); + } + resolve(callback ? callback() : undefined); + }, + }; + registration.addListener(listener); + }); +} + +function waitForServiceWorkerShutdown() { + return new Promise(function(resolve) { + let observer = { + observe(subject, topic, data) { + if (topic !== "service-worker-shutdown") { + return; + } + SpecialPowers.removeObserver(observer, "service-worker-shutdown"); + resolve(); + }, + }; + SpecialPowers.addObserver(observer, "service-worker-shutdown"); + }); +} diff --git a/dom/serviceworkers/test/browser_intercepted_channel_process_swap.js b/dom/serviceworkers/test/browser_intercepted_channel_process_swap.js new file mode 100644 index 0000000000..f4722e91c0 --- /dev/null +++ b/dom/serviceworkers/test/browser_intercepted_channel_process_swap.js @@ -0,0 +1,110 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that navigation loads through intercepted channels result in the +// appropriate process swaps. This appears to only be possible when navigating +// to a cross-origin URL, where that navigation is controlled by a ServiceWorker. + +"use strict"; + +const SAME_ORIGIN = "https://example.com"; +const CROSS_ORIGIN = "https://example.org"; + +const SAME_ORIGIN_ROOT = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + SAME_ORIGIN +); +const CROSS_ORIGIN_ROOT = SAME_ORIGIN_ROOT.replace(SAME_ORIGIN, CROSS_ORIGIN); + +const SW_REGISTER_URL = `${CROSS_ORIGIN_ROOT}empty_with_utils.html`; +const SW_SCRIPT_URL = `${CROSS_ORIGIN_ROOT}intercepted_channel_process_swap_worker.js`; +const URL_BEFORE_NAVIGATION = `${SAME_ORIGIN_ROOT}empty.html`; +const CROSS_ORIGIN_URL = `${CROSS_ORIGIN_ROOT}empty.html`; + +const TESTCASES = [ + { + url: CROSS_ORIGIN_URL, + description: + "Controlled cross-origin navigation with network-provided response", + }, + { + url: `${CROSS_ORIGIN_ROOT}this-path-does-not-exist?respondWith=${CROSS_ORIGIN_URL}`, + description: + "Controlled cross-origin navigation with ServiceWorker-provided response", + }, +]; + +async function navigateTab(aTab, aUrl) { + BrowserTestUtils.loadURI(aTab.linkedBrowser, aUrl); + + await BrowserTestUtils.waitForLocationChange(gBrowser, aUrl).then(() => + BrowserTestUtils.browserStopped(aTab.linkedBrowser) + ); +} + +async function runTestcase(aTab, aTestcase) { + info(`Testing ${aTestcase.description}`); + + await navigateTab(aTab, URL_BEFORE_NAVIGATION); + + const [initialPid] = E10SUtils.getBrowserPids(aTab.linkedBrowser); + + await navigateTab(aTab, aTestcase.url); + + const [finalPid] = E10SUtils.getBrowserPids(aTab.linkedBrowser); + + await SpecialPowers.spawn(aTab.linkedBrowser, [], () => { + Assert.ok( + content.navigator.serviceWorker.controller, + `${content.location} should be controlled.` + ); + }); + + Assert.notEqual( + initialPid, + finalPid, + `Navigating from ${URL_BEFORE_NAVIGATION} to ${aTab.linkedBrowser.currentURI.spec} should have resulted in a different PID.` + ); +} + +add_task(async function setupPrefs() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ], + }); +}); + +add_task(async function setupBrowser() { + const tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: SW_REGISTER_URL, + }); + + await SpecialPowers.spawn( + tab.linkedBrowser, + [SW_SCRIPT_URL], + async scriptUrl => { + await content.wrappedJSObject.registerAndWaitForActive(scriptUrl); + } + ); +}); + +add_task(async function runTestcases() { + for (const testcase of TESTCASES) { + await runTestcase(gBrowser.selectedTab, testcase); + } +}); + +add_task(async function cleanup() { + const tab = gBrowser.selectedTab; + + await navigateTab(tab, SW_REGISTER_URL); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.unregisterAll(); + }); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/dom/serviceworkers/test/browser_intercepted_worker_script.js b/dom/serviceworkers/test/browser_intercepted_worker_script.js new file mode 100644 index 0000000000..2e0542df07 --- /dev/null +++ b/dom/serviceworkers/test/browser_intercepted_worker_script.js @@ -0,0 +1,102 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test tests if the service worker is able to intercept the script loading + * channel of a dedicated worker. + * + * On success, the test will not crash. + */ + +const SAME_ORIGIN = "https://example.com"; + +const SAME_ORIGIN_ROOT = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + SAME_ORIGIN +); + +const SW_REGISTER_URL = `${SAME_ORIGIN_ROOT}empty_with_utils.html`; +const SW_SCRIPT_URL = `${SAME_ORIGIN_ROOT}simple_fetch_worker.js`; +const SCRIPT_URL = `${SAME_ORIGIN_ROOT}empty.js`; + +async function navigateTab(aTab, aUrl) { + BrowserTestUtils.loadURI(aTab.linkedBrowser, aUrl); + + await BrowserTestUtils.waitForLocationChange(gBrowser, aUrl).then(() => + BrowserTestUtils.browserStopped(aTab.linkedBrowser) + ); +} + +async function runTest(aTestSharedWorker) { + const tab = gBrowser.selectedTab; + + await navigateTab(tab, SW_REGISTER_URL); + + await SpecialPowers.spawn( + tab.linkedBrowser, + [SCRIPT_URL, aTestSharedWorker], + async (scriptUrl, testSharedWorker) => { + await new Promise(resolve => { + content.navigator.serviceWorker.onmessage = e => { + if (e.data == scriptUrl) { + resolve(); + } + }; + + if (testSharedWorker) { + let worker = new content.Worker(scriptUrl); + } else { + let worker = new content.SharedWorker(scriptUrl); + } + }); + } + ); + + ok(true, "The service worker has intercepted the script loading."); +} + +add_task(async function setupPrefs() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + [ + "network.cookie.cookieBehavior", + Ci.nsICookieService.BEHAVIOR_REJECT_TRACKER_AND_PARTITION_FOREIGN, + ], + ], + }); +}); + +add_task(async function setupBrowser() { + // The tab will be used by subsequent test steps via 'gBrowser.selectedTab'. + const tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: SW_REGISTER_URL, + }); + + registerCleanupFunction(async _ => { + await navigateTab(tab, SW_REGISTER_URL); + + await SpecialPowers.spawn(tab.linkedBrowser, [], async () => { + await content.wrappedJSObject.unregisterAll(); + }); + + BrowserTestUtils.removeTab(tab); + }); + + await SpecialPowers.spawn( + tab.linkedBrowser, + [SW_SCRIPT_URL], + async scriptUrl => { + await content.wrappedJSObject.registerAndWaitForActive(scriptUrl); + } + ); +}); + +add_task(async function runTests() { + await runTest(false); + await runTest(true); +}); diff --git a/dom/serviceworkers/test/browser_navigationPreload_read_after_respondWith.js b/dom/serviceworkers/test/browser_navigationPreload_read_after_respondWith.js new file mode 100644 index 0000000000..0e8a9eee20 --- /dev/null +++ b/dom/serviceworkers/test/browser_navigationPreload_read_after_respondWith.js @@ -0,0 +1,115 @@ +const TOP_DOMAIN = "http://mochi.test:8888/"; +const SW_DOMAIN = "https://example.org/"; + +const TOP_TEST_ROOT = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + TOP_DOMAIN +); +const SW_TEST_ROOT = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + SW_DOMAIN +); + +const TOP_EMPTY_PAGE = `${TOP_TEST_ROOT}empty_with_utils.html`; +const SW_REGISTER_PAGE = `${SW_TEST_ROOT}empty_with_utils.html`; +const SW_IFRAME_PAGE = `${SW_TEST_ROOT}navigationPreload_page.html`; +// An empty script suffices for our SW needs; it's by definition no-fetch. +const SW_REL_SW_SCRIPT = "sw_with_navigationPreload.js"; + +/** + * Test the FetchEvent.preloadResponse can be read after FetchEvent.respondWith() + * + * Step 1. register a ServiceWorker which only handles FetchEvent when request + * url includes navigationPreload_page.html. Otherwise, it alwasy + * fallbacks the fetch to the network. + * If the request url includes navigationPreload_page.html, it call + * FetchEvent.respondWith() with a new Resposne, and then call + * FetchEvent.waitUtil() to wait FetchEvent.preloadResponse and post the + * preloadResponse's text to clients. + * Step 2. Open a controlled page and register message event handler to receive + * the postMessage from ServiceWorker. + * Step 3. Create a iframe which url is navigationPreload_page.html, such that + * ServiceWorker can fake the response and then send preloadResponse's + * result. + * Step 4. Unregister the ServiceWorker and cleanup the environment. + */ +add_task(async function() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ], + }); + + // Step 1. + info("Opening a new tab: " + SW_REGISTER_PAGE); + let topTab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: SW_REGISTER_PAGE, + }); + + // ## Install SW + await SpecialPowers.spawn( + topTab.linkedBrowser, + [{ sw: SW_REL_SW_SCRIPT }], + async function({ sw }) { + // Waive the xray to use the content utils.js script functions. + dump(`register serviceworker...\n`); + await content.wrappedJSObject.registerAndWaitForActive(sw); + } + ); + + // Step 2. + info("Loading a controlled page: " + SW_REGISTER_PAGE); + let browserLoadedPromise = BrowserTestUtils.browserLoaded( + topTab.linkedBrowser + ); + BrowserTestUtils.loadURI(topTab.linkedBrowser, SW_REGISTER_PAGE); + await browserLoadedPromise; + + info("Create a target iframe: " + SW_IFRAME_PAGE); + let result = await SpecialPowers.spawn( + topTab.linkedBrowser, + [{ url: SW_IFRAME_PAGE }], + async function({ url }) { + async function waitForNavigationPreload() { + return new Promise(resolve => { + content.wrappedJSObject.navigator.serviceWorker.addEventListener( + `message`, + event => { + resolve(event.data); + } + ); + }); + } + + let promise = waitForNavigationPreload(); + + // Step 3. + const iframe = content.wrappedJSObject.document.createElement("iframe"); + iframe.src = url; + content.wrappedJSObject.document.body.appendChild(iframe); + await new Promise(r => { + iframe.onload = r; + }); + + let result = await promise; + return result; + } + ); + + is(result, "NavigationPreload\n", "Should get NavigationPreload result"); + + // Step 4. + info("Loading the SW unregister page: " + SW_REGISTER_PAGE); + browserLoadedPromise = BrowserTestUtils.browserLoaded(topTab.linkedBrowser); + BrowserTestUtils.loadURI(topTab.linkedBrowser, SW_REGISTER_PAGE); + await browserLoadedPromise; + + await SpecialPowers.spawn(topTab.linkedBrowser, [], async function() { + await content.wrappedJSObject.unregisterAll(); + }); + + // Close the testing tab. + BrowserTestUtils.removeTab(topTab); +}); diff --git a/dom/serviceworkers/test/browser_navigation_fetch_fault_handling.js b/dom/serviceworkers/test/browser_navigation_fetch_fault_handling.js new file mode 100644 index 0000000000..6655c52a62 --- /dev/null +++ b/dom/serviceworkers/test/browser_navigation_fetch_fault_handling.js @@ -0,0 +1,272 @@ +/** + * This test file tests our automatic recovery and any related mitigating + * heuristics that occur during intercepted navigation fetch request. + * Specifically, we should be resetting interception so that we go to the + * network in these cases and then potentially taking actions like unregistering + * the ServiceWorker and/or clearing QuotaManager-managed storage for the + * origin. + * + * See specific test permutations for specific details inline in the test. + * + * NOTE THAT CURRENTLY THIS TEST IS DISCUSSING MITIGATIONS THAT ARE NOT YET + * IMPLEMENTED, JUST PLANNED. These will be iterated on and added to the rest + * of the stack of patches on Bug 1503072. + * + * ## Test Mechanics + * + * ### Fetch Fault Injection + * + * We expose: + * - On nsIServiceWorkerInfo, the per-ServiceWorker XPCOM interface: + * - A mechanism for creating synthetic faults by setting the + * `nsIServiceWorkerInfo::testingInjectCancellation` attribute to a failing + * nsresult. The fault is applied at the beginning of the steps to dispatch + * the fetch event on the global. + * - A count of the number of times we experienced these navigation faults + * that had to be reset as `nsIServiceWorkerInfo::navigationFaultCount`. + * (This would also include real faults, but we only expect to see synthetic + * faults in this test.) + * - On nsIServiceWorkerRegistrationInfo, the per-registration XPCOM interface: + * - A readonly attribute that indicates how many times an origin storage + * usage check has been initiated. + * + * We also use: + * - `nsIServiceWorkerManager::addListener(nsIServiceWorkerManagerListener)` + * allows our test to listen for the unregistration of registrations. This + * allows us to be notified when unregistering or origin-clearing actions have + * been taken as a mitigation. + * + * ### General Test Approach + * + * For each test we: + * - Ensure/confirm the testing origin has no QuotaManager storage in use. + * - Install the ServiceWorker. + * - If we are testing the situation where we want to simulate the origin being + * near its quota limit, we also generate Cache API and IDB storage usage + * sufficient to put our origin over the threshold. + * - We run a quota check on the origin after doing this in order to make sure + * that we did this correctly and that we properly constrained the limit for + * the origin. We fail the test for test implementation reasons if we + * didn't accomplish this. + * - Verify a fetch navigation to the SW works without any fault injection, + * producing a result produced by the ServiceWorker. + * - Begin fault permutations in a loop, where for each pass of the loop: + * - We trigger a navigation which will result in an intercepted fetch + * which will fault. We wait until the navigation completes. + * - We verify that we got the request from the network. + * - We verify that the ServiceWorker's navigationFaultCount increased. + * - If this the count at which we expect a mitigation to take place, we wait + * for the registration to become unregistered AND: + * - We check whether the storage for the origin was cleared or not, which + * indicates which mitigation of the following happened: + * - Unregister the registration directly. + * - Clear the origin's data which will also unregister the registration + * as a side effect. + * - We check whether the registration indicates an origin quota check + * happened or not. + * + * ### Disk Usage Limits + * + * In order to avoid gratuitous disk I/O and related overheads, we limit QM + * ("temporary") storage to 10 MiB which ends up limiting group usage to 10 MiB. + * This lets us set a threshold situation where we claim that a SW needs at + * least 4 MiB of storage for installation/operation, meaning that any usage + * beyond 6 MiB in the group will constitute a need to clear the group or + * origin. We fill with the storage with 8 MiB of artificial usage to this end, + * storing 4 MiB in Cache API and 4 MiB in IDB. + **/ + +// Because of the amount of I/O involved in this test, pernosco reproductions +// may experience timeouts without a timeout multiplier. +requestLongerTimeout(2); + +/* import-globals-from browser_head.js */ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/dom/serviceworkers/test/browser_head.js", + this +); + +// The origin we run the tests on. +const TEST_ORIGIN = "https://test1.example.org"; +// An origin in the same group that impacts the usage of the TEST_ORIGIN. Used +// to verify heuristics related to group-clearing (where clearing the +// TEST_ORIGIN itself would not be sufficient for us to mitigate quota limits +// being reached.) +const SAME_GROUP_ORIGIN = "https://test2.example.org"; + +const TEST_SW_SETUP = { + origin: TEST_ORIGIN, + // Page with a body textContent of "NETWORK" and has utils.js loaded. + scope: "network_with_utils.html", + // SW that serves a body with a textContent of "SERVICEWORKER" and + // has utils.js loaded. + script: "sw_respondwith_serviceworker.js", +}; + +const TEST_STORAGE_SETUP = { + cacheBytes: 4 * 1024 * 1024, // 4 MiB + idbBytes: 4 * 1024 * 1024, // 4 MiB +}; + +const FAULTS_BEFORE_MITIGATION = 3; + +/** + * Core test iteration logic. + * + * Parameters: + * - name: Human readable name of the fault we're injecting. + * - useError: The nsresult failure code to inject into fetch. + * - errorPage: The "about" page that we expect errors to leave us on. + * - consumeQuotaOrigin: If truthy, the origin to place the storage usage in. + * If falsey, we won't fill storage. + */ +async function do_fault_injection_test({ + name, + useError, + errorPage, + consumeQuotaOrigin, +}) { + info( + `### testing: error: ${name} (${useError}) consumeQuotaOrigin: ${consumeQuotaOrigin}` + ); + + // ## Ensure/confirm the testing origins have no QuotaManager storage in use. + await clear_qm_origin_group_via_clearData(TEST_ORIGIN); + + // ## Install the ServiceWorker + const reg = await install_sw(TEST_SW_SETUP); + const sw = reg.activeWorker; + + // ## Generate quota usage if appropriate + if (consumeQuotaOrigin) { + await consume_storage(consumeQuotaOrigin, TEST_STORAGE_SETUP); + } + + // ## Verify normal navigation is served by the SW. + info(`## Checking normal operation.`); + { + const debugTag = `err=${name}&fault=0`; + const docInfo = await navigate_and_get_body(TEST_SW_SETUP, debugTag); + is( + docInfo.body, + "SERVICEWORKER", + "navigation without injected fault originates from ServiceWorker" + ); + + is( + docInfo.controlled, + true, + "successfully intercepted navigation should be controlled" + ); + } + + // Make sure the test is listening on the ServiceWorker unregistration, since + // we expect it happens after navigation fault threshold reached. + const unregisteredPromise = waitForUnregister(reg.scope); + + // Make sure the test is listening on the finish of quota checking, since we + // expect it happens after navigation fault threshold reached. + const quotaUsageCheckFinishPromise = waitForQuotaUsageCheckFinish(reg.scope); + + // ## Inject faults in a loop until expected mitigation. + sw.testingInjectCancellation = useError; + for (let iFault = 0; iFault < FAULTS_BEFORE_MITIGATION; iFault++) { + info(`## Testing with injected fault number ${iFault + 1}`); + // We should never have triggered an origin quota usage check before the + // final fault injection. + is(reg.quotaUsageCheckCount, 0, "No quota usage check yet"); + + // Make sure our loads encode the specific + const debugTag = `err=${name}&fault=${iFault + 1}`; + + const docInfo = await navigate_and_get_body(TEST_SW_SETUP, debugTag); + // We should always be receiving network fallback. + is( + docInfo.body, + "NETWORK", + "navigation with injected fault originates from network" + ); + + is(docInfo.controlled, false, "bypassed pages shouldn't be controlled"); + + // The fault count should have increased + is( + sw.navigationFaultCount, + iFault + 1, + "navigation fault increased (to expected value)" + ); + } + + await unregisteredPromise; + is(reg.unregistered, true, "registration should be unregistered"); + + //is(reg.quotaUsageCheckCount, 1, "Quota usage check must be started"); + await quotaUsageCheckFinishPromise; + + if (consumeQuotaOrigin) { + // Check that there is no longer any storage usaged by the origin in this + // case. + const originUsage = await get_qm_origin_usage(TEST_ORIGIN); + ok( + is_minimum_origin_usage(originUsage), + "origin usage should be mitigated" + ); + + if (consumeQuotaOrigin === SAME_GROUP_ORIGIN) { + const sameGroupUsage = await get_qm_origin_usage(SAME_GROUP_ORIGIN); + ok(sameGroupUsage === 0, "same group usage should be mitigated"); + } + } +} + +add_task(async function test_navigation_fetch_fault_handling() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.serviceWorkers.mitigations.bypass_on_fault", true], + ["dom.serviceWorkers.mitigations.group_usage_headroom_kb", 5 * 1024], + ["dom.quotaManager.testing", true], + // We want the temporary global limit to be 10 MiB (the pref is in KiB). + // This will result in the group limit also being 10 MiB because on small + // disks we provide a group limit value of min(10 MiB, global limit). + ["dom.quotaManager.temporaryStorage.fixedLimit", 10 * 1024], + ], + }); + + // Need to reset the storages to make dom.quotaManager.temporaryStorage.fixedLimit + // works. + await qm_reset_storage(); + + const quotaOriginVariations = [ + // Don't put us near the storage limit. + undefined, + // Put us near the storage limit in the SW origin itself. + TEST_ORIGIN, + // Put us near the storage limit in the SW origin's group but not the origin + // itself. + SAME_GROUP_ORIGIN, + ]; + + for (const consumeQuotaOrigin of quotaOriginVariations) { + await do_fault_injection_test({ + name: "NS_ERROR_DOM_ABORT_ERR", + useError: 0x80530014, // Not in `Cr`. + // Abort errors manifest as about:blank pages. + errorPage: "about:blank", + consumeQuotaOrigin, + }); + + await do_fault_injection_test({ + name: "NS_ERROR_INTERCEPTION_FAILED", + useError: 0x804b0064, // Not in `Cr`. + // Interception failures manifest as corrupt content pages. + errorPage: "about:neterror", + consumeQuotaOrigin, + }); + } + + // Cleanup: wipe the origin and group so all the ServiceWorkers go away. + await clear_qm_origin_group_via_clearData(TEST_ORIGIN); +}); diff --git a/dom/serviceworkers/test/browser_remote_type_process_swap.js b/dom/serviceworkers/test/browser_remote_type_process_swap.js new file mode 100644 index 0000000000..60c8078e0d --- /dev/null +++ b/dom/serviceworkers/test/browser_remote_type_process_swap.js @@ -0,0 +1,138 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test tests a navigation request to a Service Worker-controlled origin & + * scope that results in a cross-origin redirect to a + * non-Service Worker-controlled scope which additionally participates in + * cross-process redirect. + * + * On success, the test will not crash. + */ + +const ORIGIN = "http://mochi.test:8888"; +const TEST_ROOT = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content", + ORIGIN +); + +const SW_REGISTER_PAGE_URL = `${TEST_ROOT}empty_with_utils.html`; +const SW_SCRIPT_URL = `${TEST_ROOT}empty.js`; + +const FILE_URL = (() => { + // Get the file as an nsIFile. + const file = getChromeDir(getResolvedURI(gTestPath)); + file.append("empty.html"); + + // Convert the nsIFile to an nsIURI to access the path. + return Services.io.newFileURI(file).spec; +})(); + +const CROSS_ORIGIN = "https://example.com"; +const CROSS_ORIGIN_URL = SW_REGISTER_PAGE_URL.replace(ORIGIN, CROSS_ORIGIN); +const CROSS_ORIGIN_REDIRECT_URL = `${TEST_ROOT}redirect.sjs?${CROSS_ORIGIN_URL}`; + +async function loadURI(aXULBrowser, aURI) { + const browserLoadedPromise = BrowserTestUtils.browserLoaded(aXULBrowser); + BrowserTestUtils.loadURI(aXULBrowser, aURI); + + return browserLoadedPromise; +} + +async function runTest() { + // Step 1: register a Service Worker under `ORIGIN` so that all subsequent + // requests to `ORIGIN` will be marked as controlled. + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.testing.enabled", true], + ["devtools.console.stdout.content", true], + ], + }); + + info(`Loading tab with page ${SW_REGISTER_PAGE_URL}`); + const tab = await BrowserTestUtils.openNewForegroundTab({ + gBrowser, + opening: SW_REGISTER_PAGE_URL, + }); + info(`Loaded page ${SW_REGISTER_PAGE_URL}`); + + info(`Registering Service Worker ${SW_SCRIPT_URL}`); + await SpecialPowers.spawn( + tab.linkedBrowser, + [{ scriptURL: SW_SCRIPT_URL }], + async ({ scriptURL }) => { + await content.wrappedJSObject.registerAndWaitForActive(scriptURL); + } + ); + info(`Registered and activated Service Worker ${SW_SCRIPT_URL}`); + + // Step 2: open a page over file:// and navigate to trigger a process swap + // for the response. + info(`Loading ${FILE_URL}`); + await loadURI(tab.linkedBrowser, FILE_URL); + + Assert.equal( + tab.linkedBrowser.remoteType, + E10SUtils.FILE_REMOTE_TYPE, + `${FILE_URL} should load in a file process` + ); + + info(`Dynamically creating ${FILE_URL}'s link`); + await SpecialPowers.spawn( + tab.linkedBrowser, + [{ href: CROSS_ORIGIN_REDIRECT_URL }], + ({ href }) => { + const { document } = content; + const link = document.createElement("a"); + link.href = href; + link.id = "link"; + link.appendChild(document.createTextNode(href)); + document.body.appendChild(link); + } + ); + + const redirectPromise = BrowserTestUtils.waitForLocationChange( + gBrowser, + CROSS_ORIGIN_URL + ); + + info("Starting navigation"); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#link", + {}, + tab.linkedBrowser + ); + + info(`Waiting for location to change to ${CROSS_ORIGIN_URL}`); + await redirectPromise; + + info("Waiting for the browser to stop"); + await BrowserTestUtils.browserStopped(tab.linkedBrowser); + + if (SpecialPowers.useRemoteSubframes) { + Assert.ok( + E10SUtils.isWebRemoteType(tab.linkedBrowser.remoteType), + `${CROSS_ORIGIN_URL} should load in a web-content process` + ); + } + + // Step 3: cleanup. + info("Loading initial page to unregister all Service Workers"); + await loadURI(tab.linkedBrowser, SW_REGISTER_PAGE_URL); + + info("Unregistering all Service Workers"); + await SpecialPowers.spawn( + tab.linkedBrowser, + [], + async () => await content.wrappedJSObject.unregisterAll() + ); + + info("Closing tab"); + BrowserTestUtils.removeTab(tab); +} + +add_task(runTest); diff --git a/dom/serviceworkers/test/browser_storage_permission.js b/dom/serviceworkers/test/browser_storage_permission.js new file mode 100644 index 0000000000..68c2c18bad --- /dev/null +++ b/dom/serviceworkers/test/browser_storage_permission.js @@ -0,0 +1,297 @@ +"use strict"; + +const { PermissionTestUtils } = ChromeUtils.import( + "resource://testing-common/PermissionTestUtils.jsm" +); + +const BASE_URI = "http://mochi.test:8888/browser/dom/serviceworkers/test/"; +const PAGE_URI = BASE_URI + "empty.html"; +const SCOPE = PAGE_URI + "?storage_permission"; +const SW_SCRIPT = BASE_URI + "empty.js"; + +add_setup(async function() { + await SpecialPowers.pushPrefEnv({ + set: [ + // Until the e10s refactor is complete, use a single process to avoid + // service worker propagation race. + ["dom.ipc.processCount", 1], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ], + }); + + let tab = BrowserTestUtils.addTab(gBrowser, PAGE_URI); + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + await SpecialPowers.spawn( + browser, + [{ script: SW_SCRIPT, scope: SCOPE }], + async function(opts) { + let reg = await content.navigator.serviceWorker.register(opts.script, { + scope: opts.scope, + }); + let worker = reg.installing || reg.waiting || reg.active; + await new Promise(resolve => { + if (worker.state === "activated") { + resolve(); + return; + } + worker.addEventListener("statechange", function onStateChange() { + if (worker.state === "activated") { + worker.removeEventListener("statechange", onStateChange); + resolve(); + } + }); + }); + } + ); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_allow_permission() { + PermissionTestUtils.add( + PAGE_URI, + "cookie", + Ci.nsICookiePermission.ACCESS_ALLOW + ); + + let tab = BrowserTestUtils.addTab(gBrowser, SCOPE); + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + let controller = await SpecialPowers.spawn(browser, [], async function() { + return content.navigator.serviceWorker.controller; + }); + + ok(!!controller, "page should be controlled with storage allowed"); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_deny_permission() { + PermissionTestUtils.add( + PAGE_URI, + "cookie", + Ci.nsICookiePermission.ACCESS_DENY + ); + + let tab = BrowserTestUtils.addTab(gBrowser, SCOPE); + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + let controller = await SpecialPowers.spawn(browser, [], async function() { + return content.navigator.serviceWorker.controller; + }); + + is(controller, null, "page should be not controlled with storage denied"); + + BrowserTestUtils.removeTab(tab); + PermissionTestUtils.remove(PAGE_URI, "cookie"); +}); + +add_task(async function test_session_permission() { + PermissionTestUtils.add( + PAGE_URI, + "cookie", + Ci.nsICookiePermission.ACCESS_SESSION + ); + + let tab = BrowserTestUtils.addTab(gBrowser, SCOPE); + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + let controller = await SpecialPowers.spawn(browser, [], async function() { + return content.navigator.serviceWorker.controller; + }); + + is(controller, null, "page should be not controlled with session storage"); + + BrowserTestUtils.removeTab(tab); + PermissionTestUtils.remove(PAGE_URI, "cookie"); +}); + +// Test to verify an about:blank iframe successfully inherits the +// parent's controller when storage is blocked between opening the +// parent page and creating the iframe. +add_task(async function test_block_storage_before_blank_iframe() { + PermissionTestUtils.remove(PAGE_URI, "cookie"); + + let tab = BrowserTestUtils.addTab(gBrowser, SCOPE); + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + let controller = await SpecialPowers.spawn(browser, [], async function() { + return content.navigator.serviceWorker.controller; + }); + + ok(!!controller, "page should be controlled with storage allowed"); + + let controller2 = await SpecialPowers.spawn(browser, [], async function() { + let f = content.document.createElement("iframe"); + content.document.body.appendChild(f); + await new Promise(resolve => (f.onload = resolve)); + return !!f.contentWindow.navigator.serviceWorker.controller; + }); + + ok(!!controller2, "page should be controlled with storage allowed"); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["network.cookie.cookieBehavior", Ci.nsICookieService.BEHAVIOR_REJECT], + ], + }); + + let controller3 = await SpecialPowers.spawn(browser, [], async function() { + let f = content.document.createElement("iframe"); + content.document.body.appendChild(f); + await new Promise(resolve => (f.onload = resolve)); + return !!f.contentWindow.navigator.serviceWorker.controller; + }); + + ok(!!controller3, "page should be controlled with storage allowed"); + + await SpecialPowers.popPrefEnv(); + BrowserTestUtils.removeTab(tab); +}); + +// Test to verify a blob URL iframe successfully inherits the +// parent's controller when storage is blocked between opening the +// parent page and creating the iframe. +add_task(async function test_block_storage_before_blob_iframe() { + PermissionTestUtils.remove(PAGE_URI, "cookie"); + + let tab = BrowserTestUtils.addTab(gBrowser, SCOPE); + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + let controller = await SpecialPowers.spawn(browser, [], async function() { + return content.navigator.serviceWorker.controller; + }); + + ok(!!controller, "page should be controlled with storage allowed"); + + let controller2 = await SpecialPowers.spawn(browser, [], async function() { + let b = new content.Blob(["<!DOCTYPE html><html></html>"], { + type: "text/html", + }); + let f = content.document.createElement("iframe"); + // No need to call revokeObjectURL() since the window will be closed shortly. + f.src = content.URL.createObjectURL(b); + content.document.body.appendChild(f); + await new Promise(resolve => (f.onload = resolve)); + return !!f.contentWindow.navigator.serviceWorker.controller; + }); + + ok(!!controller2, "page should be controlled with storage allowed"); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["network.cookie.cookieBehavior", Ci.nsICookieService.BEHAVIOR_REJECT], + ], + }); + + let controller3 = await SpecialPowers.spawn(browser, [], async function() { + let b = new content.Blob(["<!DOCTYPE html><html></html>"], { + type: "text/html", + }); + let f = content.document.createElement("iframe"); + // No need to call revokeObjectURL() since the window will be closed shortly. + f.src = content.URL.createObjectURL(b); + content.document.body.appendChild(f); + await new Promise(resolve => (f.onload = resolve)); + return !!f.contentWindow.navigator.serviceWorker.controller; + }); + + ok(!!controller3, "page should be controlled with storage allowed"); + + await SpecialPowers.popPrefEnv(); + BrowserTestUtils.removeTab(tab); +}); + +// Test to verify a blob worker script does not hit our service +// worker storage assertions when storage is blocked between opening +// the parent page and creating the worker. Note, we cannot +// explicitly check if the worker is controlled since we don't expose +// WorkerNavigator.serviceWorkers.controller yet. +add_task(async function test_block_storage_before_blob_worker() { + PermissionTestUtils.remove(PAGE_URI, "cookie"); + + let tab = BrowserTestUtils.addTab(gBrowser, SCOPE); + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + let controller = await SpecialPowers.spawn(browser, [], async function() { + return content.navigator.serviceWorker.controller; + }); + + ok(!!controller, "page should be controlled with storage allowed"); + + let scriptURL = await SpecialPowers.spawn(browser, [], async function() { + let b = new content.Blob( + ["self.postMessage(self.location.href);self.close()"], + { type: "application/javascript" } + ); + // No need to call revokeObjectURL() since the window will be closed shortly. + let u = content.URL.createObjectURL(b); + let w = new content.Worker(u); + return await new Promise(resolve => { + w.onmessage = e => resolve(e.data); + }); + }); + + ok(scriptURL.startsWith("blob:"), "blob URL worker should run"); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["network.cookie.cookieBehavior", Ci.nsICookieService.BEHAVIOR_REJECT], + ], + }); + + let scriptURL2 = await SpecialPowers.spawn(browser, [], async function() { + let b = new content.Blob( + ["self.postMessage(self.location.href);self.close()"], + { type: "application/javascript" } + ); + // No need to call revokeObjectURL() since the window will be closed shortly. + let u = content.URL.createObjectURL(b); + let w = new content.Worker(u); + return await new Promise(resolve => { + w.onmessage = e => resolve(e.data); + }); + }); + + ok(scriptURL2.startsWith("blob:"), "blob URL worker should run"); + + await SpecialPowers.popPrefEnv(); + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function cleanup() { + PermissionTestUtils.remove(PAGE_URI, "cookie"); + + let tab = BrowserTestUtils.addTab(gBrowser, PAGE_URI); + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + await SpecialPowers.spawn(browser, [SCOPE], async function(uri) { + let reg = await content.navigator.serviceWorker.getRegistration(uri); + let worker = reg.active; + await reg.unregister(); + await new Promise(resolve => { + if (worker.state === "redundant") { + resolve(); + return; + } + worker.addEventListener("statechange", function onStateChange() { + if (worker.state === "redundant") { + worker.removeEventListener("statechange", onStateChange); + resolve(); + } + }); + }); + }); + + BrowserTestUtils.removeTab(tab); +}); diff --git a/dom/serviceworkers/test/browser_storage_recovery.js b/dom/serviceworkers/test/browser_storage_recovery.js new file mode 100644 index 0000000000..5ecc135021 --- /dev/null +++ b/dom/serviceworkers/test/browser_storage_recovery.js @@ -0,0 +1,152 @@ +"use strict"; + +// This test registers a SW for a scope that will never control a document +// and therefore never trigger a "fetch" functional event that would +// automatically attempt to update the registration. The overlap of the +// PAGE_URI and SCOPE is incidental. checkForUpdate is the only thing that +// will trigger an update of the registration and so there is no need to +// worry about Schedule Job races to coalesce an update job. + +const BASE_URI = "http://mochi.test:8888/browser/dom/serviceworkers/test/"; +const PAGE_URI = BASE_URI + "empty.html"; +const SCOPE = PAGE_URI + "?storage_recovery"; +const SW_SCRIPT = BASE_URI + "storage_recovery_worker.sjs"; + +async function checkForUpdate(browser) { + return SpecialPowers.spawn(browser, [SCOPE], async function(uri) { + let reg = await content.navigator.serviceWorker.getRegistration(uri); + await reg.update(); + return !!reg.installing; + }); +} + +// Delete all of our chrome-namespace Caches for this origin, leaving any +// content-owned caches in place. This is exclusively for simulating loss +// of the origin's storage without loss of the registration and without +// having to worry that future enhancements to QuotaClients/ServiceWorkerRegistrar +// will break this test. If you want to wipe storage for an origin, use +// QuotaManager APIs +async function wipeStorage(u) { + let uri = Services.io.newURI(u); + let principal = Services.scriptSecurityManager.createContentPrincipal( + uri, + {} + ); + let caches = new CacheStorage("chrome", principal); + let list = await caches.keys(); + return Promise.all(list.map(c => caches.delete(c))); +} + +add_setup(async function() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.serviceWorkers.idle_timeout", 0], + ], + }); + + // Configure the server script to not redirect. + await fetch(SW_SCRIPT + "?clear-redirect"); + + let tab = BrowserTestUtils.addTab(gBrowser, PAGE_URI); + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + await SpecialPowers.spawn( + browser, + [{ script: SW_SCRIPT, scope: SCOPE }], + async function(opts) { + let reg = await content.navigator.serviceWorker.register(opts.script, { + scope: opts.scope, + }); + let worker = reg.installing || reg.waiting || reg.active; + await new Promise(resolve => { + if (worker.state === "activated") { + resolve(); + return; + } + worker.addEventListener("statechange", function onStateChange() { + if (worker.state === "activated") { + worker.removeEventListener("statechange", onStateChange); + resolve(); + } + }); + }); + } + ); + + BrowserTestUtils.removeTab(tab); +}); + +// Verify that our service worker doesn't update normally. +add_task(async function normal_update_check() { + let tab = BrowserTestUtils.addTab(gBrowser, PAGE_URI); + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + let updated = await checkForUpdate(browser); + ok(!updated, "normal update check should not trigger an update"); + + BrowserTestUtils.removeTab(tab); +}); + +// Test what happens when we wipe the service worker scripts +// out from under the site before triggering the update. This +// should cause an update to occur. +add_task(async function wiped_update_check() { + // Wipe the backing cache storage, but leave the SW registered. + await wipeStorage(PAGE_URI); + + let tab = BrowserTestUtils.addTab(gBrowser, PAGE_URI); + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + let updated = await checkForUpdate(browser); + ok(updated, "wiping the service worker scripts should trigger an update"); + + BrowserTestUtils.removeTab(tab); +}); + +// Test what happens when we wipe the service worker scripts +// out from under the site before triggering the update. This +// should cause an update to occur. +add_task(async function wiped_and_failed_update_check() { + // Wipe the backing cache storage, but leave the SW registered. + await wipeStorage(PAGE_URI); + + // Configure the service worker script to redirect. This will + // prevent the update from completing successfully. + await fetch(SW_SCRIPT + "?set-redirect"); + + let tab = BrowserTestUtils.addTab(gBrowser, PAGE_URI); + let browser = gBrowser.getBrowserForTab(tab); + await BrowserTestUtils.browserLoaded(browser); + + // Attempt to update the service worker. This should throw + // an error because the script is now redirecting. + let updateFailed = false; + try { + await checkForUpdate(browser); + } catch (e) { + updateFailed = true; + } + ok(updateFailed, "redirecting service worker script should fail to update"); + + // Also, since the existing service worker's scripts are broken + // we should also remove the registration completely when the + // update fails. + let exists = await SpecialPowers.spawn(browser, [SCOPE], async function(uri) { + let reg = await content.navigator.serviceWorker.getRegistration(uri); + return !!reg; + }); + ok( + !exists, + "registration should be removed after scripts are wiped and update fails" + ); + + // Note, we don't have to clean up the service worker registration + // since its effectively been force-removed here. + + BrowserTestUtils.removeTab(tab); +}); diff --git a/dom/serviceworkers/test/browser_unregister_with_containers.js b/dom/serviceworkers/test/browser_unregister_with_containers.js new file mode 100644 index 0000000000..0304f70c1d --- /dev/null +++ b/dom/serviceworkers/test/browser_unregister_with_containers.js @@ -0,0 +1,153 @@ +"use strict"; + +const BASE_URI = "http://mochi.test:8888/browser/dom/serviceworkers/test/"; +const PAGE_URI = BASE_URI + "empty.html"; +const SCOPE = PAGE_URI + "?unregister_with_containers"; +const SW_SCRIPT = BASE_URI + "empty.js"; + +function doRegister(browser) { + return SpecialPowers.spawn( + browser, + [{ script: SW_SCRIPT, scope: SCOPE }], + async function(opts) { + let reg = await content.navigator.serviceWorker.register(opts.script, { + scope: opts.scope, + }); + let worker = reg.installing || reg.waiting || reg.active; + await new Promise(resolve => { + if (worker.state === "activated") { + resolve(); + return; + } + worker.addEventListener("statechange", function onStateChange() { + if (worker.state === "activated") { + worker.removeEventListener("statechange", onStateChange); + resolve(); + } + }); + }); + } + ); +} + +function doUnregister(browser) { + return SpecialPowers.spawn(browser, [SCOPE], async function(uri) { + let reg = await content.navigator.serviceWorker.getRegistration(uri); + let worker = reg.active; + await reg.unregister(); + await new Promise(resolve => { + if (worker.state === "redundant") { + resolve(); + return; + } + worker.addEventListener("statechange", function onStateChange() { + if (worker.state === "redundant") { + worker.removeEventListener("statechange", onStateChange); + resolve(); + } + }); + }); + }); +} + +function isControlled(browser) { + return SpecialPowers.spawn(browser, [], function() { + return !!content.navigator.serviceWorker.controller; + }); +} + +async function checkControlled(browser) { + let controlled = await isControlled(browser); + ok(controlled, "window should be controlled"); +} + +async function checkUncontrolled(browser) { + let controlled = await isControlled(browser); + ok(!controlled, "window should not be controlled"); +} + +add_task(async function test() { + await SpecialPowers.pushPrefEnv({ + set: [ + // Avoid service worker propagation races by disabling multi-e10s for now. + // This can be removed after the e10s refactor is complete. + ["dom.ipc.processCount", 1], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ], + }); + + // Setup service workers in two different contexts with the same scope. + let containerTab1 = BrowserTestUtils.addTab(gBrowser, PAGE_URI, { + userContextId: 1, + }); + let containerBrowser1 = gBrowser.getBrowserForTab(containerTab1); + await BrowserTestUtils.browserLoaded(containerBrowser1); + + let containerTab2 = BrowserTestUtils.addTab(gBrowser, PAGE_URI, { + userContextId: 2, + }); + let containerBrowser2 = gBrowser.getBrowserForTab(containerTab2); + await BrowserTestUtils.browserLoaded(containerBrowser2); + + await doRegister(containerBrowser1); + await doRegister(containerBrowser2); + + await checkUncontrolled(containerBrowser1); + await checkUncontrolled(containerBrowser2); + + // Close the tabs we used to register the service workers. These are not + // controlled. + BrowserTestUtils.removeTab(containerTab1); + BrowserTestUtils.removeTab(containerTab2); + + // Open a controlled tab in each container. + containerTab1 = BrowserTestUtils.addTab(gBrowser, SCOPE, { + userContextId: 1, + }); + containerBrowser1 = gBrowser.getBrowserForTab(containerTab1); + await BrowserTestUtils.browserLoaded(containerBrowser1); + + containerTab2 = BrowserTestUtils.addTab(gBrowser, SCOPE, { + userContextId: 2, + }); + containerBrowser2 = gBrowser.getBrowserForTab(containerTab2); + await BrowserTestUtils.browserLoaded(containerBrowser2); + + await checkControlled(containerBrowser1); + await checkControlled(containerBrowser2); + + // Remove the first container's controlled tab + BrowserTestUtils.removeTab(containerTab1); + + // Create a new uncontrolled tab for the first container and use it to + // unregister the service worker. + containerTab1 = BrowserTestUtils.addTab(gBrowser, PAGE_URI, { + userContextId: 1, + }); + containerBrowser1 = gBrowser.getBrowserForTab(containerTab1); + await BrowserTestUtils.browserLoaded(containerBrowser1); + await doUnregister(containerBrowser1); + + await checkUncontrolled(containerBrowser1); + await checkControlled(containerBrowser2); + + // Remove the second container's controlled tab + BrowserTestUtils.removeTab(containerTab2); + + // Create a new uncontrolled tab for the second container and use it to + // unregister the service worker. + containerTab2 = BrowserTestUtils.addTab(gBrowser, PAGE_URI, { + userContextId: 2, + }); + containerBrowser2 = gBrowser.getBrowserForTab(containerTab2); + await BrowserTestUtils.browserLoaded(containerBrowser2); + await doUnregister(containerBrowser2); + + await checkUncontrolled(containerBrowser1); + await checkUncontrolled(containerBrowser2); + + // Close the two tabs we used to unregister the service worker. + BrowserTestUtils.removeTab(containerTab1); + BrowserTestUtils.removeTab(containerTab2); +}); diff --git a/dom/serviceworkers/test/browser_userContextId_openWindow.js b/dom/serviceworkers/test/browser_userContextId_openWindow.js new file mode 100644 index 0000000000..8b07a532fb --- /dev/null +++ b/dom/serviceworkers/test/browser_userContextId_openWindow.js @@ -0,0 +1,163 @@ +let Cm = Components.manager; + +let swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService( + Ci.nsIServiceWorkerManager +); + +const URI = "https://example.com/browser/dom/serviceworkers/test/empty.html"; +const MOCK_CID = Components.ID("{2a0f83c4-8818-4914-a184-f1172b4eaaa7}"); +const ALERTS_SERVICE_CONTRACT_ID = "@mozilla.org/alerts-service;1"; +const USER_CONTEXT_ID = 3; + +let mockAlertsService = { + showAlert(alert, alertListener) { + ok(true, "Showing alert"); + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + setTimeout(function() { + alertListener.observe(null, "alertshow", alert.cookie); + }, 100); + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + setTimeout(function() { + alertListener.observe(null, "alertclickcallback", alert.cookie); + }, 100); + }, + + showAlertNotification( + imageUrl, + title, + text, + textClickable, + cookie, + alertListener, + name, + dir, + lang, + data + ) { + this.showAlert(); + }, + + QueryInterface(aIID) { + if (aIID.equals(Ci.nsISupports) || aIID.equals(Ci.nsIAlertsService)) { + return this; + } + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + }, + + createInstance(aIID) { + return this.QueryInterface(aIID); + }, +}; + +registerCleanupFunction(() => { + Cm.QueryInterface(Ci.nsIComponentRegistrar).unregisterFactory( + MOCK_CID, + mockAlertsService + ); +}); + +add_setup(async function() { + // make sure userContext, SW and notifications are enabled. + await SpecialPowers.pushPrefEnv({ + set: [ + ["privacy.userContext.enabled", true], + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.webnotifications.workers.enabled", true], + ["dom.webnotifications.serviceworker.enabled", true], + ["notification.prompt.testing", true], + ["dom.serviceWorkers.disable_open_click_delay", 1000], + ["dom.serviceWorkers.idle_timeout", 299999], + ["dom.serviceWorkers.idle_extended_timeout", 299999], + ["browser.link.open_newwindow", 3], + ], + }); +}); + +add_task(async function test() { + Cm.QueryInterface(Ci.nsIComponentRegistrar).registerFactory( + MOCK_CID, + "alerts service", + ALERTS_SERVICE_CONTRACT_ID, + mockAlertsService + ); + + // open the tab in the correct userContextId + let tab = BrowserTestUtils.addTab(gBrowser, URI, { + userContextId: USER_CONTEXT_ID, + }); + let browser = gBrowser.getBrowserForTab(tab); + + // select tab and make sure its browser is focused + gBrowser.selectedTab = tab; + tab.ownerGlobal.focus(); + + // wait for tab load + await BrowserTestUtils.browserLoaded(gBrowser.getBrowserForTab(tab)); + + // Waiting for new tab. + let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, null, true); + + // here the test. + /* eslint-disable no-shadow */ + let uci = await SpecialPowers.spawn(browser, [URI], uri => { + let uci = content.document.nodePrincipal.userContextId; + + // Registration of the SW + return ( + content.navigator.serviceWorker + .register("file_userContextId_openWindow.js") + + // Activation + .then(swr => { + return new content.window.Promise(resolve => { + let worker = swr.installing; + worker.addEventListener("statechange", () => { + if (worker.state === "activated") { + resolve(swr); + } + }); + }); + }) + + // Ask for an openWindow. + .then(swr => { + swr.showNotification("testPopup"); + return uci; + }) + ); + }); + /* eslint-enable no-shadow */ + + is(uci, USER_CONTEXT_ID, "Tab runs with UCI " + USER_CONTEXT_ID); + + let newTab = await newTabPromise; + + is( + newTab.getAttribute("usercontextid"), + USER_CONTEXT_ID, + "New tab has UCI equal " + USER_CONTEXT_ID + ); + + // wait for SW unregistration + /* eslint-disable no-shadow */ + uci = await SpecialPowers.spawn(browser, [], () => { + let uci = content.document.nodePrincipal.userContextId; + + return content.navigator.serviceWorker + .getRegistration(".") + .then(registration => { + return registration.unregister(); + }) + .then(() => { + return uci; + }); + }); + /* eslint-enable no-shadow */ + + is(uci, USER_CONTEXT_ID, "Tab runs with UCI " + USER_CONTEXT_ID); + + BrowserTestUtils.removeTab(newTab); + BrowserTestUtils.removeTab(tab); +}); diff --git a/dom/serviceworkers/test/bug1151916_driver.html b/dom/serviceworkers/test/bug1151916_driver.html new file mode 100644 index 0000000000..08e7d9414f --- /dev/null +++ b/dom/serviceworkers/test/bug1151916_driver.html @@ -0,0 +1,53 @@ +<html> + <body> + <script language="javascript"> + function fail(msg) { + window.parent.postMessage({ status: "failed", message: msg }, "*"); + } + + function success(msg) { + window.parent.postMessage({ status: "success", message: msg }, "*"); + } + + if (!window.parent) { + dump("This file must be embedded in an iframe!"); + } + + navigator.serviceWorker.getRegistration() + .then(function(reg) { + if (!reg) { + navigator.serviceWorker.ready.then(function(registration) { + if (registration.active.state == "activating") { + registration.active.onstatechange = function(e) { + registration.active.onstatechange = null; + if (registration.active.state == "activated") { + success("Registered and activated"); + } + } + } else { + success("Registered and activated"); + } + }); + navigator.serviceWorker.register("bug1151916_worker.js", + { scope: "." }); + } else { + // Simply force the sw to load a resource and touch self.caches. + if (!reg.active) { + fail("no-active-worker"); + return; + } + + fetch("madeup.txt").then(function(res) { + res.text().then(function(v) { + if (v == "Hi there") { + success("Loaded from cache"); + } else { + fail("Response text did not match"); + } + }, fail); + }, fail); + } + }, fail); + </script> + </body> +</html> diff --git a/dom/serviceworkers/test/bug1151916_worker.js b/dom/serviceworkers/test/bug1151916_worker.js new file mode 100644 index 0000000000..2688b78668 --- /dev/null +++ b/dom/serviceworkers/test/bug1151916_worker.js @@ -0,0 +1,15 @@ +onactivate = function(e) { + e.waitUntil( + self.caches.open("default-cache").then(function(cache) { + var response = new Response("Hi there"); + return cache.put("madeup.txt", response); + }) + ); +}; + +onfetch = function(e) { + if (e.request.url.match(/madeup.txt$/)) { + var p = self.caches.match("madeup.txt", { cacheName: "default-cache" }); + e.respondWith(p); + } +}; diff --git a/dom/serviceworkers/test/bug1240436_worker.js b/dom/serviceworkers/test/bug1240436_worker.js new file mode 100644 index 0000000000..c21f60b60f --- /dev/null +++ b/dom/serviceworkers/test/bug1240436_worker.js @@ -0,0 +1,2 @@ +// a contains a ZERO WIDTH JOINER (0x200D) +var a = ""; diff --git a/dom/serviceworkers/test/chrome-common.ini b/dom/serviceworkers/test/chrome-common.ini new file mode 100644 index 0000000000..b5cae318e1 --- /dev/null +++ b/dom/serviceworkers/test/chrome-common.ini @@ -0,0 +1,21 @@ +[DEFAULT] +skip-if = os == 'android' +support-files = + chrome_helpers.js + empty.js + fetch.js + hello.html + serviceworker.html + serviceworkerinfo_iframe.html + serviceworkermanager_iframe.html + serviceworkerregistrationinfo_iframe.html + utils.js + worker.js + worker2.js + +[test_devtools_track_serviceworker_time.html] +[test_privateBrowsing.html] +[test_serviceworkerinfo.xhtml] +skip-if = serviceworker_e10s # nsIWorkerDebugger attribute not implemented +[test_serviceworkermanager.xhtml] +[test_serviceworkerregistrationinfo.xhtml] diff --git a/dom/serviceworkers/test/chrome-dFPI.ini b/dom/serviceworkers/test/chrome-dFPI.ini new file mode 100644 index 0000000000..b07337f753 --- /dev/null +++ b/dom/serviceworkers/test/chrome-dFPI.ini @@ -0,0 +1,7 @@ +[DEFAULT] +# Enable dFPI(cookieBehavior 5) for service worker tests. +prefs = + network.cookie.cookieBehavior=5 +dupe-manifest = true + +[include:chrome-common.ini] diff --git a/dom/serviceworkers/test/chrome.ini b/dom/serviceworkers/test/chrome.ini new file mode 100644 index 0000000000..e6f7e3206d --- /dev/null +++ b/dom/serviceworkers/test/chrome.ini @@ -0,0 +1,4 @@ +[DEFAULT] +dupe-manifest = true + +[include:chrome-common.ini] diff --git a/dom/serviceworkers/test/chrome_helpers.js b/dom/serviceworkers/test/chrome_helpers.js new file mode 100644 index 0000000000..566ba52eca --- /dev/null +++ b/dom/serviceworkers/test/chrome_helpers.js @@ -0,0 +1,71 @@ +let swm = Cc["@mozilla.org/serviceworkers/manager;1"].getService( + Ci.nsIServiceWorkerManager +); + +let EXAMPLE_URL = "https://example.com/chrome/dom/serviceworkers/test/"; + +function waitForIframeLoad(iframe) { + return new Promise(function(resolve) { + iframe.onload = resolve; + }); +} + +function waitForRegister(scope, callback) { + return new Promise(function(resolve) { + let listener = { + onRegister(registration) { + if (registration.scope !== scope) { + return; + } + swm.removeListener(listener); + resolve(callback ? callback(registration) : registration); + }, + }; + swm.addListener(listener); + }); +} + +function waitForUnregister(scope) { + return new Promise(function(resolve) { + let listener = { + onUnregister(registration) { + if (registration.scope !== scope) { + return; + } + swm.removeListener(listener); + resolve(registration); + }, + }; + swm.addListener(listener); + }); +} + +function waitForServiceWorkerRegistrationChange(registration, callback) { + return new Promise(function(resolve) { + let listener = { + onChange() { + registration.removeListener(listener); + if (callback) { + callback(); + } + resolve(callback ? callback() : undefined); + }, + }; + registration.addListener(listener); + }); +} + +function waitForServiceWorkerShutdown() { + return new Promise(function(resolve) { + let observer = { + observe(subject, topic, data) { + if (topic !== "service-worker-shutdown") { + return; + } + SpecialPowers.removeObserver(observer, "service-worker-shutdown"); + resolve(); + }, + }; + SpecialPowers.addObserver(observer, "service-worker-shutdown"); + }); +} diff --git a/dom/serviceworkers/test/claim_clients/client.html b/dom/serviceworkers/test/claim_clients/client.html new file mode 100644 index 0000000000..969a6dbf10 --- /dev/null +++ b/dom/serviceworkers/test/claim_clients/client.html @@ -0,0 +1,43 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1130684 - claim client </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + if (!parent) { + info("This page shouldn't be launched directly!"); + } + + window.onload = function() { + parent.postMessage("READY", "*"); + } + + navigator.serviceWorker.oncontrollerchange = function() { + parent.postMessage({ + event: "controllerchange", + controller: (navigator.serviceWorker.controller !== null) + }, "*"); + } + + navigator.serviceWorker.onmessage = function(e) { + parent.postMessage({ + event: "message", + data: e.data + }, "*"); + } + +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/claim_oninstall_worker.js b/dom/serviceworkers/test/claim_oninstall_worker.js new file mode 100644 index 0000000000..6d79c919b0 --- /dev/null +++ b/dom/serviceworkers/test/claim_oninstall_worker.js @@ -0,0 +1,7 @@ +oninstall = function(e) { + var claimFailedPromise = new Promise(function(resolve, reject) { + clients.claim().then(reject, () => resolve()); + }); + + e.waitUntil(claimFailedPromise); +}; diff --git a/dom/serviceworkers/test/claim_worker_1.js b/dom/serviceworkers/test/claim_worker_1.js new file mode 100644 index 0000000000..493798a88b --- /dev/null +++ b/dom/serviceworkers/test/claim_worker_1.js @@ -0,0 +1,32 @@ +onactivate = function(e) { + var result = { + resolve_value: false, + match_count_before: -1, + match_count_after: -1, + message: "claim_worker_1", + }; + + self.clients + .matchAll() + .then(function(matched) { + // should be 0 + result.match_count_before = matched.length; + }) + .then(function() { + return self.clients.claim(); + }) + .then(function(ret) { + result.resolve_value = ret; + return self.clients.matchAll(); + }) + .then(function(matched) { + // should be 2 + result.match_count_after = matched.length; + for (i = 0; i < matched.length; i++) { + matched[i].postMessage(result); + } + if (result.match_count_after !== 2) { + dump("ERROR: claim_worker_1 failed to capture clients.\n"); + } + }); +}; diff --git a/dom/serviceworkers/test/claim_worker_2.js b/dom/serviceworkers/test/claim_worker_2.js new file mode 100644 index 0000000000..b244495ecd --- /dev/null +++ b/dom/serviceworkers/test/claim_worker_2.js @@ -0,0 +1,34 @@ +onactivate = function(e) { + var result = { + resolve_value: false, + match_count_before: -1, + match_count_after: -1, + message: "claim_worker_2", + }; + + self.clients + .matchAll() + .then(function(matched) { + // should be 0 + result.match_count_before = matched.length; + }) + .then(function() { + return clients.claim(); + }) + .then(function(ret) { + result.resolve_value = ret; + return clients.matchAll(); + }) + .then(function(matched) { + // should be 1 + result.match_count_after = matched.length; + if (result.match_count_after === 1) { + matched[0].postMessage(result); + } else { + dump("ERROR: claim_worker_2 failed to capture clients.\n"); + for (let i = 0; i < matched.length; ++i) { + dump("### ### matched[" + i + "]: " + matched[i].url + "\n"); + } + } + }); +}; diff --git a/dom/serviceworkers/test/close_test.js b/dom/serviceworkers/test/close_test.js new file mode 100644 index 0000000000..cc0ec4d335 --- /dev/null +++ b/dom/serviceworkers/test/close_test.js @@ -0,0 +1,22 @@ +function ok(v, msg) { + client.postMessage({ status: "ok", result: !!v, message: msg }); +} + +var client; +onmessage = function(e) { + if (e.data.message == "start") { + self.clients.matchAll().then(function(clients) { + client = clients[0]; + try { + close(); + ok(false, "close() should throw"); + } catch (ex) { + ok( + ex.name === "InvalidAccessError", + "close() should throw InvalidAccessError" + ); + } + client.postMessage({ status: "done" }); + }); + } +}; diff --git a/dom/serviceworkers/test/console_monitor.js b/dom/serviceworkers/test/console_monitor.js new file mode 100644 index 0000000000..e3c347b153 --- /dev/null +++ b/dom/serviceworkers/test/console_monitor.js @@ -0,0 +1,44 @@ +/* eslint-env mozilla/chrome-script */ + +let consoleListener; + +function ConsoleListener() { + Services.console.registerListener(this); +} + +ConsoleListener.prototype = { + callbacks: [], + + observe: aMsg => { + if (!(aMsg instanceof Ci.nsIScriptError)) { + return; + } + + let msg = { + cssSelectors: aMsg.cssSelectors, + errorMessage: aMsg.errorMessage, + sourceName: aMsg.sourceName, + sourceLine: aMsg.sourceLine, + lineNumber: aMsg.lineNumber, + columnNumber: aMsg.columnNumber, + category: aMsg.category, + windowID: aMsg.outerWindowID, + innerWindowID: aMsg.innerWindowID, + isScriptError: true, + isWarning: (aMsg.flags & Ci.nsIScriptError.warningFlag) === 1, + }; + + sendAsyncMessage("monitor", msg); + }, +}; + +addMessageListener("load", function(e) { + consoleListener = new ConsoleListener(); + sendAsyncMessage("ready", {}); +}); + +addMessageListener("unload", function(e) { + Services.console.unregisterListener(consoleListener); + consoleListener = null; + sendAsyncMessage("unloaded", {}); +}); diff --git a/dom/serviceworkers/test/controller/index.html b/dom/serviceworkers/test/controller/index.html new file mode 100644 index 0000000000..2a68e3f4bb --- /dev/null +++ b/dom/serviceworkers/test/controller/index.html @@ -0,0 +1,72 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 94048 - test install event.</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + // Make sure to use good, unique messages, since the actual expression will not show up in test results. + function my_ok(result, msg) { + parent.postMessage({status: "ok", result, message: msg}, "*"); + } + + function finish() { + parent.postMessage({status: "done"}, "*"); + } + + navigator.serviceWorker.ready.then(function(swr) { + my_ok(swr.scope.match(/serviceworkers\/test\/control$/), + "This page should be controlled by upper level registration"); + my_ok(swr.installing == undefined, + "Upper level registration should not have a installing worker."); + if (navigator.serviceWorker.controller) { + // We are controlled. + // Register a new worker for this sub-scope. After that, controller should still be for upper level, but active should change to be this scope's. + navigator.serviceWorker.register("../worker2.js", { scope: "./" }).then(function(e) { + my_ok("installing" in e, "ServiceWorkerRegistration.installing exists."); + my_ok(e.installing instanceof ServiceWorker, "ServiceWorkerRegistration.installing is a ServiceWorker."); + + my_ok("waiting" in e, "ServiceWorkerRegistration.waiting exists."); + my_ok("active" in e, "ServiceWorkerRegistration.active exists."); + + my_ok(e.installing && + e.installing.scriptURL.match(/worker2.js$/), + "Installing is serviceworker/controller"); + + my_ok("scope" in e, "ServiceWorkerRegistration.scope exists."); + my_ok(e.scope.match(/serviceworkers\/test\/controller\/$/), "Scope is serviceworker/test/controller " + e.scope); + + my_ok("unregister" in e, "ServiceWorkerRegistration.unregister exists."); + + my_ok(navigator.serviceWorker.controller.scriptURL.match(/worker\.js$/), + "Controller is still worker.js"); + + e.unregister().then(function(result) { + my_ok(result, "Unregistering the SW should succeed"); + finish(); + }, function(error) { + dump("Error unregistering the SW: " + error + "\n"); + }); + }); + } else { + my_ok(false, "Should've been controlled!"); + finish(); + } + }).catch(function(e) { + my_ok(false, "Some test threw an error " + e); + finish(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/create_another_sharedWorker.html b/dom/serviceworkers/test/create_another_sharedWorker.html new file mode 100644 index 0000000000..f49194fa50 --- /dev/null +++ b/dom/serviceworkers/test/create_another_sharedWorker.html @@ -0,0 +1,6 @@ +<!DOCTYPE HTML> +<title>Shared workers: create antoehr sharedworekr client</title> +<pre id=log>Hello World</pre> +<script> + var worker = new SharedWorker('sharedWorker_fetch.js'); +</script> diff --git a/dom/serviceworkers/test/download/window.html b/dom/serviceworkers/test/download/window.html new file mode 100644 index 0000000000..7d7893e0e6 --- /dev/null +++ b/dom/serviceworkers/test/download/window.html @@ -0,0 +1,46 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> +</head> +<body> +<script type="text/javascript"> + +function wait_until_controlled() { + return new Promise(function(resolve) { + if (navigator.serviceWorker.controller) { + return resolve(); + } + navigator.serviceWorker.addEventListener('controllerchange', function onController() { + if (navigator.serviceWorker.controller) { + navigator.serviceWorker.removeEventListener('controllerchange', onController); + return resolve(); + } + }); + }); +} +addEventListener('load', function(event) { + var registration; + navigator.serviceWorker.register('worker.js').then(function(swr) { + registration = swr; + + // While the iframe below is a navigation, we still wait until we are + // controlled here. We want an active client to hold the service worker + // alive since it calls unregister() on itself. + return wait_until_controlled(); + + }).then(function() { + var frame = document.createElement('iframe'); + document.body.appendChild(frame); + frame.src = 'fake_download'; + + // The service worker is unregistered in the fetch event. The window and + // frame are cleaned up from the browser chrome script. + }); +}); +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/download/worker.js b/dom/serviceworkers/test/download/worker.js new file mode 100644 index 0000000000..71fb502f1c --- /dev/null +++ b/dom/serviceworkers/test/download/worker.js @@ -0,0 +1,34 @@ +addEventListener("install", function(evt) { + evt.waitUntil(self.skipWaiting()); +}); + +addEventListener("activate", function(evt) { + // We claim the current clients in order to ensure that we have an + // active client when we call unregister in the fetch handler. Otherwise + // the unregister() can kill the current worker before returning a + // response. + evt.waitUntil(clients.claim()); +}); + +addEventListener("fetch", function(evt) { + // This worker may live long enough to receive a fetch event from the next + // test. Just pass such requests through to the network. + if (!evt.request.url.includes("fake_download")) { + return; + } + + // We should only get a single download fetch event. Automatically unregister. + evt.respondWith( + registration.unregister().then(function() { + return new Response("service worker generated download", { + headers: { + "Content-Disposition": 'attachment; filename="fake_download.bin"', + // Prevent the default text editor from being launched + "Content-Type": "application/octet-stream", + // fake encoding header that should have no effect + "Content-Encoding": "gzip", + }, + }); + }) + ); +}); diff --git a/dom/serviceworkers/test/download_canceled/page_download_canceled.html b/dom/serviceworkers/test/download_canceled/page_download_canceled.html new file mode 100644 index 0000000000..e3904c4967 --- /dev/null +++ b/dom/serviceworkers/test/download_canceled/page_download_canceled.html @@ -0,0 +1,58 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> +</head> +<body> + +<script src="../utils.js"></script> +<script type="text/javascript"> +function wait_until_controlled() { + return new Promise(function(resolve) { + if (navigator.serviceWorker.controller) { + return resolve('controlled'); + } + navigator.serviceWorker.addEventListener('controllerchange', function onController() { + if (navigator.serviceWorker.controller) { + navigator.serviceWorker.removeEventListener('controllerchange', onController); + return resolve('controlled'); + } + }); + }); +} +addEventListener('load', async function(event) { + window.controlled = wait_until_controlled(); + window.registration = + await navigator.serviceWorker.register('sw_download_canceled.js'); + let sw = registration.installing || registration.waiting || + registration.active; + await waitForState(sw, 'activated'); + sw.postMessage('claim'); +}); + +// Place to hold promises for stream closures reported by the SW. +window.streamClosed = {}; + +// The ServiceWorker will postMessage to this BroadcastChannel when the streams +// are closed. (Alternately, the SW could have used the clients API to post at +// us, but the mechanism by which that operates would be different when this +// test is uplifted, and it's desirable to avoid timing changes.) +// +// The browser test will use this promise to wait on stream shutdown. +window.swStreamChannel = new BroadcastChannel("stream-closed"); +function trackStreamClosure(path) { + let resolve; + const promise = new Promise(r => { resolve = r }); + window.streamClosed[path] = { promise, resolve }; +} +window.swStreamChannel.onmessage = ({ data }) => { + window.streamClosed[data.what].resolve(data); +}; +</script> + +</body> +</html> diff --git a/dom/serviceworkers/test/download_canceled/server-stream-download.sjs b/dom/serviceworkers/test/download_canceled/server-stream-download.sjs new file mode 100644 index 0000000000..e6ebd68126 --- /dev/null +++ b/dom/serviceworkers/test/download_canceled/server-stream-download.sjs @@ -0,0 +1,133 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const { setInterval, clearInterval } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +// stolen from file_blocked_script.sjs +function setGlobalState(data, key) { + x = { + data, + QueryInterface(iid) { + return this; + }, + }; + x.wrappedJSObject = x; + setObjectState(key, x); +} + +function getGlobalState(key) { + var data; + getObjectState(key, function(x) { + data = x && x.wrappedJSObject.data; + }); + return data; +} + +/* + * We want to let the sw_download_canceled.js service worker know when the + * stream was canceled. To this end, we let it issue a monitor request which we + * fulfill when the stream has been canceled. In order to coordinate between + * multiple requests, we use the getObjectState/setObjectState mechanism that + * httpd.js exposes to let data be shared and/or persist between requests. We + * handle both possible orderings of the requests because we currently don't + * try and impose an ordering between the two requests as issued by the SW, and + * file_blocked_script.sjs encourages us to do this, but we probably could order + * them. + */ +const MONITOR_KEY = "stream-monitor"; +function completeMonitorResponse(response, data) { + response.write(JSON.stringify(data)); + response.finish(); +} +function handleMonitorRequest(request, response) { + response.setHeader("Content-Type", "application/json"); + response.setStatusLine(request.httpVersion, 200, "Found"); + + response.processAsync(); + // Necessary to cause the headers to be flushed; that or touching the + // bodyOutputStream getter. + response.write(""); + dump("server-stream-download.js: monitor headers issued\n"); + + const alreadyCompleted = getGlobalState(MONITOR_KEY); + if (alreadyCompleted) { + completeMonitorResponse(response, alreadyCompleted); + setGlobalState(null, MONITOR_KEY); + } else { + setGlobalState(response, MONITOR_KEY); + } +} + +const MAX_TICK_COUNT = 3000; +const TICK_INTERVAL = 2; +function handleStreamRequest(request, response) { + const name = "server-stream-download"; + + // Create some payload to send. + let strChunk = + "Static routes are the future of ServiceWorkers! So say we all!\n"; + while (strChunk.length < 1024) { + strChunk += strChunk; + } + + response.setHeader("Content-Disposition", `attachment; filename="${name}"`); + response.setHeader( + "Content-Type", + `application/octet-stream; name="${name}"` + ); + response.setHeader("Content-Length", `${strChunk.length * MAX_TICK_COUNT}`); + response.setStatusLine(request.httpVersion, 200, "Found"); + + response.processAsync(); + response.write(strChunk); + dump("server-stream-download.js: stream headers + first payload issued\n"); + + let count = 0; + let intervalId; + function closeStream(why, message) { + dump("server-stream-download.js: closing stream: " + why + "\n"); + clearInterval(intervalId); + response.finish(); + + const data = { why, message }; + const monitorResponse = getGlobalState(MONITOR_KEY); + if (monitorResponse) { + completeMonitorResponse(monitorResponse, data); + setGlobalState(null, MONITOR_KEY); + } else { + setGlobalState(data, MONITOR_KEY); + } + } + function tick() { + try { + // bound worst-case behavior. + if (count++ > MAX_TICK_COUNT) { + closeStream("timeout", "timeout"); + return; + } + response.write(strChunk); + } catch (e) { + closeStream("canceled", e.message); + } + } + intervalId = setInterval(tick, TICK_INTERVAL); +} + +Components.utils.importGlobalProperties(["URLSearchParams"]); +function handleRequest(request, response) { + dump( + "server-stream-download.js: processing request for " + + request.path + + "?" + + request.queryString + + "\n" + ); + const query = new URLSearchParams(request.queryString); + if (query.has("monitor")) { + handleMonitorRequest(request, response); + } else { + handleStreamRequest(request, response); + } +} diff --git a/dom/serviceworkers/test/download_canceled/sw_download_canceled.js b/dom/serviceworkers/test/download_canceled/sw_download_canceled.js new file mode 100644 index 0000000000..5d9d5f9bfd --- /dev/null +++ b/dom/serviceworkers/test/download_canceled/sw_download_canceled.js @@ -0,0 +1,150 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This file is derived from :bkelly's https://glitch.com/edit/#!/html-sw-stream + +addEventListener("install", evt => { + evt.waitUntil(self.skipWaiting()); +}); + +// Create a BroadcastChannel to notify when we have closed our streams. +const channel = new BroadcastChannel("stream-closed"); + +const MAX_TICK_COUNT = 3000; +const TICK_INTERVAL = 4; +/** + * Generate a continuous stream of data at a sufficiently high frequency that a + * there"s a good chance of racing channel cancellation. + */ +function handleStream(evt, filename) { + // Create some payload to send. + const encoder = new TextEncoder(); + let strChunk = + "Static routes are the future of ServiceWorkers! So say we all!\n"; + while (strChunk.length < 1024) { + strChunk += strChunk; + } + const dataChunk = encoder.encode(strChunk); + + evt.waitUntil( + new Promise(resolve => { + let body = new ReadableStream({ + start: controller => { + const closeStream = why => { + console.log("closing stream: " + JSON.stringify(why) + "\n"); + clearInterval(intervalId); + resolve(); + // In event of error, the controller will automatically have closed. + if (why.why != "canceled") { + try { + controller.close(); + } catch (ex) { + // If we thought we should cancel but experienced a problem, + // that's a different kind of failure and we need to report it. + // (If we didn't catch the exception here, we'd end up erroneously + // in the tick() method's canceled handler.) + channel.postMessage({ + what: filename, + why: "close-failure", + message: ex.message, + ticks: why.ticks, + }); + return; + } + } + // Post prior to performing any attempt to close... + channel.postMessage(why); + }; + + controller.enqueue(dataChunk); + let count = 0; + let intervalId; + function tick() { + try { + // bound worst-case behavior. + if (count++ > MAX_TICK_COUNT) { + closeStream({ + what: filename, + why: "timeout", + message: "timeout", + ticks: count, + }); + return; + } + controller.enqueue(dataChunk); + } catch (e) { + closeStream({ + what: filename, + why: "canceled", + message: e.message, + ticks: count, + }); + } + } + // Alternately, streams' pull mechanism could be used here, but this + // test doesn't so much want to saturate the stream as to make sure the + // data is at least flowing a little bit. (Also, the author had some + // concern about slowing down the test by overwhelming the event loop + // and concern that we might not have sufficent back-pressure plumbed + // through and an infinite pipe might make bad things happen.) + intervalId = setInterval(tick, TICK_INTERVAL); + tick(); + }, + }); + evt.respondWith( + new Response(body, { + headers: { + "Content-Disposition": `attachment; filename="${filename}"`, + "Content-Type": "application/octet-stream", + }, + }) + ); + }) + ); +} + +/** + * Use an .sjs to generate a similar stream of data to the above, passing the + * response through directly. Because we're handing off the response but also + * want to be able to report when cancellation occurs, we create a second, + * overlapping long-poll style fetch that will not finish resolving until the + * .sjs experiences closure of its socket and terminates the payload stream. + */ +function handlePassThrough(evt, filename) { + evt.waitUntil( + (async () => { + console.log("issuing monitor fetch request"); + const response = await fetch("server-stream-download.sjs?monitor"); + console.log("monitor headers received, awaiting body"); + const data = await response.json(); + console.log("passthrough monitor fetch completed, notifying."); + channel.postMessage({ + what: filename, + why: data.why, + message: data.message, + }); + })() + ); + evt.respondWith( + fetch("server-stream-download.sjs").then(response => { + console.log("server-stream-download.sjs Response received, propagating"); + return response; + }) + ); +} + +addEventListener("fetch", evt => { + console.log(`SW processing fetch of ${evt.request.url}`); + if (evt.request.url.includes("sw-stream-download")) { + return handleStream(evt, "sw-stream-download"); + } + if (evt.request.url.includes("sw-passthrough-download")) { + return handlePassThrough(evt, "sw-passthrough-download"); + } +}); + +addEventListener("message", evt => { + if (evt.data === "claim") { + evt.waitUntil(clients.claim()); + } +}); diff --git a/dom/serviceworkers/test/empty.html b/dom/serviceworkers/test/empty.html new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/dom/serviceworkers/test/empty.html diff --git a/dom/serviceworkers/test/empty.js b/dom/serviceworkers/test/empty.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/dom/serviceworkers/test/empty.js diff --git a/dom/serviceworkers/test/empty_with_utils.html b/dom/serviceworkers/test/empty_with_utils.html new file mode 100644 index 0000000000..75f0aa8872 --- /dev/null +++ b/dom/serviceworkers/test/empty_with_utils.html @@ -0,0 +1,13 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <script src="utils.js" type="text/javascript"></script> +</head> +<body> +</body> +</html> diff --git a/dom/serviceworkers/test/error_reporting_helpers.js b/dom/serviceworkers/test/error_reporting_helpers.js new file mode 100644 index 0000000000..42ddbe42a2 --- /dev/null +++ b/dom/serviceworkers/test/error_reporting_helpers.js @@ -0,0 +1,73 @@ +"use strict"; + +/** + * Helpers for use in tests that want to verify that localized error messages + * are logged during the test. Because most of our errors (ex: + * ServiceWorkerManager) generate nsIScriptError instances with flattened + * strings (the interpolated arguments aren't kept around), we load the string + * bundle and use it to derive the exact string message we expect for the given + * payload. + **/ + +let stringBundleService = SpecialPowers.Cc[ + "@mozilla.org/intl/stringbundle;1" +].getService(SpecialPowers.Ci.nsIStringBundleService); +let localizer = stringBundleService.createBundle( + "chrome://global/locale/dom/dom.properties" +); + +/** + * Start monitoring the console for the given localized error message string(s) + * with the given arguments to be logged. Call before running code that will + * generate the console message. Pair with a call to + * `wait_for_expected_message` invoked after the message should have been + * generated. + * + * Multiple error messages can be expected, just repeat the msgId and args + * argument pair as needed. + * + * @param {String} msgId + * The localization message identifier used in the properties file. + * @param {String[]} args + * The list of formatting arguments we expect the error to be generated with. + * @return {Object} Promise/handle to pass to wait_for_expected_message. + */ +function expect_console_message(/* msgId, args, ... */) { + let expectations = []; + // process repeated paired arguments of: msgId, args + for (let i = 0; i < arguments.length; i += 2) { + let msgId = arguments[i]; + let args = arguments[i + 1]; + if (args.length === 0) { + expectations.push({ errorMessage: localizer.GetStringFromName(msgId) }); + } else { + expectations.push({ + errorMessage: localizer.formatStringFromName(msgId, args), + }); + } + } + return new Promise(resolve => { + SimpleTest.monitorConsole(resolve, expectations); + }); +} +let expect_console_messages = expect_console_message; + +/** + * Stop monitoring the console, returning a Promise that will be resolved when + * the sentinel console message sent through the async data path has been + * received. The Promise will not reject on failure; instead a mochitest + * failure will have been generated by ok(false)/equivalent by the time it is + * resolved. + */ +function wait_for_expected_message(expectedPromise) { + SimpleTest.endMonitorConsole(); + return expectedPromise; +} + +/** + * Derive an absolute URL string from a relative URL to simplify error message + * argument generation. + */ +function make_absolute_url(relUrl) { + return new URL(relUrl, window.location).href; +} diff --git a/dom/serviceworkers/test/eval_worker.js b/dom/serviceworkers/test/eval_worker.js new file mode 100644 index 0000000000..79b4808e66 --- /dev/null +++ b/dom/serviceworkers/test/eval_worker.js @@ -0,0 +1 @@ +eval("1+1"); diff --git a/dom/serviceworkers/test/eventsource/eventsource.resource b/dom/serviceworkers/test/eventsource/eventsource.resource new file mode 100644 index 0000000000..eb62cbd4c5 --- /dev/null +++ b/dom/serviceworkers/test/eventsource/eventsource.resource @@ -0,0 +1,22 @@ +:this file must be enconded in utf8 +:and its Content-Type must be equal to text/event-stream + +retry:500 +data: 2 +unknow: unknow + +event: other_event_name +retry:500 +data: 2 +unknow: unknow + +event: click +retry:500 + +event: blur +retry:500 + +event:keypress +retry:500 + + diff --git a/dom/serviceworkers/test/eventsource/eventsource.resource^headers^ b/dom/serviceworkers/test/eventsource/eventsource.resource^headers^ new file mode 100644 index 0000000000..5b88be7c32 --- /dev/null +++ b/dom/serviceworkers/test/eventsource/eventsource.resource^headers^ @@ -0,0 +1,3 @@ +Content-Type: text/event-stream +Cache-Control: no-cache, must-revalidate +Access-Control-Allow-Origin: * diff --git a/dom/serviceworkers/test/eventsource/eventsource_cors_response.html b/dom/serviceworkers/test/eventsource/eventsource_cors_response.html new file mode 100644 index 0000000000..115a0f5c65 --- /dev/null +++ b/dom/serviceworkers/test/eventsource/eventsource_cors_response.html @@ -0,0 +1,75 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1182103 - Test EventSource scenarios with fetch interception</title> + <script type="text/javascript"> + + var prefix = "http://mochi.test:8888/tests/dom/serviceworkers/test/eventsource/"; + + function ok(aCondition, aMessage) { + parent.postMessage({status: "callback", data: "ok", condition: aCondition, message: aMessage}, "*"); + } + + function doUnregister() { + navigator.serviceWorker.getRegistration().then(swr => { + swr.unregister().then(function(result) { + ok(result, "Unregister should return true."); + parent.postMessage({status: "callback", data: "done"}, "*"); + }, function(e) { + ok(false, "Unregistering the SW failed with " + e); + }); + }); + } + + function doEventSource() { + var source = new EventSource(prefix + "eventsource.resource"); + source.onmessage = function(e) { + source.onmessage = null; + source.close(); + ok(true, "EventSource should work with cors responses"); + doUnregister(); + }; + source.onerror = function(error) { + source.onerror = null; + source.close(); + ok(false, "Something went wrong"); + }; + } + + function onLoad() { + if (!parent) { + dump("eventsource/eventsource_cors_response.html shouldn't be launched directly!"); + } + + window.addEventListener("message", function onMessage(e) { + if (e.data.status === "callback") { + switch(e.data.data) { + case "eventsource": + doEventSource(); + window.removeEventListener("message", onMessage); + break; + default: + ok(false, "Something went wrong") + break + } + } + }); + + navigator.serviceWorker.ready.then(function() { + parent.postMessage({status: "callback", data: "ready"}, "*"); + }); + + navigator.serviceWorker.addEventListener("message", function(event) { + parent.postMessage(event.data, "*"); + }); + } + + </script> +</head> +<body onload="onLoad()"> +</body> +</html> diff --git a/dom/serviceworkers/test/eventsource/eventsource_cors_response_intercept_worker.js b/dom/serviceworkers/test/eventsource/eventsource_cors_response_intercept_worker.js new file mode 100644 index 0000000000..0b9535d08e --- /dev/null +++ b/dom/serviceworkers/test/eventsource/eventsource_cors_response_intercept_worker.js @@ -0,0 +1,30 @@ +// Cross origin request +var prefix = "http://example.com/tests/dom/serviceworkers/test/eventsource/"; + +self.importScripts("eventsource_worker_helper.js"); + +self.addEventListener("fetch", function(event) { + var request = event.request; + var url = new URL(request.url); + + if ( + url.pathname !== + "/tests/dom/serviceworkers/test/eventsource/eventsource.resource" + ) { + return; + } + + ok(request.mode === "cors", "EventSource should make a CORS request"); + ok( + request.cache === "no-store", + "EventSource should make a no-store request" + ); + var fetchRequest = new Request(prefix + "eventsource.resource", { + mode: "cors", + }); + event.respondWith( + fetch(fetchRequest).then(fetchResponse => { + return fetchResponse; + }) + ); +}); diff --git a/dom/serviceworkers/test/eventsource/eventsource_mixed_content_cors_response.html b/dom/serviceworkers/test/eventsource/eventsource_mixed_content_cors_response.html new file mode 100644 index 0000000000..970cae517f --- /dev/null +++ b/dom/serviceworkers/test/eventsource/eventsource_mixed_content_cors_response.html @@ -0,0 +1,75 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1182103 - Test EventSource scenarios with fetch interception</title> + <script type="text/javascript"> + + var prefix = "https://example.com/tests/dom/serviceworkers/test/eventsource/"; + + function ok(aCondition, aMessage) { + parent.postMessage({status: "callback", data: "ok", condition: aCondition, message: aMessage}, "*"); + } + + function doUnregister() { + navigator.serviceWorker.getRegistration().then(swr => { + swr.unregister().then(function(result) { + ok(result, "Unregister should return true."); + parent.postMessage({status: "callback", data: "done"}, "*"); + }, function(e) { + ok(false, "Unregistering the SW failed with " + e); + }); + }); + } + + function doEventSource() { + var source = new EventSource(prefix + "eventsource.resource"); + source.onmessage = function(e) { + source.onmessage = null; + source.close(); + ok(false, "Something went wrong"); + }; + source.onerror = function(error) { + source.onerror = null; + source.close(); + ok(true, "EventSource should not work with mixed content cors responses"); + doUnregister(); + }; + } + + function onLoad() { + if (!parent) { + dump("eventsource/eventsource_cors_response.html shouldn't be launched directly!"); + } + + window.addEventListener("message", function onMessage(e) { + if (e.data.status === "callback") { + switch(e.data.data) { + case "eventsource": + doEventSource(); + window.removeEventListener("message", onMessage); + break; + default: + ok(false, "Something went wrong") + break + } + } + }); + + navigator.serviceWorker.ready.then(function() { + parent.postMessage({status: "callback", data: "ready"}, "*"); + }); + + navigator.serviceWorker.addEventListener("message", function(event) { + parent.postMessage(event.data, "*"); + }); + } + + </script> +</head> +<body onload="onLoad()"> +</body> +</html> diff --git a/dom/serviceworkers/test/eventsource/eventsource_mixed_content_cors_response_intercept_worker.js b/dom/serviceworkers/test/eventsource/eventsource_mixed_content_cors_response_intercept_worker.js new file mode 100644 index 0000000000..754e461a01 --- /dev/null +++ b/dom/serviceworkers/test/eventsource/eventsource_mixed_content_cors_response_intercept_worker.js @@ -0,0 +1,29 @@ +var prefix = "http://example.com/tests/dom/serviceworkers/test/eventsource/"; + +self.importScripts("eventsource_worker_helper.js"); + +self.addEventListener("fetch", function(event) { + var request = event.request; + var url = new URL(request.url); + + if ( + url.pathname !== + "/tests/dom/serviceworkers/test/eventsource/eventsource.resource" + ) { + return; + } + + ok(request.mode === "cors", "EventSource should make a CORS request"); + ok( + request.cache === "no-store", + "EventSource should make a no-store request" + ); + var fetchRequest = new Request(prefix + "eventsource.resource", { + mode: "cors", + }); + event.respondWith( + fetch(fetchRequest).then(fetchResponse => { + return fetchResponse; + }) + ); +}); diff --git a/dom/serviceworkers/test/eventsource/eventsource_opaque_response.html b/dom/serviceworkers/test/eventsource/eventsource_opaque_response.html new file mode 100644 index 0000000000..bce12259cc --- /dev/null +++ b/dom/serviceworkers/test/eventsource/eventsource_opaque_response.html @@ -0,0 +1,75 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1182103 - Test EventSource scenarios with fetch interception</title> + <script type="text/javascript"> + + var prefix = "http://mochi.test:8888/tests/dom/serviceworkers/test/eventsource/"; + + function ok(aCondition, aMessage) { + parent.postMessage({status: "callback", data: "ok", condition: aCondition, message: aMessage}, "*"); + } + + function doUnregister() { + navigator.serviceWorker.getRegistration().then(swr => { + swr.unregister().then(function(result) { + ok(result, "Unregister should return true."); + parent.postMessage({status: "callback", data: "done"}, "*"); + }, function(e) { + ok(false, "Unregistering the SW failed with " + e); + }); + }); + } + + function doEventSource() { + var source = new EventSource(prefix + "eventsource.resource"); + source.onmessage = function(e) { + source.onmessage = null; + source.close(); + ok(false, "Something went wrong"); + }; + source.onerror = function(error) { + source.onerror = null; + source.close(); + ok(true, "EventSource should not work with opaque responses"); + doUnregister(); + }; + } + + function onLoad() { + if (!parent) { + dump("eventsource/eventsource_opaque_response.html shouldn't be launched directly!"); + } + + window.addEventListener("message", function onMessage(e) { + if (e.data.status === "callback") { + switch(e.data.data) { + case "eventsource": + doEventSource(); + window.removeEventListener("message", onMessage); + break; + default: + ok(false, "Something went wrong") + break + } + } + }); + + navigator.serviceWorker.ready.then(function() { + parent.postMessage({status: "callback", data: "ready"}, "*"); + }); + + navigator.serviceWorker.addEventListener("message", function(event) { + parent.postMessage(event.data, "*"); + }); + } + + </script> +</head> +<body onload="onLoad()"> +</body> +</html> diff --git a/dom/serviceworkers/test/eventsource/eventsource_opaque_response_intercept_worker.js b/dom/serviceworkers/test/eventsource/eventsource_opaque_response_intercept_worker.js new file mode 100644 index 0000000000..f663e7e39c --- /dev/null +++ b/dom/serviceworkers/test/eventsource/eventsource_opaque_response_intercept_worker.js @@ -0,0 +1,30 @@ +// Cross origin request +var prefix = "http://example.com/tests/dom/serviceworkers/test/eventsource/"; + +self.importScripts("eventsource_worker_helper.js"); + +self.addEventListener("fetch", function(event) { + var request = event.request; + var url = new URL(request.url); + + if ( + url.pathname !== + "/tests/dom/serviceworkers/test/eventsource/eventsource.resource" + ) { + return; + } + + ok(request.mode === "cors", "EventSource should make a CORS request"); + ok( + request.cache === "no-store", + "EventSource should make a no-store request" + ); + var fetchRequest = new Request(prefix + "eventsource.resource", { + mode: "no-cors", + }); + event.respondWith( + fetch(fetchRequest).then(fetchResponse => { + return fetchResponse; + }) + ); +}); diff --git a/dom/serviceworkers/test/eventsource/eventsource_register_worker.html b/dom/serviceworkers/test/eventsource/eventsource_register_worker.html new file mode 100644 index 0000000000..59e8e92ab6 --- /dev/null +++ b/dom/serviceworkers/test/eventsource/eventsource_register_worker.html @@ -0,0 +1,27 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1182103 - Test EventSource scenarios with fetch interception</title> + <script type="text/javascript"> + + function getURLParam (aTarget, aValue) { + return decodeURI(aTarget.search.replace(new RegExp("^(?:.*[&\\?]" + encodeURI(aValue).replace(/[\.\+\*]/g, "\\$&") + "(?:\\=([^&]*))?)?.*$", "i"), "$1")); + } + + function onLoad() { + navigator.serviceWorker.ready.then(function() { + parent.postMessage({status: "callback", data: "done"}, "*"); + }); + + navigator.serviceWorker.register(getURLParam(document.location, "script"), {scope: "."}); + } + + </script> +</head> +<body onload="onLoad()"> +</body> +</html> diff --git a/dom/serviceworkers/test/eventsource/eventsource_synthetic_response.html b/dom/serviceworkers/test/eventsource/eventsource_synthetic_response.html new file mode 100644 index 0000000000..7f6228c91e --- /dev/null +++ b/dom/serviceworkers/test/eventsource/eventsource_synthetic_response.html @@ -0,0 +1,75 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1182103 - Test EventSource scenarios with fetch interception</title> + <script type="text/javascript"> + + var prefix = "http://mochi.test:8888/tests/dom/serviceworkers/test/eventsource/"; + + function ok(aCondition, aMessage) { + parent.postMessage({status: "callback", data: "ok", condition: aCondition, message: aMessage}, "*"); + } + + function doUnregister() { + navigator.serviceWorker.getRegistration().then(swr => { + swr.unregister().then(function(result) { + ok(result, "Unregister should return true."); + parent.postMessage({status: "callback", data: "done"}, "*"); + }, function(e) { + ok(false, "Unregistering the SW failed with " + e); + }); + }); + } + + function doEventSource() { + var source = new EventSource(prefix + "eventsource.resource"); + source.onmessage = function(e) { + source.onmessage = null; + source.close(); + ok(true, "EventSource should work with synthetic responses"); + doUnregister(); + }; + source.onerror = function(error) { + source.onmessage = null; + source.close(); + ok(false, "Something went wrong"); + }; + } + + function onLoad() { + if (!parent) { + dump("eventsource/eventsource_synthetic_response.html shouldn't be launched directly!"); + } + + window.addEventListener("message", function onMessage(e) { + if (e.data.status === "callback") { + switch(e.data.data) { + case "eventsource": + doEventSource(); + window.removeEventListener("message", onMessage); + break; + default: + ok(false, "Something went wrong") + break + } + } + }); + + navigator.serviceWorker.ready.then(function() { + parent.postMessage({status: "callback", data: "ready"}, "*"); + }); + + navigator.serviceWorker.addEventListener("message", function(event) { + parent.postMessage(event.data, "*"); + }); + } + + </script> +</head> +<body onload="onLoad()"> +</body> +</html> diff --git a/dom/serviceworkers/test/eventsource/eventsource_synthetic_response_intercept_worker.js b/dom/serviceworkers/test/eventsource/eventsource_synthetic_response_intercept_worker.js new file mode 100644 index 0000000000..32aeb91b81 --- /dev/null +++ b/dom/serviceworkers/test/eventsource/eventsource_synthetic_response_intercept_worker.js @@ -0,0 +1,27 @@ +self.importScripts("eventsource_worker_helper.js"); + +self.addEventListener("fetch", function(event) { + var request = event.request; + var url = new URL(request.url); + + if ( + url.pathname !== + "/tests/dom/serviceworkers/test/eventsource/eventsource.resource" + ) { + return; + } + + ok(request.mode === "cors", "EventSource should make a CORS request"); + var headerList = { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache, must-revalidate", + }; + var headers = new Headers(headerList); + var init = { + headers, + mode: "cors", + }; + var body = "data: data0\r\r"; + var response = new Response(body, init); + event.respondWith(response); +}); diff --git a/dom/serviceworkers/test/eventsource/eventsource_worker_helper.js b/dom/serviceworkers/test/eventsource/eventsource_worker_helper.js new file mode 100644 index 0000000000..b110ae5f58 --- /dev/null +++ b/dom/serviceworkers/test/eventsource/eventsource_worker_helper.js @@ -0,0 +1,17 @@ +function ok(aCondition, aMessage) { + return new Promise(function(resolve, reject) { + self.clients.matchAll().then(function(res) { + if (!res.length) { + reject(); + return; + } + res[0].postMessage({ + status: "callback", + data: "ok", + condition: aCondition, + message: aMessage, + }); + resolve(); + }); + }); +} diff --git a/dom/serviceworkers/test/fetch.js b/dom/serviceworkers/test/fetch.js new file mode 100644 index 0000000000..ca723821ea --- /dev/null +++ b/dom/serviceworkers/test/fetch.js @@ -0,0 +1,33 @@ +function get_query_params(url) { + var search = new URL(url).search; + if (!search) { + return {}; + } + var ret = {}; + var params = search.substring(1).split("&"); + params.forEach(function(param) { + var element = param.split("="); + ret[decodeURIComponent(element[0])] = decodeURIComponent(element[1]); + }); + return ret; +} + +addEventListener("fetch", function(event) { + if (event.request.url.includes("fail.html")) { + event.respondWith(fetch("hello.html", { integrity: "abc" })); + } else if (event.request.url.includes("fake.html")) { + event.respondWith(fetch("hello.html")); + } else if (event.request.url.includes("file_js_cache")) { + event.respondWith(fetch(event.request)); + } else if (event.request.url.includes("redirect")) { + let param = get_query_params(event.request.url); + let url = param.url; + let mode = param.mode; + + event.respondWith(fetch(url, { mode })); + } +}); + +addEventListener("activate", function(event) { + event.waitUntil(clients.claim()); +}); diff --git a/dom/serviceworkers/test/fetch/cookie/cookie_test.js b/dom/serviceworkers/test/fetch/cookie/cookie_test.js new file mode 100644 index 0000000000..cdfee8949d --- /dev/null +++ b/dom/serviceworkers/test/fetch/cookie/cookie_test.js @@ -0,0 +1,11 @@ +self.addEventListener("fetch", function(event) { + if (event.request.url.includes("synth.html")) { + var body = + "<script>" + + 'window.parent.postMessage({status: "done", cookie: document.cookie}, "*");' + + "</script>"; + event.respondWith( + new Response(body, { headers: { "Content-Type": "text/html" } }) + ); + } +}); diff --git a/dom/serviceworkers/test/fetch/cookie/register.html b/dom/serviceworkers/test/fetch/cookie/register.html new file mode 100644 index 0000000000..99eabaf0a2 --- /dev/null +++ b/dom/serviceworkers/test/fetch/cookie/register.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<script src="../../utils.js"></script> +<script> + function ok(v, msg) { + window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*"); + } + + function done(reg) { + ok(reg.active, "The active worker should be available."); + window.parent.postMessage({status: "registrationdone"}, "*"); + } + + document.cookie = "foo=bar"; + + navigator.serviceWorker.register("cookie_test.js", {scope: "."}) + .then(reg => { + return waitForState(reg.installing, "activated", reg); + }).then(done); +</script> diff --git a/dom/serviceworkers/test/fetch/cookie/unregister.html b/dom/serviceworkers/test/fetch/cookie/unregister.html new file mode 100644 index 0000000000..1f13508fa7 --- /dev/null +++ b/dom/serviceworkers/test/fetch/cookie/unregister.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.getRegistration(".").then(function(registration) { + registration.unregister().then(function(success) { + if (success) { + window.parent.postMessage({status: "unregistrationdone"}, "*"); + } + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + }); +</script> diff --git a/dom/serviceworkers/test/fetch/deliver-gzip.sjs b/dom/serviceworkers/test/fetch/deliver-gzip.sjs new file mode 100644 index 0000000000..31d2a71f7a --- /dev/null +++ b/dom/serviceworkers/test/fetch/deliver-gzip.sjs @@ -0,0 +1,23 @@ +"use strict"; + +function handleRequest(request, response) { + // The string "hello" repeated 10 times followed by newline. Compressed using gzip. + /* eslint-disable prettier/prettier */ + let bytes = [0x1f, 0x8b, 0x08, 0x08, 0x4d, 0xe2, 0xf9, 0x54, 0x00, 0x03, 0x68, + 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0xcb, 0x48, 0xcd, 0xc9, 0xc9, 0xcf, + 0x20, 0x85, 0xe0, 0x02, 0x00, 0xf5, 0x4b, 0x38, 0xcf, 0x33, 0x00, + 0x00, 0x00]; + + /* eslint-enable prettier/prettier */ + + response.setHeader("Content-Encoding", "gzip", false); + response.setHeader("Content-Length", "" + bytes.length, false); + response.setHeader("Content-Type", "text/plain", false); + + let bos = Components.classes[ + "@mozilla.org/binaryoutputstream;1" + ].createInstance(Components.interfaces.nsIBinaryOutputStream); + bos.setOutputStream(response.bodyOutputStream); + + bos.writeByteArray(bytes); +} diff --git a/dom/serviceworkers/test/fetch/fetch_tests.js b/dom/serviceworkers/test/fetch/fetch_tests.js new file mode 100644 index 0000000000..bbe55c3a35 --- /dev/null +++ b/dom/serviceworkers/test/fetch/fetch_tests.js @@ -0,0 +1,716 @@ +var origin = "http://mochi.test:8888"; + +function fetchXHRWithMethod(name, method, onload, onerror, headers) { + expectAsyncResult(); + + onload = + onload || + function() { + my_ok(false, "XHR load should not complete successfully"); + finish(); + }; + onerror = + onerror || + function() { + my_ok( + false, + "XHR load for " + name + " should be intercepted successfully" + ); + finish(); + }; + + var x = new XMLHttpRequest(); + x.open(method, name, true); + x.onload = function() { + onload(x); + }; + x.onerror = function() { + onerror(x); + }; + headers = headers || []; + headers.forEach(function(header) { + x.setRequestHeader(header[0], header[1]); + }); + x.send(); +} + +var corsServerPath = + "/tests/dom/security/test/cors/file_CrossSiteXHR_server.sjs"; +var corsServerURL = "http://example.com" + corsServerPath; + +function redirectURL(hops) { + return ( + hops[0].server + + corsServerPath + + "?hop=1&hops=" + + encodeURIComponent(JSON.stringify(hops)) + ); +} + +function fetchXHR(name, onload, onerror, headers) { + return fetchXHRWithMethod(name, "GET", onload, onerror, headers); +} + +fetchXHR("bare-synthesized.txt", function(xhr) { + my_ok(xhr.status == 200, "load should be successful"); + my_ok( + xhr.responseText == "synthesized response body", + "load should have synthesized response" + ); + finish(); +}); + +fetchXHR("test-respondwith-response.txt", function(xhr) { + my_ok( + xhr.status == 200, + "test-respondwith-response load should be successful" + ); + my_ok( + xhr.responseText == "test-respondwith-response response body", + "load should have response" + ); + finish(); +}); + +fetchXHR("synthesized-404.txt", function(xhr) { + my_ok(xhr.status == 404, "load should 404"); + my_ok( + xhr.responseText == "synthesized response body", + "404 load should have synthesized response" + ); + finish(); +}); + +fetchXHR("synthesized-headers.txt", function(xhr) { + my_ok(xhr.status == 200, "load should be successful"); + my_ok( + xhr.getResponseHeader("X-Custom-Greeting") === "Hello", + "custom header should be set" + ); + my_ok( + xhr.responseText == "synthesized response body", + "custom header load should have synthesized response" + ); + finish(); +}); + +fetchXHR("synthesized-redirect-real-file.txt", function(xhr) { + dump("Got status AARRGH " + xhr.status + " " + xhr.responseText + "\n"); + my_ok(xhr.status == 200, "load should be successful"); + my_ok( + xhr.responseText == "This is a real file.\n", + "Redirect to real file should complete." + ); + finish(); +}); + +fetchXHR("synthesized-redirect-twice-real-file.txt", function(xhr) { + my_ok(xhr.status == 200, "load should be successful"); + my_ok( + xhr.responseText == "This is a real file.\n", + "Redirect to real file (twice) should complete." + ); + finish(); +}); + +fetchXHR("synthesized-redirect-synthesized.txt", function(xhr) { + my_ok(xhr.status == 200, "synth+redirect+synth load should be successful"); + my_ok( + xhr.responseText == "synthesized response body", + "load should have redirected+synthesized response" + ); + finish(); +}); + +fetchXHR("synthesized-redirect-twice-synthesized.txt", function(xhr) { + my_ok( + xhr.status == 200, + "synth+redirect+synth (twice) load should be successful" + ); + my_ok( + xhr.responseText == "synthesized response body", + "load should have redirected+synthesized (twice) response" + ); + finish(); +}); + +fetchXHR("redirect.sjs", function(xhr) { + my_ok(xhr.status == 404, "redirected load should be uninterrupted"); + finish(); +}); + +fetchXHR("ignored.txt", function(xhr) { + my_ok(xhr.status == 404, "load should be uninterrupted"); + finish(); +}); + +fetchXHR("rejected.txt", null, function(xhr) { + my_ok(xhr.status == 0, "load should not complete"); + finish(); +}); + +fetchXHR("nonresponse.txt", null, function(xhr) { + my_ok(xhr.status == 0, "load should not complete"); + finish(); +}); + +fetchXHR("nonresponse2.txt", null, function(xhr) { + my_ok(xhr.status == 0, "load should not complete"); + finish(); +}); + +fetchXHR("nonpromise.txt", null, function(xhr) { + my_ok(xhr.status == 0, "load should not complete"); + finish(); +}); + +fetchXHR( + "headers.txt", + function(xhr) { + my_ok(xhr.status == 200, "load should be successful"); + my_ok(xhr.responseText == "1", "request header checks should have passed"); + finish(); + }, + null, + [ + ["X-Test1", "header1"], + ["X-Test2", "header2"], + ] +); + +fetchXHR("http://user:pass@mochi.test:8888/user-pass", function(xhr) { + my_ok(xhr.status == 200, "load should be successful"); + my_ok( + xhr.responseText == "http://user:pass@mochi.test:8888/user-pass", + "The username and password should be preserved" + ); + finish(); +}); + +fetchXHR("readable-stream.txt", function(xhr) { + my_ok(xhr.status == 200, "loading completed"); + my_ok(xhr.responseText == "Hello!", "The message is correct!"); + finish(); +}); + +fetchXHR( + "readable-stream-locked.txt", + function(xhr) { + my_ok(false, "This should not be called!"); + finish(); + }, + function() { + my_ok(true, "The exception has been correctly handled!"); + finish(); + } +); + +fetchXHR( + "readable-stream-with-exception.txt", + function(xhr) { + my_ok(false, "This should not be called!"); + finish(); + }, + function() { + my_ok(true, "The exception has been correctly handled!"); + finish(); + } +); + +fetchXHR( + "readable-stream-with-exception2.txt", + function(xhr) { + my_ok(false, "This should not be called!"); + finish(); + }, + function() { + my_ok(true, "The exception has been correctly handled!"); + finish(); + } +); + +fetchXHR( + "readable-stream-already-consumed.txt", + function(xhr) { + my_ok(false, "This should not be called!"); + finish(); + }, + function() { + my_ok(true, "The exception has been correctly handled!"); + finish(); + } +); + +var expectedUncompressedResponse = ""; +for (let i = 0; i < 10; ++i) { + expectedUncompressedResponse += "hello"; +} +expectedUncompressedResponse += "\n"; + +// ServiceWorker does not intercept, at which point the network request should +// be correctly decoded. +fetchXHR("deliver-gzip.sjs", function(xhr) { + my_ok(xhr.status == 200, "network gzip load should be successful"); + my_ok( + xhr.responseText == expectedUncompressedResponse, + "network gzip load should have synthesized response." + ); + my_ok( + xhr.getResponseHeader("Content-Encoding") == "gzip", + "network Content-Encoding should be gzip." + ); + my_ok( + xhr.getResponseHeader("Content-Length") == "35", + "network Content-Length should be of original gzipped file." + ); + finish(); +}); + +fetchXHR("hello.gz", function(xhr) { + my_ok(xhr.status == 200, "gzip load should be successful"); + my_ok( + xhr.responseText == expectedUncompressedResponse, + "gzip load should have synthesized response." + ); + my_ok( + xhr.getResponseHeader("Content-Encoding") == "gzip", + "Content-Encoding should be gzip." + ); + my_ok( + xhr.getResponseHeader("Content-Length") == "35", + "Content-Length should be of original gzipped file." + ); + finish(); +}); + +fetchXHR("hello-after-extracting.gz", function(xhr) { + my_ok(xhr.status == 200, "gzip load after extracting should be successful"); + my_ok( + xhr.responseText == expectedUncompressedResponse, + "gzip load after extracting should have synthesized response." + ); + my_ok( + xhr.getResponseHeader("Content-Encoding") == "gzip", + "Content-Encoding after extracting should be gzip." + ); + my_ok( + xhr.getResponseHeader("Content-Length") == "35", + "Content-Length after extracting should be of original gzipped file." + ); + finish(); +}); + +fetchXHR(corsServerURL + "?status=200&allowOrigin=*", function(xhr) { + my_ok( + xhr.status == 200, + "cross origin load with correct headers should be successful" + ); + my_ok( + xhr.getResponseHeader("access-control-allow-origin") == null, + "cors headers should be filtered out" + ); + finish(); +}); + +// Verify origin header is sent properly even when we have a no-intercept SW. +var uriOrigin = encodeURIComponent(origin); +fetchXHR( + "http://example.org" + + corsServerPath + + "?ignore&status=200&origin=" + + uriOrigin + + "&allowOrigin=" + + uriOrigin, + function(xhr) { + my_ok( + xhr.status == 200, + "cross origin load with correct headers should be successful" + ); + my_ok( + xhr.getResponseHeader("access-control-allow-origin") == null, + "cors headers should be filtered out" + ); + finish(); + } +); + +// Verify that XHR is considered CORS tainted even when original URL is same-origin +// redirected to cross-origin. +fetchXHR( + redirectURL([ + { server: origin }, + { server: "http://example.org", allowOrigin: origin }, + ]), + function(xhr) { + my_ok( + xhr.status == 200, + "cross origin load with correct headers should be successful" + ); + my_ok( + xhr.getResponseHeader("access-control-allow-origin") == null, + "cors headers should be filtered out" + ); + finish(); + } +); + +// Test that CORS preflight requests cannot be intercepted. Performs a +// cross-origin XHR that the SW chooses not to intercept. This requires a +// preflight request, which the SW must not be allowed to intercept. +fetchXHR( + corsServerURL + "?status=200&allowOrigin=*", + null, + function(xhr) { + my_ok( + xhr.status == 0, + "cross origin load with incorrect headers should be a failure" + ); + finish(); + }, + [["X-Unsafe", "unsafe"]] +); + +// Test that CORS preflight requests cannot be intercepted. Performs a +// cross-origin XHR that the SW chooses to intercept and respond with a +// cross-origin fetch. This requires a preflight request, which the SW must not +// be allowed to intercept. +fetchXHR( + "http://example.org" + corsServerPath + "?status=200&allowOrigin=*", + null, + function(xhr) { + my_ok( + xhr.status == 0, + "cross origin load with incorrect headers should be a failure" + ); + finish(); + }, + [["X-Unsafe", "unsafe"]] +); + +// Test that when the page fetches a url the controlling SW forces a redirect to +// another location. This other location fetch should also be intercepted by +// the SW. +fetchXHR("something.txt", function(xhr) { + my_ok(xhr.status == 200, "load should be successful"); + my_ok( + xhr.responseText == "something else response body", + "load should have something else" + ); + finish(); +}); + +// Test fetch will internally get it's SkipServiceWorker flag set. The request is +// made from the SW through fetch(). fetch() fetches a server-side JavaScript +// file that force a redirect. The redirect location fetch does not go through +// the SW. +fetchXHR("redirect_serviceworker.sjs", function(xhr) { + my_ok(xhr.status == 200, "load should be successful"); + my_ok( + xhr.responseText == "// empty worker, always succeed!\n", + "load should have redirection content" + ); + finish(); +}); + +fetchXHR( + "empty-header", + function(xhr) { + my_ok(xhr.status == 200, "load should be successful"); + my_ok( + xhr.responseText == "emptyheader", + "load should have the expected content" + ); + finish(); + }, + null, + [["emptyheader", ""]] +); + +expectAsyncResult(); +fetch( + "http://example.com/tests/dom/security/test/cors/file_CrossSiteXHR_server.sjs?status=200&allowOrigin=*" +).then( + function(res) { + my_ok(res.ok, "Valid CORS request should receive valid response"); + my_ok(res.type == "cors", "Response type should be CORS"); + res.text().then(function(body) { + my_ok( + body === "<res>hello pass</res>\n", + "cors response body should match" + ); + finish(); + }); + }, + function(e) { + my_ok(false, "CORS Fetch failed"); + finish(); + } +); + +expectAsyncResult(); +fetch( + "http://example.com/tests/dom/security/test/cors/file_CrossSiteXHR_server.sjs?status=200", + { mode: "no-cors" } +).then( + function(res) { + my_ok(res.type == "opaque", "Response type should be opaque"); + my_ok(res.status == 0, "Status should be 0"); + res.text().then(function(body) { + my_ok(body === "", "opaque response body should be empty"); + finish(); + }); + }, + function(e) { + my_ok(false, "no-cors Fetch failed"); + finish(); + } +); + +expectAsyncResult(); +fetch("opaque-on-same-origin").then( + function(res) { + my_ok( + false, + "intercepted opaque response for non no-cors request should fail." + ); + finish(); + }, + function(e) { + my_ok( + true, + "intercepted opaque response for non no-cors request should fail." + ); + finish(); + } +); + +expectAsyncResult(); +fetch("http://example.com/opaque-no-cors", { mode: "no-cors" }).then( + function(res) { + my_ok( + res.type == "opaque", + "intercepted opaque response for no-cors request should have type opaque." + ); + finish(); + }, + function(e) { + my_ok( + false, + "intercepted opaque response for no-cors request should pass." + ); + finish(); + } +); + +expectAsyncResult(); +fetch("http://example.com/cors-for-no-cors", { mode: "no-cors" }).then( + function(res) { + my_ok( + res.type == "cors", + "synthesize CORS response should result in outer CORS response" + ); + finish(); + }, + function(e) { + my_ok(false, "cors-for-no-cors request should not reject"); + finish(); + } +); + +function arrayBufferFromString(str) { + var arr = new Uint8Array(str.length); + for (let i = 0; i < str.length; ++i) { + arr[i] = str.charCodeAt(i); + } + return arr; +} + +expectAsyncResult(); +fetch(new Request("body-simple", { method: "POST", body: "my body" })) + .then(function(res) { + return res.text(); + }) + .then(function(body) { + my_ok( + body == "my bodymy body", + "the body of the intercepted fetch should be visible in the SW" + ); + finish(); + }); + +expectAsyncResult(); +fetch( + new Request("body-arraybufferview", { + method: "POST", + body: arrayBufferFromString("my body"), + }) +) + .then(function(res) { + return res.text(); + }) + .then(function(body) { + my_ok( + body == "my bodymy body", + "the ArrayBufferView body of the intercepted fetch should be visible in the SW" + ); + finish(); + }); + +expectAsyncResult(); +fetch( + new Request("body-arraybuffer", { + method: "POST", + body: arrayBufferFromString("my body").buffer, + }) +) + .then(function(res) { + return res.text(); + }) + .then(function(body) { + my_ok( + body == "my bodymy body", + "the ArrayBuffer body of the intercepted fetch should be visible in the SW" + ); + finish(); + }); + +expectAsyncResult(); +var usp = new URLSearchParams(); +usp.set("foo", "bar"); +usp.set("baz", "qux"); +fetch(new Request("body-urlsearchparams", { method: "POST", body: usp })) + .then(function(res) { + return res.text(); + }) + .then(function(body) { + my_ok( + body == "foo=bar&baz=quxfoo=bar&baz=qux", + "the URLSearchParams body of the intercepted fetch should be visible in the SW" + ); + finish(); + }); + +expectAsyncResult(); +var fd = new FormData(); +fd.set("foo", "bar"); +fd.set("baz", "qux"); +fetch(new Request("body-formdata", { method: "POST", body: fd })) + .then(function(res) { + return res.text(); + }) + .then(function(body) { + my_ok( + body.indexOf('Content-Disposition: form-data; name="foo"\r\n\r\nbar') < + body.indexOf('Content-Disposition: form-data; name="baz"\r\n\r\nqux'), + "the FormData body of the intercepted fetch should be visible in the SW" + ); + finish(); + }); + +expectAsyncResult(); +fetch( + new Request("body-blob", { + method: "POST", + body: new Blob(new String("my body")), + }) +) + .then(function(res) { + return res.text(); + }) + .then(function(body) { + my_ok( + body == "my bodymy body", + "the Blob body of the intercepted fetch should be visible in the SW" + ); + finish(); + }); + +expectAsyncResult(); +fetch("interrupt.sjs").then( + function(res) { + my_ok(true, "interrupted fetch succeeded"); + res.text().then( + function(body) { + my_ok(false, "interrupted fetch shouldn't have complete body"); + finish(); + }, + function() { + my_ok(true, "interrupted fetch shouldn't have complete body"); + finish(); + } + ); + }, + function(e) { + my_ok(false, "interrupted fetch failed"); + finish(); + } +); + +["DELETE", "GET", "HEAD", "OPTIONS", "POST", "PUT"].forEach(function(method) { + fetchXHRWithMethod("xhr-method-test.txt", method, function(xhr) { + my_ok(xhr.status == 200, method + " load should be successful"); + if (method === "HEAD") { + my_ok( + xhr.responseText == "", + method + "load should not have synthesized response" + ); + } else { + my_ok( + xhr.responseText == "intercepted " + method, + method + " load should have synthesized response" + ); + } + finish(); + }); +}); + +expectAsyncResult(); +fetch(new Request("empty-header", { headers: { emptyheader: "" } })) + .then(function(res) { + return res.text(); + }) + .then( + function(body) { + my_ok( + body == "emptyheader", + "The empty header was observed in the fetch event" + ); + finish(); + }, + function(err) { + my_ok(false, "A promise was rejected with " + err); + finish(); + } + ); + +expectAsyncResult(); +fetch("fetchevent-extendable") + .then(function(res) { + return res.text(); + }) + .then( + function(body) { + my_ok(body == "extendable", "FetchEvent inherits from ExtendableEvent"); + finish(); + }, + function(err) { + my_ok(false, "A promise was rejected with " + err); + finish(); + } + ); + +expectAsyncResult(); +fetch("fetchevent-request") + .then(function(res) { + return res.text(); + }) + .then( + function(body) { + my_ok(body == "non-nullable", "FetchEvent.request must be non-nullable"); + finish(); + }, + function(err) { + my_ok(false, "A promise was rejected with " + err); + finish(); + } + ); diff --git a/dom/serviceworkers/test/fetch/fetch_worker_script.js b/dom/serviceworkers/test/fetch/fetch_worker_script.js new file mode 100644 index 0000000000..6eb0b18a77 --- /dev/null +++ b/dom/serviceworkers/test/fetch/fetch_worker_script.js @@ -0,0 +1,28 @@ +function my_ok(v, msg) { + postMessage({ type: "ok", value: v, msg }); +} + +function finish() { + postMessage("finish"); +} + +function expectAsyncResult() { + postMessage("expect"); +} + +expectAsyncResult(); +try { + var success = false; + importScripts("nonexistent_imported_script.js"); +} catch (x) {} + +my_ok(success, "worker imported script should be intercepted"); +finish(); + +function check_intercepted_script() { + success = true; +} + +importScripts("fetch_tests.js"); + +finish(); //corresponds to the gExpected increment before creating this worker diff --git a/dom/serviceworkers/test/fetch/hsts/embedder.html b/dom/serviceworkers/test/fetch/hsts/embedder.html new file mode 100644 index 0000000000..ad44809042 --- /dev/null +++ b/dom/serviceworkers/test/fetch/hsts/embedder.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<script> + window.onmessage = function(e) { + window.parent.postMessage(e.data, "*"); + }; +</script> +<iframe src="http://example.com/tests/dom/serviceworkers/test/fetch/hsts/index.html"></iframe> diff --git a/dom/serviceworkers/test/fetch/hsts/hsts_test.js b/dom/serviceworkers/test/fetch/hsts/hsts_test.js new file mode 100644 index 0000000000..f79307f4e4 --- /dev/null +++ b/dom/serviceworkers/test/fetch/hsts/hsts_test.js @@ -0,0 +1,11 @@ +self.addEventListener("fetch", function(event) { + if (event.request.url.includes("index.html")) { + event.respondWith(fetch("realindex.html")); + } else if (event.request.url.includes("image-20px.png")) { + if (event.request.url.indexOf("https://") == 0) { + event.respondWith(fetch("image-40px.png")); + } else { + event.respondWith(Response.error()); + } + } +}); diff --git a/dom/serviceworkers/test/fetch/hsts/image-20px.png b/dom/serviceworkers/test/fetch/hsts/image-20px.png Binary files differnew file mode 100644 index 0000000000..ae6a8a6b88 --- /dev/null +++ b/dom/serviceworkers/test/fetch/hsts/image-20px.png diff --git a/dom/serviceworkers/test/fetch/hsts/image-40px.png b/dom/serviceworkers/test/fetch/hsts/image-40px.png Binary files differnew file mode 100644 index 0000000000..fe391dc8a2 --- /dev/null +++ b/dom/serviceworkers/test/fetch/hsts/image-40px.png diff --git a/dom/serviceworkers/test/fetch/hsts/image.html b/dom/serviceworkers/test/fetch/hsts/image.html new file mode 100644 index 0000000000..7036ea954e --- /dev/null +++ b/dom/serviceworkers/test/fetch/hsts/image.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<script> +onload=function(){ + var img = new Image(); + img.src = "http://example.com/tests/dom/serviceworkers/test/fetch/hsts/image-20px.png"; + img.onload = function() { + window.parent.postMessage({status: "image", data: img.width}, "*"); + }; + img.onerror = function() { + window.parent.postMessage({status: "image", data: "error"}, "*"); + }; +}; +</script> diff --git a/dom/serviceworkers/test/fetch/hsts/realindex.html b/dom/serviceworkers/test/fetch/hsts/realindex.html new file mode 100644 index 0000000000..e7d282fe83 --- /dev/null +++ b/dom/serviceworkers/test/fetch/hsts/realindex.html @@ -0,0 +1,8 @@ +<!DOCTYPE html> +<script> + var securityInfoPresent = !!SpecialPowers.wrap(window).docShell.currentDocumentChannel.securityInfo; + window.parent.postMessage({status: "protocol", + data: location.protocol, + securityInfoPresent}, + "*"); +</script> diff --git a/dom/serviceworkers/test/fetch/hsts/register.html b/dom/serviceworkers/test/fetch/hsts/register.html new file mode 100644 index 0000000000..bcdc146aec --- /dev/null +++ b/dom/serviceworkers/test/fetch/hsts/register.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<script> + function ok(v, msg) { + window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*"); + } + + function done(reg) { + ok(reg.active, "The active worker should be available."); + window.parent.postMessage({status: "registrationdone"}, "*"); + } + + navigator.serviceWorker.ready.then(done); + navigator.serviceWorker.register("hsts_test.js", {scope: "."}); +</script> diff --git a/dom/serviceworkers/test/fetch/hsts/register.html^headers^ b/dom/serviceworkers/test/fetch/hsts/register.html^headers^ new file mode 100644 index 0000000000..a46bf65bd9 --- /dev/null +++ b/dom/serviceworkers/test/fetch/hsts/register.html^headers^ @@ -0,0 +1,2 @@ +Cache-Control: no-cache +Strict-Transport-Security: max-age=60 diff --git a/dom/serviceworkers/test/fetch/hsts/unregister.html b/dom/serviceworkers/test/fetch/hsts/unregister.html new file mode 100644 index 0000000000..1f13508fa7 --- /dev/null +++ b/dom/serviceworkers/test/fetch/hsts/unregister.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.getRegistration(".").then(function(registration) { + registration.unregister().then(function(success) { + if (success) { + window.parent.postMessage({status: "unregistrationdone"}, "*"); + } + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + }); +</script> diff --git a/dom/serviceworkers/test/fetch/https/clonedresponse/https_test.js b/dom/serviceworkers/test/fetch/https/clonedresponse/https_test.js new file mode 100644 index 0000000000..0490017332 --- /dev/null +++ b/dom/serviceworkers/test/fetch/https/clonedresponse/https_test.js @@ -0,0 +1,19 @@ +self.addEventListener("install", function(event) { + event.waitUntil( + caches.open("cache").then(function(cache) { + return cache.add("index.html"); + }) + ); +}); + +self.addEventListener("fetch", function(event) { + if (event.request.url.includes("index.html")) { + event.respondWith( + new Promise(function(resolve, reject) { + caches.match(event.request).then(function(response) { + resolve(response.clone()); + }); + }) + ); + } +}); diff --git a/dom/serviceworkers/test/fetch/https/clonedresponse/index.html b/dom/serviceworkers/test/fetch/https/clonedresponse/index.html new file mode 100644 index 0000000000..a435548443 --- /dev/null +++ b/dom/serviceworkers/test/fetch/https/clonedresponse/index.html @@ -0,0 +1,4 @@ +<!DOCTYPE html> +<script> + window.parent.postMessage({status: "done"}, "*"); +</script> diff --git a/dom/serviceworkers/test/fetch/https/clonedresponse/register.html b/dom/serviceworkers/test/fetch/https/clonedresponse/register.html new file mode 100644 index 0000000000..41774f70d1 --- /dev/null +++ b/dom/serviceworkers/test/fetch/https/clonedresponse/register.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<script> + function ok(v, msg) { + window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*"); + } + + function done(reg) { + ok(reg.active, "The active worker should be available."); + window.parent.postMessage({status: "registrationdone"}, "*"); + } + + navigator.serviceWorker.ready.then(done); + navigator.serviceWorker.register("https_test.js", {scope: "."}); +</script> diff --git a/dom/serviceworkers/test/fetch/https/clonedresponse/unregister.html b/dom/serviceworkers/test/fetch/https/clonedresponse/unregister.html new file mode 100644 index 0000000000..1f13508fa7 --- /dev/null +++ b/dom/serviceworkers/test/fetch/https/clonedresponse/unregister.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.getRegistration(".").then(function(registration) { + registration.unregister().then(function(success) { + if (success) { + window.parent.postMessage({status: "unregistrationdone"}, "*"); + } + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + }); +</script> diff --git a/dom/serviceworkers/test/fetch/https/https_test.js b/dom/serviceworkers/test/fetch/https/https_test.js new file mode 100644 index 0000000000..ad144d58ab --- /dev/null +++ b/dom/serviceworkers/test/fetch/https/https_test.js @@ -0,0 +1,31 @@ +self.addEventListener("install", function(event) { + event.waitUntil( + caches.open("cache").then(function(cache) { + var synth = new Response( + '<!DOCTYPE html><script>window.parent.postMessage({status: "done-synth-sw"}, "*");</script>', + { headers: { "Content-Type": "text/html" } } + ); + return Promise.all([ + cache.add("index.html"), + cache.put("synth-sw.html", synth), + ]); + }) + ); +}); + +self.addEventListener("fetch", function(event) { + if (event.request.url.includes("index.html")) { + event.respondWith(caches.match(event.request)); + } else if (event.request.url.includes("synth-sw.html")) { + event.respondWith(caches.match(event.request)); + } else if (event.request.url.includes("synth-window.html")) { + event.respondWith(caches.match(event.request)); + } else if (event.request.url.includes("synth.html")) { + event.respondWith( + new Response( + '<!DOCTYPE html><script>window.parent.postMessage({status: "done-synth"}, "*");</script>', + { headers: { "Content-Type": "text/html" } } + ) + ); + } +}); diff --git a/dom/serviceworkers/test/fetch/https/index.html b/dom/serviceworkers/test/fetch/https/index.html new file mode 100644 index 0000000000..a435548443 --- /dev/null +++ b/dom/serviceworkers/test/fetch/https/index.html @@ -0,0 +1,4 @@ +<!DOCTYPE html> +<script> + window.parent.postMessage({status: "done"}, "*"); +</script> diff --git a/dom/serviceworkers/test/fetch/https/register.html b/dom/serviceworkers/test/fetch/https/register.html new file mode 100644 index 0000000000..fa666fe957 --- /dev/null +++ b/dom/serviceworkers/test/fetch/https/register.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<script> + function ok(v, msg) { + window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*"); + } + + function done(reg) { + ok(reg.active, "The active worker should be available."); + window.parent.postMessage({status: "registrationdone"}, "*"); + } + + navigator.serviceWorker.ready.then(reg => { + return window.caches.open("cache").then(function(cache) { + var synth = new Response('<!DOCTYPE html><script>window.parent.postMessage({status: "done-synth-window"}, "*");</scri' + 'pt>', + {headers:{"Content-Type": "text/html"}}); + return cache.put('synth-window.html', synth).then(_ => done(reg)); + }); + }); + navigator.serviceWorker.register("https_test.js", {scope: "."}); +</script> diff --git a/dom/serviceworkers/test/fetch/https/unregister.html b/dom/serviceworkers/test/fetch/https/unregister.html new file mode 100644 index 0000000000..1f13508fa7 --- /dev/null +++ b/dom/serviceworkers/test/fetch/https/unregister.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.getRegistration(".").then(function(registration) { + registration.unregister().then(function(success) { + if (success) { + window.parent.postMessage({status: "unregistrationdone"}, "*"); + } + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + }); +</script> diff --git a/dom/serviceworkers/test/fetch/imagecache-maxage/image-20px.png b/dom/serviceworkers/test/fetch/imagecache-maxage/image-20px.png Binary files differnew file mode 100644 index 0000000000..ae6a8a6b88 --- /dev/null +++ b/dom/serviceworkers/test/fetch/imagecache-maxage/image-20px.png diff --git a/dom/serviceworkers/test/fetch/imagecache-maxage/image-40px.png b/dom/serviceworkers/test/fetch/imagecache-maxage/image-40px.png Binary files differnew file mode 100644 index 0000000000..fe391dc8a2 --- /dev/null +++ b/dom/serviceworkers/test/fetch/imagecache-maxage/image-40px.png diff --git a/dom/serviceworkers/test/fetch/imagecache-maxage/index.html b/dom/serviceworkers/test/fetch/imagecache-maxage/index.html new file mode 100644 index 0000000000..0d4c52eedd --- /dev/null +++ b/dom/serviceworkers/test/fetch/imagecache-maxage/index.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<script> +var width, url, width2, url2; +function maybeReport() { + if (width !== undefined && url !== undefined && + width2 !== undefined && url2 !== undefined) { + window.parent.postMessage({status: "result", + width, + width2, + url, + url2}, "*"); + } +} +onload = function() { + width = document.querySelector("img").width; + width2 = document.querySelector("img").width; + maybeReport(); +}; +navigator.serviceWorker.onmessage = function(event) { + if (event.data.suffix == "2") { + url2 = event.data.url; + } else { + url = event.data.url; + } + maybeReport(); +}; +</script> +<img src="image.png"> +<img src="image2.png"> diff --git a/dom/serviceworkers/test/fetch/imagecache-maxage/maxage_test.js b/dom/serviceworkers/test/fetch/imagecache-maxage/maxage_test.js new file mode 100644 index 0000000000..62707566ea --- /dev/null +++ b/dom/serviceworkers/test/fetch/imagecache-maxage/maxage_test.js @@ -0,0 +1,45 @@ +function synthesizeImage(suffix) { + // Serve image-20px for the first page, and image-40px for the second page. + return clients + .matchAll() + .then(clients => { + var url = "image-20px.png"; + clients.forEach(client => { + if (client.url.indexOf("?new") > 0) { + url = "image-40px.png"; + } + client.postMessage({ suffix, url }); + }); + return fetch(url); + }) + .then(response => { + return response.arrayBuffer(); + }) + .then(ab => { + var headers; + if (suffix == "") { + headers = { + "Content-Type": "image/png", + Date: "Tue, 1 Jan 1990 01:02:03 GMT", + "Cache-Control": "max-age=1", + }; + } else { + headers = { + "Content-Type": "image/png", + "Cache-Control": "no-cache", + }; + } + return new Response(ab, { + status: 200, + headers, + }); + }); +} + +self.addEventListener("fetch", function(event) { + if (event.request.url.includes("image.png")) { + event.respondWith(synthesizeImage("")); + } else if (event.request.url.includes("image2.png")) { + event.respondWith(synthesizeImage("2")); + } +}); diff --git a/dom/serviceworkers/test/fetch/imagecache-maxage/register.html b/dom/serviceworkers/test/fetch/imagecache-maxage/register.html new file mode 100644 index 0000000000..af4dde2e29 --- /dev/null +++ b/dom/serviceworkers/test/fetch/imagecache-maxage/register.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<script> + function ok(v, msg) { + window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*"); + } + + function done(reg) { + ok(reg.active, "The active worker should be available."); + window.parent.postMessage({status: "registrationdone"}, "*"); + } + + navigator.serviceWorker.ready.then(done); + navigator.serviceWorker.register("maxage_test.js", {scope: "."}); +</script> diff --git a/dom/serviceworkers/test/fetch/imagecache-maxage/unregister.html b/dom/serviceworkers/test/fetch/imagecache-maxage/unregister.html new file mode 100644 index 0000000000..1f13508fa7 --- /dev/null +++ b/dom/serviceworkers/test/fetch/imagecache-maxage/unregister.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.getRegistration(".").then(function(registration) { + registration.unregister().then(function(success) { + if (success) { + window.parent.postMessage({status: "unregistrationdone"}, "*"); + } + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + }); +</script> diff --git a/dom/serviceworkers/test/fetch/imagecache/image-20px.png b/dom/serviceworkers/test/fetch/imagecache/image-20px.png Binary files differnew file mode 100644 index 0000000000..ae6a8a6b88 --- /dev/null +++ b/dom/serviceworkers/test/fetch/imagecache/image-20px.png diff --git a/dom/serviceworkers/test/fetch/imagecache/image-40px.png b/dom/serviceworkers/test/fetch/imagecache/image-40px.png Binary files differnew file mode 100644 index 0000000000..fe391dc8a2 --- /dev/null +++ b/dom/serviceworkers/test/fetch/imagecache/image-40px.png diff --git a/dom/serviceworkers/test/fetch/imagecache/imagecache_test.js b/dom/serviceworkers/test/fetch/imagecache/imagecache_test.js new file mode 100644 index 0000000000..e1f2af736b --- /dev/null +++ b/dom/serviceworkers/test/fetch/imagecache/imagecache_test.js @@ -0,0 +1,15 @@ +function synthesizeImage() { + return clients.matchAll().then(clients => { + var url = "image-40px.png"; + clients.forEach(client => { + client.postMessage(url); + }); + return fetch(url); + }); +} + +self.addEventListener("fetch", function(event) { + if (event.request.url.includes("image-20px.png")) { + event.respondWith(synthesizeImage()); + } +}); diff --git a/dom/serviceworkers/test/fetch/imagecache/index.html b/dom/serviceworkers/test/fetch/imagecache/index.html new file mode 100644 index 0000000000..f634f68bb7 --- /dev/null +++ b/dom/serviceworkers/test/fetch/imagecache/index.html @@ -0,0 +1,20 @@ +<!DOCTYPE html> +<script> +var width, url; +function maybeReport() { + if (width !== undefined && url !== undefined) { + window.parent.postMessage({status: "result", + width, + url}, "*"); + } +} +onload = function() { + width = document.querySelector("img").width; + maybeReport(); +}; +navigator.serviceWorker.onmessage = function(event) { + url = event.data; + maybeReport(); +}; +</script> +<img src="image-20px.png"> diff --git a/dom/serviceworkers/test/fetch/imagecache/postmortem.html b/dom/serviceworkers/test/fetch/imagecache/postmortem.html new file mode 100644 index 0000000000..53356cd02c --- /dev/null +++ b/dom/serviceworkers/test/fetch/imagecache/postmortem.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<script> +onload = function() { + var width = document.querySelector("img").width; + window.parent.postMessage({status: "postmortem", + width}, "*"); +}; +</script> +<img src="image-20px.png"> diff --git a/dom/serviceworkers/test/fetch/imagecache/register.html b/dom/serviceworkers/test/fetch/imagecache/register.html new file mode 100644 index 0000000000..f6d1eb382f --- /dev/null +++ b/dom/serviceworkers/test/fetch/imagecache/register.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<!-- Load the image here to put it in the image cache --> +<img src="image-20px.png"> +<script> + function ok(v, msg) { + window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*"); + } + + function done(reg) { + ok(reg.active, "The active worker should be available."); + window.parent.postMessage({status: "registrationdone"}, "*"); + } + + navigator.serviceWorker.ready.then(done); + navigator.serviceWorker.register("imagecache_test.js", {scope: "."}); +</script> diff --git a/dom/serviceworkers/test/fetch/imagecache/unregister.html b/dom/serviceworkers/test/fetch/imagecache/unregister.html new file mode 100644 index 0000000000..1f13508fa7 --- /dev/null +++ b/dom/serviceworkers/test/fetch/imagecache/unregister.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.getRegistration(".").then(function(registration) { + registration.unregister().then(function(success) { + if (success) { + window.parent.postMessage({status: "unregistrationdone"}, "*"); + } + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + }); +</script> diff --git a/dom/serviceworkers/test/fetch/importscript-mixedcontent/https_test.js b/dom/serviceworkers/test/fetch/importscript-mixedcontent/https_test.js new file mode 100644 index 0000000000..593171bafe --- /dev/null +++ b/dom/serviceworkers/test/fetch/importscript-mixedcontent/https_test.js @@ -0,0 +1,31 @@ +function sendResponseToParent(response) { + return ` + <!DOCTYPE html> + <script> + window.parent.postMessage({status: "done", data: "${response}"}, "*"); + </script> + `; +} + +self.addEventListener("fetch", function(event) { + if (event.request.url.includes("index.html")) { + var response = "good"; + try { + importScripts("http://example.org/tests/dom/workers/test/foreign.js"); + } catch (e) { + dump("Got error " + e + " when importing the script\n"); + } + if (response === "good") { + try { + importScripts("/tests/dom/workers/test/redirect_to_foreign.sjs"); + } catch (e) { + dump("Got error " + e + " when importing the script\n"); + } + } + event.respondWith( + new Response(sendResponseToParent(response), { + headers: { "Content-Type": "text/html" }, + }) + ); + } +}); diff --git a/dom/serviceworkers/test/fetch/importscript-mixedcontent/register.html b/dom/serviceworkers/test/fetch/importscript-mixedcontent/register.html new file mode 100644 index 0000000000..41774f70d1 --- /dev/null +++ b/dom/serviceworkers/test/fetch/importscript-mixedcontent/register.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<script> + function ok(v, msg) { + window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*"); + } + + function done(reg) { + ok(reg.active, "The active worker should be available."); + window.parent.postMessage({status: "registrationdone"}, "*"); + } + + navigator.serviceWorker.ready.then(done); + navigator.serviceWorker.register("https_test.js", {scope: "."}); +</script> diff --git a/dom/serviceworkers/test/fetch/importscript-mixedcontent/unregister.html b/dom/serviceworkers/test/fetch/importscript-mixedcontent/unregister.html new file mode 100644 index 0000000000..1f13508fa7 --- /dev/null +++ b/dom/serviceworkers/test/fetch/importscript-mixedcontent/unregister.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.getRegistration(".").then(function(registration) { + registration.unregister().then(function(success) { + if (success) { + window.parent.postMessage({status: "unregistrationdone"}, "*"); + } + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + }); +</script> diff --git a/dom/serviceworkers/test/fetch/index.html b/dom/serviceworkers/test/fetch/index.html new file mode 100644 index 0000000000..693810c6fc --- /dev/null +++ b/dom/serviceworkers/test/fetch/index.html @@ -0,0 +1,191 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 94048 - test install event.</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<div id="style-test" style="background-color: white"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + function my_ok(result, msg) { + window.opener.postMessage({status: "ok", result, message: msg}, "*"); + } + + function check_intercepted_script() { + document.getElementById('intercepted-script').test_result = + document.currentScript == document.getElementById('intercepted-script'); + } + + function fetchXHR(name, onload, onerror, headers) { + gExpected++; + + onload = onload || function() { + my_ok(false, "load should not complete successfully"); + finish(); + }; + onerror = onerror || function() { + my_ok(false, "load should be intercepted successfully"); + finish(); + }; + + var x = new XMLHttpRequest(); + x.open('GET', name, true); + x.onload = function() { onload(x) }; + x.onerror = function() { onerror(x) }; + headers = headers || []; + headers.forEach(function(header) { + x.setRequestHeader(header[0], header[1]); + }); + x.send(); + } + + var gExpected = 0; + var gEncountered = 0; + function finish() { + gEncountered++; + if (gEncountered == gExpected) { + window.opener.postMessage({status: "done"}, "*"); + } + } + + function test_onload(creator, complete) { + gExpected++; + var elem = creator(); + elem.onload = function() { + complete.call(elem); + finish(); + }; + elem.onerror = function() { + my_ok(false, elem.tagName + " load should complete successfully"); + finish(); + }; + document.body.appendChild(elem); + } + + function expectAsyncResult() { + gExpected++; + } + + my_ok(navigator.serviceWorker.controller != null, "should be controlled"); +</script> +<script src="fetch_tests.js"></script> +<script> + test_onload(function() { + var elem = document.createElement('img'); + elem.src = "nonexistent_image.gifs"; + elem.id = 'intercepted-img'; + return elem; + }, function() { + my_ok(this.complete, "image should be complete"); + my_ok(this.naturalWidth == 1 && this.naturalHeight == 1, "image should be 1x1 gif"); + }); + + test_onload(function() { + var elem = document.createElement('script'); + elem.id = 'intercepted-script'; + elem.src = "nonexistent_script.js"; + return elem; + }, function() { + my_ok(this.test_result, "script load should be intercepted"); + }); + + test_onload(function() { + var elem = document.createElement('link'); + elem.href = "nonexistent_stylesheet.css"; + elem.rel = "stylesheet"; + return elem; + }, function() { + var styled = document.getElementById('style-test'); + my_ok(window.getComputedStyle(styled).backgroundColor == 'rgb(0, 0, 0)', + "stylesheet load should be intercepted"); + }); + + test_onload(function() { + var elem = document.createElement('iframe'); + elem.id = 'intercepted-iframe'; + elem.src = "nonexistent_page.html"; + return elem; + }, function() { + my_ok(this.test_result, "iframe load should be intercepted"); + }); + + test_onload(function() { + var elem = document.createElement('iframe'); + elem.id = 'intercepted-iframe-2'; + elem.src = "navigate.html"; + return elem; + }, function() { + my_ok(this.test_result, "iframe should successfully load"); + }); + + gExpected++; + var xhr = new XMLHttpRequest(); + xhr.addEventListener("load", function(evt) { + my_ok(evt.target.responseXML === null, "Load synthetic cross origin XML Document should be allowed"); + finish(); + }); + xhr.responseType = "document"; + xhr.open("GET", "load_cross_origin_xml_document_synthetic.xml"); + xhr.send(); + + gExpected++; + fetch( + "load_cross_origin_xml_document_cors.xml", + {mode: "same-origin"} + ).then(function(response) { + // issue: https://github.com/whatwg/fetch/issues/629 + my_ok(false, "Load CORS cross origin XML Document should not be allowed"); + finish(); + }, function(error) { + my_ok(true, "Load CORS cross origin XML Document should not be allowed"); + finish(); + }); + + gExpected++; + fetch( + "load_cross_origin_xml_document_opaque.xml", + {mode: "same-origin"} + ).then(function(response) { + my_ok(false, "Load opaque cross origin XML Document should not be allowed"); + finish(); + }, function(error) { + my_ok(true, "Load opaque cross origin XML Document should not be allowed"); + finish(); + }); + + gExpected++; + var worker = new Worker('nonexistent_worker_script.js'); + worker.onmessage = function(e) { + my_ok(e.data == "worker-intercept-success", "worker load intercepted"); + finish(); + }; + worker.onerror = function() { + my_ok(false, "worker load should be intercepted"); + }; + + gExpected++; + var worker = new Worker('fetch_worker_script.js'); + worker.onmessage = function(e) { + if (e.data == "finish") { + finish(); + } else if (e.data == "expect") { + gExpected++; + } else if (e.data.type == "ok") { + my_ok(e.data.value, "Fetch test on worker: " + e.data.msg); + } + }; + worker.onerror = function() { + my_ok(false, "worker should not cause any errors"); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/fetch/interrupt.sjs b/dom/serviceworkers/test/fetch/interrupt.sjs new file mode 100644 index 0000000000..6e5deeb832 --- /dev/null +++ b/dom/serviceworkers/test/fetch/interrupt.sjs @@ -0,0 +1,20 @@ +function handleRequest(request, response) { + var body = "a"; + for (var i = 0; i < 20; i++) { + body += body; + } + + response.seizePower(); + response.write("HTTP/1.1 200 OK\r\n"); + var count = 10; + response.write("Content-Length: " + body.length * count + "\r\n"); + response.write("Content-Type: text/plain; charset=utf-8\r\n"); + response.write("Cache-Control: no-cache, must-revalidate\r\n"); + response.write("\r\n"); + + for (var i = 0; i < count; i++) { + response.write(body); + } + + throw Components.Exception("", Components.results.NS_BINDING_ABORTED); +} diff --git a/dom/serviceworkers/test/fetch/origin/https/index-https.sjs b/dom/serviceworkers/test/fetch/origin/https/index-https.sjs new file mode 100644 index 0000000000..5250467ec7 --- /dev/null +++ b/dom/serviceworkers/test/fetch/origin/https/index-https.sjs @@ -0,0 +1,8 @@ +function handleRequest(request, response) { + response.setStatusLine(null, 308, "Permanent Redirect"); + response.setHeader( + "Location", + "https://example.org/tests/dom/serviceworkers/test/fetch/origin/https/realindex.html", + false + ); +} diff --git a/dom/serviceworkers/test/fetch/origin/https/origin_test.js b/dom/serviceworkers/test/fetch/origin/https/origin_test.js new file mode 100644 index 0000000000..5f76befc00 --- /dev/null +++ b/dom/serviceworkers/test/fetch/origin/https/origin_test.js @@ -0,0 +1,29 @@ +var prefix = "/tests/dom/serviceworkers/test/fetch/origin/https/"; + +function addOpaqueRedirect(cache, file) { + return fetch(new Request(prefix + file, { redirect: "manual" })).then( + function(response) { + return cache.put(prefix + file, response); + } + ); +} + +self.addEventListener("install", function(event) { + event.waitUntil( + self.caches.open("origin-cache").then(c => { + return addOpaqueRedirect(c, "index-https.sjs"); + }) + ); +}); + +self.addEventListener("fetch", function(event) { + if (event.request.url.includes("index-cached-https.sjs")) { + event.respondWith( + self.caches.open("origin-cache").then(c => { + return c.match(prefix + "index-https.sjs"); + }) + ); + } else { + event.respondWith(fetch(event.request)); + } +}); diff --git a/dom/serviceworkers/test/fetch/origin/https/realindex.html b/dom/serviceworkers/test/fetch/origin/https/realindex.html new file mode 100644 index 0000000000..87f3489455 --- /dev/null +++ b/dom/serviceworkers/test/fetch/origin/https/realindex.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<script> + window.opener.postMessage({status: "domain", data: document.domain}, "*"); + window.opener.postMessage({status: "origin", data: location.origin}, "*"); + window.opener.postMessage({status: "done"}, "*"); +</script> diff --git a/dom/serviceworkers/test/fetch/origin/https/realindex.html^headers^ b/dom/serviceworkers/test/fetch/origin/https/realindex.html^headers^ new file mode 100644 index 0000000000..5ed82fd065 --- /dev/null +++ b/dom/serviceworkers/test/fetch/origin/https/realindex.html^headers^ @@ -0,0 +1 @@ +Access-Control-Allow-Origin: https://example.com diff --git a/dom/serviceworkers/test/fetch/origin/https/register.html b/dom/serviceworkers/test/fetch/origin/https/register.html new file mode 100644 index 0000000000..2e99adba53 --- /dev/null +++ b/dom/serviceworkers/test/fetch/origin/https/register.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<script> + function ok(v, msg) { + window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*"); + } + + function done(reg) { + ok(reg.active, "The active worker should be available."); + window.parent.postMessage({status: "registrationdone"}, "*"); + } + + navigator.serviceWorker.ready.then(done); + navigator.serviceWorker.register("origin_test.js", {scope: "."}); +</script> diff --git a/dom/serviceworkers/test/fetch/origin/https/unregister.html b/dom/serviceworkers/test/fetch/origin/https/unregister.html new file mode 100644 index 0000000000..1f13508fa7 --- /dev/null +++ b/dom/serviceworkers/test/fetch/origin/https/unregister.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.getRegistration(".").then(function(registration) { + registration.unregister().then(function(success) { + if (success) { + window.parent.postMessage({status: "unregistrationdone"}, "*"); + } + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + }); +</script> diff --git a/dom/serviceworkers/test/fetch/origin/index-to-https.sjs b/dom/serviceworkers/test/fetch/origin/index-to-https.sjs new file mode 100644 index 0000000000..2505c03686 --- /dev/null +++ b/dom/serviceworkers/test/fetch/origin/index-to-https.sjs @@ -0,0 +1,8 @@ +function handleRequest(request, response) { + response.setStatusLine(null, 308, "Permanent Redirect"); + response.setHeader( + "Location", + "https://example.org/tests/dom/serviceworkers/test/fetch/origin/realindex.html", + false + ); +} diff --git a/dom/serviceworkers/test/fetch/origin/index.sjs b/dom/serviceworkers/test/fetch/origin/index.sjs new file mode 100644 index 0000000000..ca11827792 --- /dev/null +++ b/dom/serviceworkers/test/fetch/origin/index.sjs @@ -0,0 +1,8 @@ +function handleRequest(request, response) { + response.setStatusLine(null, 308, "Permanent Redirect"); + response.setHeader( + "Location", + "http://example.org/tests/dom/serviceworkers/test/fetch/origin/realindex.html", + false + ); +} diff --git a/dom/serviceworkers/test/fetch/origin/origin_test.js b/dom/serviceworkers/test/fetch/origin/origin_test.js new file mode 100644 index 0000000000..005c3b8c39 --- /dev/null +++ b/dom/serviceworkers/test/fetch/origin/origin_test.js @@ -0,0 +1,38 @@ +var prefix = "/tests/dom/serviceworkers/test/fetch/origin/"; + +function addOpaqueRedirect(cache, file) { + return fetch(new Request(prefix + file, { redirect: "manual" })).then( + function(response) { + return cache.put(prefix + file, response); + } + ); +} + +self.addEventListener("install", function(event) { + event.waitUntil( + self.caches.open("origin-cache").then(c => { + return Promise.all([ + addOpaqueRedirect(c, "index.sjs"), + addOpaqueRedirect(c, "index-to-https.sjs"), + ]); + }) + ); +}); + +self.addEventListener("fetch", function(event) { + if (event.request.url.includes("index-cached.sjs")) { + event.respondWith( + self.caches.open("origin-cache").then(c => { + return c.match(prefix + "index.sjs"); + }) + ); + } else if (event.request.url.includes("index-to-https-cached.sjs")) { + event.respondWith( + self.caches.open("origin-cache").then(c => { + return c.match(prefix + "index-to-https.sjs"); + }) + ); + } else { + event.respondWith(fetch(event.request)); + } +}); diff --git a/dom/serviceworkers/test/fetch/origin/realindex.html b/dom/serviceworkers/test/fetch/origin/realindex.html new file mode 100644 index 0000000000..87f3489455 --- /dev/null +++ b/dom/serviceworkers/test/fetch/origin/realindex.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<script> + window.opener.postMessage({status: "domain", data: document.domain}, "*"); + window.opener.postMessage({status: "origin", data: location.origin}, "*"); + window.opener.postMessage({status: "done"}, "*"); +</script> diff --git a/dom/serviceworkers/test/fetch/origin/realindex.html^headers^ b/dom/serviceworkers/test/fetch/origin/realindex.html^headers^ new file mode 100644 index 0000000000..3a6a85d894 --- /dev/null +++ b/dom/serviceworkers/test/fetch/origin/realindex.html^headers^ @@ -0,0 +1 @@ +Access-Control-Allow-Origin: http://mochi.test:8888 diff --git a/dom/serviceworkers/test/fetch/origin/register.html b/dom/serviceworkers/test/fetch/origin/register.html new file mode 100644 index 0000000000..2e99adba53 --- /dev/null +++ b/dom/serviceworkers/test/fetch/origin/register.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<script> + function ok(v, msg) { + window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*"); + } + + function done(reg) { + ok(reg.active, "The active worker should be available."); + window.parent.postMessage({status: "registrationdone"}, "*"); + } + + navigator.serviceWorker.ready.then(done); + navigator.serviceWorker.register("origin_test.js", {scope: "."}); +</script> diff --git a/dom/serviceworkers/test/fetch/origin/unregister.html b/dom/serviceworkers/test/fetch/origin/unregister.html new file mode 100644 index 0000000000..1f13508fa7 --- /dev/null +++ b/dom/serviceworkers/test/fetch/origin/unregister.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.getRegistration(".").then(function(registration) { + registration.unregister().then(function(success) { + if (success) { + window.parent.postMessage({status: "unregistrationdone"}, "*"); + } + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + }); +</script> diff --git a/dom/serviceworkers/test/fetch/plugin/plugins.html b/dom/serviceworkers/test/fetch/plugin/plugins.html new file mode 100644 index 0000000000..b268f6d99e --- /dev/null +++ b/dom/serviceworkers/test/fetch/plugin/plugins.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> +<script> + var obj, embed; + + function ok(v, msg) { + window.opener.postMessage({status: "ok", result: !!v, message: msg}, "*"); + } + + function finish() { + document.documentElement.removeChild(obj); + document.documentElement.removeChild(embed); + window.opener.postMessage({status: "done"}, "*"); + } + + function test_object() { + obj = document.createElement("object"); + obj.setAttribute('data', "object"); + document.documentElement.appendChild(obj); + } + + function test_embed() { + embed = document.createElement("embed"); + embed.setAttribute('src', "embed"); + document.documentElement.appendChild(embed); + } + + navigator.serviceWorker.addEventListener("message", function onMessage(e) { + if (e.data.destination === "object") { + ok(false, "<object> should not be intercepted"); + } else if (e.data.destination === "embed") { + ok(false, "<embed> should not be intercepted"); + } else if (e.data.destination === "" && e.data.resource === "foo.txt") { + navigator.serviceWorker.removeEventListener("message", onMessage); + finish(); + } + }); + + test_object(); + test_embed(); + // SW will definitely intercept fetch API, use this to see if plugins are + // intercepted before fetch(). + fetch("foo.txt"); +</script> diff --git a/dom/serviceworkers/test/fetch/plugin/worker.js b/dom/serviceworkers/test/fetch/plugin/worker.js new file mode 100644 index 0000000000..da3c1ba54a --- /dev/null +++ b/dom/serviceworkers/test/fetch/plugin/worker.js @@ -0,0 +1,15 @@ +self.addEventListener("fetch", function(event) { + var resource = event.request.url.split("/").pop(); + event.waitUntil( + clients.matchAll().then(clients => { + clients.forEach(client => { + if (client.url.includes("plugins.html")) { + client.postMessage({ + destination: event.request.destination, + resource, + }); + } + }); + }) + ); +}); diff --git a/dom/serviceworkers/test/fetch/real-file.txt b/dom/serviceworkers/test/fetch/real-file.txt new file mode 100644 index 0000000000..3ca2088ec0 --- /dev/null +++ b/dom/serviceworkers/test/fetch/real-file.txt @@ -0,0 +1 @@ +This is a real file. diff --git a/dom/serviceworkers/test/fetch/redirect.sjs b/dom/serviceworkers/test/fetch/redirect.sjs new file mode 100644 index 0000000000..dab558f4a8 --- /dev/null +++ b/dom/serviceworkers/test/fetch/redirect.sjs @@ -0,0 +1,4 @@ +function handleRequest(request, response) { + response.setStatusLine(request.httpVersion, 301, "Moved Permanently"); + response.setHeader("Location", "synthesized-redirect-twice-real-file.txt"); +} diff --git a/dom/serviceworkers/test/fetch/requesturl/index.html b/dom/serviceworkers/test/fetch/requesturl/index.html new file mode 100644 index 0000000000..bc3e400a94 --- /dev/null +++ b/dom/serviceworkers/test/fetch/requesturl/index.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.onmessage = window.onmessage = e => { + window.parent.postMessage(e.data, "*"); + }; +</script> +<iframe src="redirector.html"></iframe> diff --git a/dom/serviceworkers/test/fetch/requesturl/redirect.sjs b/dom/serviceworkers/test/fetch/requesturl/redirect.sjs new file mode 100644 index 0000000000..ae50a78aef --- /dev/null +++ b/dom/serviceworkers/test/fetch/requesturl/redirect.sjs @@ -0,0 +1,8 @@ +function handleRequest(request, response) { + response.setStatusLine(null, 308, "Permanent Redirect"); + response.setHeader( + "Location", + "http://example.org/tests/dom/serviceworkers/test/fetch/requesturl/secret.html", + false + ); +} diff --git a/dom/serviceworkers/test/fetch/requesturl/redirector.html b/dom/serviceworkers/test/fetch/requesturl/redirector.html new file mode 100644 index 0000000000..0a3afab9ee --- /dev/null +++ b/dom/serviceworkers/test/fetch/requesturl/redirector.html @@ -0,0 +1,2 @@ +<!DOCTYPE html> +<meta http-equiv="refresh" content="3;URL=/tests/dom/serviceworkers/test/fetch/requesturl/redirect.sjs"> diff --git a/dom/serviceworkers/test/fetch/requesturl/register.html b/dom/serviceworkers/test/fetch/requesturl/register.html new file mode 100644 index 0000000000..19a2e022c2 --- /dev/null +++ b/dom/serviceworkers/test/fetch/requesturl/register.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<script> + function ok(v, msg) { + window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*"); + } + + function done(reg) { + ok(reg.active, "The active worker should be available."); + window.parent.postMessage({status: "registrationdone"}, "*"); + } + + navigator.serviceWorker.ready.then(done); + navigator.serviceWorker.register("requesturl_test.js", {scope: "."}); +</script> diff --git a/dom/serviceworkers/test/fetch/requesturl/requesturl_test.js b/dom/serviceworkers/test/fetch/requesturl/requesturl_test.js new file mode 100644 index 0000000000..4d2680538f --- /dev/null +++ b/dom/serviceworkers/test/fetch/requesturl/requesturl_test.js @@ -0,0 +1,21 @@ +addEventListener("fetch", event => { + var url = event.request.url; + var badURL = url.indexOf("secret.html") > -1; + event.respondWith( + new Promise(resolve => { + clients.matchAll().then(clients => { + for (var client of clients) { + if (client.url.indexOf("index.html") > -1) { + client.postMessage({ + status: "ok", + result: !badURL, + message: "Should not find a bad URL (" + url + ")", + }); + break; + } + } + resolve(fetch(event.request)); + }); + }) + ); +}); diff --git a/dom/serviceworkers/test/fetch/requesturl/secret.html b/dom/serviceworkers/test/fetch/requesturl/secret.html new file mode 100644 index 0000000000..694c336355 --- /dev/null +++ b/dom/serviceworkers/test/fetch/requesturl/secret.html @@ -0,0 +1,5 @@ +<!DOCTYPE html> +secret stuff +<script> + window.parent.postMessage({status: "done"}, "*"); +</script> diff --git a/dom/serviceworkers/test/fetch/requesturl/unregister.html b/dom/serviceworkers/test/fetch/requesturl/unregister.html new file mode 100644 index 0000000000..1f13508fa7 --- /dev/null +++ b/dom/serviceworkers/test/fetch/requesturl/unregister.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.getRegistration(".").then(function(registration) { + registration.unregister().then(function(success) { + if (success) { + window.parent.postMessage({status: "unregistrationdone"}, "*"); + } + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + }); +</script> diff --git a/dom/serviceworkers/test/fetch/sandbox/index.html b/dom/serviceworkers/test/fetch/sandbox/index.html new file mode 100644 index 0000000000..1094a3995d --- /dev/null +++ b/dom/serviceworkers/test/fetch/sandbox/index.html @@ -0,0 +1,5 @@ +<!DOCTYPE html> +<script> + window.parent.postMessage({status: "ok", result: true, message: "The iframe is not being intercepted"}, "*"); + window.parent.postMessage({status: "done"}, "*"); +</script> diff --git a/dom/serviceworkers/test/fetch/sandbox/intercepted_index.html b/dom/serviceworkers/test/fetch/sandbox/intercepted_index.html new file mode 100644 index 0000000000..87261a495f --- /dev/null +++ b/dom/serviceworkers/test/fetch/sandbox/intercepted_index.html @@ -0,0 +1,5 @@ +<!DOCTYPE html> +<script> + window.parent.postMessage({status: "ok", result: false, message: "The iframe is being intercepted"}, "*"); + window.parent.postMessage({status: "done"}, "*"); +</script> diff --git a/dom/serviceworkers/test/fetch/sandbox/register.html b/dom/serviceworkers/test/fetch/sandbox/register.html new file mode 100644 index 0000000000..427b1a8da9 --- /dev/null +++ b/dom/serviceworkers/test/fetch/sandbox/register.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<script> + function ok(v, msg) { + window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*"); + } + + function done(reg) { + ok(reg.active, "The active worker should be available."); + window.parent.postMessage({status: "registrationdone"}, "*"); + } + + navigator.serviceWorker.ready.then(done); + navigator.serviceWorker.register("sandbox_test.js", {scope: "."}); +</script> diff --git a/dom/serviceworkers/test/fetch/sandbox/sandbox_test.js b/dom/serviceworkers/test/fetch/sandbox/sandbox_test.js new file mode 100644 index 0000000000..81740edf6d --- /dev/null +++ b/dom/serviceworkers/test/fetch/sandbox/sandbox_test.js @@ -0,0 +1,5 @@ +self.addEventListener("fetch", function(event) { + if (event.request.url.includes("index.html")) { + event.respondWith(fetch("intercepted_index.html")); + } +}); diff --git a/dom/serviceworkers/test/fetch/sandbox/unregister.html b/dom/serviceworkers/test/fetch/sandbox/unregister.html new file mode 100644 index 0000000000..1f13508fa7 --- /dev/null +++ b/dom/serviceworkers/test/fetch/sandbox/unregister.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.getRegistration(".").then(function(registration) { + registration.unregister().then(function(success) { + if (success) { + window.parent.postMessage({status: "unregistrationdone"}, "*"); + } + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + }); +</script> diff --git a/dom/serviceworkers/test/fetch/upgrade-insecure/embedder.html b/dom/serviceworkers/test/fetch/upgrade-insecure/embedder.html new file mode 100644 index 0000000000..e99209aa4d --- /dev/null +++ b/dom/serviceworkers/test/fetch/upgrade-insecure/embedder.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<script> + window.onmessage = function(e) { + window.parent.postMessage(e.data, "*"); + if (e.data.status == "protocol") { + document.querySelector("iframe").src = "image.html"; + } + }; +</script> +<iframe src="http://example.com/tests/dom/serviceworkers/test/fetch/upgrade-insecure/index.html"></iframe> diff --git a/dom/serviceworkers/test/fetch/upgrade-insecure/embedder.html^headers^ b/dom/serviceworkers/test/fetch/upgrade-insecure/embedder.html^headers^ new file mode 100644 index 0000000000..602d9dc38d --- /dev/null +++ b/dom/serviceworkers/test/fetch/upgrade-insecure/embedder.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: upgrade-insecure-requests diff --git a/dom/serviceworkers/test/fetch/upgrade-insecure/image-20px.png b/dom/serviceworkers/test/fetch/upgrade-insecure/image-20px.png Binary files differnew file mode 100644 index 0000000000..ae6a8a6b88 --- /dev/null +++ b/dom/serviceworkers/test/fetch/upgrade-insecure/image-20px.png diff --git a/dom/serviceworkers/test/fetch/upgrade-insecure/image-40px.png b/dom/serviceworkers/test/fetch/upgrade-insecure/image-40px.png Binary files differnew file mode 100644 index 0000000000..fe391dc8a2 --- /dev/null +++ b/dom/serviceworkers/test/fetch/upgrade-insecure/image-40px.png diff --git a/dom/serviceworkers/test/fetch/upgrade-insecure/image.html b/dom/serviceworkers/test/fetch/upgrade-insecure/image.html new file mode 100644 index 0000000000..dfcfd80014 --- /dev/null +++ b/dom/serviceworkers/test/fetch/upgrade-insecure/image.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<script> +onload=function(){ + var img = new Image(); + img.src = "http://example.com/tests/dom/serviceworkers/test/fetch/upgrade-insecure/image-20px.png"; + img.onload = function() { + window.parent.postMessage({status: "image", data: img.width}, "*"); + }; + img.onerror = function() { + window.parent.postMessage({status: "image", data: "error"}, "*"); + }; +}; +</script> diff --git a/dom/serviceworkers/test/fetch/upgrade-insecure/realindex.html b/dom/serviceworkers/test/fetch/upgrade-insecure/realindex.html new file mode 100644 index 0000000000..aaa255aad3 --- /dev/null +++ b/dom/serviceworkers/test/fetch/upgrade-insecure/realindex.html @@ -0,0 +1,4 @@ +<!DOCTYPE html> +<script> + window.parent.postMessage({status: "protocol", data: location.protocol}, "*"); +</script> diff --git a/dom/serviceworkers/test/fetch/upgrade-insecure/register.html b/dom/serviceworkers/test/fetch/upgrade-insecure/register.html new file mode 100644 index 0000000000..6309b9b218 --- /dev/null +++ b/dom/serviceworkers/test/fetch/upgrade-insecure/register.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<script> + function ok(v, msg) { + window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*"); + } + + function done(reg) { + ok(reg.active, "The active worker should be available."); + window.parent.postMessage({status: "registrationdone"}, "*"); + } + + navigator.serviceWorker.ready.then(done); + navigator.serviceWorker.register("upgrade-insecure_test.js", {scope: "."}); +</script> diff --git a/dom/serviceworkers/test/fetch/upgrade-insecure/unregister.html b/dom/serviceworkers/test/fetch/upgrade-insecure/unregister.html new file mode 100644 index 0000000000..1f13508fa7 --- /dev/null +++ b/dom/serviceworkers/test/fetch/upgrade-insecure/unregister.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.getRegistration(".").then(function(registration) { + registration.unregister().then(function(success) { + if (success) { + window.parent.postMessage({status: "unregistrationdone"}, "*"); + } + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + }); +</script> diff --git a/dom/serviceworkers/test/fetch/upgrade-insecure/upgrade-insecure_test.js b/dom/serviceworkers/test/fetch/upgrade-insecure/upgrade-insecure_test.js new file mode 100644 index 0000000000..f79307f4e4 --- /dev/null +++ b/dom/serviceworkers/test/fetch/upgrade-insecure/upgrade-insecure_test.js @@ -0,0 +1,11 @@ +self.addEventListener("fetch", function(event) { + if (event.request.url.includes("index.html")) { + event.respondWith(fetch("realindex.html")); + } else if (event.request.url.includes("image-20px.png")) { + if (event.request.url.indexOf("https://") == 0) { + event.respondWith(fetch("image-40px.png")); + } else { + event.respondWith(Response.error()); + } + } +}); diff --git a/dom/serviceworkers/test/fetch_event_worker.js b/dom/serviceworkers/test/fetch_event_worker.js new file mode 100644 index 0000000000..2775ef5068 --- /dev/null +++ b/dom/serviceworkers/test/fetch_event_worker.js @@ -0,0 +1,364 @@ +// eslint-disable-next-line complexity +onfetch = function(ev) { + if (ev.request.url.includes("ignore")) { + return; + } + + if (ev.request.url.includes("bare-synthesized.txt")) { + ev.respondWith( + Promise.resolve(new Response("synthesized response body", {})) + ); + } else if (ev.request.url.includes("file_CrossSiteXHR_server.sjs")) { + // N.B. this response would break the rules of CORS if it were allowed, but + // this test relies upon the preflight request not being intercepted and + // thus this response should not be used. + if (ev.request.method == "OPTIONS") { + ev.respondWith( + new Response("", { + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "X-Unsafe", + }, + }) + ); + } else if (ev.request.url.includes("example.org")) { + ev.respondWith(fetch(ev.request)); + } + } else if (ev.request.url.includes("synthesized-404.txt")) { + ev.respondWith( + Promise.resolve( + new Response("synthesized response body", { status: 404 }) + ) + ); + } else if (ev.request.url.includes("synthesized-headers.txt")) { + ev.respondWith( + Promise.resolve( + new Response("synthesized response body", { + headers: { + "X-Custom-Greeting": "Hello", + }, + }) + ) + ); + } else if (ev.request.url.includes("test-respondwith-response.txt")) { + ev.respondWith(new Response("test-respondwith-response response body", {})); + } else if (ev.request.url.includes("synthesized-redirect-real-file.txt")) { + ev.respondWith(Promise.resolve(Response.redirect("fetch/real-file.txt"))); + } else if ( + ev.request.url.includes("synthesized-redirect-twice-real-file.txt") + ) { + ev.respondWith( + Promise.resolve(Response.redirect("synthesized-redirect-real-file.txt")) + ); + } else if (ev.request.url.includes("synthesized-redirect-synthesized.txt")) { + ev.respondWith(Promise.resolve(Response.redirect("bare-synthesized.txt"))); + } else if ( + ev.request.url.includes("synthesized-redirect-twice-synthesized.txt") + ) { + ev.respondWith( + Promise.resolve(Response.redirect("synthesized-redirect-synthesized.txt")) + ); + } else if (ev.request.url.includes("rejected.txt")) { + ev.respondWith(Promise.reject()); + } else if (ev.request.url.includes("nonresponse.txt")) { + ev.respondWith(Promise.resolve(5)); + } else if (ev.request.url.includes("nonresponse2.txt")) { + ev.respondWith(Promise.resolve({})); + } else if (ev.request.url.includes("nonpromise.txt")) { + try { + // This should coerce to Promise(5) instead of throwing + ev.respondWith(5); + } catch (e) { + // test is expecting failure, so return a success if we get a thrown + // exception + ev.respondWith(new Response("respondWith(5) threw " + e)); + } + } else if (ev.request.url.includes("headers.txt")) { + var ok = true; + ok &= ev.request.headers.get("X-Test1") == "header1"; + ok &= ev.request.headers.get("X-Test2") == "header2"; + ev.respondWith(Promise.resolve(new Response(ok.toString(), {}))); + } else if (ev.request.url.includes("readable-stream.txt")) { + ev.respondWith( + new Response( + new ReadableStream({ + start(controller) { + controller.enqueue( + new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x21]) + ); + controller.close(); + }, + }) + ) + ); + } else if (ev.request.url.includes("readable-stream-locked.txt")) { + let stream = new ReadableStream({ + start(controller) { + controller.enqueue( + new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x21]) + ); + controller.close(); + }, + }); + + ev.respondWith(new Response(stream)); + + // This locks the stream. + stream.getReader(); + } else if (ev.request.url.includes("readable-stream-with-exception.txt")) { + ev.respondWith( + new Response( + new ReadableStream({ + start(controller) {}, + pull() { + throw "EXCEPTION!"; + }, + }) + ) + ); + } else if (ev.request.url.includes("readable-stream-with-exception2.txt")) { + ev.respondWith( + new Response( + new ReadableStream({ + _controller: null, + _count: 0, + + start(controller) { + this._controller = controller; + }, + pull() { + if (++this._count == 5) { + throw "EXCEPTION 2!"; + } + this._controller.enqueue(new Uint8Array([this._count])); + }, + }) + ) + ); + } else if (ev.request.url.includes("readable-stream-already-consumed.txt")) { + let r = new Response( + new ReadableStream({ + start(controller) { + controller.enqueue( + new Uint8Array([0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x21]) + ); + controller.close(); + }, + }) + ); + + r.blob(); + + ev.respondWith(r); + } else if (ev.request.url.includes("user-pass")) { + ev.respondWith(new Response(ev.request.url)); + } else if (ev.request.url.includes("nonexistent_image.gif")) { + var imageAsBinaryString = atob( + "R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs" + ); + var imageLength = imageAsBinaryString.length; + + // If we just pass |imageAsBinaryString| to the Response constructor, an + // encoding conversion occurs that corrupts the image. Instead, we need to + // convert it to a typed array. + // typed array. + var imageAsArray = new Uint8Array(imageLength); + for (var i = 0; i < imageLength; ++i) { + imageAsArray[i] = imageAsBinaryString.charCodeAt(i); + } + + ev.respondWith( + Promise.resolve( + new Response(imageAsArray, { headers: { "Content-Type": "image/gif" } }) + ) + ); + } else if (ev.request.url.includes("nonexistent_script.js")) { + ev.respondWith( + Promise.resolve(new Response("check_intercepted_script();", {})) + ); + } else if (ev.request.url.includes("nonexistent_stylesheet.css")) { + ev.respondWith( + Promise.resolve( + new Response("#style-test { background-color: black !important; }", { + headers: { + "Content-Type": "text/css", + }, + }) + ) + ); + } else if (ev.request.url.includes("nonexistent_page.html")) { + ev.respondWith( + Promise.resolve( + new Response( + "<script>window.frameElement.test_result = true;</script>", + { + headers: { + "Content-Type": "text/html", + }, + } + ) + ) + ); + } else if (ev.request.url.includes("navigate.html")) { + var requests = [ + // should not throw + new Request(ev.request), + new Request(ev.request, undefined), + new Request(ev.request, null), + new Request(ev.request, {}), + new Request(ev.request, { someUnrelatedProperty: 42 }), + new Request(ev.request, { method: "GET" }), + ]; + ev.respondWith( + Promise.resolve( + new Response( + "<script>window.frameElement.test_result = true;</script>", + { + headers: { + "Content-Type": "text/html", + }, + } + ) + ) + ); + } else if (ev.request.url.includes("nonexistent_worker_script.js")) { + ev.respondWith( + Promise.resolve( + new Response("postMessage('worker-intercept-success')", { + headers: { "Content-Type": "text/javascript" }, + }) + ) + ); + } else if (ev.request.url.includes("nonexistent_imported_script.js")) { + ev.respondWith( + Promise.resolve( + new Response("check_intercepted_script();", { + headers: { "Content-Type": "text/javascript" }, + }) + ) + ); + } else if (ev.request.url.includes("deliver-gzip")) { + // Don't handle the request, this will make Necko perform a network request, at + // which point SetApplyConversion must be re-enabled, otherwise the request + // will fail. + return; + } else if (ev.request.url.includes("hello.gz")) { + ev.respondWith(fetch("fetch/deliver-gzip.sjs")); + } else if (ev.request.url.includes("hello-after-extracting.gz")) { + ev.respondWith( + fetch("fetch/deliver-gzip.sjs").then(function(res) { + return res.text().then(function(body) { + return new Response(body, { + status: res.status, + statusText: res.statusText, + headers: res.headers, + }); + }); + }) + ); + } else if (ev.request.url.includes("opaque-on-same-origin")) { + var url = + "http://example.com/tests/dom/security/test/cors/file_CrossSiteXHR_server.sjs?status=200"; + ev.respondWith(fetch(url, { mode: "no-cors" })); + } else if (ev.request.url.includes("opaque-no-cors")) { + if (ev.request.mode != "no-cors") { + ev.respondWith(Promise.reject()); + return; + } + + var url = + "http://example.com/tests/dom/security/test/cors/file_CrossSiteXHR_server.sjs?status=200"; + ev.respondWith(fetch(url, { mode: ev.request.mode })); + } else if (ev.request.url.includes("cors-for-no-cors")) { + if (ev.request.mode != "no-cors") { + ev.respondWith(Promise.reject()); + return; + } + + var url = + "http://example.com/tests/dom/security/test/cors/file_CrossSiteXHR_server.sjs?status=200&allowOrigin=*"; + ev.respondWith(fetch(url)); + } else if (ev.request.url.includes("example.com")) { + ev.respondWith(fetch(ev.request)); + } else if (ev.request.url.includes("body-")) { + ev.respondWith( + ev.request.text().then(function(body) { + return new Response(body + body); + }) + ); + } else if (ev.request.url.includes("something.txt")) { + ev.respondWith(Response.redirect("fetch/somethingelse.txt")); + } else if (ev.request.url.includes("somethingelse.txt")) { + ev.respondWith(new Response("something else response body", {})); + } else if (ev.request.url.includes("redirect_serviceworker.sjs")) { + // The redirect_serviceworker.sjs server-side JavaScript file redirects to + // 'http://mochi.test:8888/tests/dom/serviceworkers/test/worker.js' + // The redirected fetch should not go through the SW since the original + // fetch was initiated from a SW. + ev.respondWith(fetch("redirect_serviceworker.sjs")); + } else if ( + ev.request.url.includes("load_cross_origin_xml_document_synthetic.xml") + ) { + ev.respondWith( + Promise.resolve( + new Response("<response>body</response>", { + headers: { "Content-Type": "text/xtml" }, + }) + ) + ); + } else if ( + ev.request.url.includes("load_cross_origin_xml_document_cors.xml") + ) { + if (ev.request.mode != "same-origin") { + ev.respondWith(Promise.reject()); + return; + } + + var url = + "http://example.com/tests/dom/security/test/cors/file_CrossSiteXHR_server.sjs?status=200&allowOrigin=*"; + ev.respondWith(fetch(url, { mode: "cors" })); + } else if ( + ev.request.url.includes("load_cross_origin_xml_document_opaque.xml") + ) { + if (ev.request.mode != "same-origin") { + Promise.resolve( + new Response("<error>Invalid Request mode</error>", { + headers: { "Content-Type": "text/xtml" }, + }) + ); + return; + } + + var url = + "http://example.com/tests/dom/security/test/cors/file_CrossSiteXHR_server.sjs?status=200"; + ev.respondWith(fetch(url, { mode: "no-cors" })); + } else if (ev.request.url.includes("xhr-method-test.txt")) { + ev.respondWith(new Response("intercepted " + ev.request.method)); + } else if (ev.request.url.includes("empty-header")) { + if ( + !ev.request.headers.has("emptyheader") || + ev.request.headers.get("emptyheader") !== "" + ) { + ev.respondWith(Promise.reject()); + return; + } + ev.respondWith(new Response("emptyheader")); + } else if (ev.request.url.includes("fetchevent-extendable")) { + if (ev instanceof ExtendableEvent) { + ev.respondWith(new Response("extendable")); + } else { + ev.respondWith(Promise.reject()); + } + } else if (ev.request.url.includes("fetchevent-request")) { + var threw = false; + try { + new FetchEvent("foo"); + } catch (e) { + if (e.name == "TypeError") { + threw = true; + } + } finally { + ev.respondWith(new Response(threw ? "non-nullable" : "nullable")); + } + } +}; diff --git a/dom/serviceworkers/test/file_blob_response_worker.js b/dom/serviceworkers/test/file_blob_response_worker.js new file mode 100644 index 0000000000..05e82b8c5d --- /dev/null +++ b/dom/serviceworkers/test/file_blob_response_worker.js @@ -0,0 +1,39 @@ +function makeFileBlob(obj) { + return new Promise(function(resolve, reject) { + var request = indexedDB.open("file_blob_response_worker", 1); + request.onerror = reject; + request.onupgradeneeded = function(evt) { + var db = evt.target.result; + db.onerror = reject; + + var objectStore = db.createObjectStore("test", { autoIncrement: true }); + var index = objectStore.createIndex("test", "index"); + }; + + request.onsuccess = function(evt) { + var db = evt.target.result; + db.onerror = reject; + + var blob = new Blob([JSON.stringify(obj)], { type: "application/json" }); + var data = { blob, index: 5 }; + + objectStore = db.transaction("test", "readwrite").objectStore("test"); + objectStore.add(data).onsuccess = function(event) { + var key = event.target.result; + objectStore = db.transaction("test").objectStore("test"); + objectStore.get(key).onsuccess = function(event1) { + resolve(event1.target.result.blob); + }; + }; + }; + }); +} + +self.addEventListener("fetch", function(evt) { + var result = { value: "success" }; + evt.respondWith( + makeFileBlob(result).then(function(blob) { + return new Response(blob); + }) + ); +}); diff --git a/dom/serviceworkers/test/file_js_cache.html b/dom/serviceworkers/test/file_js_cache.html new file mode 100644 index 0000000000..6feb94d872 --- /dev/null +++ b/dom/serviceworkers/test/file_js_cache.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Add a tag script to save the bytecode</title> +</head> +<body> + <script id="watchme" src="file_js_cache.js"></script> +</body> +</html> diff --git a/dom/serviceworkers/test/file_js_cache.js b/dom/serviceworkers/test/file_js_cache.js new file mode 100644 index 0000000000..b9b966775c --- /dev/null +++ b/dom/serviceworkers/test/file_js_cache.js @@ -0,0 +1,5 @@ +function baz() {} +function bar() {} +function foo() { bar() } +foo(); + diff --git a/dom/serviceworkers/test/file_js_cache_cleanup.js b/dom/serviceworkers/test/file_js_cache_cleanup.js new file mode 100644 index 0000000000..5a647046a1 --- /dev/null +++ b/dom/serviceworkers/test/file_js_cache_cleanup.js @@ -0,0 +1,16 @@ +"use strict"; +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +function clearCache() { + const cacheStorageSrv = Cc[ + "@mozilla.org/netwerk/cache-storage-service;1" + ].getService(Ci.nsICacheStorageService); + cacheStorageSrv.clear(); +} + +addMessageListener("teardown", function() { + clearCache(); + sendAsyncMessage("teardown-complete"); +}); diff --git a/dom/serviceworkers/test/file_js_cache_save_after_load.html b/dom/serviceworkers/test/file_js_cache_save_after_load.html new file mode 100644 index 0000000000..8a696c0026 --- /dev/null +++ b/dom/serviceworkers/test/file_js_cache_save_after_load.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Save the bytecode when all scripts are executed</title> +</head> +<body> + <script id="watchme" src="file_js_cache_save_after_load.js"></script> +</body> +</html> diff --git a/dom/serviceworkers/test/file_js_cache_save_after_load.js b/dom/serviceworkers/test/file_js_cache_save_after_load.js new file mode 100644 index 0000000000..2c2536c584 --- /dev/null +++ b/dom/serviceworkers/test/file_js_cache_save_after_load.js @@ -0,0 +1,15 @@ +function send_ping() { + window.dispatchEvent(new Event("ping")); +} +send_ping(); // ping (=1) + +window.addEventListener("load", function() { + send_ping(); // ping (=2) + + // Append a script which should call |foo|, before the encoding of this script + // bytecode. + var script = document.createElement("script"); + script.type = "text/javascript"; + script.innerText = "send_ping();"; // ping (=3) + document.head.appendChild(script); +}); diff --git a/dom/serviceworkers/test/file_js_cache_syntax_error.html b/dom/serviceworkers/test/file_js_cache_syntax_error.html new file mode 100644 index 0000000000..cc4a9b2daa --- /dev/null +++ b/dom/serviceworkers/test/file_js_cache_syntax_error.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Do not save bytecode on compilation errors</title> +</head> +<body> + <script id="watchme" src="file_js_cache_syntax_error.js"></script> +</body> +</html> diff --git a/dom/serviceworkers/test/file_js_cache_syntax_error.js b/dom/serviceworkers/test/file_js_cache_syntax_error.js new file mode 100644 index 0000000000..fcf587ae70 --- /dev/null +++ b/dom/serviceworkers/test/file_js_cache_syntax_error.js @@ -0,0 +1 @@ +var // SyntaxError: missing variable name. diff --git a/dom/serviceworkers/test/file_js_cache_with_sri.html b/dom/serviceworkers/test/file_js_cache_with_sri.html new file mode 100644 index 0000000000..38ecb26984 --- /dev/null +++ b/dom/serviceworkers/test/file_js_cache_with_sri.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>Add a tag script to save the bytecode</title> +</head> +<body> + <script id="watchme" src="file_js_cache.js" + integrity="sha384-8YSwN2ywq1SVThihWhj7uTVZ4UeIDwo3GgdPYnug+C+OS0oa6kH2IXBclwMaDJFb"> + </script> +</body> +</html> diff --git a/dom/serviceworkers/test/file_notification_openWindow.html b/dom/serviceworkers/test/file_notification_openWindow.html new file mode 100644 index 0000000000..f220f4808d --- /dev/null +++ b/dom/serviceworkers/test/file_notification_openWindow.html @@ -0,0 +1,26 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1578070</title> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> +window.onload = () => { + navigator.serviceWorker.ready.then(() => { + // Open and close a new window. + window.open("https://example.org/").close(); + + // If we make it here, then we didn't crash. Tell the worker we're done. + navigator.serviceWorker.controller.postMessage("DONE"); + + // We're done! + window.close(); + }); +} +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/file_userContextId_openWindow.js b/dom/serviceworkers/test/file_userContextId_openWindow.js new file mode 100644 index 0000000000..649a3152ba --- /dev/null +++ b/dom/serviceworkers/test/file_userContextId_openWindow.js @@ -0,0 +1,3 @@ +onnotificationclick = event => { + clients.openWindow("empty.html"); +}; diff --git a/dom/serviceworkers/test/force_refresh_browser_worker.js b/dom/serviceworkers/test/force_refresh_browser_worker.js new file mode 100644 index 0000000000..9c5be48530 --- /dev/null +++ b/dom/serviceworkers/test/force_refresh_browser_worker.js @@ -0,0 +1,42 @@ +var name = "browserRefresherCache"; + +self.addEventListener("install", function(event) { + event.waitUntil( + Promise.all([ + caches.open(name), + fetch("./browser_cached_force_refresh.html"), + ]).then(function(results) { + var cache = results[0]; + var response = results[1]; + return cache.put("./browser_base_force_refresh.html", response); + }) + ); +}); + +self.addEventListener("fetch", function(event) { + event.respondWith( + caches + .open(name) + .then(function(cache) { + return cache.match(event.request); + }) + .then(function(response) { + return response || fetch(event.request); + }) + ); +}); + +self.addEventListener("message", function(event) { + if (event.data.type === "GET_UNCONTROLLED_CLIENTS") { + event.waitUntil( + clients + .matchAll({ includeUncontrolled: true }) + .then(function(clientList) { + var resultList = clientList.map(function(c) { + return { url: c.url, frameType: c.frameType }; + }); + event.source.postMessage({ type: "CLIENTS", detail: resultList }); + }) + ); + } +}); diff --git a/dom/serviceworkers/test/force_refresh_worker.js b/dom/serviceworkers/test/force_refresh_worker.js new file mode 100644 index 0000000000..2e266b54d7 --- /dev/null +++ b/dom/serviceworkers/test/force_refresh_worker.js @@ -0,0 +1,43 @@ +var name = "refresherCache"; + +self.addEventListener("install", function(event) { + event.waitUntil( + Promise.all([ + caches.open(name), + fetch("./sw_clients/refresher_cached.html"), + fetch("./sw_clients/refresher_cached_compressed.html"), + ]).then(function(results) { + var cache = results[0]; + var response = results[1]; + var compressed = results[2]; + return Promise.all([ + cache.put("./sw_clients/refresher.html", response), + cache.put("./sw_clients/refresher_compressed.html", compressed), + ]); + }) + ); +}); + +self.addEventListener("fetch", function(event) { + event.respondWith( + caches + .open(name) + .then(function(cache) { + return cache.match(event.request); + }) + .then(function(response) { + // If this is one of our primary cached responses, then the window + // must have generated the request via a normal window reload. That + // should be detectable in the event.request.cache attribute. + if (response && event.request.cache !== "no-cache") { + dump( + '### ### FetchEvent.request.cache is "' + + event.request.cache + + '" instead of expected "no-cache"\n' + ); + return Response.error(); + } + return response || fetch(event.request); + }) + ); +}); diff --git a/dom/serviceworkers/test/gtest/TestReadWrite.cpp b/dom/serviceworkers/test/gtest/TestReadWrite.cpp new file mode 100644 index 0000000000..fbe71d7108 --- /dev/null +++ b/dom/serviceworkers/test/gtest/TestReadWrite.cpp @@ -0,0 +1,952 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/dom/ServiceWorkerRegistrar.h" +#include "mozilla/dom/ServiceWorkerRegistrarTypes.h" +#include "mozilla/ipc/PBackgroundSharedTypes.h" + +#include "nsAppDirectoryServiceDefs.h" +#include "nsIFile.h" +#include "nsIOutputStream.h" +#include "nsNetUtil.h" +#include "nsPrintfCString.h" +#include "nsIServiceWorkerManager.h" + +#include "prtime.h" + +using namespace mozilla::dom; +using namespace mozilla::ipc; + +class ServiceWorkerRegistrarTest : public ServiceWorkerRegistrar { + public: + ServiceWorkerRegistrarTest() { +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(mProfileDir)); + MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv)); +#else + NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(mProfileDir)); +#endif + MOZ_DIAGNOSTIC_ASSERT(mProfileDir); + } + + nsresult TestReadData() { return ReadData(); } + nsresult TestWriteData() MOZ_NO_THREAD_SAFETY_ANALYSIS { + return WriteData(mData); + } + void TestDeleteData() { DeleteData(); } + + void TestRegisterServiceWorker(const ServiceWorkerRegistrationData& aData) { + mozilla::MonitorAutoLock lock(mMonitor); + RegisterServiceWorkerInternal(aData); + } + + nsTArray<ServiceWorkerRegistrationData>& TestGetData() { return mData; } +}; + +already_AddRefed<nsIFile> GetFile() { + nsCOMPtr<nsIFile> file; + nsresult rv = + NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(file)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return nullptr; + } + + file->Append(nsLiteralString(SERVICEWORKERREGISTRAR_FILE)); + return file.forget(); +} + +bool CreateFile(const nsACString& aData) { + nsCOMPtr<nsIFile> file = GetFile(); + + nsCOMPtr<nsIOutputStream> stream; + nsresult rv = NS_NewLocalFileOutputStream(getter_AddRefs(stream), file); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + uint32_t count; + rv = stream->Write(aData.Data(), aData.Length(), &count); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + if (count != aData.Length()) { + return false; + } + + return true; +} + +TEST(ServiceWorkerRegistrar, TestNoFile) +{ + nsCOMPtr<nsIFile> file = GetFile(); + ASSERT_TRUE(file) + << "GetFile must return a nsIFIle"; + + bool exists; + nsresult rv = file->Exists(&exists); + ASSERT_EQ(NS_OK, rv) << "nsIFile::Exists cannot fail"; + + if (exists) { + rv = file->Remove(false); + ASSERT_EQ(NS_OK, rv) << "nsIFile::Remove cannot fail"; + } + + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + rv = swr->TestReadData(); + ASSERT_EQ(NS_OK, rv) << "ReadData() should not fail"; + + const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData(); + ASSERT_EQ((uint32_t)0, data.Length()) + << "No data should be found in an empty file"; +} + +TEST(ServiceWorkerRegistrar, TestEmptyFile) +{ + ASSERT_TRUE(CreateFile(""_ns)) + << "CreateFile should not fail"; + + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + nsresult rv = swr->TestReadData(); + ASSERT_NE(NS_OK, rv) << "ReadData() should fail if the file is empty"; + + const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData(); + ASSERT_EQ((uint32_t)0, data.Length()) + << "No data should be found in an empty file"; +} + +TEST(ServiceWorkerRegistrar, TestRightVersionFile) +{ + ASSERT_TRUE(CreateFile(nsLiteralCString(SERVICEWORKERREGISTRAR_VERSION "\n"))) + << "CreateFile should not fail"; + + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + nsresult rv = swr->TestReadData(); + ASSERT_EQ(NS_OK, rv) + << "ReadData() should not fail when the version is correct"; + + const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData(); + ASSERT_EQ((uint32_t)0, data.Length()) + << "No data should be found in an empty file"; +} + +TEST(ServiceWorkerRegistrar, TestWrongVersionFile) +{ + ASSERT_TRUE( + CreateFile(nsLiteralCString(SERVICEWORKERREGISTRAR_VERSION "bla\n"))) + << "CreateFile should not fail"; + + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + nsresult rv = swr->TestReadData(); + ASSERT_NE(NS_OK, rv) + << "ReadData() should fail when the version is not correct"; + + const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData(); + ASSERT_EQ((uint32_t)0, data.Length()) + << "No data should be found in an empty file"; +} + +TEST(ServiceWorkerRegistrar, TestReadData) +{ + nsAutoCString buffer(SERVICEWORKERREGISTRAR_VERSION "\n"); + + buffer.AppendLiteral("^inBrowser=1\n"); + buffer.AppendLiteral("https://scope_0.org\ncurrentWorkerURL 0\n"); + buffer.Append(SERVICEWORKERREGISTRAR_TRUE "\n"); + buffer.AppendLiteral("cacheName 0\n"); + buffer.AppendInt(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS, + 16); + buffer.AppendLiteral("\n"); + buffer.AppendInt(0); + buffer.AppendLiteral("\n"); + buffer.AppendInt(0); + buffer.AppendLiteral("\n"); + buffer.AppendInt(0); + buffer.AppendLiteral("\n"); + buffer.AppendInt(0); + buffer.AppendLiteral("\n"); + buffer.AppendLiteral("true\n"); + buffer.Append(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + buffer.AppendLiteral("\n"); + buffer.AppendLiteral("https://scope_1.org\ncurrentWorkerURL 1\n"); + buffer.Append(SERVICEWORKERREGISTRAR_FALSE "\n"); + buffer.AppendLiteral("cacheName 1\n"); + buffer.AppendInt(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_ALL, 16); + buffer.AppendLiteral("\n"); + PRTime ts = PR_Now(); + buffer.AppendInt(ts); + buffer.AppendLiteral("\n"); + buffer.AppendInt(ts); + buffer.AppendLiteral("\n"); + buffer.AppendInt(ts); + buffer.AppendLiteral("\n"); + buffer.AppendInt(1); + buffer.AppendLiteral("\n"); + buffer.AppendLiteral("false\n"); + buffer.Append(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + ASSERT_TRUE(CreateFile(buffer)) + << "CreateFile should not fail"; + + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + nsresult rv = swr->TestReadData(); + ASSERT_EQ(NS_OK, rv) << "ReadData() should not fail"; + + const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData(); + ASSERT_EQ((uint32_t)2, data.Length()) << "2 entries should be found"; + + const mozilla::ipc::PrincipalInfo& info0 = data[0].principal(); + ASSERT_EQ(info0.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) + << "First principal must be content"; + const mozilla::ipc::ContentPrincipalInfo& cInfo0 = data[0].principal(); + + nsAutoCString suffix0; + cInfo0.attrs().CreateSuffix(suffix0); + + ASSERT_STREQ("^inBrowser=1", suffix0.get()); + ASSERT_STREQ("https://scope_0.org", cInfo0.spec().get()); + ASSERT_STREQ("https://scope_0.org", data[0].scope().get()); + ASSERT_STREQ("currentWorkerURL 0", data[0].currentWorkerURL().get()); + ASSERT_TRUE(data[0].currentWorkerHandlesFetch()); + ASSERT_STREQ("cacheName 0", NS_ConvertUTF16toUTF8(data[0].cacheName()).get()); + ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS, + data[0].updateViaCache()); + ASSERT_EQ((int64_t)0, data[0].currentWorkerInstalledTime()); + ASSERT_EQ((int64_t)0, data[0].currentWorkerActivatedTime()); + ASSERT_EQ((int64_t)0, data[0].lastUpdateTime()); + ASSERT_EQ(false, data[0].navigationPreloadState().enabled()); + ASSERT_STREQ("true", data[0].navigationPreloadState().headerValue().get()); + + const mozilla::ipc::PrincipalInfo& info1 = data[1].principal(); + ASSERT_EQ(info1.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) + << "First principal must be content"; + const mozilla::ipc::ContentPrincipalInfo& cInfo1 = data[1].principal(); + + nsAutoCString suffix1; + cInfo1.attrs().CreateSuffix(suffix1); + + ASSERT_STREQ("", suffix1.get()); + ASSERT_STREQ("https://scope_1.org", cInfo1.spec().get()); + ASSERT_STREQ("https://scope_1.org", data[1].scope().get()); + ASSERT_STREQ("currentWorkerURL 1", data[1].currentWorkerURL().get()); + ASSERT_FALSE(data[1].currentWorkerHandlesFetch()); + ASSERT_STREQ("cacheName 1", NS_ConvertUTF16toUTF8(data[1].cacheName()).get()); + ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_ALL, + data[1].updateViaCache()); + ASSERT_EQ((int64_t)ts, data[1].currentWorkerInstalledTime()); + ASSERT_EQ((int64_t)ts, data[1].currentWorkerActivatedTime()); + ASSERT_EQ((int64_t)ts, data[1].lastUpdateTime()); + ASSERT_EQ(true, data[1].navigationPreloadState().enabled()); + ASSERT_STREQ("false", data[1].navigationPreloadState().headerValue().get()); +} + +TEST(ServiceWorkerRegistrar, TestDeleteData) +{ + ASSERT_TRUE(CreateFile("Foobar"_ns)) + << "CreateFile should not fail"; + + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + swr->TestDeleteData(); + + nsCOMPtr<nsIFile> file = GetFile(); + + bool exists; + nsresult rv = file->Exists(&exists); + ASSERT_EQ(NS_OK, rv) << "nsIFile::Exists cannot fail"; + + ASSERT_FALSE(exists) + << "The file should not exist after a DeleteData()."; +} + +TEST(ServiceWorkerRegistrar, TestWriteData) +{ + { + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + for (int i = 0; i < 2; ++i) { + ServiceWorkerRegistrationData reg; + + reg.scope() = nsPrintfCString("https://scope_write_%d.org", i); + reg.currentWorkerURL() = nsPrintfCString("currentWorkerURL write %d", i); + reg.currentWorkerHandlesFetch() = true; + reg.cacheName() = + NS_ConvertUTF8toUTF16(nsPrintfCString("cacheName write %d", i)); + reg.updateViaCache() = + nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS; + + reg.currentWorkerInstalledTime() = PR_Now(); + reg.currentWorkerActivatedTime() = PR_Now(); + reg.lastUpdateTime() = PR_Now(); + + nsAutoCString spec; + spec.AppendPrintf("spec write %d", i); + + reg.principal() = mozilla::ipc::ContentPrincipalInfo( + mozilla::OriginAttributes(i % 2), spec, spec, mozilla::Nothing(), + spec); + + swr->TestRegisterServiceWorker(reg); + } + + nsresult rv = swr->TestWriteData(); + ASSERT_EQ(NS_OK, rv) << "WriteData() should not fail"; + } + + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + nsresult rv = swr->TestReadData(); + ASSERT_EQ(NS_OK, rv) << "ReadData() should not fail"; + + const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData(); + ASSERT_EQ((uint32_t)2, data.Length()) << "2 entries should be found"; + + for (int i = 0; i < 2; ++i) { + nsAutoCString test; + + ASSERT_EQ(data[i].principal().type(), + mozilla::ipc::PrincipalInfo::TContentPrincipalInfo); + const mozilla::ipc::ContentPrincipalInfo& cInfo = data[i].principal(); + + mozilla::OriginAttributes attrs(i % 2); + nsAutoCString suffix, expectSuffix; + attrs.CreateSuffix(expectSuffix); + cInfo.attrs().CreateSuffix(suffix); + + ASSERT_STREQ(expectSuffix.get(), suffix.get()); + + test.AppendPrintf("https://scope_write_%d.org", i); + ASSERT_STREQ(test.get(), cInfo.spec().get()); + + test.Truncate(); + test.AppendPrintf("https://scope_write_%d.org", i); + ASSERT_STREQ(test.get(), data[i].scope().get()); + + test.Truncate(); + test.AppendPrintf("currentWorkerURL write %d", i); + ASSERT_STREQ(test.get(), data[i].currentWorkerURL().get()); + + ASSERT_EQ(true, data[i].currentWorkerHandlesFetch()); + + test.Truncate(); + test.AppendPrintf("cacheName write %d", i); + ASSERT_STREQ(test.get(), NS_ConvertUTF16toUTF8(data[i].cacheName()).get()); + + ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS, + data[i].updateViaCache()); + + ASSERT_NE((int64_t)0, data[i].currentWorkerInstalledTime()); + ASSERT_NE((int64_t)0, data[i].currentWorkerActivatedTime()); + ASSERT_NE((int64_t)0, data[i].lastUpdateTime()); + } +} + +TEST(ServiceWorkerRegistrar, TestVersion2Migration) +{ + nsAutoCString buffer( + "2" + "\n"); + + buffer.AppendLiteral("^appId=123&inBrowser=1\n"); + buffer.AppendLiteral( + "spec 0\nhttps://scope_0.org\nscriptSpec 0\ncurrentWorkerURL " + "0\nactiveCache 0\nwaitingCache 0\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + buffer.AppendLiteral("\n"); + buffer.AppendLiteral( + "spec 1\nhttps://scope_1.org\nscriptSpec 1\ncurrentWorkerURL " + "1\nactiveCache 1\nwaitingCache 1\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + ASSERT_TRUE(CreateFile(buffer)) + << "CreateFile should not fail"; + + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + nsresult rv = swr->TestReadData(); + ASSERT_EQ(NS_OK, rv) << "ReadData() should not fail"; + + const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData(); + ASSERT_EQ((uint32_t)2, data.Length()) << "2 entries should be found"; + + const mozilla::ipc::PrincipalInfo& info0 = data[0].principal(); + ASSERT_EQ(info0.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) + << "First principal must be content"; + const mozilla::ipc::ContentPrincipalInfo& cInfo0 = data[0].principal(); + + nsAutoCString suffix0; + cInfo0.attrs().CreateSuffix(suffix0); + + ASSERT_STREQ("^inBrowser=1", suffix0.get()); + ASSERT_STREQ("https://scope_0.org", cInfo0.spec().get()); + ASSERT_STREQ("https://scope_0.org", data[0].scope().get()); + ASSERT_STREQ("currentWorkerURL 0", data[0].currentWorkerURL().get()); + ASSERT_EQ(true, data[0].currentWorkerHandlesFetch()); + ASSERT_STREQ("activeCache 0", + NS_ConvertUTF16toUTF8(data[0].cacheName()).get()); + ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS, + data[0].updateViaCache()); + ASSERT_EQ((int64_t)0, data[0].currentWorkerInstalledTime()); + ASSERT_EQ((int64_t)0, data[0].currentWorkerActivatedTime()); + ASSERT_EQ((int64_t)0, data[0].lastUpdateTime()); + + const mozilla::ipc::PrincipalInfo& info1 = data[1].principal(); + ASSERT_EQ(info1.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) + << "First principal must be content"; + const mozilla::ipc::ContentPrincipalInfo& cInfo1 = data[1].principal(); + + nsAutoCString suffix1; + cInfo1.attrs().CreateSuffix(suffix1); + + ASSERT_STREQ("", suffix1.get()); + ASSERT_STREQ("https://scope_1.org", cInfo1.spec().get()); + ASSERT_STREQ("https://scope_1.org", data[1].scope().get()); + ASSERT_STREQ("currentWorkerURL 1", data[1].currentWorkerURL().get()); + ASSERT_EQ(true, data[1].currentWorkerHandlesFetch()); + ASSERT_STREQ("activeCache 1", + NS_ConvertUTF16toUTF8(data[1].cacheName()).get()); + ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS, + data[1].updateViaCache()); + ASSERT_EQ((int64_t)0, data[1].currentWorkerInstalledTime()); + ASSERT_EQ((int64_t)0, data[1].currentWorkerActivatedTime()); + ASSERT_EQ((int64_t)0, data[1].lastUpdateTime()); +} + +TEST(ServiceWorkerRegistrar, TestVersion3Migration) +{ + nsAutoCString buffer( + "3" + "\n"); + + buffer.AppendLiteral("^appId=123&inBrowser=1\n"); + buffer.AppendLiteral( + "spec 0\nhttps://scope_0.org\ncurrentWorkerURL 0\ncacheName 0\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + buffer.AppendLiteral("\n"); + buffer.AppendLiteral( + "spec 1\nhttps://scope_1.org\ncurrentWorkerURL 1\ncacheName 1\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + ASSERT_TRUE(CreateFile(buffer)) + << "CreateFile should not fail"; + + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + nsresult rv = swr->TestReadData(); + ASSERT_EQ(NS_OK, rv) << "ReadData() should not fail"; + + const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData(); + ASSERT_EQ((uint32_t)2, data.Length()) << "2 entries should be found"; + + const mozilla::ipc::PrincipalInfo& info0 = data[0].principal(); + ASSERT_EQ(info0.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) + << "First principal must be content"; + const mozilla::ipc::ContentPrincipalInfo& cInfo0 = data[0].principal(); + + nsAutoCString suffix0; + cInfo0.attrs().CreateSuffix(suffix0); + + ASSERT_STREQ("^inBrowser=1", suffix0.get()); + ASSERT_STREQ("https://scope_0.org", cInfo0.spec().get()); + ASSERT_STREQ("https://scope_0.org", data[0].scope().get()); + ASSERT_STREQ("currentWorkerURL 0", data[0].currentWorkerURL().get()); + ASSERT_EQ(true, data[0].currentWorkerHandlesFetch()); + ASSERT_STREQ("cacheName 0", NS_ConvertUTF16toUTF8(data[0].cacheName()).get()); + ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS, + data[0].updateViaCache()); + ASSERT_EQ((int64_t)0, data[0].currentWorkerInstalledTime()); + ASSERT_EQ((int64_t)0, data[0].currentWorkerActivatedTime()); + ASSERT_EQ((int64_t)0, data[0].lastUpdateTime()); + + const mozilla::ipc::PrincipalInfo& info1 = data[1].principal(); + ASSERT_EQ(info1.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) + << "First principal must be content"; + const mozilla::ipc::ContentPrincipalInfo& cInfo1 = data[1].principal(); + + nsAutoCString suffix1; + cInfo1.attrs().CreateSuffix(suffix1); + + ASSERT_STREQ("", suffix1.get()); + ASSERT_STREQ("https://scope_1.org", cInfo1.spec().get()); + ASSERT_STREQ("https://scope_1.org", data[1].scope().get()); + ASSERT_STREQ("currentWorkerURL 1", data[1].currentWorkerURL().get()); + ASSERT_EQ(true, data[1].currentWorkerHandlesFetch()); + ASSERT_STREQ("cacheName 1", NS_ConvertUTF16toUTF8(data[1].cacheName()).get()); + ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS, + data[1].updateViaCache()); + ASSERT_EQ((int64_t)0, data[1].currentWorkerInstalledTime()); + ASSERT_EQ((int64_t)0, data[1].currentWorkerActivatedTime()); + ASSERT_EQ((int64_t)0, data[1].lastUpdateTime()); +} + +TEST(ServiceWorkerRegistrar, TestVersion4Migration) +{ + nsAutoCString buffer( + "4" + "\n"); + + buffer.AppendLiteral("^appId=123&inBrowser=1\n"); + buffer.AppendLiteral( + "https://scope_0.org\ncurrentWorkerURL 0\ncacheName 0\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + buffer.AppendLiteral("\n"); + buffer.AppendLiteral( + "https://scope_1.org\ncurrentWorkerURL 1\ncacheName 1\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + ASSERT_TRUE(CreateFile(buffer)) + << "CreateFile should not fail"; + + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + nsresult rv = swr->TestReadData(); + ASSERT_EQ(NS_OK, rv) << "ReadData() should not fail"; + + const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData(); + ASSERT_EQ((uint32_t)2, data.Length()) << "2 entries should be found"; + + const mozilla::ipc::PrincipalInfo& info0 = data[0].principal(); + ASSERT_EQ(info0.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) + << "First principal must be content"; + const mozilla::ipc::ContentPrincipalInfo& cInfo0 = data[0].principal(); + + nsAutoCString suffix0; + cInfo0.attrs().CreateSuffix(suffix0); + + ASSERT_STREQ("^inBrowser=1", suffix0.get()); + ASSERT_STREQ("https://scope_0.org", cInfo0.spec().get()); + ASSERT_STREQ("https://scope_0.org", data[0].scope().get()); + ASSERT_STREQ("currentWorkerURL 0", data[0].currentWorkerURL().get()); + // default is true + ASSERT_EQ(true, data[0].currentWorkerHandlesFetch()); + ASSERT_STREQ("cacheName 0", NS_ConvertUTF16toUTF8(data[0].cacheName()).get()); + ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS, + data[0].updateViaCache()); + ASSERT_EQ((int64_t)0, data[0].currentWorkerInstalledTime()); + ASSERT_EQ((int64_t)0, data[0].currentWorkerActivatedTime()); + ASSERT_EQ((int64_t)0, data[0].lastUpdateTime()); + + const mozilla::ipc::PrincipalInfo& info1 = data[1].principal(); + ASSERT_EQ(info1.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) + << "First principal must be content"; + const mozilla::ipc::ContentPrincipalInfo& cInfo1 = data[1].principal(); + + nsAutoCString suffix1; + cInfo1.attrs().CreateSuffix(suffix1); + + ASSERT_STREQ("", suffix1.get()); + ASSERT_STREQ("https://scope_1.org", cInfo1.spec().get()); + ASSERT_STREQ("https://scope_1.org", data[1].scope().get()); + ASSERT_STREQ("currentWorkerURL 1", data[1].currentWorkerURL().get()); + // default is true + ASSERT_EQ(true, data[1].currentWorkerHandlesFetch()); + ASSERT_STREQ("cacheName 1", NS_ConvertUTF16toUTF8(data[1].cacheName()).get()); + ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS, + data[1].updateViaCache()); + ASSERT_EQ((int64_t)0, data[1].currentWorkerInstalledTime()); + ASSERT_EQ((int64_t)0, data[1].currentWorkerActivatedTime()); + ASSERT_EQ((int64_t)0, data[1].lastUpdateTime()); +} + +TEST(ServiceWorkerRegistrar, TestVersion5Migration) +{ + nsAutoCString buffer( + "5" + "\n"); + + buffer.AppendLiteral("^appId=123&inBrowser=1\n"); + buffer.AppendLiteral("https://scope_0.org\ncurrentWorkerURL 0\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TRUE "\n"); + buffer.AppendLiteral("cacheName 0\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + buffer.AppendLiteral("\n"); + buffer.AppendLiteral("https://scope_1.org\ncurrentWorkerURL 1\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_FALSE "\n"); + buffer.AppendLiteral("cacheName 1\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + ASSERT_TRUE(CreateFile(buffer)) + << "CreateFile should not fail"; + + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + nsresult rv = swr->TestReadData(); + ASSERT_EQ(NS_OK, rv) << "ReadData() should not fail"; + + const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData(); + ASSERT_EQ((uint32_t)2, data.Length()) << "2 entries should be found"; + + const mozilla::ipc::PrincipalInfo& info0 = data[0].principal(); + ASSERT_EQ(info0.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) + << "First principal must be content"; + const mozilla::ipc::ContentPrincipalInfo& cInfo0 = data[0].principal(); + + nsAutoCString suffix0; + cInfo0.attrs().CreateSuffix(suffix0); + + ASSERT_STREQ("^inBrowser=1", suffix0.get()); + ASSERT_STREQ("https://scope_0.org", cInfo0.spec().get()); + ASSERT_STREQ("https://scope_0.org", data[0].scope().get()); + ASSERT_STREQ("currentWorkerURL 0", data[0].currentWorkerURL().get()); + ASSERT_TRUE(data[0].currentWorkerHandlesFetch()); + ASSERT_STREQ("cacheName 0", NS_ConvertUTF16toUTF8(data[0].cacheName()).get()); + ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS, + data[0].updateViaCache()); + ASSERT_EQ((int64_t)0, data[0].currentWorkerInstalledTime()); + ASSERT_EQ((int64_t)0, data[0].currentWorkerActivatedTime()); + ASSERT_EQ((int64_t)0, data[0].lastUpdateTime()); + + const mozilla::ipc::PrincipalInfo& info1 = data[1].principal(); + ASSERT_EQ(info1.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) + << "First principal must be content"; + const mozilla::ipc::ContentPrincipalInfo& cInfo1 = data[1].principal(); + + nsAutoCString suffix1; + cInfo1.attrs().CreateSuffix(suffix1); + + ASSERT_STREQ("", suffix1.get()); + ASSERT_STREQ("https://scope_1.org", cInfo1.spec().get()); + ASSERT_STREQ("https://scope_1.org", data[1].scope().get()); + ASSERT_STREQ("currentWorkerURL 1", data[1].currentWorkerURL().get()); + ASSERT_FALSE(data[1].currentWorkerHandlesFetch()); + ASSERT_STREQ("cacheName 1", NS_ConvertUTF16toUTF8(data[1].cacheName()).get()); + ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS, + data[1].updateViaCache()); + ASSERT_EQ((int64_t)0, data[1].currentWorkerInstalledTime()); + ASSERT_EQ((int64_t)0, data[1].currentWorkerActivatedTime()); + ASSERT_EQ((int64_t)0, data[1].lastUpdateTime()); +} + +TEST(ServiceWorkerRegistrar, TestVersion6Migration) +{ + nsAutoCString buffer( + "6" + "\n"); + + buffer.AppendLiteral("^appId=123&inBrowser=1\n"); + buffer.AppendLiteral("https://scope_0.org\ncurrentWorkerURL 0\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TRUE "\n"); + buffer.AppendLiteral("cacheName 0\n"); + buffer.AppendInt(nsIRequest::LOAD_NORMAL, 16); + buffer.AppendLiteral("\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + buffer.AppendLiteral("\n"); + buffer.AppendLiteral("https://scope_1.org\ncurrentWorkerURL 1\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_FALSE "\n"); + buffer.AppendLiteral("cacheName 1\n"); + buffer.AppendInt(nsIRequest::VALIDATE_ALWAYS, 16); + buffer.AppendLiteral("\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + ASSERT_TRUE(CreateFile(buffer)) + << "CreateFile should not fail"; + + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + nsresult rv = swr->TestReadData(); + ASSERT_EQ(NS_OK, rv) << "ReadData() should not fail"; + + const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData(); + ASSERT_EQ((uint32_t)2, data.Length()) << "2 entries should be found"; + + const mozilla::ipc::PrincipalInfo& info0 = data[0].principal(); + ASSERT_EQ(info0.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) + << "First principal must be content"; + const mozilla::ipc::ContentPrincipalInfo& cInfo0 = data[0].principal(); + + nsAutoCString suffix0; + cInfo0.attrs().CreateSuffix(suffix0); + + ASSERT_STREQ("^inBrowser=1", suffix0.get()); + ASSERT_STREQ("https://scope_0.org", cInfo0.spec().get()); + ASSERT_STREQ("https://scope_0.org", data[0].scope().get()); + ASSERT_STREQ("currentWorkerURL 0", data[0].currentWorkerURL().get()); + ASSERT_TRUE(data[0].currentWorkerHandlesFetch()); + ASSERT_STREQ("cacheName 0", NS_ConvertUTF16toUTF8(data[0].cacheName()).get()); + ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_ALL, + data[0].updateViaCache()); + ASSERT_EQ((int64_t)0, data[0].currentWorkerInstalledTime()); + ASSERT_EQ((int64_t)0, data[0].currentWorkerActivatedTime()); + ASSERT_EQ((int64_t)0, data[0].lastUpdateTime()); + + const mozilla::ipc::PrincipalInfo& info1 = data[1].principal(); + ASSERT_EQ(info1.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) + << "First principal must be content"; + const mozilla::ipc::ContentPrincipalInfo& cInfo1 = data[1].principal(); + + nsAutoCString suffix1; + cInfo1.attrs().CreateSuffix(suffix1); + + ASSERT_STREQ("", suffix1.get()); + ASSERT_STREQ("https://scope_1.org", cInfo1.spec().get()); + ASSERT_STREQ("https://scope_1.org", data[1].scope().get()); + ASSERT_STREQ("currentWorkerURL 1", data[1].currentWorkerURL().get()); + ASSERT_FALSE(data[1].currentWorkerHandlesFetch()); + ASSERT_STREQ("cacheName 1", NS_ConvertUTF16toUTF8(data[1].cacheName()).get()); + ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS, + data[1].updateViaCache()); + ASSERT_EQ((int64_t)0, data[1].currentWorkerInstalledTime()); + ASSERT_EQ((int64_t)0, data[1].currentWorkerActivatedTime()); + ASSERT_EQ((int64_t)0, data[1].lastUpdateTime()); +} + +TEST(ServiceWorkerRegistrar, TestVersion7Migration) +{ + nsAutoCString buffer( + "7" + "\n"); + + buffer.AppendLiteral("^appId=123&inBrowser=1\n"); + buffer.AppendLiteral("https://scope_0.org\ncurrentWorkerURL 0\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TRUE "\n"); + buffer.AppendLiteral("cacheName 0\n"); + buffer.AppendInt(nsIRequest::LOAD_NORMAL, 16); + buffer.AppendLiteral("\n"); + buffer.AppendInt(0); + buffer.AppendLiteral("\n"); + buffer.AppendInt(0); + buffer.AppendLiteral("\n"); + buffer.AppendInt(0); + buffer.AppendLiteral("\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + buffer.AppendLiteral("\n"); + buffer.AppendLiteral("https://scope_1.org\ncurrentWorkerURL 1\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_FALSE "\n"); + buffer.AppendLiteral("cacheName 1\n"); + buffer.AppendInt(nsIRequest::VALIDATE_ALWAYS, 16); + buffer.AppendLiteral("\n"); + PRTime ts = PR_Now(); + buffer.AppendInt(ts); + buffer.AppendLiteral("\n"); + buffer.AppendInt(ts); + buffer.AppendLiteral("\n"); + buffer.AppendInt(ts); + buffer.AppendLiteral("\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + ASSERT_TRUE(CreateFile(buffer)) + << "CreateFile should not fail"; + + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + nsresult rv = swr->TestReadData(); + ASSERT_EQ(NS_OK, rv) << "ReadData() should not fail"; + + const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData(); + ASSERT_EQ((uint32_t)2, data.Length()) << "2 entries should be found"; + + const mozilla::ipc::PrincipalInfo& info0 = data[0].principal(); + ASSERT_EQ(info0.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) + << "First principal must be content"; + const mozilla::ipc::ContentPrincipalInfo& cInfo0 = data[0].principal(); + + nsAutoCString suffix0; + cInfo0.attrs().CreateSuffix(suffix0); + + ASSERT_STREQ("^inBrowser=1", suffix0.get()); + ASSERT_STREQ("https://scope_0.org", cInfo0.spec().get()); + ASSERT_STREQ("https://scope_0.org", data[0].scope().get()); + ASSERT_STREQ("currentWorkerURL 0", data[0].currentWorkerURL().get()); + ASSERT_TRUE(data[0].currentWorkerHandlesFetch()); + ASSERT_STREQ("cacheName 0", NS_ConvertUTF16toUTF8(data[0].cacheName()).get()); + ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_ALL, + data[0].updateViaCache()); + ASSERT_EQ((int64_t)0, data[0].currentWorkerInstalledTime()); + ASSERT_EQ((int64_t)0, data[0].currentWorkerActivatedTime()); + ASSERT_EQ((int64_t)0, data[0].lastUpdateTime()); + + const mozilla::ipc::PrincipalInfo& info1 = data[1].principal(); + ASSERT_EQ(info1.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) + << "First principal must be content"; + const mozilla::ipc::ContentPrincipalInfo& cInfo1 = data[1].principal(); + + nsAutoCString suffix1; + cInfo1.attrs().CreateSuffix(suffix1); + + ASSERT_STREQ("", suffix1.get()); + ASSERT_STREQ("https://scope_1.org", cInfo1.spec().get()); + ASSERT_STREQ("https://scope_1.org", data[1].scope().get()); + ASSERT_STREQ("currentWorkerURL 1", data[1].currentWorkerURL().get()); + ASSERT_FALSE(data[1].currentWorkerHandlesFetch()); + ASSERT_STREQ("cacheName 1", NS_ConvertUTF16toUTF8(data[1].cacheName()).get()); + ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS, + data[1].updateViaCache()); + ASSERT_EQ((int64_t)ts, data[1].currentWorkerInstalledTime()); + ASSERT_EQ((int64_t)ts, data[1].currentWorkerActivatedTime()); + ASSERT_EQ((int64_t)ts, data[1].lastUpdateTime()); +} + +TEST(ServiceWorkerRegistrar, TestDedupeRead) +{ + nsAutoCString buffer( + "3" + "\n"); + + // unique entries + buffer.AppendLiteral("^inBrowser=1\n"); + buffer.AppendLiteral( + "spec 0\nhttps://scope_0.org\ncurrentWorkerURL 0\ncacheName 0\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + buffer.AppendLiteral("\n"); + buffer.AppendLiteral( + "spec 1\nhttps://scope_1.org\ncurrentWorkerURL 1\ncacheName 1\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + // dupe entries + buffer.AppendLiteral("^inBrowser=1\n"); + buffer.AppendLiteral( + "spec 1\nhttps://scope_0.org\ncurrentWorkerURL 0\ncacheName 0\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + buffer.AppendLiteral("^inBrowser=1\n"); + buffer.AppendLiteral( + "spec 2\nhttps://scope_0.org\ncurrentWorkerURL 0\ncacheName 0\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + buffer.AppendLiteral("\n"); + buffer.AppendLiteral( + "spec 3\nhttps://scope_1.org\ncurrentWorkerURL 1\ncacheName 1\n"); + buffer.AppendLiteral(SERVICEWORKERREGISTRAR_TERMINATOR "\n"); + + ASSERT_TRUE(CreateFile(buffer)) + << "CreateFile should not fail"; + + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + nsresult rv = swr->TestReadData(); + ASSERT_EQ(NS_OK, rv) << "ReadData() should not fail"; + + const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData(); + ASSERT_EQ((uint32_t)2, data.Length()) << "2 entries should be found"; + + const mozilla::ipc::PrincipalInfo& info0 = data[0].principal(); + ASSERT_EQ(info0.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) + << "First principal must be content"; + const mozilla::ipc::ContentPrincipalInfo& cInfo0 = data[0].principal(); + + nsAutoCString suffix0; + cInfo0.attrs().CreateSuffix(suffix0); + + ASSERT_STREQ("^inBrowser=1", suffix0.get()); + ASSERT_STREQ("https://scope_0.org", cInfo0.spec().get()); + ASSERT_STREQ("https://scope_0.org", data[0].scope().get()); + ASSERT_STREQ("currentWorkerURL 0", data[0].currentWorkerURL().get()); + ASSERT_EQ(true, data[0].currentWorkerHandlesFetch()); + ASSERT_STREQ("cacheName 0", NS_ConvertUTF16toUTF8(data[0].cacheName()).get()); + ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS, + data[0].updateViaCache()); + ASSERT_EQ((int64_t)0, data[0].currentWorkerInstalledTime()); + ASSERT_EQ((int64_t)0, data[0].currentWorkerActivatedTime()); + ASSERT_EQ((int64_t)0, data[0].lastUpdateTime()); + + const mozilla::ipc::PrincipalInfo& info1 = data[1].principal(); + ASSERT_EQ(info1.type(), mozilla::ipc::PrincipalInfo::TContentPrincipalInfo) + << "First principal must be content"; + const mozilla::ipc::ContentPrincipalInfo& cInfo1 = data[1].principal(); + + nsAutoCString suffix1; + cInfo1.attrs().CreateSuffix(suffix1); + + ASSERT_STREQ("", suffix1.get()); + ASSERT_STREQ("https://scope_1.org", cInfo1.spec().get()); + ASSERT_STREQ("https://scope_1.org", data[1].scope().get()); + ASSERT_STREQ("currentWorkerURL 1", data[1].currentWorkerURL().get()); + ASSERT_EQ(true, data[1].currentWorkerHandlesFetch()); + ASSERT_STREQ("cacheName 1", NS_ConvertUTF16toUTF8(data[1].cacheName()).get()); + ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS, + data[1].updateViaCache()); + ASSERT_EQ((int64_t)0, data[1].currentWorkerInstalledTime()); + ASSERT_EQ((int64_t)0, data[1].currentWorkerActivatedTime()); + ASSERT_EQ((int64_t)0, data[1].lastUpdateTime()); +} + +TEST(ServiceWorkerRegistrar, TestDedupeWrite) +{ + { + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + for (int i = 0; i < 2; ++i) { + ServiceWorkerRegistrationData reg; + + reg.scope() = "https://scope_write.dedupe"_ns; + reg.currentWorkerURL() = nsPrintfCString("currentWorkerURL write %d", i); + reg.currentWorkerHandlesFetch() = true; + reg.cacheName() = + NS_ConvertUTF8toUTF16(nsPrintfCString("cacheName write %d", i)); + reg.updateViaCache() = + nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS; + + nsAutoCString spec; + spec.AppendPrintf("spec write dedupe/%d", i); + + reg.principal() = mozilla::ipc::ContentPrincipalInfo( + mozilla::OriginAttributes(false), spec, spec, mozilla::Nothing(), + spec); + + swr->TestRegisterServiceWorker(reg); + } + + nsresult rv = swr->TestWriteData(); + ASSERT_EQ(NS_OK, rv) << "WriteData() should not fail"; + } + + RefPtr<ServiceWorkerRegistrarTest> swr = new ServiceWorkerRegistrarTest; + + nsresult rv = swr->TestReadData(); + ASSERT_EQ(NS_OK, rv) << "ReadData() should not fail"; + + // Duplicate entries should be removed. + const nsTArray<ServiceWorkerRegistrationData>& data = swr->TestGetData(); + ASSERT_EQ((uint32_t)1, data.Length()) << "1 entry should be found"; + + ASSERT_EQ(data[0].principal().type(), + mozilla::ipc::PrincipalInfo::TContentPrincipalInfo); + const mozilla::ipc::ContentPrincipalInfo& cInfo = data[0].principal(); + + mozilla::OriginAttributes attrs(false); + nsAutoCString suffix, expectSuffix; + attrs.CreateSuffix(expectSuffix); + cInfo.attrs().CreateSuffix(suffix); + + // Last entry passed to RegisterServiceWorkerInternal() should overwrite + // previous values. So expect "1" in values here. + ASSERT_STREQ(expectSuffix.get(), suffix.get()); + ASSERT_STREQ("https://scope_write.dedupe", cInfo.spec().get()); + ASSERT_STREQ("https://scope_write.dedupe", data[0].scope().get()); + ASSERT_STREQ("currentWorkerURL write 1", data[0].currentWorkerURL().get()); + ASSERT_EQ(true, data[0].currentWorkerHandlesFetch()); + ASSERT_STREQ("cacheName write 1", + NS_ConvertUTF16toUTF8(data[0].cacheName()).get()); + ASSERT_EQ(nsIServiceWorkerRegistrationInfo::UPDATE_VIA_CACHE_IMPORTS, + data[0].updateViaCache()); + ASSERT_EQ((int64_t)0, data[0].currentWorkerInstalledTime()); + ASSERT_EQ((int64_t)0, data[0].currentWorkerActivatedTime()); + ASSERT_EQ((int64_t)0, data[0].lastUpdateTime()); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + + int rv = RUN_ALL_TESTS(); + return rv; +} diff --git a/dom/serviceworkers/test/gtest/moz.build b/dom/serviceworkers/test/gtest/moz.build new file mode 100644 index 0000000000..99e2945332 --- /dev/null +++ b/dom/serviceworkers/test/gtest/moz.build @@ -0,0 +1,13 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, you can obtain one at http://mozilla.org/MPL/2.0/. + +UNIFIED_SOURCES = [ + "TestReadWrite.cpp", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul-gtest" diff --git a/dom/serviceworkers/test/gzip_redirect_worker.js b/dom/serviceworkers/test/gzip_redirect_worker.js new file mode 100644 index 0000000000..b66df51478 --- /dev/null +++ b/dom/serviceworkers/test/gzip_redirect_worker.js @@ -0,0 +1,15 @@ +self.addEventListener("fetch", function(event) { + if (!event.request.url.endsWith("sw_clients/does_not_exist.html")) { + return; + } + + event.respondWith( + new Response("", { + status: 301, + statusText: "Moved Permanently", + headers: { + Location: "refresher_compressed.html", + }, + }) + ); +}); diff --git a/dom/serviceworkers/test/header_checker.sjs b/dom/serviceworkers/test/header_checker.sjs new file mode 100644 index 0000000000..7061041039 --- /dev/null +++ b/dom/serviceworkers/test/header_checker.sjs @@ -0,0 +1,9 @@ +function handleRequest(request, response) { + if (request.getHeader("Service-Worker") === "script") { + response.setStatusLine("1.1", 200, "OK"); + response.setHeader("Content-Type", "text/javascript"); + response.write("// empty"); + } else { + response.setStatusLine("1.1", 404, "Not Found"); + } +} diff --git a/dom/serviceworkers/test/hello.html b/dom/serviceworkers/test/hello.html new file mode 100644 index 0000000000..97eb03c902 --- /dev/null +++ b/dom/serviceworkers/test/hello.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + </head> + <body> + Hello. + </body> +<html> diff --git a/dom/serviceworkers/test/importscript.sjs b/dom/serviceworkers/test/importscript.sjs new file mode 100644 index 0000000000..e075eadd87 --- /dev/null +++ b/dom/serviceworkers/test/importscript.sjs @@ -0,0 +1,11 @@ +function handleRequest(request, response) { + if (request.queryString == "clearcounter") { + setState("counter", ""); + } else if (!getState("counter")) { + response.setHeader("Content-Type", "application/javascript", false); + response.write("callByScript();"); + setState("counter", "1"); + } else { + response.write("no cache no party!"); + } +} diff --git a/dom/serviceworkers/test/importscript_worker.js b/dom/serviceworkers/test/importscript_worker.js new file mode 100644 index 0000000000..6d639e792c --- /dev/null +++ b/dom/serviceworkers/test/importscript_worker.js @@ -0,0 +1,46 @@ +var counter = 0; +function callByScript() { + ++counter; +} + +// Use multiple scripts in this load to verify we support that case correctly. +// See bug 1249351 for a case where we broke this. +importScripts("lorem_script.js", "importscript.sjs"); + +importScripts("importscript.sjs"); + +var missingScriptFailed = false; +try { + importScripts(["there-is-nothing-here.js"]); +} catch (e) { + missingScriptFailed = true; +} + +onmessage = function(e) { + self.clients.matchAll().then(function(res) { + if (!res.length) { + dump("ERROR: no clients are currently controlled.\n"); + } + + if (!missingScriptFailed) { + res[0].postMessage("KO"); + } + + try { + // new unique script should fail + importScripts(["importscript.sjs?unique=true"]); + res[0].postMessage("KO"); + return; + } catch (ex) {} + + try { + // duplicate script previously offlined should succeed + importScripts(["importscript.sjs"]); + } catch (ex) { + res[0].postMessage("KO"); + return; + } + + res[0].postMessage(counter == 3 ? "OK" : "KO"); + }); +}; diff --git a/dom/serviceworkers/test/install_event_error_worker.js b/dom/serviceworkers/test/install_event_error_worker.js new file mode 100644 index 0000000000..e4208705b3 --- /dev/null +++ b/dom/serviceworkers/test/install_event_error_worker.js @@ -0,0 +1,9 @@ +// Worker that errors on receiving an install event. +oninstall = function(e) { + e.waitUntil( + new Promise(function(resolve, reject) { + undefined.doSomething; + resolve(); + }) + ); +}; diff --git a/dom/serviceworkers/test/install_event_worker.js b/dom/serviceworkers/test/install_event_worker.js new file mode 100644 index 0000000000..1f0815c8f0 --- /dev/null +++ b/dom/serviceworkers/test/install_event_worker.js @@ -0,0 +1,3 @@ +oninstall = function(e) { + dump("Got install event\n"); +}; diff --git a/dom/serviceworkers/test/intercepted_channel_process_swap_worker.js b/dom/serviceworkers/test/intercepted_channel_process_swap_worker.js new file mode 100644 index 0000000000..ccc74bc895 --- /dev/null +++ b/dom/serviceworkers/test/intercepted_channel_process_swap_worker.js @@ -0,0 +1,7 @@ +onfetch = e => { + const url = new URL(e.request.url).searchParams.get("respondWith"); + + if (url) { + e.respondWith(fetch(url)); + } +}; diff --git a/dom/serviceworkers/test/isolated/README.md b/dom/serviceworkers/test/isolated/README.md new file mode 100644 index 0000000000..2b462385af --- /dev/null +++ b/dom/serviceworkers/test/isolated/README.md @@ -0,0 +1,19 @@ +This directory contains tests that are flaky when run with other tests +but that we don't want to disable and where it's not trivial to make +the tests not flaky at this time, but we have a plan to fix them via +systemic fixes that are improving the codebase rather than hacking a +test until it works. + +This directory and ugly hack structure needs to exist because of +multi-e10s propagation races that will go away when we finish +implementing the multi-e10s overhaul for ServiceWorkers. Most +specifically, unregister() calls need to propagate across all +content processes. There are fixes on bug 1318142, but they're +ugly and complicate things. + +Specific test notes and rationalizations: +- multi-e10s-update: This test relies on there being no registrations + existing at its start. The preceding test that induces the breakage + (`browser_force_refresh.js`) was made to clean itself up, but the + unregister() race issue is not easily/cleanly hacked around and this + test will itself become moot when the multi-e10s changes land. diff --git a/dom/serviceworkers/test/isolated/multi-e10s-update/browser.ini b/dom/serviceworkers/test/isolated/multi-e10s-update/browser.ini new file mode 100644 index 0000000000..bb913e2583 --- /dev/null +++ b/dom/serviceworkers/test/isolated/multi-e10s-update/browser.ini @@ -0,0 +1,7 @@ +[DEFAULT] +support-files = + file_multie10s_update.html + server_multie10s_update.sjs + +[browser_multie10s_update.js] +skip-if = true # bug 1429794 is to re-enable diff --git a/dom/serviceworkers/test/isolated/multi-e10s-update/browser_multie10s_update.js b/dom/serviceworkers/test/isolated/multi-e10s-update/browser_multie10s_update.js new file mode 100644 index 0000000000..4b9a2b9ca4 --- /dev/null +++ b/dom/serviceworkers/test/isolated/multi-e10s-update/browser_multie10s_update.js @@ -0,0 +1,143 @@ +"use strict"; + +// Testing if 2 child processes are correctly managed when they both try to do +// an SW update. + +const BASE_URI = + "http://mochi.test:8888/browser/dom/serviceworkers/test/isolated/multi-e10s-update/"; + +add_task(async function test_update() { + info("Setting the prefs to having multi-e10s enabled"); + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.ipc.processCount", 4], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ], + }); + + let url = BASE_URI + "file_multie10s_update.html"; + + info("Creating the first tab..."); + let tab1 = BrowserTestUtils.addTab(gBrowser, url); + let browser1 = gBrowser.getBrowserForTab(tab1); + await BrowserTestUtils.browserLoaded(browser1); + + info("Creating the second tab..."); + let tab2 = BrowserTestUtils.addTab(gBrowser, url); + let browser2 = gBrowser.getBrowserForTab(tab2); + await BrowserTestUtils.browserLoaded(browser2); + + let sw = BASE_URI + "server_multie10s_update.sjs"; + + info("Let's make sure there are no existing registrations..."); + let existingCount = await SpecialPowers.spawn(browser1, [], async function() { + const regs = await content.navigator.serviceWorker.getRegistrations(); + return regs.length; + }); + is(existingCount, 0, "Previous tests should have cleaned up!"); + + info("Let's start the test..."); + /* eslint-disable no-shadow */ + let status = await SpecialPowers.spawn(browser1, [sw], function(url) { + // Let the SW be served immediately once by triggering a relase immediately. + // We don't need to await this. We do this from a frame script because + // it has fetch. + content.fetch(url + "?release"); + + // Registration of the SW + return ( + content.navigator.serviceWorker + .register(url) + + // Activation + .then(function(r) { + content.registration = r; + return new content.window.Promise(resolve => { + let worker = r.installing; + worker.addEventListener("statechange", () => { + if (worker.state === "installed") { + resolve(true); + } + }); + }); + }) + + // Waiting for the result. + .then(() => { + return new content.window.Promise(resolveResults => { + // Once both updates have been issued and a single update has failed, we + // can tell the .sjs to release a single copy of the SW script. + let updateCount = 0; + const uc = new content.window.BroadcastChannel("update"); + // This promise tracks the updates tally. + const updatesIssued = new Promise(resolveUpdatesIssued => { + uc.onmessage = function(e) { + updateCount++; + console.log("got update() number", updateCount); + if (updateCount === 2) { + resolveUpdatesIssued(); + } + }; + }); + + let results = []; + const rc = new content.window.BroadcastChannel("result"); + // This promise resolves when an update has failed. + const oneFailed = new Promise(resolveOneFailed => { + rc.onmessage = function(e) { + console.log("got result", e.data); + results.push(e.data); + if (e.data === 1) { + resolveOneFailed(); + } + if (results.length != 2) { + return; + } + + resolveResults(results[0] + results[1]); + }; + }); + + Promise.all([updatesIssued, oneFailed]).then(() => { + console.log("releasing update"); + content.fetch(url + "?release").catch(ex => { + console.error("problem releasing:", ex); + }); + }); + + // Let's inform the tabs. + const sc = new content.window.BroadcastChannel("start"); + sc.postMessage("go"); + }); + }) + ); + }); + /* eslint-enable no-shadow */ + + if (status == 0) { + ok(false, "both succeeded. This is wrong."); + } else if (status == 1) { + ok(true, "one succeded, one failed. This is good."); + } else { + ok(false, "both failed. This is definitely wrong."); + } + + // let's clean up the registration and get the fetch count. The count + // should be 1 for the initial fetch and 1 for the update. + /* eslint-disable no-shadow */ + const count = await SpecialPowers.spawn(browser1, [sw], async function(url) { + // We stored the registration on the frame script's wrapper, hence directly + // accesss content without using wrappedJSObject. + await content.registration.unregister(); + const { count } = await content + .fetch(url + "?get-and-clear-count") + .then(r => r.json()); + return count; + }); + /* eslint-enable no-shadow */ + is(count, 2, "SW should have been fetched only twice"); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); +}); diff --git a/dom/serviceworkers/test/isolated/multi-e10s-update/file_multie10s_update.html b/dom/serviceworkers/test/isolated/multi-e10s-update/file_multie10s_update.html new file mode 100644 index 0000000000..d611b01b59 --- /dev/null +++ b/dom/serviceworkers/test/isolated/multi-e10s-update/file_multie10s_update.html @@ -0,0 +1,40 @@ +<html> +<body> +<script> + +var bc = new BroadcastChannel('start'); +bc.onmessage = function(e) { + // This message is not for us. + if (e.data != 'go') { + return; + } + + // It can happen that we don't have the registrations yet. Let's try with a + // timeout. + function proceed() { + return navigator.serviceWorker.getRegistrations().then(regs => { + if (!regs.length) { + setTimeout(proceed, 200); + return; + } + + bc = new BroadcastChannel('result'); + regs[0].update().then(() => { + bc.postMessage(0); + }, () => { + bc.postMessage(1); + }); + + // Tell the coordinating frame script that we've kicked off our update + // call so that the SW script can be released once both instances of us + // have triggered update() and 1 has failed. + const blockingChannel = new BroadcastChannel('update'); + blockingChannel.postMessage(true); + }); + } + + proceed(); +} +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/isolated/multi-e10s-update/server_multie10s_update.sjs b/dom/serviceworkers/test/isolated/multi-e10s-update/server_multie10s_update.sjs new file mode 100644 index 0000000000..154f8a3ecd --- /dev/null +++ b/dom/serviceworkers/test/isolated/multi-e10s-update/server_multie10s_update.sjs @@ -0,0 +1,100 @@ +// stolen from file_blocked_script.sjs +function setGlobalState(data, key) { + x = { + data, + QueryInterface(iid) { + return this; + }, + }; + x.wrappedJSObject = x; + setObjectState(key, x); +} + +function getGlobalState(key) { + var data; + getObjectState(key, function(x) { + data = x && x.wrappedJSObject.data; + }); + return data; +} + +function completeBlockingRequest(response) { + response.write("42"); + response.finish(); +} + +// This stores the response that's currently blocking, or true if the release +// got here before the blocking request. +const BLOCKING_KEY = "multie10s-update-release"; +// This tracks the number of blocking requests we received up to this point in +// time. This value will be cleared when fetched. It's on the caller to make +// sure that all the blocking requests that might occurr have already occurred. +const COUNT_KEY = "multie10s-update-count"; + +/** + * Serve a request that will only be completed when the ?release variant of this + * .sjs is fetched. This allows us to avoid using a timer, which slows down the + * tests and is brittle under slow hardware. + */ +function handleBlockingRequest(request, response) { + response.processAsync(); + response.setHeader("Content-Type", "application/javascript", false); + + const existingCount = getGlobalState(COUNT_KEY) || 0; + setGlobalState(existingCount + 1, COUNT_KEY); + + const alreadyReleased = getGlobalState(BLOCKING_KEY); + if (alreadyReleased === true) { + completeBlockingRequest(response); + setGlobalState(null, BLOCKING_KEY); + } else if (alreadyReleased) { + // If we've got another response stacked up, this means something is wrong + // with the test. The count mechanism will detect this, so just let this + // one through so we fail fast rather than hanging. + dump("we got multiple blocking requests stacked up!!\n"); + completeBlockingRequest(response); + } else { + setGlobalState(response, BLOCKING_KEY); + } +} + +function handleReleaseRequest(request, response) { + const blockingResponse = getGlobalState(BLOCKING_KEY); + if (blockingResponse) { + completeBlockingRequest(blockingResponse); + setGlobalState(null, BLOCKING_KEY); + } else { + setGlobalState(true, BLOCKING_KEY); + } + + response.setHeader("Content-Type", "application/json", false); + response.write(JSON.stringify({ released: true })); +} + +function handleCountRequest(request, response) { + const count = getGlobalState(COUNT_KEY) || 0; + // --verify requires that we clear this so the test can be re-run. + setGlobalState(0, COUNT_KEY); + + response.setHeader("Content-Type", "application/json", false); + response.write(JSON.stringify({ count })); +} + +Components.utils.importGlobalProperties(["URLSearchParams"]); +function handleRequest(request, response) { + dump( + "server_multie10s_update.sjs: processing request for " + + request.path + + "?" + + request.queryString + + "\n" + ); + const query = new URLSearchParams(request.queryString); + if (query.has("release")) { + handleReleaseRequest(request, response); + } else if (query.has("get-and-clear-count")) { + handleCountRequest(request, response); + } else { + handleBlockingRequest(request, response); + } +} diff --git a/dom/serviceworkers/test/lazy_worker.js b/dom/serviceworkers/test/lazy_worker.js new file mode 100644 index 0000000000..08a0ec13f7 --- /dev/null +++ b/dom/serviceworkers/test/lazy_worker.js @@ -0,0 +1,8 @@ +onactivate = function(event) { + var promise = new Promise(function(res) { + setTimeout(function() { + res(); + }, 500); + }); + event.waitUntil(promise); +}; diff --git a/dom/serviceworkers/test/lorem_script.js b/dom/serviceworkers/test/lorem_script.js new file mode 100644 index 0000000000..bc8f3c8085 --- /dev/null +++ b/dom/serviceworkers/test/lorem_script.js @@ -0,0 +1,8 @@ +var lorem_str = ` +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor +incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis +nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo +consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum +dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, +sunt in culpa qui officia deserunt mollit anim id est laborum. +`; diff --git a/dom/serviceworkers/test/match_all_advanced_worker.js b/dom/serviceworkers/test/match_all_advanced_worker.js new file mode 100644 index 0000000000..d1bc5ab323 --- /dev/null +++ b/dom/serviceworkers/test/match_all_advanced_worker.js @@ -0,0 +1,5 @@ +onmessage = function(e) { + self.clients.matchAll().then(function(clients) { + e.source.postMessage(clients.length); + }); +}; diff --git a/dom/serviceworkers/test/match_all_client/match_all_client_id.html b/dom/serviceworkers/test/match_all_client/match_all_client_id.html new file mode 100644 index 0000000000..7ac6fc9d05 --- /dev/null +++ b/dom/serviceworkers/test/match_all_client/match_all_client_id.html @@ -0,0 +1,31 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1139425 - controlled page</title> +<script class="testbody" type="text/javascript"> + var testWindow = parent; + if (opener) { + testWindow = opener; + } + + window.onload = function() { + navigator.serviceWorker.ready.then(function(swr) { + swr.active.postMessage("Start"); + }); + } + + navigator.serviceWorker.onmessage = function(msg) { + // worker message; + testWindow.postMessage(msg.data, "*"); + window.close(); + }; +</script> + +</head> +<body> +</body> +</html> diff --git a/dom/serviceworkers/test/match_all_client_id_worker.js b/dom/serviceworkers/test/match_all_client_id_worker.js new file mode 100644 index 0000000000..e8e51a00e7 --- /dev/null +++ b/dom/serviceworkers/test/match_all_client_id_worker.js @@ -0,0 +1,28 @@ +onmessage = function(e) { + dump("MatchAllClientIdWorker:" + e.data + "\n"); + var id = []; + var iterations = 5; + var counter = 0; + + for (var i = 0; i < iterations; i++) { + self.clients.matchAll().then(function(res) { + if (!res.length) { + dump("ERROR: no clients are currently controlled.\n"); + } + + client = res[0]; + id[counter] = client.id; + counter++; + if (counter >= iterations) { + var response = true; + for (var index = 1; index < iterations; index++) { + if (id[0] != id[index]) { + response = false; + break; + } + } + client.postMessage(response); + } + }); + } +}; diff --git a/dom/serviceworkers/test/match_all_clients/match_all_controlled.html b/dom/serviceworkers/test/match_all_clients/match_all_controlled.html new file mode 100644 index 0000000000..35f064815d --- /dev/null +++ b/dom/serviceworkers/test/match_all_clients/match_all_controlled.html @@ -0,0 +1,83 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1058311 - controlled page</title> +<script class="testbody" type="text/javascript"> + var re = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; + var frameType = "none"; + var testWindow = parent; + + if (parent != window) { + frameType = "nested"; + } else if (opener) { + frameType = "auxiliary"; + testWindow = opener; + } else if (parent == window) { + frameType = "top-level"; + } else { + postResult(false, "Unexpected frameType"); + } + + window.onload = function() { + navigator.serviceWorker.ready.then(function(swr) { + // Send a message to our SW that will cause it to do clients.matchAll() + // and send a message *to each client about themselves* (rather than + // replying directly to us with all the clients it finds). + swr.active.postMessage("Start"); + }); + } + + function postResult(result, msg) { + response = { + result, + message: msg + }; + + testWindow.postMessage(response, "*"); + } + + navigator.serviceWorker.onmessage = async function(msg) { + // ## Verify the contents of the SW's serialized rep of our client info. + // Clients are opaque identifiers at a spec level, but we want to verify + // that they are UUID's *without wrapping "{}" characters*. + result = re.test(msg.data.id); + postResult(result, "Client id test"); + + result = msg.data.url == window.location; + postResult(result, "Client url test"); + + result = msg.data.visibilityState === document.visibilityState; + postResult(result, "Client visibility test. expected=" +document.visibilityState); + + result = msg.data.focused === document.hasFocus(); + postResult(result, "Client focus test. expected=" + document.hasFocus()); + + result = msg.data.frameType === frameType; + postResult(result, "Client frameType test. expected=" + frameType); + + result = msg.data.type === "window"; + postResult(result, "Client type test. expected=window"); + + // ## Verify the FetchEvent.clientId + // In bug 1446225 it turned out we provided UUID's wrapped with {}'s. We + // now also get coverage by forcing our clients.get() to forbid UUIDs + // with that form. + + const clientIdResp = await fetch('clientId'); + const fetchClientId = await clientIdResp.text(); + result = re.test(fetchClientId); + postResult(result, "Fetch clientId test"); + + postResult(true, "DONE"); + window.close(); + }; +</script> + +</head> +<body> +</body> +</html> diff --git a/dom/serviceworkers/test/match_all_properties_worker.js b/dom/serviceworkers/test/match_all_properties_worker.js new file mode 100644 index 0000000000..84156c93e0 --- /dev/null +++ b/dom/serviceworkers/test/match_all_properties_worker.js @@ -0,0 +1,28 @@ +onfetch = function(e) { + if (/\/clientId$/.test(e.request.url)) { + e.respondWith(new Response(e.clientId)); + return; + } +}; + +onmessage = function(e) { + dump("MatchAllPropertiesWorker:" + e.data + "\n"); + self.clients.matchAll().then(function(res) { + if (!res.length) { + dump("ERROR: no clients are currently controlled.\n"); + } + + for (i = 0; i < res.length; i++) { + client = res[i]; + response = { + type: client.type, + id: client.id, + url: client.url, + visibilityState: client.visibilityState, + focused: client.focused, + frameType: client.frameType, + }; + client.postMessage(response); + } + }); +}; diff --git a/dom/serviceworkers/test/match_all_worker.js b/dom/serviceworkers/test/match_all_worker.js new file mode 100644 index 0000000000..3eddbe1191 --- /dev/null +++ b/dom/serviceworkers/test/match_all_worker.js @@ -0,0 +1,10 @@ +function loop() { + self.clients.matchAll().then(function(result) { + setTimeout(loop, 0); + }); +} + +onactivate = function(e) { + // spam matchAll until the worker is closed. + loop(); +}; diff --git a/dom/serviceworkers/test/message_posting_worker.js b/dom/serviceworkers/test/message_posting_worker.js new file mode 100644 index 0000000000..26db997759 --- /dev/null +++ b/dom/serviceworkers/test/message_posting_worker.js @@ -0,0 +1,8 @@ +onmessage = function(e) { + self.clients.matchAll().then(function(res) { + if (!res.length) { + dump("ERROR: no clients are currently controlled.\n"); + } + res[0].postMessage(e.data); + }); +}; diff --git a/dom/serviceworkers/test/message_receiver.html b/dom/serviceworkers/test/message_receiver.html new file mode 100644 index 0000000000..82cb587c72 --- /dev/null +++ b/dom/serviceworkers/test/message_receiver.html @@ -0,0 +1,6 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.onmessage = function(e) { + window.parent.postMessage(e.data, "*"); + }; +</script> diff --git a/dom/serviceworkers/test/mochitest-common.ini b/dom/serviceworkers/test/mochitest-common.ini new file mode 100644 index 0000000000..280273a7b4 --- /dev/null +++ b/dom/serviceworkers/test/mochitest-common.ini @@ -0,0 +1,347 @@ +[DEFAULT] +tags = condprof +support-files = + abrupt_completion_worker.js + worker.js + worker2.js + worker3.js + fetch_event_worker.js + parse_error_worker.js + activate_event_error_worker.js + install_event_worker.js + install_event_error_worker.js + simpleregister/index.html + simpleregister/ready.html + controller/index.html + unregister/index.html + unregister/unregister.html + workerUpdate/update.html + sw_clients/simple.html + sw_clients/service_worker_controlled.html + match_all_worker.js + match_all_advanced_worker.js + worker_unregister.js + worker_update.js + message_posting_worker.js + fetch/index.html + fetch/fetch_worker_script.js + fetch/fetch_tests.js + fetch/deliver-gzip.sjs + fetch/redirect.sjs + fetch/real-file.txt + fetch/cookie/cookie_test.js + fetch/cookie/register.html + fetch/cookie/unregister.html + fetch/hsts/hsts_test.js + fetch/hsts/embedder.html + fetch/hsts/image.html + fetch/hsts/image-20px.png + fetch/hsts/image-40px.png + fetch/hsts/realindex.html + fetch/hsts/register.html + fetch/hsts/register.html^headers^ + fetch/hsts/unregister.html + fetch/https/index.html + fetch/https/register.html + fetch/https/unregister.html + fetch/https/https_test.js + fetch/https/clonedresponse/index.html + fetch/https/clonedresponse/register.html + fetch/https/clonedresponse/unregister.html + fetch/https/clonedresponse/https_test.js + fetch/imagecache/image-20px.png + fetch/imagecache/image-40px.png + fetch/imagecache/imagecache_test.js + fetch/imagecache/index.html + fetch/imagecache/postmortem.html + fetch/imagecache/register.html + fetch/imagecache/unregister.html + fetch/imagecache-maxage/index.html + fetch/imagecache-maxage/image-20px.png + fetch/imagecache-maxage/image-40px.png + fetch/imagecache-maxage/maxage_test.js + fetch/imagecache-maxage/register.html + fetch/imagecache-maxage/unregister.html + fetch/importscript-mixedcontent/register.html + fetch/importscript-mixedcontent/unregister.html + fetch/importscript-mixedcontent/https_test.js + fetch/interrupt.sjs + fetch/origin/index.sjs + fetch/origin/index-to-https.sjs + fetch/origin/realindex.html + fetch/origin/realindex.html^headers^ + fetch/origin/register.html + fetch/origin/unregister.html + fetch/origin/origin_test.js + fetch/origin/https/index-https.sjs + fetch/origin/https/realindex.html + fetch/origin/https/realindex.html^headers^ + fetch/origin/https/register.html + fetch/origin/https/unregister.html + fetch/origin/https/origin_test.js + fetch/requesturl/index.html + fetch/requesturl/redirect.sjs + fetch/requesturl/redirector.html + fetch/requesturl/register.html + fetch/requesturl/requesturl_test.js + fetch/requesturl/secret.html + fetch/requesturl/unregister.html + fetch/sandbox/index.html + fetch/sandbox/intercepted_index.html + fetch/sandbox/register.html + fetch/sandbox/unregister.html + fetch/sandbox/sandbox_test.js + fetch/upgrade-insecure/upgrade-insecure_test.js + fetch/upgrade-insecure/embedder.html + fetch/upgrade-insecure/embedder.html^headers^ + fetch/upgrade-insecure/image.html + fetch/upgrade-insecure/image-20px.png + fetch/upgrade-insecure/image-40px.png + fetch/upgrade-insecure/realindex.html + fetch/upgrade-insecure/register.html + fetch/upgrade-insecure/unregister.html + match_all_properties_worker.js + match_all_clients/match_all_controlled.html + test_serviceworker_interfaces.js + serviceworker_wrapper.js + message_receiver.html + serviceworker_not_sharedworker.js + match_all_client/match_all_client_id.html + match_all_client_id_worker.js + source_message_posting_worker.js + scope/scope_worker.js + redirect_serviceworker.sjs + importscript.sjs + importscript_worker.js + bug1151916_worker.js + bug1151916_driver.html + bug1240436_worker.js + notificationclick.html + notificationclick-otherwindow.html + notificationclick.js + notificationclick_focus.html + notificationclick_focus.js + notificationclose.html + notificationclose.js + worker_updatefoundevent.js + worker_updatefoundevent2.js + updatefoundevent.html + empty.html + empty.js + notification_constructor_error.js + notification_get_sw.js + notification/register.html + notification/unregister.html + notification/listener.html + notification_alt/register.html + notification_alt/unregister.html + sanitize/frame.html + sanitize/register.html + sanitize/example_check_and_unregister.html + sanitize_worker.js + streamfilter_server.sjs + streamfilter_worker.js + swa/worker_scope_different.js + swa/worker_scope_different.js^headers^ + swa/worker_scope_different2.js + swa/worker_scope_different2.js^headers^ + swa/worker_scope_precise.js + swa/worker_scope_precise.js^headers^ + swa/worker_scope_too_deep.js + swa/worker_scope_too_deep.js^headers^ + swa/worker_scope_too_narrow.js + swa/worker_scope_too_narrow.js^headers^ + claim_oninstall_worker.js + claim_worker_1.js + claim_worker_2.js + claim_clients/client.html + force_refresh_worker.js + sw_clients/refresher.html + sw_clients/refresher_compressed.html + sw_clients/refresher_compressed.html^headers^ + sw_clients/refresher_cached.html + sw_clients/refresher_cached_compressed.html + sw_clients/refresher_cached_compressed.html^headers^ + strict_mode_warning.js + skip_waiting_installed_worker.js + skip_waiting_scope/index.html + thirdparty/iframe1.html + thirdparty/iframe2.html + thirdparty/register.html + thirdparty/unregister.html + thirdparty/sw.js + thirdparty/worker.js + register_https.html + gzip_redirect_worker.js + sw_clients/navigator.html + eval_worker.js + test_eval_allowed.html^headers^ + opaque_intercept_worker.js + notify_loaded.js + fetch/plugin/worker.js + fetch/plugin/plugins.html + eventsource/* + sw_clients/file_blob_upload_frame.html + redirect_post.sjs + xslt_worker.js + xslt/* + unresolved_fetch_worker.js + header_checker.sjs + openWindow_worker.js + redirect.sjs + open_window/client.sjs + lorem_script.js + file_blob_response_worker.js + file_js_cache_cleanup.js + file_js_cache.html + file_js_cache_with_sri.html + file_js_cache.js + file_js_cache_save_after_load.html + file_js_cache_save_after_load.js + file_js_cache_syntax_error.html + file_js_cache_syntax_error.js + !/dom/security/test/cors/file_CrossSiteXHR_server.sjs + !/dom/notification/test/mochitest/MockServices.js + !/dom/notification/test/mochitest/NotificationTest.js + blocking_install_event_worker.js + sw_bad_mime_type.js + sw_bad_mime_type.js^headers^ + error_reporting_helpers.js + fetch.js + hello.html + create_another_sharedWorker.html + sharedWorker_fetch.js + async_waituntil_worker.js + lazy_worker.js + nofetch_handler_worker.js + service_worker.js + service_worker_client.html + utils.js + sw_storage_not_allow.js + update_worker.sjs + self_update_worker.sjs + !/dom/events/test/event_leak_utils.js + onmessageerror_worker.js + +[test_abrupt_completion.html] +skip-if = + os == 'linux' #Bug 1615164 + win10_2004 # Bug 1615164 +[test_bug1151916.html] +[test_bug1240436.html] +[test_bug1408734.html] +[test_claim.html] +[test_claim_oninstall.html] +[test_controller.html] +[test_cross_origin_url_after_redirect.html] +[test_devtools_bypass_serviceworker.html] +[test_empty_serviceworker.html] +[test_error_reporting.html] +skip-if = serviceworker_e10s +[test_escapedSlashes.html] +[test_eval_allowed.html] +[test_event_listener_leaks.html] +skip-if = (os == "win" && processor == "aarch64") #bug 1535784 +[test_fetch_event.html] +skip-if = debug # Bug 1262224 +[test_fetch_event_with_thirdpartypref.html] +skip-if = debug # Bug 1262224 +[test_fetch_integrity.html] +skip-if = serviceworker_e10s +support-files = console_monitor.js +[test_file_blob_response.html] +[test_file_blob_upload.html] +[test_force_refresh.html] +[test_gzip_redirect.html] +[test_hsts_upgrade_intercept.html] +skip-if = + win10_2004 && !debug # Bug 1717091 + win11_2009 && !debug # Bug 1797751 + os == "linux" && bits == 64 && debug # Bug 1749068 + apple_catalina && !debug # Bug 1717091 +scheme = https +[test_imagecache.html] +[test_imagecache_max_age.html] +skip-if = + os == 'linux' && bits == 64 && !debug && asan && os_version == '18.04' # Bug 1585668 +[test_importscript.html] +[test_install_event.html] +[test_install_event_gc.html] +skip-if = xorigin # JavaScript error: http://mochi.xorigin-test:8888/tests/SimpleTest/TestRunner.js, line 157: SecurityError: Permission denied to access property "wrappedJSObject" on cross-origin object +[test_installation_simple.html] +[test_match_all.html] +[test_match_all_advanced.html] +[test_match_all_client_id.html] +skip-if = toolkit == 'android' && !is_fennec +[test_match_all_client_properties.html] +skip-if = toolkit == 'android' && !is_fennec +[test_navigationPreload_disable_crash.html] +scheme = https +skip-if = + os == "linux" && bits == 64 && debug # Bug 1749068 +[test_navigator.html] +[test_not_intercept_plugin.html] +skip-if = serviceworker_e10s # leaks InterceptedHttpChannel and others things +[test_notification_constructor_error.html] +[test_notification_get.html] +skip-if = xorigin # Bug 1792790 +[test_notification_openWindow.html] +skip-if = + toolkit == 'android' && !is_fennec # Bug 1620052 + xorigin # JavaScript error: http://mochi.xorigin-test:8888/tests/SimpleTest/TestRunner.js, line 157: SecurityError: Permission denied to access property "wrappedJSObject" on cross-origin object +support-files = notification_openWindow_worker.js file_notification_openWindow.html +tags = openwindow +[test_notificationclick.html] +skip-if = xorigin # Bug 1792790 +[test_notificationclick_focus.html] +skip-if = xorigin # Bug 1792790 +[test_notificationclick-otherwindow.html] +skip-if = xorigin # Bug 1792790 +[test_notificationclose.html] +skip-if = xorigin # Bug 1792790 +[test_onmessageerror.html] +skip-if = xorigin # Hangs with no error log +[test_opaque_intercept.html] +[test_origin_after_redirect.html] +[test_origin_after_redirect_cached.html] +[test_origin_after_redirect_to_https.html] +[test_origin_after_redirect_to_https_cached.html] +[test_post_message.html] +[test_post_message_advanced.html] +[test_post_message_source.html] +[test_register_base.html] +[test_register_https_in_http.html] +[test_sandbox_intercept.html] +[test_scopes.html] +[test_script_loader_intercepted_js_cache.html] +skip-if = serviceworker_e10s +[test_sanitize.html] +[test_serviceworker.html] +[test_service_worker_allowed.html] +[test_serviceworker_header.html] +[test_serviceworker_interfaces.html] +[test_serviceworker_not_sharedworker.html] +[test_skip_waiting.html] +[test_streamfilter.html] +[test_strict_mode_warning.html] +[test_third_party_iframes.html] +support-files = + window_party_iframes.html +[test_unregister.html] +[test_unresolved_fetch_interception.html] +skip-if = verify + serviceworker_e10s +[test_workerUnregister.html] +[test_workerUpdate.html] +[test_workerupdatefoundevent.html] +[test_xslt.html] +[test_async_waituntil.html] +[test_worker_reference_gc_timeout.html] +[test_nofetch_handler.html] +[test_bad_script_cache.html] +[test_file_upload.html] +skip-if = toolkit == 'android' #Bug 1430182 +support-files = script_file_upload.js sw_file_upload.js server_file_upload.sjs +[test_self_update_worker.html] +skip-if = serviceworker_e10s + (toolkit == 'android' && !is_fennec) diff --git a/dom/serviceworkers/test/mochitest-dFPI.ini b/dom/serviceworkers/test/mochitest-dFPI.ini new file mode 100644 index 0000000000..37d6171837 --- /dev/null +++ b/dom/serviceworkers/test/mochitest-dFPI.ini @@ -0,0 +1,11 @@ +[DEFAULT] +# Enable dFPI(cookieBehavior 5) for service worker tests. +prefs = + network.cookie.cookieBehavior=5 +tags = serviceworker-dfpi +# We disable service workers for third-party contexts when dFPI is enabled. So, +# we disable xorigin tests for dFPI. +skip-if = xorigin +dupe-manifest = true + +[include:mochitest-common.ini] diff --git a/dom/serviceworkers/test/mochitest.ini b/dom/serviceworkers/test/mochitest.ini new file mode 100644 index 0000000000..49b6c91201 --- /dev/null +++ b/dom/serviceworkers/test/mochitest.ini @@ -0,0 +1,35 @@ +[DEFAULT] +# Mochitests are executed in iframes. Several ServiceWorker tests use iframes +# too. The result is that we have nested iframes. CookieBehavior 4 +# (BEHAVIOR_REJECT_TRACKER) doesn't grant storage access permission to nested +# iframes because trackers could use them to follow users across sites. Let's +# use cookieBehavior 0 (BEHAVIOR_ACCEPT) here. +prefs = + network.cookie.cookieBehavior=0 +dupe-manifest = true +tags = condprof + +# Following tests are not working currently when dFPI is enabled. So, we put +# these tests here instead of mochitest-common.ini so that these tests won't run +# when dFPI is enabled. +[test_cookie_fetch.html] +[test_csp_upgrade-insecure_intercept.html] +[test_eventsource_intercept.html] +[test_https_fetch.html] +skip-if = condprof #: timed out +[test_https_fetch_cloned_response.html] +[test_https_origin_after_redirect.html] +[test_https_origin_after_redirect_cached.html] +skip-if = condprof #: timed out +[test_https_synth_fetch_from_cached_sw.html] +[test_importscript_mixedcontent.html] +tags = mcb +[test_openWindow.html] +skip-if = + toolkit == 'android' && !is_fennec # Bug 1620052 + xorigin # Bug 1792790 + condprof #: timed out +tags = openwindow +[test_sanitize_domain.html] + +[include:mochitest-common.ini] diff --git a/dom/serviceworkers/test/navigationPreload_page.html b/dom/serviceworkers/test/navigationPreload_page.html new file mode 100644 index 0000000000..39d4a79378 --- /dev/null +++ b/dom/serviceworkers/test/navigationPreload_page.html @@ -0,0 +1 @@ +NavigationPreload diff --git a/dom/serviceworkers/test/network_with_utils.html b/dom/serviceworkers/test/network_with_utils.html new file mode 100644 index 0000000000..63f6b0e796 --- /dev/null +++ b/dom/serviceworkers/test/network_with_utils.html @@ -0,0 +1,14 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <script src="utils.js" type="text/javascript"></script> +</head> +<body> +NETWORK +</body> +</html> diff --git a/dom/serviceworkers/test/nofetch_handler_worker.js b/dom/serviceworkers/test/nofetch_handler_worker.js new file mode 100644 index 0000000000..77ba0c734a --- /dev/null +++ b/dom/serviceworkers/test/nofetch_handler_worker.js @@ -0,0 +1,14 @@ +function handleFetch(event) { + event.respondWith(new Response("intercepted")); +} + +self.oninstall = function(event) { + addEventListener("fetch", handleFetch); + self.onfetch = handleFetch; +}; + +// Bug 1325101. Make sure adding event listeners for other events +// doesn't set the fetch flag. +addEventListener("push", function() {}); +addEventListener("message", function() {}); +addEventListener("non-sw-event", function() {}); diff --git a/dom/serviceworkers/test/notification/listener.html b/dom/serviceworkers/test/notification/listener.html new file mode 100644 index 0000000000..1c6e282ece --- /dev/null +++ b/dom/serviceworkers/test/notification/listener.html @@ -0,0 +1,27 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1114554 - proxy to forward messages from SW to test</title> +<script class="testbody" type="text/javascript"> + var testWindow = parent; + if (opener) { + testWindow = opener; + } + + navigator.serviceWorker.onmessage = function(msg) { + // worker message; + testWindow.postMessage(msg.data, "*"); + if (msg.data.type == 'finish') { + window.close(); + } + }; +</script> + +</head> +<body> +</body> +</html> diff --git a/dom/serviceworkers/test/notification/register.html b/dom/serviceworkers/test/notification/register.html new file mode 100644 index 0000000000..b7df73bede --- /dev/null +++ b/dom/serviceworkers/test/notification/register.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<script> + function done() { + parent.callback(); + } + + navigator.serviceWorker.ready.then(done); + navigator.serviceWorker.register("../notification_get_sw.js", {scope: "."}).catch(function(e) { + dump("Registration failure " + e.message + "\n"); + }); +</script> diff --git a/dom/serviceworkers/test/notification/unregister.html b/dom/serviceworkers/test/notification/unregister.html new file mode 100644 index 0000000000..d5a141f830 --- /dev/null +++ b/dom/serviceworkers/test/notification/unregister.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.getRegistration(".").then(function(registration) { + registration.unregister().then(function(success) { + if (success) { + window.parent.callback(); + } + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + }); +</script> diff --git a/dom/serviceworkers/test/notification_alt/register.html b/dom/serviceworkers/test/notification_alt/register.html new file mode 100644 index 0000000000..b7df73bede --- /dev/null +++ b/dom/serviceworkers/test/notification_alt/register.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<script> + function done() { + parent.callback(); + } + + navigator.serviceWorker.ready.then(done); + navigator.serviceWorker.register("../notification_get_sw.js", {scope: "."}).catch(function(e) { + dump("Registration failure " + e.message + "\n"); + }); +</script> diff --git a/dom/serviceworkers/test/notification_alt/unregister.html b/dom/serviceworkers/test/notification_alt/unregister.html new file mode 100644 index 0000000000..d5a141f830 --- /dev/null +++ b/dom/serviceworkers/test/notification_alt/unregister.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<script> + navigator.serviceWorker.getRegistration(".").then(function(registration) { + registration.unregister().then(function(success) { + if (success) { + window.parent.callback(); + } + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + }); +</script> diff --git a/dom/serviceworkers/test/notification_constructor_error.js b/dom/serviceworkers/test/notification_constructor_error.js new file mode 100644 index 0000000000..644dba480e --- /dev/null +++ b/dom/serviceworkers/test/notification_constructor_error.js @@ -0,0 +1 @@ +new Notification("Hi there"); diff --git a/dom/serviceworkers/test/notification_get_sw.js b/dom/serviceworkers/test/notification_get_sw.js new file mode 100644 index 0000000000..9b7c24f496 --- /dev/null +++ b/dom/serviceworkers/test/notification_get_sw.js @@ -0,0 +1,55 @@ +function postAll(data) { + self.clients.matchAll().then(function(clients) { + if (!clients.length) { + dump( + "***************** NO CLIENTS FOUND! Test messages are being lost *******************\n" + ); + } + clients.forEach(function(client) { + client.postMessage(data); + }); + }); +} + +function ok(a, msg) { + postAll({ type: "status", status: !!a, msg: a + ": " + msg }); +} + +function is(a, b, msg) { + postAll({ + type: "status", + status: a === b, + msg: a + " === " + b + ": " + msg, + }); +} + +function done() { + postAll({ type: "finish" }); +} + +onmessage = function(e) { + dump("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% MESSAGE " + e.data + "\n"); + var start; + if (e.data == "create") { + start = registration.showNotification("This is a title"); + } else { + start = Promise.resolve(); + } + + start.then(function() { + dump("CALLING getNotification\n"); + registration.getNotifications().then(function(notifications) { + dump("RECD getNotification\n"); + is(notifications.length, 1, "There should be one stored notification"); + var notification = notifications[0]; + if (!notification) { + done(); + return; + } + ok(notification instanceof Notification, "Should be a Notification"); + is(notification.title, "This is a title", "Title should match"); + notification.close(); + done(); + }); + }); +}; diff --git a/dom/serviceworkers/test/notification_openWindow_worker.js b/dom/serviceworkers/test/notification_openWindow_worker.js new file mode 100644 index 0000000000..174199b14a --- /dev/null +++ b/dom/serviceworkers/test/notification_openWindow_worker.js @@ -0,0 +1,25 @@ +const gRoot = "http://mochi.test:8888/tests/dom/serviceworkers/test/"; +const gTestURL = gRoot + "test_notification_openWindow.html"; +const gClientURL = gRoot + "file_notification_openWindow.html"; + +onmessage = function(event) { + if (event.data !== "DONE") { + dump(`ERROR: received unexpected message: ${JSON.stringify(event.data)}\n`); + } + + event.waitUntil( + clients.matchAll({ includeUncontrolled: true }).then(cl => { + for (let client of cl) { + // The |gClientURL| window closes itself after posting the DONE message, + // so we don't need to send it anything here. + if (client.url === gTestURL) { + client.postMessage("DONE"); + } + } + }) + ); +}; + +onnotificationclick = function(event) { + clients.openWindow(gClientURL); +}; diff --git a/dom/serviceworkers/test/notificationclick-otherwindow.html b/dom/serviceworkers/test/notificationclick-otherwindow.html new file mode 100644 index 0000000000..f64e82aabd --- /dev/null +++ b/dom/serviceworkers/test/notificationclick-otherwindow.html @@ -0,0 +1,30 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1114554 - controlled page</title> +<script class="testbody" type="text/javascript"> + var testWindow = parent; + if (opener) { + testWindow = opener; + } + + navigator.serviceWorker.ready.then(function(swr) { + var ifr = document.createElement("iframe"); + document.documentElement.appendChild(ifr); + ifr.contentWindow.ServiceWorkerRegistration.prototype.showNotification + .call(swr, "Hi there. The ServiceWorker should receive a click event for this.", { data: { complex: ["jsval", 5] }}); + }); + + navigator.serviceWorker.onmessage = function(msg) { + testWindow.callback(msg.data.result); + }; +</script> + +</head> +<body> +</body> +</html> diff --git a/dom/serviceworkers/test/notificationclick.html b/dom/serviceworkers/test/notificationclick.html new file mode 100644 index 0000000000..448764a1cb --- /dev/null +++ b/dom/serviceworkers/test/notificationclick.html @@ -0,0 +1,27 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1114554 - controlled page</title> +<script class="testbody" type="text/javascript"> + var testWindow = parent; + if (opener) { + testWindow = opener; + } + + navigator.serviceWorker.ready.then(function(swr) { + swr.showNotification("Hi there. The ServiceWorker should receive a click event for this.", { data: { complex: ["jsval", 5] }}); + }); + + navigator.serviceWorker.onmessage = function(msg) { + testWindow.callback(msg.data.result); + }; +</script> + +</head> +<body> +</body> +</html> diff --git a/dom/serviceworkers/test/notificationclick.js b/dom/serviceworkers/test/notificationclick.js new file mode 100644 index 0000000000..ba7a7cb14a --- /dev/null +++ b/dom/serviceworkers/test/notificationclick.js @@ -0,0 +1,23 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ +// +onnotificationclick = function(e) { + self.clients.matchAll().then(function(clients) { + if (clients.length === 0) { + dump( + "********************* CLIENTS LIST EMPTY! Test will timeout! ***********************\n" + ); + return; + } + + clients.forEach(function(client) { + client.postMessage({ + result: + e.notification.data && + e.notification.data.complex && + e.notification.data.complex[0] == "jsval" && + e.notification.data.complex[1] == 5, + }); + }); + }); +}; diff --git a/dom/serviceworkers/test/notificationclick_focus.html b/dom/serviceworkers/test/notificationclick_focus.html new file mode 100644 index 0000000000..0152d397f3 --- /dev/null +++ b/dom/serviceworkers/test/notificationclick_focus.html @@ -0,0 +1,28 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1144660 - controlled page</title> +<script class="testbody" type="text/javascript"> + var testWindow = parent; + if (opener) { + testWindow = opener; + } + + navigator.serviceWorker.ready.then(function(swr) { + swr.showNotification("Hi there. The ServiceWorker should receive a click event for this."); + }); + + navigator.serviceWorker.onmessage = function(msg) { + dump("GOT Message " + JSON.stringify(msg.data) + "\n"); + testWindow.callback(msg.data.ok); + }; +</script> + +</head> +<body> +</body> +</html> diff --git a/dom/serviceworkers/test/notificationclick_focus.js b/dom/serviceworkers/test/notificationclick_focus.js new file mode 100644 index 0000000000..4c88340041 --- /dev/null +++ b/dom/serviceworkers/test/notificationclick_focus.js @@ -0,0 +1,49 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ +// + +function promisifyTimerFocus(client, delay) { + return new Promise(function(resolve, reject) { + setTimeout(function() { + client.focus().then(resolve, reject); + }, delay); + }); +} + +onnotificationclick = function(e) { + e.waitUntil( + self.clients.matchAll().then(function(clients) { + if (clients.length === 0) { + dump( + "********************* CLIENTS LIST EMPTY! Test will timeout! ***********************\n" + ); + return Promise.resolve(); + } + + var immediatePromise = clients[0].focus(); + var withinTimeout = promisifyTimerFocus(clients[0], 100); + + var afterTimeout = promisifyTimerFocus(clients[0], 2000).then( + function() { + throw "Should have failed!"; + }, + function() { + return Promise.resolve(); + } + ); + + return Promise.all([immediatePromise, withinTimeout, afterTimeout]) + .then(function() { + clients.forEach(function(client) { + client.postMessage({ ok: true }); + }); + }) + .catch(function(ex) { + dump("Error " + ex + "\n"); + clients.forEach(function(client) { + client.postMessage({ ok: false }); + }); + }); + }) + ); +}; diff --git a/dom/serviceworkers/test/notificationclose.html b/dom/serviceworkers/test/notificationclose.html new file mode 100644 index 0000000000..f18801122e --- /dev/null +++ b/dom/serviceworkers/test/notificationclose.html @@ -0,0 +1,37 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1265841 - controlled page</title> +<script class="testbody" type="text/javascript"> + var testWindow = parent; + if (opener) { + testWindow = opener; + } + + navigator.serviceWorker.ready.then(function(swr) { + return swr.showNotification( + "Hi there. The ServiceWorker should receive a close event for this.", + { data: { complex: ["jsval", 5] }}).then(function() { + return swr; + }); + }).then(function(swr) { + return swr.getNotifications(); + }).then(function(notifications) { + notifications.forEach(function(notification) { + notification.close(); + }); + }); + + navigator.serviceWorker.onmessage = function(msg) { + testWindow.callback(msg.data); + }; +</script> + +</head> +<body> +</body> +</html> diff --git a/dom/serviceworkers/test/notificationclose.js b/dom/serviceworkers/test/notificationclose.js new file mode 100644 index 0000000000..75271e604e --- /dev/null +++ b/dom/serviceworkers/test/notificationclose.js @@ -0,0 +1,31 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ +// +onnotificationclose = function(e) { + e.waitUntil( + (async function() { + let windowOpened = true; + await clients.openWindow("hello.html").catch(err => { + windowOpened = false; + }); + + self.clients.matchAll().then(function(clients) { + if (clients.length === 0) { + dump("*** CLIENTS LIST EMPTY! Test will timeout! ***\n"); + return; + } + + clients.forEach(function(client) { + client.postMessage({ + result: + e.notification.data && + e.notification.data.complex && + e.notification.data.complex[0] == "jsval" && + e.notification.data.complex[1] == 5, + windowOpened, + }); + }); + }); + })() + ); +}; diff --git a/dom/serviceworkers/test/notify_loaded.js b/dom/serviceworkers/test/notify_loaded.js new file mode 100644 index 0000000000..3bf001abd6 --- /dev/null +++ b/dom/serviceworkers/test/notify_loaded.js @@ -0,0 +1 @@ +parent.postMessage("SCRIPT_LOADED", "*"); diff --git a/dom/serviceworkers/test/onmessageerror_worker.js b/dom/serviceworkers/test/onmessageerror_worker.js new file mode 100644 index 0000000000..15426128a6 --- /dev/null +++ b/dom/serviceworkers/test/onmessageerror_worker.js @@ -0,0 +1,54 @@ +async function getSwContainer() { + const clients = await self.clients.matchAll({ + type: "window", + includeUncontrolled: true, + }); + + for (let client of clients) { + if (client.url.endsWith("test_onmessageerror.html")) { + return client; + } + } +} + +self.addEventListener("message", async e => { + const config = e.data; + const swContainer = await getSwContainer(); + + if (config == "send-bad-message") { + const serializable = true; + const deserializable = false; + + swContainer.postMessage( + new StructuredCloneTester(serializable, deserializable) + ); + + return; + } + + if (!config.serializable) { + swContainer.postMessage({ + result: "Error", + reason: "Service Worker received an unserializable object", + }); + + return; + } + + if (!config.deserializable) { + swContainer.postMessage({ + result: "Error", + reason: + "Service Worker received (and deserialized) an un-deserializable object", + }); + + return; + } + + swContainer.postMessage({ received: "message" }); +}); + +self.addEventListener("messageerror", async () => { + const swContainer = await getSwContainer(); + swContainer.postMessage({ received: "messageerror" }); +}); diff --git a/dom/serviceworkers/test/opaque_intercept_worker.js b/dom/serviceworkers/test/opaque_intercept_worker.js new file mode 100644 index 0000000000..5dbf6fbc28 --- /dev/null +++ b/dom/serviceworkers/test/opaque_intercept_worker.js @@ -0,0 +1,40 @@ +var name = "opaqueInterceptCache"; + +// Cross origin request to ensure that an opaque response is used +var prefix = "http://example.com/tests/dom/serviceworkers/test/"; + +var testReady = new Promise(resolve => { + self.addEventListener( + "message", + m => { + resolve(); + }, + { once: true } + ); +}); + +self.addEventListener("install", function(event) { + var request = new Request(prefix + "notify_loaded.js", { mode: "no-cors" }); + event.waitUntil( + Promise.all([caches.open(name), fetch(request), testReady]).then(function( + results + ) { + var cache = results[0]; + var response = results[1]; + return cache.put("./sw_clients/does_not_exist.js", response); + }) + ); +}); + +self.addEventListener("fetch", function(event) { + event.respondWith( + caches + .open(name) + .then(function(cache) { + return cache.match(event.request); + }) + .then(function(response) { + return response || fetch(event.request); + }) + ); +}); diff --git a/dom/serviceworkers/test/openWindow_worker.js b/dom/serviceworkers/test/openWindow_worker.js new file mode 100644 index 0000000000..eb440da6e5 --- /dev/null +++ b/dom/serviceworkers/test/openWindow_worker.js @@ -0,0 +1,178 @@ +// the worker won't shut down between events because we increased +// the timeout values. +var client; +var window_count = 0; +var expected_window_count = 9; +var isolated_window_count = 0; +var expected_isolated_window_count = 2; +var resolve_got_all_windows = null; +var got_all_windows = new Promise(function(res, rej) { + resolve_got_all_windows = res; +}); + +// |expected_window_count| needs to be updated for every new call that's +// expected to actually open a new window regardless of what |clients.openWindow| +// returns. +function testForUrl(url, throwType, clientProperties, resultsArray) { + return clients + .openWindow(url) + .then(function(e) { + if (throwType != null) { + resultsArray.push({ + result: false, + message: "openWindow should throw " + throwType, + }); + } else if (clientProperties) { + resultsArray.push({ + result: e instanceof WindowClient, + message: `openWindow should resolve to a WindowClient for url ${url}, got ${e}`, + }); + resultsArray.push({ + result: e.url == clientProperties.url, + message: "Client url should be " + clientProperties.url, + }); + // Add more properties + } else { + resultsArray.push({ + result: e == null, + message: "Open window should resolve to null. Got: " + e, + }); + } + }) + .catch(function(err) { + if (throwType == null) { + resultsArray.push({ + result: false, + message: "Unexpected throw: " + err, + }); + } else { + resultsArray.push({ + result: err.toString().includes(throwType), + message: "openWindow should throw: " + err, + }); + } + }); +} + +onmessage = function(event) { + if (event.data == "testNoPopup") { + client = event.source; + + var results = []; + var promises = []; + promises.push(testForUrl("about:blank", "TypeError", null, results)); + promises.push( + testForUrl("http://example.com", "InvalidAccessError", null, results) + ); + promises.push( + testForUrl("_._*`InvalidURL", "InvalidAccessError", null, results) + ); + event.waitUntil( + Promise.all(promises).then(function(e) { + client.postMessage(results); + }) + ); + } + + if (event.data == "NEW_WINDOW" || event.data == "NEW_ISOLATED_WINDOW") { + window_count += 1; + if (event.data == "NEW_ISOLATED_WINDOW") { + isolated_window_count += 1; + } + if (window_count == expected_window_count) { + resolve_got_all_windows(); + } + } + + if (event.data == "CHECK_NUMBER_OF_WINDOWS") { + event.waitUntil( + got_all_windows + .then(function() { + return clients.matchAll(); + }) + .then(function(cl) { + event.source.postMessage([ + { + result: cl.length == expected_window_count, + message: `The number of windows is correct. ${cl.length} == ${expected_window_count}`, + }, + { + result: isolated_window_count == expected_isolated_window_count, + message: `The number of isolated windows is correct. ${isolated_window_count} == ${expected_isolated_window_count}`, + }, + ]); + for (i = 0; i < cl.length; i++) { + cl[i].postMessage("CLOSE"); + } + }) + ); + } +}; + +onnotificationclick = function(e) { + var results = []; + var promises = []; + + var redirect = + "http://mochi.test:8888/tests/dom/serviceworkers/test/redirect.sjs?"; + var redirect_xorigin = + "http://example.com/tests/dom/serviceworkers/test/redirect.sjs?"; + var same_origin = + "http://mochi.test:8888/tests/dom/serviceworkers/test/open_window/client.sjs"; + var different_origin = + "http://example.com/tests/dom/serviceworkers/test/open_window/client.sjs"; + + promises.push(testForUrl("about:blank", "TypeError", null, results)); + promises.push(testForUrl(different_origin, null, null, results)); + promises.push(testForUrl(same_origin, null, { url: same_origin }, results)); + promises.push( + testForUrl("open_window/client.sjs", null, { url: same_origin }, results) + ); + + // redirect tests + promises.push( + testForUrl( + redirect + "open_window/client.sjs", + null, + { url: same_origin }, + results + ) + ); + promises.push(testForUrl(redirect + different_origin, null, null, results)); + + promises.push( + testForUrl(redirect_xorigin + "open_window/client.sjs", null, null, results) + ); + promises.push( + testForUrl( + redirect_xorigin + same_origin, + null, + { url: same_origin }, + results + ) + ); + + // coop+coep tests + promises.push( + testForUrl( + same_origin + "?crossOriginIsolated=true", + null, + { url: same_origin + "?crossOriginIsolated=true" }, + results + ) + ); + promises.push( + testForUrl( + different_origin + "?crossOriginIsolated=true", + null, + null, + results + ) + ); + + e.waitUntil( + Promise.all(promises).then(function() { + client.postMessage(results); + }) + ); +}; diff --git a/dom/serviceworkers/test/open_window/client.sjs b/dom/serviceworkers/test/open_window/client.sjs new file mode 100644 index 0000000000..236a4a1226 --- /dev/null +++ b/dom/serviceworkers/test/open_window/client.sjs @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const RESPONSE = ` +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1172870 - page opened by ServiceWorkerClients.OpenWindow</title> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<h1>client.sjs</h1> +<script class="testbody" type="text/javascript"> + + window.onload = function() { + if (document.domain === "mochi.test") { + navigator.serviceWorker.ready.then(function(result) { + navigator.serviceWorker.onmessage = function(event) { + if (event.data !== "CLOSE") { + dump("ERROR: unexepected reply from the service worker.\\n"); + } + if (parent) { + parent.postMessage("CLOSE", "*"); + } + window.close(); + } + + let message = window.crossOriginIsolated ? "NEW_ISOLATED_WINDOW" : "NEW_WINDOW"; + navigator.serviceWorker.controller.postMessage(message); + }) + } else { + window.onmessage = function(event) { + if (event.data !== "CLOSE") { + dump("ERROR: unexepected reply from the iframe.\\n"); + } + window.close(); + } + + var iframe = document.createElement('iframe'); + iframe.src = "http://mochi.test:8888/tests/dom/serviceworkers/test/open_window/client.sjs"; + document.body.appendChild(iframe); + } + } + +</script> +</pre> +</body> +</html> +`; + +function handleRequest(request, response) { + Components.utils.importGlobalProperties(["URLSearchParams"]); + let query = new URLSearchParams(request.queryString); + + // If the request has been marked to be isolated with COOP+COEP, set the appropriate headers. + if (query.get("crossOriginIsolated") == "true") { + response.setHeader("Cross-Origin-Opener-Policy", "same-origin", false); + } + + // Always set the COEP and CORP headers, so that this document can be framed + // by a document which has also set COEP to require-corp. + response.setHeader("Cross-Origin-Embedder-Policy", "require-corp", false); + response.setHeader("Cross-Origin-Resource-Policy", "cross-origin", false); + + response.setHeader("Content-Type", "text/html", false); + response.write(RESPONSE); +} diff --git a/dom/serviceworkers/test/page_post_controlled.html b/dom/serviceworkers/test/page_post_controlled.html new file mode 100644 index 0000000000..27694c0027 --- /dev/null +++ b/dom/serviceworkers/test/page_post_controlled.html @@ -0,0 +1,27 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> +</head> +<body> +<script type="text/javascript"> + window.parent.postMessage({ + controlled: !!navigator.serviceWorker.controller + }, "*"); + + addEventListener("message", e => { + if (e.data == "create nested iframe") { + const iframe = document.createElement('iframe'); + document.body.appendChild(iframe); + iframe.src = location.href; + } else { + window.parent.postMessage(e.data, "*"); + } + }); +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/parse_error_worker.js b/dom/serviceworkers/test/parse_error_worker.js new file mode 100644 index 0000000000..b6a8ef0a1a --- /dev/null +++ b/dom/serviceworkers/test/parse_error_worker.js @@ -0,0 +1,2 @@ +// intentional parse error. +var foo = {; diff --git a/dom/serviceworkers/test/redirect.sjs b/dom/serviceworkers/test/redirect.sjs new file mode 100644 index 0000000000..43fec90b5a --- /dev/null +++ b/dom/serviceworkers/test/redirect.sjs @@ -0,0 +1,4 @@ +function handleRequest(request, response) { + response.setStatusLine(request.httpVersion, 301, "Moved Permanently"); + response.setHeader("Location", request.queryString, false); +} diff --git a/dom/serviceworkers/test/redirect_post.sjs b/dom/serviceworkers/test/redirect_post.sjs new file mode 100644 index 0000000000..3b15f397b1 --- /dev/null +++ b/dom/serviceworkers/test/redirect_post.sjs @@ -0,0 +1,39 @@ +const CC = Components.Constructor; +const BinaryInputStream = CC( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); + +function handleRequest(request, response) { + var query = {}; + request.queryString.split("&").forEach(function(val) { + var [name, value] = val.split("="); + query[name] = unescape(value); + }); + + var bodyStream = new BinaryInputStream(request.bodyInputStream); + var bodyBytes = []; + while ((bodyAvail = bodyStream.available()) > 0) { + Array.prototype.push.apply(bodyBytes, bodyStream.readByteArray(bodyAvail)); + } + + var body = decodeURIComponent( + escape(String.fromCharCode.apply(null, bodyBytes)) + ); + + var currentHop = query.hop ? parseInt(query.hop) : 0; + + var obj = JSON.parse(body); + if (currentHop < obj.hops) { + var newURL = + "/tests/dom/serviceworkers/test/redirect_post.sjs?hop=" + + (1 + currentHop); + response.setStatusLine(null, 307, "redirect"); + response.setHeader("Location", newURL); + return; + } + + response.setHeader("Content-Type", "application/json"); + response.write(body); +} diff --git a/dom/serviceworkers/test/redirect_serviceworker.sjs b/dom/serviceworkers/test/redirect_serviceworker.sjs new file mode 100644 index 0000000000..858e6d4824 --- /dev/null +++ b/dom/serviceworkers/test/redirect_serviceworker.sjs @@ -0,0 +1,7 @@ +function handleRequest(request, response) { + response.setStatusLine("1.1", 302, "Found"); + response.setHeader( + "Location", + "http://mochi.test:8888/tests/dom/serviceworkers/test/worker.js" + ); +} diff --git a/dom/serviceworkers/test/register_https.html b/dom/serviceworkers/test/register_https.html new file mode 100644 index 0000000000..572c7ce6b8 --- /dev/null +++ b/dom/serviceworkers/test/register_https.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<script> +function ok(condition, message) { + parent.postMessage({type: "ok", status: condition, msg: message}, "*"); +} + +function done() { + parent.postMessage({type: "done"}, "*"); +} + +ok(location.protocol == "https:", "We should be loaded from HTTPS"); +ok(!window.isSecureContext, "Should not be secure context"); +ok(!("serviceWorker" in navigator), "ServiceWorkerContainer not availalble in insecure context"); +done(); +</script> diff --git a/dom/serviceworkers/test/sanitize/example_check_and_unregister.html b/dom/serviceworkers/test/sanitize/example_check_and_unregister.html new file mode 100644 index 0000000000..8553e442d6 --- /dev/null +++ b/dom/serviceworkers/test/sanitize/example_check_and_unregister.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<script> + function done(exists) { + parent.postMessage(exists, '*'); + } + + function fail() { + parent.postMessage("FAIL", '*'); + } + + navigator.serviceWorker.getRegistration(".").then(function(reg) { + if (reg) { + reg.unregister().then(done.bind(undefined, true), fail); + } else { + dump("getRegistration() returned undefined registration\n"); + done(false); + } + }, function(e) { + dump("getRegistration() failed\n"); + fail(); + }); +</script> diff --git a/dom/serviceworkers/test/sanitize/frame.html b/dom/serviceworkers/test/sanitize/frame.html new file mode 100644 index 0000000000..b4bf7a1ff1 --- /dev/null +++ b/dom/serviceworkers/test/sanitize/frame.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<script> + fetch("intercept-this").then(function(r) { + if (!r.ok) { + return "FAIL"; + } + return r.text(); + }).then(function(body) { + parent.postMessage(body, '*'); + }); +</script> diff --git a/dom/serviceworkers/test/sanitize/register.html b/dom/serviceworkers/test/sanitize/register.html new file mode 100644 index 0000000000..4ae74bec11 --- /dev/null +++ b/dom/serviceworkers/test/sanitize/register.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<script> + function done() { + parent.postMessage('', '*'); + } + + navigator.serviceWorker.ready.then(done); + navigator.serviceWorker.register("../sanitize_worker.js", {scope: "."}); +</script> diff --git a/dom/serviceworkers/test/sanitize_worker.js b/dom/serviceworkers/test/sanitize_worker.js new file mode 100644 index 0000000000..efd25a3856 --- /dev/null +++ b/dom/serviceworkers/test/sanitize_worker.js @@ -0,0 +1,5 @@ +onfetch = function(e) { + if (e.request.url.includes("intercept-this")) { + e.respondWith(new Response("intercepted")); + } +}; diff --git a/dom/serviceworkers/test/scope/scope_worker.js b/dom/serviceworkers/test/scope/scope_worker.js new file mode 100644 index 0000000000..4164e7a244 --- /dev/null +++ b/dom/serviceworkers/test/scope/scope_worker.js @@ -0,0 +1,2 @@ +// This worker is used to test if calling register() without a scope argument +// leads to scope being relative to service worker script. diff --git a/dom/serviceworkers/test/script_file_upload.js b/dom/serviceworkers/test/script_file_upload.js new file mode 100644 index 0000000000..ff839744b2 --- /dev/null +++ b/dom/serviceworkers/test/script_file_upload.js @@ -0,0 +1,15 @@ +/* eslint-env mozilla/chrome-script */ + +Cu.importGlobalProperties(["File"]); + +addMessageListener("file.open", function(e) { + var testFile = Cc["@mozilla.org/file/directory_service;1"] + .getService(Ci.nsIDirectoryService) + .QueryInterface(Ci.nsIProperties) + .get("ProfD", Ci.nsIFile); + testFile.append("prefs.js"); + + File.createFromNsIFile(testFile).then(function(file) { + sendAsyncMessage("file.opened", { file }); + }); +}); diff --git a/dom/serviceworkers/test/self_update_worker.sjs b/dom/serviceworkers/test/self_update_worker.sjs new file mode 100644 index 0000000000..8081b20afd --- /dev/null +++ b/dom/serviceworkers/test/self_update_worker.sjs @@ -0,0 +1,42 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const WORKER_BODY = ` +onactivate = function(event) { + let promise = clients.matchAll({includeUncontrolled: true}).then(function(clients) { + for (i = 0; i < clients.length; i++) { + clients[i].postMessage({version: version}); + } + }).then(function() { + return self.registration.update(); + }); + event.waitUntil(promise); +}; +`; + +function handleRequest(request, response) { + if (request.queryString == "clearcounter") { + setState("count", "1"); + response.write("ok"); + return; + } + + let count = getState("count"); + if (count === "") { + count = 1; + } else { + count = parseInt(count); + } + + let worker = "var version = " + count + ";\n"; + worker = worker + WORKER_BODY; + + // This header is necessary for making this script able to be loaded. + response.setHeader("Content-Type", "application/javascript"); + + // If this is the first request, return the first source. + response.write(worker); + setState("count", "" + (count + 1)); +} diff --git a/dom/serviceworkers/test/server_file_upload.sjs b/dom/serviceworkers/test/server_file_upload.sjs new file mode 100644 index 0000000000..a2f960af94 --- /dev/null +++ b/dom/serviceworkers/test/server_file_upload.sjs @@ -0,0 +1,22 @@ +const CC = Components.Constructor; +const BinaryInputStream = CC( + "@mozilla.org/binaryinputstream;1", + "nsIBinaryInputStream", + "setInputStream" +); +const BinaryOutputStream = CC( + "@mozilla.org/binaryoutputstream;1", + "nsIBinaryOutputStream", + "setOutputStream" +); + +function handleRequest(request, response) { + var bodyStream = new BinaryInputStream(request.bodyInputStream); + var bodyBytes = []; + while ((bodyAvail = bodyStream.available()) > 0) { + Array.prototype.push.apply(bodyBytes, bodyStream.readByteArray(bodyAvail)); + } + + var bos = new BinaryOutputStream(response.bodyOutputStream); + bos.writeByteArray(bodyBytes, bodyBytes.length); +} diff --git a/dom/serviceworkers/test/service_worker.js b/dom/serviceworkers/test/service_worker.js new file mode 100644 index 0000000000..432b943d8b --- /dev/null +++ b/dom/serviceworkers/test/service_worker.js @@ -0,0 +1,9 @@ +onmessage = function(e) { + self.clients.matchAll().then(function(res) { + if (!res.length) { + dump("Error: no clients are currently controlled.\n"); + return; + } + res[0].postMessage(indexedDB ? { available: true } : { available: false }); + }); +}; diff --git a/dom/serviceworkers/test/service_worker_client.html b/dom/serviceworkers/test/service_worker_client.html new file mode 100644 index 0000000000..c1c98eaabb --- /dev/null +++ b/dom/serviceworkers/test/service_worker_client.html @@ -0,0 +1,28 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> +<title>controlled page</title> +<script class="testbody" type="text/javascript"> + if (!parent) { + info("service_worker_client.html should not be launched directly!"); + } + + window.onload = function() { + navigator.serviceWorker.onmessage = function(msg) { + // Forward messages coming from the service worker to the test page. + parent.postMessage(msg.data, "*"); + }; + navigator.serviceWorker.ready.then(function(swr) { + parent.postMessage("READY", "*"); + }); + } +</script> + +</head> +<body> +</body> +</html> diff --git a/dom/serviceworkers/test/serviceworker.html b/dom/serviceworkers/test/serviceworker.html new file mode 100644 index 0000000000..11edd001a2 --- /dev/null +++ b/dom/serviceworkers/test/serviceworker.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <script> + navigator.serviceWorker.register("worker.js"); + </script> + </head> + <body> + This is a test page. + </body> +<html> diff --git a/dom/serviceworkers/test/serviceworker_not_sharedworker.js b/dom/serviceworkers/test/serviceworker_not_sharedworker.js new file mode 100644 index 0000000000..ef89fb01b4 --- /dev/null +++ b/dom/serviceworkers/test/serviceworker_not_sharedworker.js @@ -0,0 +1,20 @@ +function OnMessage(e) { + if (e.data.msg == "whoareyou") { + if ("ServiceWorker" in self) { + self.clients.matchAll().then(function(clients) { + clients[0].postMessage({ result: "serviceworker" }); + }); + } else { + port.postMessage({ result: "sharedworker" }); + } + } +} + +var port; +onconnect = function(e) { + port = e.ports[0]; + port.onmessage = OnMessage; + port.start(); +}; + +onmessage = OnMessage; diff --git a/dom/serviceworkers/test/serviceworker_wrapper.js b/dom/serviceworkers/test/serviceworker_wrapper.js new file mode 100644 index 0000000000..c9716c381f --- /dev/null +++ b/dom/serviceworkers/test/serviceworker_wrapper.js @@ -0,0 +1,92 @@ +// Any copyright is dedicated to the Public Domain. +// http://creativecommons.org/publicdomain/zero/1.0/ +// +// ServiceWorker equivalent of worker_wrapper.js. + +let client; + +function ok(a, msg) { + dump("OK: " + !!a + " => " + a + ": " + msg + "\n"); + client.postMessage({ type: "status", status: !!a, msg: a + ": " + msg }); +} + +function is(a, b, msg) { + dump("IS: " + (a === b) + " => " + a + " | " + b + ": " + msg + "\n"); + client.postMessage({ + type: "status", + status: a === b, + msg: a + " === " + b + ": " + msg, + }); +} + +function workerTestArrayEquals(a, b) { + if (!Array.isArray(a) || !Array.isArray(b) || a.length != b.length) { + return false; + } + for (var i = 0, n = a.length; i < n; ++i) { + if (a[i] !== b[i]) { + return false; + } + } + return true; +} + +function workerTestDone() { + client.postMessage({ type: "finish" }); +} + +function workerTestGetHelperData(cb) { + addEventListener("message", function workerTestGetHelperDataCB(e) { + if (e.data.type !== "returnHelperData") { + return; + } + removeEventListener("message", workerTestGetHelperDataCB); + cb(e.data.result); + }); + client.postMessage({ + type: "getHelperData", + }); +} + +function workerTestGetStorageManager(cb) { + addEventListener("message", function workerTestGetStorageManagerCB(e) { + if (e.data.type !== "returnStorageManager") { + return; + } + removeEventListener("message", workerTestGetStorageManagerCB); + cb(e.data.result); + }); + client.postMessage({ + type: "getStorageManager", + }); +} + +let completeInstall; + +addEventListener("message", function workerWrapperOnMessage(e) { + removeEventListener("message", workerWrapperOnMessage); + var data = e.data; + self.clients.matchAll({ includeUncontrolled: true }).then(function(clients) { + for (var i = 0; i < clients.length; ++i) { + if (clients[i].url.includes("message_receiver.html")) { + client = clients[i]; + break; + } + } + try { + importScripts(data.script); + } catch (ex) { + client.postMessage({ + type: "status", + status: false, + msg: + "worker failed to import " + data.script + "; error: " + ex.message, + }); + } + completeInstall(); + }); +}); + +addEventListener("install", e => { + e.waitUntil(new Promise(resolve => (completeInstall = resolve))); +}); diff --git a/dom/serviceworkers/test/serviceworkerinfo_iframe.html b/dom/serviceworkers/test/serviceworkerinfo_iframe.html new file mode 100644 index 0000000000..24103d1757 --- /dev/null +++ b/dom/serviceworkers/test/serviceworkerinfo_iframe.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <script> + window.onmessage = function (event) { + if (event.data !== "register") { + return; + } + var promise = navigator.serviceWorker.register("worker.js", + { updateViaCache: 'all' }); + window.onmessage = function (e) { + if (e.data !== "unregister") { + return; + } + promise.then(function (registration) { + registration.unregister(); + }); + window.onmessage = null; + }; + }; + </script> + </head> + <body> + This is a test page. + </body> +<html> diff --git a/dom/serviceworkers/test/serviceworkermanager_iframe.html b/dom/serviceworkers/test/serviceworkermanager_iframe.html new file mode 100644 index 0000000000..4ea21010cb --- /dev/null +++ b/dom/serviceworkers/test/serviceworkermanager_iframe.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <script> + window.onmessage = function (event) { + if (event.data !== "register") { + return; + } + var promise = navigator.serviceWorker.register("worker.js"); + window.onmessage = function (event1) { + if (event1.data !== "register") { + return; + } + promise = promise.then(function (registration) { + return navigator.serviceWorker.register("worker2.js"); + }); + window.onmessage = function (event2) { + if (event2.data !== "unregister") { + return; + } + promise.then(function (registration) { + registration.unregister(); + }); + window.onmessage = null; + }; + }; + }; + </script> + </head> + <body> + This is a test page. + </body> +<html> diff --git a/dom/serviceworkers/test/serviceworkerregistrationinfo_iframe.html b/dom/serviceworkers/test/serviceworkerregistrationinfo_iframe.html new file mode 100644 index 0000000000..8f382cf0dc --- /dev/null +++ b/dom/serviceworkers/test/serviceworkerregistrationinfo_iframe.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <script> + var reg; + window.onmessage = function (event) { + if (event.data !== "register") { + return; + } + var promise = navigator.serviceWorker.register("worker.js"); + window.onmessage = function (e) { + if (e.data === "register") { + promise.then(function() { + return navigator.serviceWorker.register("worker2.js") + .then(function(registration) { + reg = registration; + }); + }); + } else if (e.data === "unregister") { + reg.unregister(); + } + }; + }; + </script> + </head> + <body> + This is a test page. + </body> +<html> diff --git a/dom/serviceworkers/test/sharedWorker_fetch.js b/dom/serviceworkers/test/sharedWorker_fetch.js new file mode 100644 index 0000000000..b0a72674d4 --- /dev/null +++ b/dom/serviceworkers/test/sharedWorker_fetch.js @@ -0,0 +1,30 @@ +var clients = new Array(); +clients.length = 0; + +var broadcast = function(message) { + var length = clients.length; + for (var i = 0; i < length; i++) { + port = clients[i]; + port.postMessage(message); + } +}; + +onconnect = function(e) { + clients.push(e.ports[0]); + if (clients.length == 1) { + clients[0].postMessage("Connected"); + } else if (clients.length == 2) { + broadcast("BothConnected"); + clients[0].onmessage = function(msg) { + if (msg.data == "StartFetchWithWrongIntegrity") { + // The fetch will succeed because the integrity value is invalid and we + // are looking for the console message regarding the bad integrity value. + fetch("SharedWorker_SRIFailed.html", { integrity: "abc" }).then( + function() { + clients[0].postMessage("SRI_failed"); + } + ); + } + }; + } +}; diff --git a/dom/serviceworkers/test/simple_fetch_worker.js b/dom/serviceworkers/test/simple_fetch_worker.js new file mode 100644 index 0000000000..d5b1d8d9a7 --- /dev/null +++ b/dom/serviceworkers/test/simple_fetch_worker.js @@ -0,0 +1,18 @@ +// A simple worker script that forward intercepted url to the controlled window. + +function responseMsg(msg) { + self.clients + .matchAll({ + includeUncontrolled: true, + type: "window", + }) + .then(clients => { + if (clients && clients.length) { + clients[0].postMessage(msg); + } + }); +} + +onfetch = function(e) { + responseMsg(e.request.url); +}; diff --git a/dom/serviceworkers/test/simpleregister/index.html b/dom/serviceworkers/test/simpleregister/index.html new file mode 100644 index 0000000000..99e4fe3f23 --- /dev/null +++ b/dom/serviceworkers/test/simpleregister/index.html @@ -0,0 +1,51 @@ +<html> + <head></head> + <body> + <script type="text/javascript"> + var expectedEvents = 2; + function eventReceived() { + window.parent.postMessage({ type: "check", status: expectedEvents > 0, msg: "updatefound received" }, "*"); + + if (--expectedEvents) { + window.parent.postMessage({ type: "finish" }, "*"); + } + } + + navigator.serviceWorker.getRegistrations().then(function(a) { + window.parent.postMessage({ type: "check", status: Array.isArray(a), + msg: "getRegistrations returns an array" }, "*"); + window.parent.postMessage({ type: "check", status: !!a.length, + msg: "getRegistrations returns an array with 1 item" }, "*"); + for (var i = 0; i < a.length; ++i) { + window.parent.postMessage({ type: "check", status: a[i] instanceof ServiceWorkerRegistration, + msg: "getRegistrations returns an array of ServiceWorkerRegistration objects" }, "*"); + if (a[i].scope.match(/simpleregister\//)) { + a[i].onupdatefound = function(e) { + eventReceived(); + } + } + } + }); + + navigator.serviceWorker.getRegistration('http://mochi.test:8888/tests/dom/serviceworkers/test/simpleregister/') + .then(function(a) { + window.parent.postMessage({ type: "check", status: a instanceof ServiceWorkerRegistration, + msg: "getRegistration returns a ServiceWorkerRegistration" }, "*"); + a.onupdatefound = function(e) { + eventReceived(); + } + }); + + navigator.serviceWorker.getRegistration('http://www.something_else.net/') + .then(function(a) { + window.parent.postMessage({ type: "check", status: false, + msg: "getRegistration should throw for security error!" }, "*"); + }, function(a) { + window.parent.postMessage({ type: "check", status: true, + msg: "getRegistration should throw for security error!" }, "*"); + }); + + window.parent.postMessage({ type: "ready" }, "*"); + </script> + </body> +</html> diff --git a/dom/serviceworkers/test/simpleregister/ready.html b/dom/serviceworkers/test/simpleregister/ready.html new file mode 100644 index 0000000000..6bc163e5f4 --- /dev/null +++ b/dom/serviceworkers/test/simpleregister/ready.html @@ -0,0 +1,14 @@ +<html> + <head></head> + <body> + <script type="text/javascript"> + + window.addEventListener('message', function(evt) { + navigator.serviceWorker.ready.then(function() { + evt.ports[0].postMessage("WOW!"); + }); + }); + + </script> + </body> +</html> diff --git a/dom/serviceworkers/test/skip_waiting_installed_worker.js b/dom/serviceworkers/test/skip_waiting_installed_worker.js new file mode 100644 index 0000000000..a142576b9d --- /dev/null +++ b/dom/serviceworkers/test/skip_waiting_installed_worker.js @@ -0,0 +1,6 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +self.addEventListener("install", evt => { + evt.waitUntil(self.skipWaiting()); +}); diff --git a/dom/serviceworkers/test/skip_waiting_scope/index.html b/dom/serviceworkers/test/skip_waiting_scope/index.html new file mode 100644 index 0000000000..2b480d8707 --- /dev/null +++ b/dom/serviceworkers/test/skip_waiting_scope/index.html @@ -0,0 +1,33 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1131352 - Add ServiceWorkerGlobalScope skipWaiting()</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + if (!parent) { + info("skip_waiting_scope/index.html shouldn't be launched directly!"); + } + + navigator.serviceWorker.oncontrollerchange = function() { + parent.postMessage({ + event: "controllerchange", + controllerScriptURL: navigator.serviceWorker.controller && + navigator.serviceWorker.controller.scriptURL + }, "*"); + } + +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/source_message_posting_worker.js b/dom/serviceworkers/test/source_message_posting_worker.js new file mode 100644 index 0000000000..e48c005294 --- /dev/null +++ b/dom/serviceworkers/test/source_message_posting_worker.js @@ -0,0 +1,16 @@ +onmessage = function(e) { + if (!e.source) { + dump("ERROR: message doesn't have a source."); + } + + if (!(e instanceof ExtendableMessageEvent)) { + e.source.postMessage("ERROR. event is not an extendable message event."); + } + + // The client should be a window client + if (e.source instanceof WindowClient) { + e.source.postMessage(e.data); + } else { + e.source.postMessage("ERROR. source is not a window client."); + } +}; diff --git a/dom/serviceworkers/test/storage_recovery_worker.sjs b/dom/serviceworkers/test/storage_recovery_worker.sjs new file mode 100644 index 0000000000..9c9ce6a8d7 --- /dev/null +++ b/dom/serviceworkers/test/storage_recovery_worker.sjs @@ -0,0 +1,23 @@ +const BASE_URI = "http://mochi.test:8888/browser/dom/serviceworkers/test/"; + +function handleRequest(request, response) { + let redirect = getState("redirect"); + setState("redirect", "false"); + + if (request.queryString.includes("set-redirect")) { + setState("redirect", "true"); + } else if (request.queryString.includes("clear-redirect")) { + setState("redirect", "false"); + } + + response.setHeader("Cache-Control", "no-store"); + + if (redirect === "true") { + response.setStatusLine(request.httpVersion, 307, "Moved Temporarily"); + response.setHeader("Location", BASE_URI + "empty.js"); + return; + } + + response.setHeader("Content-Type", "application/javascript"); + response.write(""); +} diff --git a/dom/serviceworkers/test/streamfilter_server.sjs b/dom/serviceworkers/test/streamfilter_server.sjs new file mode 100644 index 0000000000..0e5a62d1dd --- /dev/null +++ b/dom/serviceworkers/test/streamfilter_server.sjs @@ -0,0 +1,9 @@ +Components.utils.importGlobalProperties(["URLSearchParams"]); + +function handleRequest(request, response) { + const searchParams = new URLSearchParams(request.queryString); + + if (searchParams.get("syntheticResponse") === "0") { + response.write(String(searchParams)); + } +} diff --git a/dom/serviceworkers/test/streamfilter_worker.js b/dom/serviceworkers/test/streamfilter_worker.js new file mode 100644 index 0000000000..03a0f0a933 --- /dev/null +++ b/dom/serviceworkers/test/streamfilter_worker.js @@ -0,0 +1,9 @@ +onactivate = e => e.waitUntil(clients.claim()); + +onfetch = e => { + const searchParams = new URL(e.request.url).searchParams; + + if (searchParams.get("syntheticResponse") === "1") { + e.respondWith(new Response(String(searchParams))); + } +}; diff --git a/dom/serviceworkers/test/strict_mode_warning.js b/dom/serviceworkers/test/strict_mode_warning.js new file mode 100644 index 0000000000..38418de3d8 --- /dev/null +++ b/dom/serviceworkers/test/strict_mode_warning.js @@ -0,0 +1,4 @@ +function f() { + return 1; + return 2; +} diff --git a/dom/serviceworkers/test/sw_bad_mime_type.js b/dom/serviceworkers/test/sw_bad_mime_type.js new file mode 100644 index 0000000000..f371807db9 --- /dev/null +++ b/dom/serviceworkers/test/sw_bad_mime_type.js @@ -0,0 +1 @@ +// I need some contents. diff --git a/dom/serviceworkers/test/sw_bad_mime_type.js^headers^ b/dom/serviceworkers/test/sw_bad_mime_type.js^headers^ new file mode 100644 index 0000000000..a1f9e38d90 --- /dev/null +++ b/dom/serviceworkers/test/sw_bad_mime_type.js^headers^ @@ -0,0 +1 @@ +Content-Type: text/plain diff --git a/dom/serviceworkers/test/sw_clients/file_blob_upload_frame.html b/dom/serviceworkers/test/sw_clients/file_blob_upload_frame.html new file mode 100644 index 0000000000..ff9803622a --- /dev/null +++ b/dom/serviceworkers/test/sw_clients/file_blob_upload_frame.html @@ -0,0 +1,76 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>test file blob upload with SW interception</title> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + +if (!parent) { + dump("sw_clients/file_blob_upload_frame.html shouldn't be launched directly!"); +} + +function makeFileBlob(obj) { + return new Promise(function(resolve, reject) { + + var request = indexedDB.open(window.location.pathname, 1); + request.onerror = reject; + request.onupgradeneeded = function(evt) { + var db = evt.target.result; + db.onerror = reject; + + var objectStore = db.createObjectStore('test', { autoIncrement: true }); + var index = objectStore.createIndex('test', 'index'); + }; + + request.onsuccess = function(evt) { + var db = evt.target.result; + db.onerror = reject; + + var blob = new Blob([JSON.stringify(obj)], + { type: 'application/json' }); + var data = { blob, index: 5 }; + + objectStore = db.transaction('test', 'readwrite').objectStore('test'); + objectStore.add(data).onsuccess = function(evt1) { + var key = evt1.target.result; + objectStore = db.transaction('test').objectStore('test'); + objectStore.get(key).onsuccess = function(evt2) { + resolve(evt2.target.result.blob); + }; + }; + }; + }); +} + +navigator.serviceWorker.ready.then(function() { + parent.postMessage({ status: 'READY' }, '*'); +}); + +var URL = '/tests/dom/serviceworkers/test/redirect_post.sjs'; + +addEventListener('message', function(evt) { + if (evt.data.type = 'TEST') { + makeFileBlob(evt.data.body).then(function(blob) { + return fetch(URL, { method: 'POST', body: blob }); + }).then(function(response) { + return response.json(); + }).then(function(result) { + parent.postMessage({ status: 'OK', result }, '*'); + }).catch(function(e) { + parent.postMessage({ status: 'ERROR', result: e.toString() }, '*'); + }); + } +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/sw_clients/navigator.html b/dom/serviceworkers/test/sw_clients/navigator.html new file mode 100644 index 0000000000..16a4fe9189 --- /dev/null +++ b/dom/serviceworkers/test/sw_clients/navigator.html @@ -0,0 +1,34 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 982726 - test match_all not crashing</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + if (!parent) { + dump("sw_clients/navigator.html shouldn't be launched directly!\n"); + } + + window.addEventListener("message", function(event) { + if (event.data.type === "NAVIGATE") { + window.location = event.data.url; + } + }); + + navigator.serviceWorker.ready.then(function() { + parent.postMessage("NAVIGATOR_READY", "*"); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/sw_clients/refresher.html b/dom/serviceworkers/test/sw_clients/refresher.html new file mode 100644 index 0000000000..b3c6e00152 --- /dev/null +++ b/dom/serviceworkers/test/sw_clients/refresher.html @@ -0,0 +1,38 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 982726 - test match_all not crashing</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <!-- some tests will intercept this bogus script request --> + <script type="text/javascript" src="does_not_exist.js"></script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + if (!parent) { + dump("sw_clients/simple.html shouldn't be launched directly!"); + } + + window.addEventListener("message", function(event) { + if (event.data === "REFRESH") { + window.location.reload(); + } else if (event.data === "FORCE_REFRESH") { + window.location.reload(true); + } + }); + + navigator.serviceWorker.ready.then(function() { + parent.postMessage("READY", "*"); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/sw_clients/refresher_cached.html b/dom/serviceworkers/test/sw_clients/refresher_cached.html new file mode 100644 index 0000000000..4a91e46e99 --- /dev/null +++ b/dom/serviceworkers/test/sw_clients/refresher_cached.html @@ -0,0 +1,37 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 982726 - test match_all not crashing</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + if (!parent) { + info("sw_clients/simple.html shouldn't be launched directly!"); + } + + window.addEventListener("message", function(event) { + if (event.data === "REFRESH") { + window.location.reload(); + } else if (event.data === "FORCE_REFRESH") { + window.location.reload(true); + } + }); + + navigator.serviceWorker.ready.then(function() { + parent.postMessage("READY_CACHED", "*"); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/sw_clients/refresher_cached_compressed.html b/dom/serviceworkers/test/sw_clients/refresher_cached_compressed.html Binary files differnew file mode 100644 index 0000000000..6b6a328211 --- /dev/null +++ b/dom/serviceworkers/test/sw_clients/refresher_cached_compressed.html diff --git a/dom/serviceworkers/test/sw_clients/refresher_cached_compressed.html^headers^ b/dom/serviceworkers/test/sw_clients/refresher_cached_compressed.html^headers^ new file mode 100644 index 0000000000..4204d8601d --- /dev/null +++ b/dom/serviceworkers/test/sw_clients/refresher_cached_compressed.html^headers^ @@ -0,0 +1,2 @@ +Content-Type: text/html +Content-Encoding: gzip diff --git a/dom/serviceworkers/test/sw_clients/refresher_compressed.html b/dom/serviceworkers/test/sw_clients/refresher_compressed.html Binary files differnew file mode 100644 index 0000000000..e0861a5180 --- /dev/null +++ b/dom/serviceworkers/test/sw_clients/refresher_compressed.html diff --git a/dom/serviceworkers/test/sw_clients/refresher_compressed.html^headers^ b/dom/serviceworkers/test/sw_clients/refresher_compressed.html^headers^ new file mode 100644 index 0000000000..4204d8601d --- /dev/null +++ b/dom/serviceworkers/test/sw_clients/refresher_compressed.html^headers^ @@ -0,0 +1,2 @@ +Content-Type: text/html +Content-Encoding: gzip diff --git a/dom/serviceworkers/test/sw_clients/service_worker_controlled.html b/dom/serviceworkers/test/sw_clients/service_worker_controlled.html new file mode 100644 index 0000000000..e0d7bce573 --- /dev/null +++ b/dom/serviceworkers/test/sw_clients/service_worker_controlled.html @@ -0,0 +1,38 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>controlled page</title> + <!-- + Paged controlled by a service worker for testing matchAll(). + See bug 982726, 1058311. + --> +<script class="testbody" type="text/javascript"> + function fail(msg) { + info("service_worker_controlled.html: " + msg); + opener.postMessage("FAIL", "*"); + } + + if (!parent) { + info("service_worker_controlled.html should not be launched directly!"); + } + + window.onload = function() { + navigator.serviceWorker.ready.then(function(swr) { + parent.postMessage("READY", "*"); + }); + } + + navigator.serviceWorker.onmessage = function(msg) { + // forward message to the test page. + parent.postMessage(msg.data, "*"); + }; +</script> + +</head> +<body> +</body> +</html> diff --git a/dom/serviceworkers/test/sw_clients/simple.html b/dom/serviceworkers/test/sw_clients/simple.html new file mode 100644 index 0000000000..bbe6782e2a --- /dev/null +++ b/dom/serviceworkers/test/sw_clients/simple.html @@ -0,0 +1,29 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 982726 - test match_all not crashing</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + if (!parent) { + info("sw_clients/simple.html shouldn't be launched directly!"); + } + + navigator.serviceWorker.ready.then(function() { + parent.postMessage("READY", "*"); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/sw_file_upload.js b/dom/serviceworkers/test/sw_file_upload.js new file mode 100644 index 0000000000..c35269924b --- /dev/null +++ b/dom/serviceworkers/test/sw_file_upload.js @@ -0,0 +1,16 @@ +self.skipWaiting(); + +addEventListener("fetch", event => { + const url = new URL(event.request.url); + const params = new URLSearchParams(url.search); + + if (params.get("clone") === "1") { + event.respondWith(fetch(event.request.clone())); + } else { + event.respondWith(fetch(event.request)); + } +}); + +addEventListener("activate", function(event) { + event.waitUntil(clients.claim()); +}); diff --git a/dom/serviceworkers/test/sw_respondwith_serviceworker.js b/dom/serviceworkers/test/sw_respondwith_serviceworker.js new file mode 100644 index 0000000000..6ddbc3d5c1 --- /dev/null +++ b/dom/serviceworkers/test/sw_respondwith_serviceworker.js @@ -0,0 +1,24 @@ +const SERVICEWORKER_DOC = `<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <script src="utils.js" type="text/javascript"></script> +</head> +<body> +SERVICEWORKER +</body> +</html> +`; + +const SERVICEWORKER_RESPONSE = new Response(SERVICEWORKER_DOC, { + headers: { "content-type": "text/html" }, +}); + +addEventListener("fetch", event => { + // Allow utils.js which we explicitly include to be loaded by resetting + // interception. + if (event.request.url.endsWith("/utils.js")) { + return; + } + event.respondWith(SERVICEWORKER_RESPONSE.clone()); +}); diff --git a/dom/serviceworkers/test/sw_storage_not_allow.js b/dom/serviceworkers/test/sw_storage_not_allow.js new file mode 100644 index 0000000000..c0bd782326 --- /dev/null +++ b/dom/serviceworkers/test/sw_storage_not_allow.js @@ -0,0 +1,33 @@ +let clientId; +addEventListener("fetch", function(event) { + event.respondWith( + (async function() { + if (event.request.url.includes("getClients")) { + // Expected to fail since the storage access is not allowed. + try { + await self.clients.matchAll(); + } catch (e) { + // expected failure + } + } else if (event.request.url.includes("getClient-stage1")) { + let clients = await self.clients.matchAll(); + clientId = clients[0].id; + } else if (event.request.url.includes("getClient-stage2")) { + // Expected to fail since the storage access is not allowed. + try { + await self.clients.get(clientId); + } catch (e) { + // expected failure + } + } + + // Pass through the network request once our various Clients API + // promises have completed. + return await fetch(event.request); + })() + ); +}); + +addEventListener("activate", function(event) { + event.waitUntil(clients.claim()); +}); diff --git a/dom/serviceworkers/test/sw_with_navigationPreload.js b/dom/serviceworkers/test/sw_with_navigationPreload.js new file mode 100644 index 0000000000..75e06787cd --- /dev/null +++ b/dom/serviceworkers/test/sw_with_navigationPreload.js @@ -0,0 +1,28 @@ +addEventListener("activate", event => { + event.waitUntil(self.registration.navigationPreload.enable()); +}); + +async function post_to_page(data) { + let cs = await self.clients.matchAll(); + for (const client of cs) { + client.postMessage(data); + } +} + +addEventListener("fetch", event => { + if (event.request.url.includes("navigationPreload_page.html")) { + event.respondWith( + new Response("<!DOCTYPE html>", { + headers: { "Content-Type": "text/html; charset=utf-8" }, + }) + ); + + event.waitUntil( + (async function() { + let preloadResponse = await event.preloadResponse; + let text = await preloadResponse.text(); + await post_to_page(text); + })() + ); + } +}); diff --git a/dom/serviceworkers/test/swa/worker_scope_different.js b/dom/serviceworkers/test/swa/worker_scope_different.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/dom/serviceworkers/test/swa/worker_scope_different.js diff --git a/dom/serviceworkers/test/swa/worker_scope_different.js^headers^ b/dom/serviceworkers/test/swa/worker_scope_different.js^headers^ new file mode 100644 index 0000000000..e85a7f09de --- /dev/null +++ b/dom/serviceworkers/test/swa/worker_scope_different.js^headers^ @@ -0,0 +1 @@ +Service-Worker-Allowed: different/path diff --git a/dom/serviceworkers/test/swa/worker_scope_different2.js b/dom/serviceworkers/test/swa/worker_scope_different2.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/dom/serviceworkers/test/swa/worker_scope_different2.js diff --git a/dom/serviceworkers/test/swa/worker_scope_different2.js^headers^ b/dom/serviceworkers/test/swa/worker_scope_different2.js^headers^ new file mode 100644 index 0000000000..e37307d666 --- /dev/null +++ b/dom/serviceworkers/test/swa/worker_scope_different2.js^headers^ @@ -0,0 +1 @@ +Service-Worker-Allowed: /different/path diff --git a/dom/serviceworkers/test/swa/worker_scope_precise.js b/dom/serviceworkers/test/swa/worker_scope_precise.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/dom/serviceworkers/test/swa/worker_scope_precise.js diff --git a/dom/serviceworkers/test/swa/worker_scope_precise.js^headers^ b/dom/serviceworkers/test/swa/worker_scope_precise.js^headers^ new file mode 100644 index 0000000000..7488cafbb0 --- /dev/null +++ b/dom/serviceworkers/test/swa/worker_scope_precise.js^headers^ @@ -0,0 +1 @@ +Service-Worker-Allowed: /tests/dom/serviceworkers/test/swa diff --git a/dom/serviceworkers/test/swa/worker_scope_too_deep.js b/dom/serviceworkers/test/swa/worker_scope_too_deep.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/dom/serviceworkers/test/swa/worker_scope_too_deep.js diff --git a/dom/serviceworkers/test/swa/worker_scope_too_deep.js^headers^ b/dom/serviceworkers/test/swa/worker_scope_too_deep.js^headers^ new file mode 100644 index 0000000000..9a66c3d153 --- /dev/null +++ b/dom/serviceworkers/test/swa/worker_scope_too_deep.js^headers^ @@ -0,0 +1 @@ +Service-Worker-Allowed: /tests/dom/serviceworkers/test/swa/deep/way/too/specific diff --git a/dom/serviceworkers/test/swa/worker_scope_too_narrow.js b/dom/serviceworkers/test/swa/worker_scope_too_narrow.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/dom/serviceworkers/test/swa/worker_scope_too_narrow.js diff --git a/dom/serviceworkers/test/swa/worker_scope_too_narrow.js^headers^ b/dom/serviceworkers/test/swa/worker_scope_too_narrow.js^headers^ new file mode 100644 index 0000000000..407361a3c7 --- /dev/null +++ b/dom/serviceworkers/test/swa/worker_scope_too_narrow.js^headers^ @@ -0,0 +1 @@ +Service-Worker-Allowed: /tests/dom/serviceworkers diff --git a/dom/serviceworkers/test/test_abrupt_completion.html b/dom/serviceworkers/test/test_abrupt_completion.html new file mode 100644 index 0000000000..bbf9e965f0 --- /dev/null +++ b/dom/serviceworkers/test/test_abrupt_completion.html @@ -0,0 +1,144 @@ +<!doctype html> +<meta charset=utf-8> +<title></title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script> + +// Tests a _registered_ ServiceWorker whose script evaluation results in an +// "abrupt completion", e.g. threw an uncaught exception. Such a ServiceWorker's +// first script evaluation must result in a "normal completion", however, for +// the Update algorithm to not abort in its step 18 when registering: +// +// 18. If runResult is failure or an abrupt completion, then: [...] + +const script = "./abrupt_completion_worker.js"; +const scope = "./empty.html"; +const expectedMessage = "handler-before-throw"; +let registration = null; + +// Should only be called once registration.active is non-null. Uses +// implementation details by zero-ing the "idle timeout"s and then sending an +// event to the ServiceWorker, which should immediately cause its termination. +// The idle timeouts are restored after the ServiceWorker is terminated. +async function startAndStopServiceWorker() { + SpecialPowers.registerObservers("service-worker-shutdown"); + + const spTopic = "specialpowers-service-worker-shutdown"; + + const origIdleTimeout = + SpecialPowers.getIntPref("dom.serviceWorkers.idle_timeout"); + + const origIdleExtendedTimeout = + SpecialPowers.getIntPref("dom.serviceWorkers.idle_extended_timeout"); + + await new Promise(resolve => { + const observer = { + async observe(subject, topic, data) { + if (topic !== spTopic) { + return; + } + + SpecialPowers.removeObserver(observer, spTopic); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.serviceWorkers.idle_timeout", origIdleTimeout], + ["dom.serviceWorkers.idle_extended_timeout", origIdleExtendedTimeout] + ] + }); + + resolve(); + }, + }; + + // Speed things up. + SpecialPowers.pushPrefEnv({ + set: [ + ["dom.serviceWorkers.idle_timeout", 0], + ["dom.serviceWorkers.idle_extended_timeout", 0] + ] + }).then(() => { + SpecialPowers.addObserver(observer, spTopic); + + registration.active.postMessage(""); + }); + }); +} + +// eslint-disable-next-line mozilla/no-addtask-setup +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ] + }); + + registration = await navigator.serviceWorker.register(script, { scope }); + SimpleTest.registerCleanupFunction(async function unregisterRegistration() { + await registration.unregister(); + }); + + await new Promise(resolve => { + const serviceWorker = registration.installing; + + serviceWorker.onstatechange = () => { + if (serviceWorker.state === "activated") { + resolve(); + } + }; + }); + + ok(registration.active instanceof ServiceWorker, "ServiceWorker is activated"); +}); + +// We expect that the restarted SW that experiences an abrupt completion at +// startup after adding its message handler 1) will be active in order to +// respond to our postMessage and 2) will respond with the global value set +// prior to the importScripts call that throws (and not the global value that +// would have been assigned after the importScripts call if it didn't throw). +add_task(async function testMessageHandler() { + await startAndStopServiceWorker(); + + await new Promise(resolve => { + navigator.serviceWorker.onmessage = e => { + is(e.data, expectedMessage, "Correct message handler"); + resolve(); + }; + registration.active.postMessage(""); + }); +}); + +// We expect that the restarted SW that experiences an abrupt completion at +// startup before adding its "fetch" listener will 1) successfully dispatch the +// event and 2) it will not be handled (respondWith() will not be called) so +// interception will be reset and the response will contain the contents of +// empty.html. Before the fix in bug 1603484 the SW would fail to properly start +// up and the fetch event would result in a NetworkError, breaking the +// controlled page. +add_task(async function testFetchHandler() { + await startAndStopServiceWorker(); + + const iframe = document.createElement("iframe"); + SimpleTest.registerCleanupFunction(function removeIframe() { + iframe.remove(); + }); + + await new Promise(resolve => { + iframe.src = scope; + iframe.onload = resolve; + document.body.appendChild(iframe); + }); + + const response = await iframe.contentWindow.fetch(scope); + + // NetworkError will have a status of 0, which is not "ok", and this is + // a stronger guarantee that should be true instead of just checking if there + // isn't a NetworkError. + ok(response.ok, "Fetch succeeded and didn't result in a NetworkError"); + + const text = await response.text(); + is(text, "", "Correct response text"); +}); + +</script> diff --git a/dom/serviceworkers/test/test_async_waituntil.html b/dom/serviceworkers/test/test_async_waituntil.html new file mode 100644 index 0000000000..8c15eb2b11 --- /dev/null +++ b/dom/serviceworkers/test/test_async_waituntil.html @@ -0,0 +1,91 @@ +<!DOCTYPE HTML> +<html> +<!-- + Test that: + 1. waitUntil() waits for each individual promise separately, even if + one of them was rejected. + 2. waitUntil() can be called asynchronously as long as there is still + a pending extension promise. + --> +<head> + <title>Test for Bug 1263304</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="error_reporting_helpers.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <meta http-equiv="Content-type" content="text/html;charset=UTF-8"> +</head> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1263304">Mozilla Bug 1263304</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> +add_task(function setupPrefs() { + return SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}); +}); + +function wait_for_message(expected_message) { + return new Promise(function(resolve, reject) { + navigator.serviceWorker.onmessage = function(event) { + navigator.serviceWorker.onmessage = null; + ok(event.data === expected_message, "Received expected message event: " + event.data); + resolve(); + } + }); +} + +add_task(async function async_wait_until() { + var worker; + let registration = await navigator.serviceWorker.register( + "async_waituntil_worker.js", { scope: "./"} ) + .then(function(reg) { + worker = reg.installing; + return waitForState(worker, 'activated', reg); + }); + + // The service worker will claim us when it becomes active. + ok(navigator.serviceWorker.controller, "Controlled"); + + // This will make the service worker die immediately if there are no pending + // waitUntil promises to keep it alive. + await SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.idle_timeout", 0], + ["dom.serviceWorkers.idle_extended_timeout", 299999]]}); + + // The service worker will wait on two promises, one of which + // will be rejected. We check whether the SW is killed using + // the value of a global variable. + let waitForStart = wait_for_message("Started"); + worker.postMessage("Start"); + await waitForStart; + + await new Promise((res, rej) => { + setTimeout(res, 0); + }); + + let waitResult = wait_for_message("Success"); + worker.postMessage("Result"); + await waitResult; + + // Test the behaviour of calling waitUntil asynchronously. The important + // part is that we receive the message event. + let waitForMessage = wait_for_message("Done"); + await fetch("doesnt_exist.html").then(() => { + ok(true, "Fetch was successful."); + }); + await waitForMessage; + + await SpecialPowers.popPrefEnv(); + await SpecialPowers.popPrefEnv(); + await registration.unregister(); +}); +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_bad_script_cache.html b/dom/serviceworkers/test/test_bad_script_cache.html new file mode 100644 index 0000000000..7919802678 --- /dev/null +++ b/dom/serviceworkers/test/test_bad_script_cache.html @@ -0,0 +1,96 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test updating a service worker with a bad script cache.</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script src='utils.js'></script> +<script class="testbody" type="text/javascript"> + +async function deleteCaches(cacheStorage) { + let keyList = await cacheStorage.keys(); + let promiseList = []; + keyList.forEach(key => { + promiseList.push(cacheStorage.delete(key)); + }); + return await Promise.all(keyList); +} + +function waitForUpdate(reg) { + return new Promise(resolve => { + reg.addEventListener('updatefound', resolve, { once: true }); + }); +} + +async function runTest() { + let reg; + try { + const script = 'update_worker.sjs'; + const scope = 'bad-script-cache'; + + reg = await navigator.serviceWorker.register(script, { scope }); + await waitForState(reg.installing, 'activated'); + + // Verify the service worker script cache has the worker script stored. + let chromeCaches = SpecialPowers.createChromeCache('chrome', window.origin); + let scriptURL = new URL(script, window.location.href); + let response = await chromeCaches.match(scriptURL.href); + is(response.url, scriptURL.href, 'worker script should be stored'); + + // Force delete the service worker script out from under the service worker. + // Note: Prefs are set to kill the SW thread immediately on idle. + await deleteCaches(chromeCaches); + + // Verify the service script cache no longer knows about the worker script. + response = await chromeCaches.match(scriptURL.href); + is(response, undefined, 'worker script should not be stored'); + + // Force an update and wait for it to fire an update event. + reg.update(); + await waitForUpdate(reg); + await waitForState(reg.installing, 'activated'); + + // Verify that the script cache knows about the worker script again. + response = await chromeCaches.match(scriptURL.href); + is(response.url, scriptURL.href, 'worker script should be stored'); + } catch (e) { + ok(false, e); + } + if (reg) { + await reg.unregister(); + } + + // If this test is run on windows and the process shuts down immediately after, then + // we may fail to remove some of the Cache API body files. This is because the GC + // runs late causing Cache API to cleanup after shutdown begins. It seems something + // during shutdown scans these files and conflicts with removing the file on windows. + // + // To avoid this we perform an explict GC here to ensure that Cache API can cleanup + // earlier. + await new Promise(resolve => SpecialPowers.exactGC(resolve)); + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +SpecialPowers.pushPrefEnv({"set": [ + // standard prefs + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true], + + // immediately kill the service worker thread when idle + ["dom.serviceWorkers.idle_timeout", 0], + +]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_bug1151916.html b/dom/serviceworkers/test/test_bug1151916.html new file mode 100644 index 0000000000..b541129ccb --- /dev/null +++ b/dom/serviceworkers/test/test_bug1151916.html @@ -0,0 +1,104 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1151916 - Test principal is set on cached serviceworkers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +<!-- + If the principal is not set, accessing self.caches in the worker will crash. +--> +</head> +<body> +<p id="display"></p> +<div id="content"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var frame; + + function listenForMessage() { + var p = new Promise(function(resolve, reject) { + window.onmessage = function(e) { + if (e.data.status == "failed") { + ok(false, "iframe had error " + e.data.message); + reject(e.data.message); + } else if (e.data.status == "success") { + ok(true, "iframe step success " + e.data.message); + resolve(e.data.message); + } else { + ok(false, "Unexpected message " + e.data); + reject(); + } + } + }); + + return p; + } + + // We have the iframe register for its own scope so that this page is not + // holding any references when we GC. + function register() { + var p = listenForMessage(); + + frame = document.createElement("iframe"); + document.body.appendChild(frame); + frame.src = "bug1151916_driver.html"; + + return p; + } + + function unloadFrame() { + frame.src = "about:blank"; + frame.remove(); + frame = null; + } + + function gc() { + return new Promise(function(resolve) { + SpecialPowers.exactGC(resolve); + }); + } + + function testCaches() { + var p = listenForMessage(); + + frame = document.createElement("iframe"); + document.body.appendChild(frame); + frame.src = "bug1151916_driver.html"; + + return p; + } + + function unregister() { + return navigator.serviceWorker.getRegistration("./bug1151916_driver.html").then(function(reg) { + ok(reg instanceof ServiceWorkerRegistration, "Must have valid registration."); + return reg.unregister(); + }); + } + + function runTest() { + register() + .then(unloadFrame) + .then(gc) + .then(testCaches) + .then(unregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true], + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_bug1240436.html b/dom/serviceworkers/test/test_bug1240436.html new file mode 100644 index 0000000000..8b76ada6a8 --- /dev/null +++ b/dom/serviceworkers/test/test_bug1240436.html @@ -0,0 +1,34 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for encoding of service workers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + SimpleTest.waitForExplicitFinish(); + + function runTest() { + navigator.serviceWorker.register("bug1240436_worker.js") + .then(reg => reg.unregister()) + .then(() => ok(true, "service worker register script succeed")) + .catch(err => ok(false, "service worker register script faled " + err)) + .then(() => SimpleTest.finish()); + } + + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_bug1408734.html b/dom/serviceworkers/test/test_bug1408734.html new file mode 100644 index 0000000000..27559e695f --- /dev/null +++ b/dom/serviceworkers/test/test_bug1408734.html @@ -0,0 +1,52 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1408734</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="error_reporting_helpers.js"></script> + <script src="utils.js"></script> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + +// setup prefs +add_task(() => { + return SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}); +}); + +// test for bug 1408734 +add_task(async () => { + // register a service worker + let registration = await navigator.serviceWorker.register("fetch.js", + {scope: "./"}); + // wait for service worker be activated + await waitForState(registration.installing, "activated"); + + // get the ServiceWorkerRegistration we just register through GetRegistration + registration = await navigator.serviceWorker.getRegistration("./"); + ok(registration, "should get the registration under scope './'"); + + // call unregister() + await registration.unregister(); + + // access registration.updateViaCache to trigger the bug + // we really care that we don't crash. In the future we will fix + is(registration.updateViaCache, "imports", + "registration.updateViaCache should work after unregister()"); +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_claim.html b/dom/serviceworkers/test/test_claim.html new file mode 100644 index 0000000000..e72f1173e8 --- /dev/null +++ b/dom/serviceworkers/test/test_claim.html @@ -0,0 +1,171 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1130684 - Test service worker clients claim onactivate </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + var registration_1; + var registration_2; + var client; + + function register_1() { + return navigator.serviceWorker.register("claim_worker_1.js", + { scope: "./" }) + .then((swr) => registration_1 = swr); + } + + function register_2() { + return navigator.serviceWorker.register("claim_worker_2.js", + { scope: "./claim_clients/client.html" }) + .then((swr) => registration_2 = swr); + } + + function unregister(reg) { + return reg.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }); + } + + function createClient() { + var p = new Promise(function(res, rej) { + window.onmessage = function(e) { + if (e.data === "READY") { + res(); + } + } + }); + + content = document.getElementById("content"); + ok(content, "parent exists."); + + client = document.createElement("iframe"); + client.setAttribute('src', "claim_clients/client.html"); + content.appendChild(client); + + return p; + } + + function testController() { + ok(navigator.serviceWorker.controller.scriptURL.match("claim_worker_1"), + "Controlling service worker has the correct url."); + } + + function testClientWasClaimed(expected) { + var resolveClientMessage, resolveClientControllerChange; + var messageFromClient = new Promise(function(res, rej) { + resolveClientMessage = res; + }); + var controllerChangeFromClient = new Promise(function(res, rej) { + resolveClientControllerChange = res; + }); + window.onmessage = function(e) { + if (!e.data.event) { + ok(false, "Unknown message received: " + e.data); + } + + if (e.data.event === "controllerchange") { + ok(e.data.controller, + "Client was claimed and received controllerchange event."); + resolveClientControllerChange(); + } + + if (e.data.event === "message") { + ok(e.data.data.resolve_value === undefined, + "Claim should resolve with undefined."); + ok(e.data.data.message === expected.message, + "Client received message from claiming worker."); + ok(e.data.data.match_count_before === expected.match_count_before, + "MatchAll clients count before claim should be " + expected.match_count_before); + ok(e.data.data.match_count_after === expected.match_count_after, + "MatchAll clients count after claim should be " + expected.match_count_after); + resolveClientMessage(); + } + } + + return Promise.all([messageFromClient, controllerChangeFromClient]) + .then(() => window.onmessage = null); + } + + function testClaimFirstWorker() { + // wait for the worker to control us + var controllerChange = new Promise(function(res, rej) { + navigator.serviceWorker.oncontrollerchange = function(e) { + ok(true, "controller changed event received."); + res(); + }; + }); + + var messageFromWorker = new Promise(function(res, rej) { + navigator.serviceWorker.onmessage = function(e) { + ok(e.data.resolve_value === undefined, + "Claim should resolve with undefined."); + ok(e.data.message === "claim_worker_1", + "Received message from claiming worker."); + ok(e.data.match_count_before === 0, + "Worker doesn't control any client before claim."); + ok(e.data.match_count_after === 2, "Worker should claim 2 clients."); + res(); + } + }); + + var clientClaim = testClientWasClaimed({ + message: "claim_worker_1", + match_count_before: 0, + match_count_after: 2 + }); + + return Promise.all([controllerChange, messageFromWorker, clientClaim]) + .then(testController); + } + + function testClaimSecondWorker() { + navigator.serviceWorker.oncontrollerchange = function(e) { + ok(false, "Claim_worker_2 shouldn't claim this window."); + } + + navigator.serviceWorker.onmessage = function(e) { + ok(false, "Claim_worker_2 shouldn't claim this window."); + } + + var clientClaim = testClientWasClaimed({ + message: "claim_worker_2", + match_count_before: 0, + match_count_after: 1 + }); + + return clientClaim.then(testController); + } + + function runTest() { + createClient() + .then(register_1) + .then(testClaimFirstWorker) + .then(register_2) + .then(testClaimSecondWorker) + .then(function() { return unregister(registration_1); }) + .then(function() { return unregister(registration_2); }) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_claim_oninstall.html b/dom/serviceworkers/test/test_claim_oninstall.html new file mode 100644 index 0000000000..54933405ce --- /dev/null +++ b/dom/serviceworkers/test/test_claim_oninstall.html @@ -0,0 +1,77 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1130684 - Test service worker clients.claim oninstall</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + var registration; + + function register() { + return navigator.serviceWorker.register("claim_oninstall_worker.js", + { scope: "./" }) + .then((swr) => registration = swr); + } + + + function unregister() { + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }); + } + + function testClaim() { + ok(registration.installing, "Worker should be in installing state"); + + navigator.serviceWorker.oncontrollerchange = function() { + ok(false, "Claim should not succeed when the worker is not active."); + } + + var p = new Promise(function(res, rej) { + var worker = registration.installing; + worker.onstatechange = function(e) { + if (worker.state === 'installed') { + is(worker, registration.waiting, "Worker should be in waiting state"); + } else if (worker.state === 'activated') { + // The worker will become active only if claim will reject inside the + // install handler. + is(worker, registration.active, + "Claim should reject if the worker is not active"); + ok(navigator.serviceWorker.controller === null, "Client is not controlled."); + e.target.onstatechange = null; + res(); + } + } + }); + + return p; + } + + function runTest() { + register() + .then(testClaim) + .then(unregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_controller.html b/dom/serviceworkers/test/test_controller.html new file mode 100644 index 0000000000..c0e220a36e --- /dev/null +++ b/dom/serviceworkers/test/test_controller.html @@ -0,0 +1,83 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1002570 - test controller instance.</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> + + var content; + var iframe; + var registration; + + function simpleRegister() { + // We use the control scope for the less specific registration. The window will register a worker on controller/ + return navigator.serviceWorker.register("worker.js", { scope: "./control" }) + .then(swr => waitForState(swr.installing, 'activated', swr)) + .then(swr => registration = swr); + } + + function unregister() { + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + dump("Unregistering the SW failed: " + e + "\n"); + }); + } + + function testController() { + var p = new Promise(function(resolve, reject) { + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "done") { + window.onmessage = null; + content.removeChild(iframe); + resolve(); + } + } + }); + + content = document.getElementById("content"); + ok(content, "Parent exists."); + + iframe = document.createElement("iframe"); + iframe.setAttribute('src', "controller/index.html"); + content.appendChild(iframe); + + return p; + } + + // This document just flips the prefs and opens the iframe for the actual test. + function runTest() { + simpleRegister() + .then(testController) + .then(unregister) + .then(function() { + SimpleTest.finish(); + }).catch(function(e) { + ok(false, "Some test failed with error " + e); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_cookie_fetch.html b/dom/serviceworkers/test/test_cookie_fetch.html new file mode 100644 index 0000000000..8c4324c759 --- /dev/null +++ b/dom/serviceworkers/test/test_cookie_fetch.html @@ -0,0 +1,64 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1331680 - test access to cookies in the documents synthesized from service worker responses</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/cookie/register.html"; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + // Remove the iframe and recreate a new one to ensure that any traces + // of the cookies have been removed from the child process. + iframe.remove(); + iframe = document.createElement("iframe"); + document.getElementById("content").appendChild(iframe); + + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/cookie/synth.html"; + } else if (e.data.status == "done") { + // Note, we can't do an exact is() comparison here since other + // tests can leave cookies on the domain. + ok(e.data.cookie.includes("foo=bar"), + "The synthesized document has access to its cookies"); + + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/cookie/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + ok(true, "Test finished successfully"); + SpecialPowers.clearUserPref("network.cookie.sameSite.laxByDefault"); + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default" + ["network.cookie.sameSite.laxByDefault", false], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_cross_origin_url_after_redirect.html b/dom/serviceworkers/test/test_cross_origin_url_after_redirect.html new file mode 100644 index 0000000000..bfd4f700be --- /dev/null +++ b/dom/serviceworkers/test/test_cross_origin_url_after_redirect.html @@ -0,0 +1,50 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test access to a cross origin Request.url property from a service worker for a redirected intercepted iframe</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "/tests/dom/serviceworkers/test/fetch/requesturl/register.html"; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + iframe.src = "/tests/dom/serviceworkers/test/fetch/requesturl/index.html"; + } else if (e.data.status == "done") { + iframe.src = "/tests/dom/serviceworkers/test/fetch/requesturl/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + ok(true, "Test finished successfully"); + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_csp_upgrade-insecure_intercept.html b/dom/serviceworkers/test/test_csp_upgrade-insecure_intercept.html new file mode 100644 index 0000000000..b5ddbb97b6 --- /dev/null +++ b/dom/serviceworkers/test/test_csp_upgrade-insecure_intercept.html @@ -0,0 +1,55 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test that a CSP upgraded request can be intercepted by a service worker</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/upgrade-insecure/register.html"; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/upgrade-insecure/embedder.html"; + } else if (e.data.status == "protocol") { + is(e.data.data, "https:", "Correct protocol expected"); + } else if (e.data.status == "image") { + is(e.data.data, 40, "The image request was upgraded before interception"); + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/upgrade-insecure/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + // This is needed so that we can test upgrading a non-secure load inside an https iframe. + ["security.mixed_content.block_active_content", false], + ["security.mixed_content.block_display_content", false], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_devtools_bypass_serviceworker.html b/dom/serviceworkers/test/test_devtools_bypass_serviceworker.html new file mode 100644 index 0000000000..09c05d557a --- /dev/null +++ b/dom/serviceworkers/test/test_devtools_bypass_serviceworker.html @@ -0,0 +1,107 @@ +<!DOCTYPE HTML> +<html> +<head> + <title> Verify devtools can utilize nsIChannel::LOAD_BYPASS_SERVICE_WORKER to bypass the service worker </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="error_reporting_helpers.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <meta http-equiv="Content-type" content="text/html;charset=UTF-8"> +</head> +<body> +<div id="content" style="display: none"></div> +<script src="utils.js"></script> +<script type="text/javascript"> +"use strict"; + +async function testBypassSW () { + let Ci = SpecialPowers.Ci; + + // Bypass SW imitates the "Disable Cache" option in dev-tools. + // Note: if we put the setter/getter into dev-tools, we should take care of + // the implementation of enabling/disabling cache since it just overwrite the + // defaultLoadFlags of docShell. + function setBypassServiceWorker(aDocShell, aBypass) { + if (aBypass) { + aDocShell.defaultLoadFlags |= Ci.nsIChannel.LOAD_BYPASS_SERVICE_WORKER; + return; + } + + aDocShell.defaultLoadFlags &= ~Ci.nsIChannel.LOAD_BYPASS_SERVICE_WORKER; + } + + function getBypassServiceWorker(aDocShell) { + return !!(aDocShell.defaultLoadFlags & + Ci.nsIChannel.LOAD_BYPASS_SERVICE_WORKER); + } + + async function fetchFakeDocAndCheckIfIntercepted(aWindow) { + const fakeDoc = "fake.html"; + + // Note: The fetching document doesn't exist, so the expected status of the + // repsonse is 404 unless the request is hijacked. + let response = await aWindow.fetch(fakeDoc); + if (response.status === 404) { + return false; + } else if (!response.ok) { + throw(response.statusText); + } + + let text = await response.text(); + if (text.includes("Hello")) { + // Intercepted + return true; + } + + throw("Unexpected error"); + return; + } + + let docShell = SpecialPowers.wrap(window).docShell; + + info("Test 1: Enable bypass service worker for the docShell"); + + setBypassServiceWorker(docShell, true); + ok(getBypassServiceWorker(docShell), + "The loadFlags in docShell does bypass the serviceWorker by default"); + + let intercepted = await fetchFakeDocAndCheckIfIntercepted(window); + ok(!intercepted, + "The fetched document wasn't intercepted by the serviceWorker"); + + info("Test 2: Disable the bypass service worker for the docShell"); + + setBypassServiceWorker(docShell, false); + ok(!getBypassServiceWorker(docShell), + "The loadFlags in docShell doesn't bypass the serviceWorker by default"); + + intercepted = await fetchFakeDocAndCheckIfIntercepted(window); + ok(intercepted, + "The fetched document was intercepted by the serviceWorker"); +} + +// (This doesn't really need to be its own task, but it allows the actual test +// case to be self-contained.) +add_task(function setupPrefs() { + return SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}); +}); + +add_task(async function test_bypassServiceWorker() { + const swURL = "fetch.js"; + let registration = await navigator.serviceWorker.register(swURL); + await waitForState(registration.installing, 'activated'); + + try { + await testBypassSW(); + } catch (e) { + ok(false, "Reason:" + e); + } + + await registration.unregister(); +}); + +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_devtools_track_serviceworker_time.html b/dom/serviceworkers/test/test_devtools_track_serviceworker_time.html new file mode 100644 index 0000000000..ac27ebcd33 --- /dev/null +++ b/dom/serviceworkers/test/test_devtools_track_serviceworker_time.html @@ -0,0 +1,236 @@ +<html> +<head> + <title>Bug 1251238 - track service worker install time</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +</head> +<iframe id="iframe"></iframe> +<body> + +<script type="text/javascript"> + +const State = { + BYTECHECK: -1, + PARSED: Ci.nsIServiceWorkerInfo.STATE_PARSED, + INSTALLING: Ci.nsIServiceWorkerInfo.STATE_INSTALLING, + INSTALLED: Ci.nsIServiceWorkerInfo.STATE_INSTALLED, + ACTIVATING: Ci.nsIServiceWorkerInfo.STATE_ACTIVATING, + ACTIVATED: Ci.nsIServiceWorkerInfo.STATE_ACTIVATED, + REDUNDANT: Ci.nsIServiceWorkerInfo.STATE_REDUNDANT +}; +let swm = Cc["@mozilla.org/serviceworkers/manager;1"]. + getService(Ci.nsIServiceWorkerManager); + +let EXAMPLE_URL = "https://example.com/chrome/dom/serviceworkers/test/"; + +let swrlistener = null; +let registrationInfo = null; + +// Use it to keep the sw after unregistration. +let astrayServiceWorkerInfo = null; + +let expectedResults = [ + { + // Speacial state for verifying update since we will do the byte-check + // first. + state: State.BYTECHECK, installedTimeRecorded: false, + activatedTimeRecorded: false, redundantTimeRecorded: false + }, + { + state: State.PARSED, installedTimeRecorded: false, + activatedTimeRecorded: false, redundantTimeRecorded: false + }, + { + state: State.INSTALLING, installedTimeRecorded: false, + activatedTimeRecorded: false, redundantTimeRecorded: false + }, + { + state: State.INSTALLED, installedTimeRecorded: true, + activatedTimeRecorded: false, redundantTimeRecorded: false + }, + { + state: State.ACTIVATING, installedTimeRecorded: true, + activatedTimeRecorded: false, redundantTimeRecorded: false + }, + { + state: State.ACTIVATED, installedTimeRecorded: true, + activatedTimeRecorded: true, redundantTimeRecorded: false + }, + + // When first being marked as unregistered (but the worker can remain + // actively controlling pages) + { + state: State.ACTIVATED, installedTimeRecorded: true, + activatedTimeRecorded: true, redundantTimeRecorded: false + }, + // When cleared (when idle) + { + state: State.REDUNDANT, installedTimeRecorded: true, + activatedTimeRecorded: true, redundantTimeRecorded: true + }, +]; + +function waitForRegister(aScope, aCallback) { + return new Promise(function (aResolve) { + let listener = { + onRegister (aRegistration) { + if (aRegistration.scope !== aScope) { + return; + } + swm.removeListener(listener); + registrationInfo = aRegistration; + aResolve(); + } + }; + swm.addListener(listener); + }); +} + +function waitForUnregister(aScope) { + return new Promise(function (aResolve) { + let listener = { + onUnregister (aRegistration) { + if (aRegistration.scope !== aScope) { + return; + } + swm.removeListener(listener); + aResolve(); + } + }; + swm.addListener(listener); + }); +} + +function register() { + info("Register a ServiceWorker in the iframe"); + + let iframe = document.querySelector("iframe"); + iframe.src = EXAMPLE_URL + "serviceworkerinfo_iframe.html"; + + let promise = new Promise(function(aResolve) { + iframe.onload = aResolve; + }); + + return promise.then(function() { + iframe.contentWindow.postMessage("register", "*"); + return waitForRegister(EXAMPLE_URL); + }) +} + +function verifyServiceWorkTime(aSWRInfo, resolve) { + let expectedResult = expectedResults.shift(); + ok(!!expectedResult, "We should be able to get test from expectedResults"); + + info("Check the ServiceWorker time in its state is " + expectedResult.state); + + // Get serviceWorkerInfo from swrInfo or get the astray one which we hold. + let swInfo = aSWRInfo.evaluatingWorker || + aSWRInfo.installingWorker || + aSWRInfo.waitingWorker || + aSWRInfo.activeWorker || + astrayServiceWorkerInfo; + + ok(!!aSWRInfo.lastUpdateTime, + "We should do the byte-check and update the update timeStamp"); + + if (!swInfo) { + is(expectedResult.state, State.BYTECHECK, + "We shouldn't get sw when we are notified for first time updating"); + return; + } + + ok(!!swInfo); + + is(expectedResult.state, swInfo.state, + "The service worker's state should be " + swInfo.state + ", but got " + + expectedResult.state); + + is(expectedResult.installedTimeRecorded, !!swInfo.installedTime, + "InstalledTime should be recorded when their state is greater than " + + "INSTALLING"); + + is(expectedResult.activatedTimeRecorded, !!swInfo.activatedTime, + "ActivatedTime should be recorded when their state is greater than " + + "ACTIVATING"); + + is(expectedResult.redundantTimeRecorded, !!swInfo.redundantTime, + "RedundantTime should be recorded when their state is REDUNDANT"); + + // We need to hold sw to avoid losing it since we'll unregister the swr later. + if (expectedResult.state === State.ACTIVATED) { + astrayServiceWorkerInfo = aSWRInfo.activeWorker; + + // Resolve the promise for testServiceWorkerInfo after sw is activated. + resolve(); + } +} + +function testServiceWorkerInfo() { + info("Listen onChange event and verify service worker's information"); + + let promise_resolve; + let promise = new Promise(aResolve => promise_resolve = aResolve); + + swrlistener = { + onChange: () => { + verifyServiceWorkTime(registrationInfo, promise_resolve); + } + }; + + registrationInfo.addListener(swrlistener); + + return promise; +} + +async function testHttpCacheUpdateTime() { + let iframe = document.querySelector("iframe"); + let reg = await iframe.contentWindow.navigator.serviceWorker.getRegistration(); + let lastUpdateTime = registrationInfo.lastUpdateTime; + await reg.update(); + is(lastUpdateTime, registrationInfo.lastUpdateTime, + "The update time should not change when SW script is read from http cache."); +} + +function unregister() { + info("Unregister the ServiceWorker"); + + let iframe = document.querySelector("iframe"); + iframe.contentWindow.postMessage("unregister", "*"); + return waitForUnregister(EXAMPLE_URL); +} + +function cleanAll() { + return new Promise((aResolve, aReject) => { + is(expectedResults.length, 0, "All the tests should be tested"); + + registrationInfo.removeListener(swrlistener); + + swm = null; + swrlistener = null; + registrationInfo = null; + astrayServiceWorkerInfo = null; + aResolve(); + }) +} + +function runTest() { + return Promise.resolve() + .then(register) + .then(testServiceWorkerInfo) + .then(testHttpCacheUpdateTime) + .then(unregister) + .catch(aError => ok(false, "Some test failed with error " + aError)) + .then(cleanAll) + .then(SimpleTest.finish); +} + +SimpleTest.waitForExplicitFinish(); +SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] +]}, runTest); + +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_empty_serviceworker.html b/dom/serviceworkers/test/test_empty_serviceworker.html new file mode 100644 index 0000000000..00b77939f8 --- /dev/null +++ b/dom/serviceworkers/test/test_empty_serviceworker.html @@ -0,0 +1,46 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test that registering an empty service worker works</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function runTest() { + navigator.serviceWorker.ready.then(done); + navigator.serviceWorker.register("empty.js", {scope: "."}); + } + + function done(registration) { + ok(registration.waiting || registration.active, "registration worked"); + registration.unregister().then(function(success) { + ok(success, "unregister worked"); + SimpleTest.finish(); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_error_reporting.html b/dom/serviceworkers/test/test_error_reporting.html new file mode 100644 index 0000000000..7c2d56fb9e --- /dev/null +++ b/dom/serviceworkers/test/test_error_reporting.html @@ -0,0 +1,241 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test Error Reporting of Service Worker Failures</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="error_reporting_helpers.js"></script> + <script src="utils.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <meta http-equiv="Content-type" content="text/html;charset=UTF-8"> +</head> +<body> + +<script type="text/javascript"> +"use strict"; + +/** + * Test that a bunch of service worker coding errors and failure modes that + * might otherwise be hard to diagnose are surfaced as console error messages. + * The driving use-case is minimizing cursing from a developer looking at a + * document in Firefox testing a page that involves service workers. + * + * This test assumes that errors will be reported via + * ServiceWorkerManager::ReportToAllClients and that that method is reliable and + * tested via some other file. + **/ + +add_task(function setupPrefs() { + return SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.testing.enabled", true], + ]}); +}); + +/** + * Ensure an error is logged during the initial registration of a SW when a 404 + * is received. + */ +add_task(async function register_404() { + // Start monitoring for the error + let expectedMessage = expect_console_message( + "ServiceWorkerRegisterNetworkError", + [make_absolute_url("network_error/"), "404", make_absolute_url("404.js")]); + + // Register, generating the 404 error. This will reject with a TypeError + // which we need to consume so it doesn't get thrown at our generator. + await navigator.serviceWorker.register("404.js", { scope: "network_error/" }) + .then( + () => { ok(false, "should have rejected"); }, + (e) => { ok(e.name === "TypeError", "404 failed as expected"); }); + + await wait_for_expected_message(expectedMessage); +}); + +/** + * Ensure an error is logged when the service worker is being served with a + * MIME type of text/plain rather than a JS type. + */ +add_task(async function register_bad_mime_type() { + let expectedMessage = expect_console_message( + "ServiceWorkerRegisterMimeTypeError2", + [make_absolute_url("bad_mime_type/"), "text/plain", + make_absolute_url("sw_bad_mime_type.js")]); + + // consume the expected rejection so it doesn't get thrown at us. + await navigator.serviceWorker.register("sw_bad_mime_type.js", { scope: "bad_mime_type/" }) + .then( + () => { ok(false, "should have rejected"); }, + (e) => { ok(e.name === "SecurityError", "bad MIME type failed as expected"); }); + + await wait_for_expected_message(expectedMessage); +}); + +async function notAllowStorageAccess() { + throw new Error("Storage permissions should be used when bug 1774860 overhauls this test."); +} + +async function allowStorageAccess() { + throw new Error("Storage permissions should be used when bug 1774860 overhauls this test."); +} + +/** + * Ensure an error is logged when the storage is not allowed and the content + * script is trying to register a service worker. + */ +add_task(async function register_storage_error() { + let expectedMessage = expect_console_message( + "ServiceWorkerRegisterStorageError", + [make_absolute_url("storage_not_allow/")]); + + await notAllowStorageAccess(); + + // consume the expected rejection so it doesn't get thrown at us. + await navigator.serviceWorker.register("sw_storage_not_allow.js", + { scope: "storage_not_allow/" }) + .then( + () => { ok(false, "should have rejected"); }, + (e) => { ok(e.name === "SecurityError", + "storage access failed as expected."); }); + + await wait_for_expected_message(expectedMessage); + + await allowStorageAccess(); +}); + +/** + * Ensure an error is logged when the storage is not allowed and the content + * script is trying to get the service worker registration. + */ +add_task(async function get_registration_storage_error() { + let expectedMessage = + expect_console_message("ServiceWorkerGetRegistrationStorageError", []); + + await notAllowStorageAccess(); + + // consume the expected rejection so it doesn't get thrown at us. + await navigator.serviceWorker.getRegistration() + .then( + () => { ok(false, "should have rejected"); }, + (e) => { ok(e.name === "SecurityError", + "storage access failed as expected."); }); + + await wait_for_expected_message(expectedMessage); + + await allowStorageAccess(); +}); + +/** + * Ensure an error is logged when the storage is not allowed and the content + * script is trying to get the service worker registrations. + */ +add_task(async function get_registrations_storage_error() { + let expectedMessage = + expect_console_message("ServiceWorkerGetRegistrationStorageError", []); + + await notAllowStorageAccess(); + + // consume the expected rejection so it doesn't get thrown at us. + await navigator.serviceWorker.getRegistrations() + .then( + () => { ok(false, "should have rejected"); }, + (e) => { ok(e.name === "SecurityError", + "storage access failed as expected."); }); + + await wait_for_expected_message(expectedMessage); + + await allowStorageAccess(); +}); + +/** + * Ensure an error is logged when the storage is not allowed and the content + * script is trying to post a message to the service worker. + */ +add_task(async function postMessage_storage_error() { + let expectedMessage = expect_console_message( + "ServiceWorkerPostMessageStorageError", + [make_absolute_url("storage_not_allow/")]); + + let registration; + // consume the expected rejection so it doesn't get thrown at us. + await navigator.serviceWorker.register("sw_storage_not_allow.js", + { scope: "storage_not_allow/" }) + .then(reg => { registration = reg; }) + .then(() => notAllowStorageAccess()) + .then(() => registration.installing || + registration.waiting || + registration.active) + .then(worker => worker.postMessage('ha')) + .then( + () => { ok(false, "should have rejected"); }, + (e) => { ok(e.name === "SecurityError", + "storage access failed as expected."); }); + + await wait_for_expected_message(expectedMessage); + + await registration.unregister(); + await allowStorageAccess(); +}); + +/** + * Ensure an error is logged when the storage is not allowed and the service + * worker is trying to get its client. + */ +add_task(async function get_client_storage_error() { + let expectedMessage = + expect_console_message("ServiceWorkerGetClientStorageError", []); + + await SpecialPowers.pushPrefEnv({"set": [ + // Make the test pass the IsOriginPotentiallyTrustworthy. + ["dom.securecontext.allowlist", "mochi.test"] + ]}); + + let registration; + // consume the expected rejection so it doesn't get thrown at us. + await navigator.serviceWorker.register("sw_storage_not_allow.js", + { scope: "test_error_reporting.html" }) + .then(reg => { + registration = reg; + return waitForState(registration.installing, "activated"); + }) + // Get the client's ID in the stage 1 + .then(() => fetch("getClient-stage1")) + .then(() => notAllowStorageAccess()) + // Trigger the clients.get() in the stage 2 + .then(() => fetch("getClient-stage2")) + .catch(e => ok(false, "fail due to:" + e)); + + await wait_for_expected_message(expectedMessage); + + await registration.unregister(); + await allowStorageAccess(); +}); + +/** + * Ensure an error is logged when the storage is not allowed and the service + * worker is trying to get its clients. + */ +add_task(async function get_clients_storage_error() { + let expectedMessage = + expect_console_message("ServiceWorkerGetClientStorageError", []); + + let registration; + // consume the expected rejection so it doesn't get thrown at us. + await navigator.serviceWorker.register("sw_storage_not_allow.js", + { scope: "test_error_reporting.html" }) + .then(reg => { + registration = reg; + return waitForState(registration.installing, "activated"); + }) + .then(() => notAllowStorageAccess()) + .then(() => fetch("getClients")) + .catch(e => ok(false, "fail due to:" + e)); + + await wait_for_expected_message(expectedMessage); + + await registration.unregister(); + await allowStorageAccess(); +}); +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_escapedSlashes.html b/dom/serviceworkers/test/test_escapedSlashes.html new file mode 100644 index 0000000000..001c660242 --- /dev/null +++ b/dom/serviceworkers/test/test_escapedSlashes.html @@ -0,0 +1,102 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test for escaped slashes in navigator.serviceWorker.register</title> + <script type="text/javascript" src="http://mochi.test:8888/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="http://mochi.test:8888/tests/SimpleTest/test.css" /> + <base href="https://mozilla.org/"> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + +var tests = [ + { status: true, + scriptURL: "a.js?foo%2fbar", + scopeURL: null }, + { status: false, + scriptURL: "foo%2fbar", + scopeURL: null }, + { status: true, + scriptURL: "a.js?foo%2Fbar", + scopeURL: null }, + { status: false, + scriptURL: "foo%2Fbar", + scopeURL: null }, + { status: true, + scriptURL: "a.js?foo%5cbar", + scopeURL: null }, + { status: false, + scriptURL: "foo%5cbar", + scopeURL: null }, + { status: true, + scriptURL: "a.js?foo%2Cbar", + scopeURL: null }, + { status: false, + scriptURL: "foo%5Cbar", + scopeURL: null }, + { status: true, + scriptURL: "ok.js", + scopeURL: "/scope?foo%2fbar"}, + { status: false, + scriptURL: "ok.js", + scopeURL: "/foo%2fbar"}, + { status: true, + scriptURL: "ok.js", + scopeURL: "/scope?foo%2Fbar"}, + { status: false, + scriptURL: "ok.js", + scopeURL: "foo%2Fbar"}, + { status: true, + scriptURL: "ok.js", + scopeURL: "/scope?foo%5cbar"}, + { status: false, + scriptURL: "ok.js", + scopeURL: "foo%5cbar"}, + { status: true, + scriptURL: "ok.js", + scopeURL: "/scope?foo%5Cbar"}, + { status: false, + scriptURL: "ok.js", + scopeURL: "foo%5Cbar"}, +]; + +function runTest() { + if (!tests.length) { + SimpleTest.finish(); + return; + } + + var test = tests.shift(); + navigator.serviceWorker.register(test.scriptURL, test.scopeURL) + .then(reg => { + ok(false, "Register should fail"); + }, err => { + if (!test.status) { + is(err.name, "TypeError", "Registration should fail with TypeError"); + } else { + ok(test.status, "Register should fail"); + } + }) + .then(runTest); +} + +SimpleTest.waitForExplicitFinish(); +onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.serviceWorkers.enabled", true], + ]}, runTest); +}; + +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_eval_allowed.html b/dom/serviceworkers/test/test_eval_allowed.html new file mode 100644 index 0000000000..5d6d7a7d9c --- /dev/null +++ b/dom/serviceworkers/test/test_eval_allowed.html @@ -0,0 +1,51 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1160458 - CSP activated by default in Service Workers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + function register() { + return navigator.serviceWorker.register("eval_worker.js"); + } + + function runTest() { + try { + eval("1"); + ok(false, "should throw"); + } + catch (ex) { + ok(true, "did throw"); + } + register() + .then(function(swr) { + ok(true, "eval restriction didn't get inherited"); + swr.unregister() + .then(function() { + SimpleTest.finish(); + }); + }).catch(function(e) { + ok(false, "Some test failed with error " + e); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_eval_allowed.html^headers^ b/dom/serviceworkers/test/test_eval_allowed.html^headers^ new file mode 100644 index 0000000000..51ffaa71dd --- /dev/null +++ b/dom/serviceworkers/test/test_eval_allowed.html^headers^ @@ -0,0 +1 @@ +Content-Security-Policy: "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self'" diff --git a/dom/serviceworkers/test/test_event_listener_leaks.html b/dom/serviceworkers/test/test_event_listener_leaks.html new file mode 100644 index 0000000000..33ffeb44c4 --- /dev/null +++ b/dom/serviceworkers/test/test_event_listener_leaks.html @@ -0,0 +1,63 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1447871 - Test some service worker leak conditions</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="utils.js"></script> + <script type="text/javascript" src="/tests/dom/events/test/event_leak_utils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<script class="testbody" type="text/javascript"> + +const scope = new URL("empty.html?leak_tests", location).href; +const script = new URL("empty.js", location).href; + +// Manipulate service worker DOM objects in the frame's context. +// Its important here that we create a listener callback from +// the DOM objects back to the frame's global in order to +// exercise the leak condition. +async function useServiceWorker(contentWindow) { + contentWindow.navigator.serviceWorker.oncontrollerchange = _ => { + contentWindow.controlledChangeCount += 1; + }; + let reg = await contentWindow.navigator.serviceWorker.getRegistration(scope); + reg.onupdatefound = _ => { + contentWindow.updateCount += 1; + }; + reg.active.onstatechange = _ => { + contentWindow.stateChangeCount += 1; + }; +} + +async function runTest() { + await SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}); + + let reg = await navigator.serviceWorker.register(script, { scope }); + await waitForState(reg.installing, "activated"); + + try { + await checkForEventListenerLeaks("ServiceWorker", useServiceWorker); + } catch (e) { + ok(false, e); + } finally { + await reg.unregister(); + SimpleTest.finish(); + } +} + +SimpleTest.waitForExplicitFinish(); +addEventListener("load", runTest, { once: true }); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_eventsource_intercept.html b/dom/serviceworkers/test/test_eventsource_intercept.html new file mode 100644 index 0000000000..b76c0d1d1a --- /dev/null +++ b/dom/serviceworkers/test/test_eventsource_intercept.html @@ -0,0 +1,103 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1182103 - Test EventSource scenarios with fetch interception</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function testFrame(src) { + return new Promise(function(resolve, reject) { + var iframe = document.createElement("iframe"); + iframe.src = src; + window.onmessage = function(e) { + if (e.data.status == "callback") { + switch(e.data.data) { + case "ok": + ok(e.data.condition, e.data.message); + break; + case "ready": + iframe.contentWindow.postMessage({status: "callback", data: "eventsource"}, "*"); + break; + case "done": + window.onmessage = null; + iframe.src = "about:blank"; + document.body.removeChild(iframe); + iframe = null; + resolve(); + break; + default: + ok(false, "Something went wrong"); + break; + } + } else { + ok(false, "Something went wrong"); + } + }; + document.body.appendChild(iframe); + }); + } + + function runTest() { + Promise.resolve() + .then(() => { + info("Going to intercept and test opaque responses"); + return testFrame("eventsource/eventsource_register_worker.html" + + "?script=eventsource_opaque_response_intercept_worker.js"); + }) + .then(() => { + return testFrame("eventsource/eventsource_opaque_response.html"); + }) + .then(() => { + info("Going to intercept and test cors responses"); + return testFrame("eventsource/eventsource_register_worker.html" + + "?script=eventsource_cors_response_intercept_worker.js"); + }) + .then(() => { + return testFrame("eventsource/eventsource_cors_response.html"); + }) + .then(() => { + info("Going to intercept and test synthetic responses"); + return testFrame("eventsource/eventsource_register_worker.html" + + "?script=eventsource_synthetic_response_intercept_worker.js"); + }) + .then(() => { + return testFrame("eventsource/eventsource_synthetic_response.html"); + }) + .then(() => { + info("Going to intercept and test mixed content cors responses"); + return testFrame("https://example.com/tests/dom/serviceworkers/test/" + + "eventsource/eventsource_register_worker.html" + + "?script=eventsource_mixed_content_cors_response_intercept_worker.js"); + }) + .then(() => { + return testFrame("https://example.com/tests/dom/serviceworkers/test/" + + "eventsource/eventsource_mixed_content_cors_response.html"); + }) + .then(SimpleTest.finish) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true], + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_fetch_event.html b/dom/serviceworkers/test/test_fetch_event.html new file mode 100644 index 0000000000..5227f6ae34 --- /dev/null +++ b/dom/serviceworkers/test/test_fetch_event.html @@ -0,0 +1,75 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 94048 - test install event.</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> + SimpleTest.requestCompleteLog(); + + var registration; + function simpleRegister() { + return navigator.serviceWorker.register("fetch_event_worker.js", { scope: "./fetch" }) + .then(swr => { + registration = swr; + return waitForState(swr.installing, 'activated'); + }); + } + + function unregister() { + return registration.unregister().then(function(success) { + ok(success, "Service worker should be unregistered successfully"); + }, function(e) { + dump("SW unregistration error: " + e + "\n"); + }); + } + + function testController() { + var p = new Promise(function(resolve, reject) { + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "done") { + window.onmessage = null; + w.close(); + resolve(); + } + } + }); + + var w = window.open("fetch/index.html"); + return p; + } + + function runTest() { + simpleRegister() + .then(testController) + .then(unregister) + .then(function() { + SimpleTest.finish(); + }).catch(function(e) { + ok(false, "Some test failed with error " + e); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_fetch_event_with_thirdpartypref.html b/dom/serviceworkers/test/test_fetch_event_with_thirdpartypref.html new file mode 100644 index 0000000000..53552e03c3 --- /dev/null +++ b/dom/serviceworkers/test/test_fetch_event_with_thirdpartypref.html @@ -0,0 +1,90 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 94048 - test install event.</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> + + // NOTE: This is just test_fetch_event.html but with an alternate cookie + // mode preference set to make sure that setting the preference does + // not break interception as observed in bug 1336364. + // TODO: Refactor this test so it doesn't duplicate so much code logic. + + SimpleTest.requestCompleteLog(); + + var registration; + function simpleRegister() { + return navigator.serviceWorker.register("fetch_event_worker.js", { scope: "./fetch" }) + .then(swr => { + registration = swr; + return waitForState(swr.installing, 'activated'); + }); + } + + function unregister() { + return registration.unregister().then(function(success) { + ok(success, "Service worker should be unregistered successfully"); + }, function(e) { + dump("SW unregistration error: " + e + "\n"); + }); + } + + function testController() { + var p = new Promise(function(resolve, reject) { + var reloaded = false; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "done") { + if (reloaded) { + window.onmessage = null; + w.close(); + resolve(); + } else { + w.location.reload(); + reloaded = true; + } + } + } + }); + + var w = window.open("fetch/index.html"); + return p; + } + + function runTest() { + simpleRegister() + .then(testController) + .then(unregister) + .then(function() { + SimpleTest.finish(); + }).catch(function(e) { + ok(false, "Some test failed with error " + e); + SimpleTest.finish(); + }); + } + + const COOKIE_BEHAVIOR_REJECTFOREIGN = 1; + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["network.cookie.cookieBehavior", COOKIE_BEHAVIOR_REJECTFOREIGN], + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_fetch_integrity.html b/dom/serviceworkers/test/test_fetch_integrity.html new file mode 100644 index 0000000000..35879d5749 --- /dev/null +++ b/dom/serviceworkers/test/test_fetch_integrity.html @@ -0,0 +1,228 @@ +<!DOCTYPE HTML> +<html> +<head> + <title> Test fetch.integrity on console report for serviceWorker and sharedWorker </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="error_reporting_helpers.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> + <meta http-equiv="Content-type" content="text/html;charset=UTF-8"> +</head> +<body> +<div id="content" style="display: none"></div> +<script src="utils.js"></script> +<script type="text/javascript"> +"use strict"; + +let security_localizer = + stringBundleService.createBundle("chrome://global/locale/security/security.properties"); + +let consoleScript; +let monitorCallbacks = []; + +function registerConsoleMonitor() { + return new Promise(resolve => { + var url = SimpleTest.getTestFileURL("console_monitor.js"); + consoleScript = SpecialPowers.loadChromeScript(url); + + consoleScript.addMessageListener("ready", resolve); + consoleScript.addMessageListener("monitor", function(msg) { + for (let i = 0; i < monitorCallbacks.length;) { + if (monitorCallbacks[i](msg)) { + ++i; + } else { + monitorCallbacks.splice(i, 1); + } + } + }); + consoleScript.sendAsyncMessage("load", {}); + }); +} + +function unregisterConsoleMonitor() { + return new Promise(resolve => { + consoleScript.addMessageListener("unloaded", () => { + consoleScript.destroy(); + resolve(); + }); + consoleScript.sendAsyncMessage("unload", {}); + }); +} + +function registerConsoleMonitorCallback(callback) { + monitorCallbacks.push(callback); +} + +function waitForMessages() { + let messages = []; + + // process repeated paired arguments of: msgId, args + for (let i = 0; i < arguments.length; i += 3) { + let msgId = arguments[i]; + let args = arguments[i + 1]; + messages.push(security_localizer.formatStringFromName(msgId, args)); + } + + return new Promise(resolve => { + registerConsoleMonitorCallback(msg => { + for (let i = 0; i < messages.length; ++i) { + if (messages[i] == msg.errorMessage) { + messages.splice(i, 1); + break; + } + } + + if (!messages.length) { + resolve(); + return false; + } + + return true; + }); + }); +} + +function expect_security_console_message(/* msgId, args, ... */) { + let expectations = []; + // process repeated paired arguments of: msgId, args + for (let i = 0; i < arguments.length; i += 3) { + let msgId = arguments[i]; + let args = arguments[i + 1]; + let filename = arguments[i + 2]; + expectations.push({ + errorMessage: security_localizer.formatStringFromName(msgId, args), + sourceName: filename, + }); + } + return new Promise(resolve => { + SimpleTest.monitorConsole(resolve, expectations); + }); +} + +// (This doesn't really need to be its own task, but it allows the actual test +// case to be self-contained.) +add_task(function setupPrefs() { + return SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["browser.newtab.preload", false], + ]}); +}); + +add_task(async function test_integrity_serviceWorker() { + var filename = make_absolute_url("fetch.js"); + var filename2 = make_absolute_url("fake.html"); + + let registration = await navigator.serviceWorker.register("fetch.js", + { scope: "./" }); + await waitForState(registration.installing, "activated"); + + info("Test for mNavigationInterceptions.") + // The client_win will reload to another URL after opening filename2. + let client_win = window.open(filename2); + + let expectedMessage = expect_security_console_message( + "MalformedIntegrityHash", + ["abc"], + filename, + "NoValidMetadata", + [""], + filename, + ); + let expectedMessage2 = expect_security_console_message( + "MalformedIntegrityHash", + ["abc"], + filename, + "NoValidMetadata", + [""], + filename, + ); + + info("Test for mControlledDocuments and report error message to console."); + // The fetch will succeed because the integrity value is invalid and we are + // looking for the console message regarding the bad integrity value. + await fetch("fail.html"); + + await wait_for_expected_message(expectedMessage); + + await wait_for_expected_message(expectedMessage2); + + await registration.unregister(); + client_win.close(); +}); + +add_task(async function test_integrity_sharedWorker() { + var filename = make_absolute_url("sharedWorker_fetch.js"); + + await registerConsoleMonitor(); + + info("Attach main window to a SharedWorker."); + let sharedWorker = new SharedWorker(filename); + let waitForConnected = new Promise((resolve) => { + sharedWorker.port.onmessage = function (e) { + if (e.data == "Connected") { + resolve(); + } else { + reject(); + } + } + }); + await waitForConnected; + + info("Attch another window to the same SharedWorker."); + // Open another window and its also managed by the shared worker. + let client_win = window.open("create_another_sharedWorker.html"); + let waitForBothConnected = new Promise((resolve) => { + sharedWorker.port.onmessage = function (e) { + if (e.data == "BothConnected") { + resolve(); + } else { + reject(); + } + } + }); + await waitForBothConnected; + + let expectedMessage = waitForMessages( + "MalformedIntegrityHash", + ["abc"], + filename, + "NoValidMetadata", + [""], + filename, + ); + + let expectedMessage2 = waitForMessages( + "MalformedIntegrityHash", + ["abc"], + filename, + "NoValidMetadata", + [""], + filename, + ); + + info("Start to fetch a URL with wrong integrity.") + sharedWorker.port.start(); + sharedWorker.port.postMessage("StartFetchWithWrongIntegrity"); + + let waitForSRIFailed = new Promise((resolve) => { + sharedWorker.port.onmessage = function (e) { + if (e.data == "SRI_failed") { + resolve(); + } else { + reject(); + } + } + }); + await waitForSRIFailed; + + await expectedMessage; + await expectedMessage2; + + client_win.close(); + + await unregisterConsoleMonitor(); +}); + +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_file_blob_response.html b/dom/serviceworkers/test/test_file_blob_response.html new file mode 100644 index 0000000000..3aa72c3dda --- /dev/null +++ b/dom/serviceworkers/test/test_file_blob_response.html @@ -0,0 +1,78 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1253777 - Test interception using file blob response body</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> + var registration; + var scope = './file_blob_response/'; + function start() { + return navigator.serviceWorker.register("file_blob_response_worker.js", + { scope }) + .then(function(swr) { + registration = swr; + return new waitForState(swr.installing, 'activated'); + }); + } + + function unregister() { + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + ok(false, "Unregistering the SW failed with " + e + "\n"); + }); + } + + function withFrame(url) { + return new Promise(function(resolve, reject) { + var content = document.getElementById("content"); + ok(content, "Parent exists."); + + var frame = document.createElement("iframe"); + frame.setAttribute('src', url); + content.appendChild(frame); + + frame.addEventListener('load', function(evt) { + resolve(frame); + }, {once: true}); + }); + } + + function runTest() { + start() + .then(function() { + return withFrame(scope + 'dummy.txt'); + }) + .then(function(frame) { + var result = JSON.parse(frame.contentWindow.document.body.textContent); + frame.remove(); + is(result.value, 'success'); + }) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }) + .then(unregister) + .then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_file_blob_upload.html b/dom/serviceworkers/test/test_file_blob_upload.html new file mode 100644 index 0000000000..e60e65badd --- /dev/null +++ b/dom/serviceworkers/test/test_file_blob_upload.html @@ -0,0 +1,146 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1203680 - Test interception of file blob uploads</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> + var registration; + var iframe; + function start() { + return navigator.serviceWorker.register("empty.js", + { scope: "./sw_clients/" }) + .then((swr) => { + registration = swr + return waitForState(swr.installing, 'activated', swr); + }); + } + + function unregister() { + if (iframe) { + iframe.remove(); + iframe = null; + } + + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + ok(false, "Unregistering the SW failed with " + e + "\n"); + }); + } + + function withFrame() { + var content = document.getElementById("content"); + ok(content, "Parent exists."); + + iframe = document.createElement("iframe"); + iframe.setAttribute('src', "sw_clients/file_blob_upload_frame.html"); + content.appendChild(iframe); + + return new Promise(function(resolve, reject) { + window.addEventListener('message', function(evt) { + if (evt.data.status === 'READY') { + resolve(); + } else { + reject(evt.data.result); + } + }, {once: true}); + }); + } + + function postBlob(body) { + return new Promise(function(resolve, reject) { + window.addEventListener('message', function(evt) { + if (evt.data.status === 'OK') { + is(JSON.stringify(body), JSON.stringify(evt.data.result), + 'body echoed back correctly'); + resolve(); + } else { + reject(evt.data.result); + } + }, {once: true}); + + iframe.contentWindow.postMessage({ type: 'TEST', body }, '*'); + }); + } + + function generateMessage(length) { + + var lorem = + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis egestas ' + 'vehicula tortor eget ultrices. Sed et luctus est. Nunc eu orci ligula. ' + 'In vel ornare eros, eget lacinia diam. Praesent vel metus mattis, ' + 'cursus nulla sit amet, rhoncus diam. Aliquam nulla tortor, aliquet et ' + 'viverra non, dignissim vel tellus. Praesent sed ex in dolor aliquet ' + 'aliquet. In at facilisis sem, et aliquet eros. Maecenas feugiat nisl ' + 'quis elit blandit posuere. Duis viverra odio sed eros consectetur, ' + 'viverra mattis ligula volutpat.'; + + var result = ''; + + while (result.length < length) { + var remaining = length - result.length; + if (remaining < lorem.length) { + result += lorem.slice(0, remaining); + } else { + result += lorem; + } + } + + return result; + } + + var smallBody = generateMessage(64); + var mediumBody = generateMessage(1024); + + // TODO: Test large bodies over the default pipe size. Currently stalls + // due to bug 1134372. + //var largeBody = generateMessage(100 * 1024); + + function runTest() { + start() + .then(withFrame) + .then(function() { + return postBlob({ hops: 0, message: smallBody }); + }) + .then(function() { + return postBlob({ hops: 1, message: smallBody }); + }) + .then(function() { + return postBlob({ hops: 10, message: smallBody }); + }) + .then(function() { + return postBlob({ hops: 0, message: mediumBody }); + }) + .then(function() { + return postBlob({ hops: 1, message: mediumBody }); + }) + .then(function() { + return postBlob({ hops: 10, message: mediumBody }); + }) + .then(unregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_file_upload.html b/dom/serviceworkers/test/test_file_upload.html new file mode 100644 index 0000000000..0c502686af --- /dev/null +++ b/dom/serviceworkers/test/test_file_upload.html @@ -0,0 +1,68 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1424701 - Test for service worker + file upload</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="utils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<input id="input" type="file"> +<script class="testbody" type="text/javascript"> + +function GetFormData(file) { + const formData = new FormData(); + formData.append('file', file); + return formData; +} + +async function onOpened(message) { + let input = document.getElementById("input"); + SpecialPowers.wrap(input).mozSetFileArray([message.file]); + script.destroy(); + + let reg = await navigator.serviceWorker.register('sw_file_upload.js', + {scope: "." }); + let serviceWorker = reg.installing || reg.waiting || reg.active; + await waitForState(serviceWorker, 'activated'); + + let res = await fetch('server_file_upload.sjs?clone=0', { + method: 'POST', + body: input.files[0], + }); + + let data = await res.clone().text(); + ok(data.length, "We have data for an uncloned request!"); + + res = await fetch('server_file_upload.sjs?clone=1', { + method: 'POST', + // Make sure the underlying stream is a file stream + body: GetFormData(input.files[0]), + }); + + data = await res.clone().text(); + ok(data.length, "We have data for a file-stream-backed cloned request!"); + + await reg.unregister(); + SimpleTest.finish(); +} + +let url = SimpleTest.getTestFileURL("script_file_upload.js"); +let script = SpecialPowers.loadChromeScript(url); + +SimpleTest.waitForExplicitFinish(); +SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] +]}).then(() => { + script.addMessageListener("file.opened", onOpened); + script.sendAsyncMessage("file.open"); +}); +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_force_refresh.html b/dom/serviceworkers/test/test_force_refresh.html new file mode 100644 index 0000000000..69da7b7de3 --- /dev/null +++ b/dom/serviceworkers/test/test_force_refresh.html @@ -0,0 +1,105 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 982726 - Test service worker post message </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + /** + * + */ + let iframe; + let registration; + + function start() { + return new Promise(resolve => { + const content = document.getElementById("content"); + ok(content, "Parent exists."); + + iframe = document.createElement("iframe"); + iframe.setAttribute("src", "sw_clients/refresher_compressed.html"); + + /* + * The initial iframe must be the _uncached_ version, which means its + * load must happen before the Service Worker's `activate` event. + * Rather than `waitUntil`-ing the Service Worker's `install` event + * until the load finishes (more concurrency, but involves coordinating + * `postMessage`s), just ensure the load finishes before registering + * the Service Worker (which is simpler). + */ + iframe.onload = resolve; + + content.appendChild(iframe); + }).then(async () => { + /* + * There's no need _here_ to explicitly wait for this Service Worker to be + * "activated"; this test will progress when the "READY"/"READY_CACHED" + * messages are received from the iframe, and the iframe will only send + * those messages once the Service Worker is "activated" (by chaining on + * its `navigator.serviceWorker.ready` promise). + */ + registration = await navigator.serviceWorker.register( + "force_refresh_worker.js", { scope: "./sw_clients/" }); + }); + } + + function unregister() { + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + } + + function testForceRefresh(swr) { + return new Promise(function(res, rej) { + var count = 0; + var cachedCount = 0; + window.onmessage = function(e) { + if (e.data === "READY") { + count += 1; + if (count == 2) { + is(cachedCount, 1, "should have received cached message before " + + "second non-cached message"); + res(); + } + iframe.contentWindow.postMessage("REFRESH", "*"); + } else if (e.data === "READY_CACHED") { + cachedCount += 1; + is(count, 1, "should have received non-cached message before " + + "cached message"); + iframe.contentWindow.postMessage("FORCE_REFRESH", "*"); + } + } + }).then(() => document.getElementById("content").removeChild(iframe)); + } + + function runTest() { + start() + .then(testForceRefresh) + .then(unregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true], + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_gzip_redirect.html b/dom/serviceworkers/test/test_gzip_redirect.html new file mode 100644 index 0000000000..8119303ae7 --- /dev/null +++ b/dom/serviceworkers/test/test_gzip_redirect.html @@ -0,0 +1,88 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 982726 - Test service worker post message </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> + var registration; + function start() { + return navigator.serviceWorker.register("gzip_redirect_worker.js", + { scope: "./sw_clients/" }) + .then((swr) => { + registration = swr; + return waitForState(swr.installing, 'activated', swr); + }); + } + + function unregister() { + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + } + + + function testGzipRedirect(swr) { + var p = new Promise(function(res, rej) { + var navigatorReady = false; + var finalReady = false; + + window.onmessage = function(e) { + if (e.data === "NAVIGATOR_READY") { + ok(!navigatorReady, "should only get navigator ready message once"); + ok(!finalReady, "should get navigator ready before final redirect ready message"); + navigatorReady = true; + iframe.contentWindow.postMessage({ + type: "NAVIGATE", + url: "does_not_exist.html" + }, "*"); + } else if (e.data === "READY") { + ok(navigatorReady, "should only get navigator ready message once"); + ok(!finalReady, "should get final ready message only once"); + finalReady = true; + res(); + } + } + }); + + var content = document.getElementById("content"); + ok(content, "Parent exists."); + + iframe = document.createElement("iframe"); + iframe.setAttribute('src', "sw_clients/navigator.html"); + content.appendChild(iframe); + + return p.then(() => content.removeChild(iframe)); + } + + function runTest() { + start() + .then(testGzipRedirect) + .then(unregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_hsts_upgrade_intercept.html b/dom/serviceworkers/test/test_hsts_upgrade_intercept.html new file mode 100644 index 0000000000..59fef0ec14 --- /dev/null +++ b/dom/serviceworkers/test/test_hsts_upgrade_intercept.html @@ -0,0 +1,66 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test that an HSTS upgraded request can be intercepted by a service worker</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + var framesLoaded = 0; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/hsts/register.html"; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + iframe.src = "http://example.com/tests/dom/serviceworkers/test/fetch/hsts/index.html"; + } else if (e.data.status == "protocol") { + is(e.data.data, "https:", "Correct protocol expected"); + ok(e.data.securityInfoPresent, "Security info present on intercepted value"); + switch (++framesLoaded) { + case 1: + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/hsts/embedder.html"; + break; + case 2: + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/hsts/image.html"; + break; + } + } else if (e.data.status == "image") { + is(e.data.data, 40, "The image request was upgraded before interception"); + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/hsts/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + SpecialPowers.cleanUpSTSData("http://example.com"); + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + // This is needed so that we can test upgrading a non-secure load inside an https iframe. + ["security.mixed_content.block_active_content", false], + ["security.mixed_content.block_display_content", false], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_https_fetch.html b/dom/serviceworkers/test/test_https_fetch.html new file mode 100644 index 0000000000..4ac4255889 --- /dev/null +++ b/dom/serviceworkers/test/test_https_fetch.html @@ -0,0 +1,62 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1133763 - test fetch event in HTTPS origins</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/https/register.html"; + var ios; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + ios = SpecialPowers.Cc["@mozilla.org/network/io-service;1"] + .getService(SpecialPowers.Ci.nsIIOService); + ios.offline = true; + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/https/index.html"; + } else if (e.data.status == "done") { + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/https/synth-sw.html"; + } else if (e.data.status == "done-synth-sw") { + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/https/synth-window.html"; + } else if (e.data.status == "done-synth-window") { + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/https/synth.html"; + } else if (e.data.status == "done-synth") { + ios.offline = false; + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/https/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + ok(true, "Test finished successfully"); + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true] + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_https_fetch_cloned_response.html b/dom/serviceworkers/test/test_https_fetch_cloned_response.html new file mode 100644 index 0000000000..8c7129d39d --- /dev/null +++ b/dom/serviceworkers/test/test_https_fetch_cloned_response.html @@ -0,0 +1,56 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1133763 - test fetch event in HTTPS origins with a cloned response</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/https/clonedresponse/register.html"; + var ios; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + ios = SpecialPowers.Cc["@mozilla.org/network/io-service;1"] + .getService(SpecialPowers.Ci.nsIIOService); + ios.offline = true; + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/https/clonedresponse/index.html"; + } else if (e.data.status == "done") { + ios.offline = false; + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/https/clonedresponse/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + ok(true, "Test finished successfully"); + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true] + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_https_origin_after_redirect.html b/dom/serviceworkers/test/test_https_origin_after_redirect.html new file mode 100644 index 0000000000..31ce173f34 --- /dev/null +++ b/dom/serviceworkers/test/test_https_origin_after_redirect.html @@ -0,0 +1,57 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test the origin of a redirected response from a service worker</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/origin/https/register.html"; + var win; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + win = window.open("https://example.com/tests/dom/serviceworkers/test/fetch/origin/https/index-https.sjs", "mywindow", "width=100,height=100"); + } else if (e.data.status == "domain") { + is(e.data.data, "example.org", "Correct domain expected"); + } else if (e.data.status == "origin") { + is(e.data.data, "https://example.org", "Correct origin expected"); + } else if (e.data.status == "done") { + win.close(); + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/origin/https/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + ok(true, "Test finished successfully"); + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_https_origin_after_redirect_cached.html b/dom/serviceworkers/test/test_https_origin_after_redirect_cached.html new file mode 100644 index 0000000000..8bce413f21 --- /dev/null +++ b/dom/serviceworkers/test/test_https_origin_after_redirect_cached.html @@ -0,0 +1,57 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test the origin of a redirected response from a service worker</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/origin/https/register.html"; + var win; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + win = window.open("https://example.com/tests/dom/serviceworkers/test/fetch/origin/https/index-cached-https.sjs", "mywindow", "width=100,height=100"); + } else if (e.data.status == "domain") { + is(e.data.data, "example.org", "Correct domain expected"); + } else if (e.data.status == "origin") { + is(e.data.data, "https://example.org", "Correct origin expected"); + } else if (e.data.status == "done") { + win.close(); + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/origin/https/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + ok(true, "Test finished successfully"); + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_https_synth_fetch_from_cached_sw.html b/dom/serviceworkers/test/test_https_synth_fetch_from_cached_sw.html new file mode 100644 index 0000000000..4186cfc340 --- /dev/null +++ b/dom/serviceworkers/test/test_https_synth_fetch_from_cached_sw.html @@ -0,0 +1,69 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1156847 - test fetch event generating a synthesized response in HTTPS origins from a cached SW</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" tyle="display: none"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/https/register.html"; + var ios; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + ios = SpecialPowers.Cc["@mozilla.org/network/io-service;1"] + .getService(SpecialPowers.Ci.nsIIOService); + ios.offline = true; + + // In order to load synth.html from a cached service worker, we first + // remove the existing window that is keeping the service worker alive, + // and do a GC to ensure that the SW is destroyed. This way, when we + // load synth.html for the second time, we will first recreate the + // service worker from the cache. This is intended to test that we + // properly store and retrieve the security info from the cache. + iframe.remove(); + iframe = null; + SpecialPowers.exactGC(function() { + iframe = document.createElement("iframe"); + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/https/synth.html"; + document.body.appendChild(iframe); + }); + } else if (e.data.status == "done-synth") { + ios.offline = false; + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/https/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + ok(true, "Test finished successfully"); + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true] + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_imagecache.html b/dom/serviceworkers/test/test_imagecache.html new file mode 100644 index 0000000000..52a793bfb9 --- /dev/null +++ b/dom/serviceworkers/test/test_imagecache.html @@ -0,0 +1,55 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1202085 - Test that images from different controllers don't cached together</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "/tests/dom/serviceworkers/test/fetch/imagecache/register.html"; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + iframe.src = "/tests/dom/serviceworkers/test/fetch/imagecache/index.html"; + } else if (e.data.status == "result") { + is(e.data.url, "image-40px.png", "Correct url expected"); + is(e.data.width, 40, "Correct width expected"); + iframe.src = "/tests/dom/serviceworkers/test/fetch/imagecache/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + iframe.src = "/tests/dom/serviceworkers/test/fetch/imagecache/postmortem.html"; + } else if (e.data.status == "postmortem") { + is(e.data.width, 20, "Correct width expected"); + window.onmessage = null; + ok(true, "Test finished successfully"); + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_imagecache_max_age.html b/dom/serviceworkers/test/test_imagecache_max_age.html new file mode 100644 index 0000000000..fcb8d3e306 --- /dev/null +++ b/dom/serviceworkers/test/test_imagecache_max_age.html @@ -0,0 +1,71 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test that the image cache respects a synthesized image's Cache headers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + var framesLoaded = 0; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "/tests/dom/serviceworkers/test/fetch/imagecache-maxage/register.html"; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + iframe.src = "/tests/dom/serviceworkers/test/fetch/imagecache-maxage/index.html"; + } else if (e.data.status == "result") { + switch (++framesLoaded) { + case 1: + is(e.data.url, "image-20px.png", "Correct url expected"); + is(e.data.url2, "image-20px.png", "Correct url expected"); + is(e.data.width, 20, "Correct width expected"); + is(e.data.width2, 20, "Correct width expected"); + // Wait for 100ms so that the image gets expired. + setTimeout(function() { + iframe.src = "/tests/dom/serviceworkers/test/fetch/imagecache-maxage/index.html?new" + }, 100); + break; + case 2: + is(e.data.url, "image-40px.png", "Correct url expected"); + is(e.data.url2, "image-40px.png", "Correct url expected"); + is(e.data.width, 40, "Correct width expected"); + is(e.data.width2, 40, "Correct width expected"); + iframe.src = "/tests/dom/serviceworkers/test/fetch/imagecache-maxage/unregister.html"; + break; + default: + ok(false, "This should never happen"); + } + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + SimpleTest.finish(); + } + }; + } + + SimpleTest.requestFlakyTimeout("This test needs to simulate the passing of time"); + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_importscript.html b/dom/serviceworkers/test/test_importscript.html new file mode 100644 index 0000000000..c0a894cf3c --- /dev/null +++ b/dom/serviceworkers/test/test_importscript.html @@ -0,0 +1,74 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test service worker - script cache policy</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<div id="content"></div> +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> + function start() { + return navigator.serviceWorker.register("importscript_worker.js", + { scope: "./sw_clients/" }) + .then(swr => waitForState(swr.installing, 'activated', swr)) + .then(swr => registration = swr); + } + + function unregister() { + return fetch("importscript.sjs?clearcounter").then(function() { + return registration.unregister(); + }).then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + } + + function testPostMessage(swr) { + var p = new Promise(function(res, rej) { + window.onmessage = function(e) { + if (e.data === "READY") { + swr.active.postMessage("do magic"); + return; + } + + ok(e.data === "OK", "Worker posted the correct value: " + e.data); + res(); + } + }); + + var content = document.getElementById("content"); + ok(content, "Parent exists."); + + iframe = document.createElement("iframe"); + iframe.setAttribute('src', "sw_clients/service_worker_controlled.html"); + content.appendChild(iframe); + + return p.then(() => content.removeChild(iframe)); + } + + function runTest() { + start() + .then(testPostMessage) + .then(unregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_importscript_mixedcontent.html b/dom/serviceworkers/test/test_importscript_mixedcontent.html new file mode 100644 index 0000000000..15fe5e88b6 --- /dev/null +++ b/dom/serviceworkers/test/test_importscript_mixedcontent.html @@ -0,0 +1,53 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1198078 - test that we respect mixed content blocking in importScript() inside service workers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/importscript-mixedcontent/register.html"; + var ios; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/importscript-mixedcontent/index.html"; + } else if (e.data.status == "done") { + is(e.data.data, "good", "Mixed content blocking should work correctly for service workers"); + iframe.src = "https://example.com/tests/dom/serviceworkers/test/fetch/importscript-mixedcontent/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + ok(true, "Test finished successfully"); + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["security.mixed_content.block_active_content", false], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_install_event.html b/dom/serviceworkers/test/test_install_event.html new file mode 100644 index 0000000000..87f89725dc --- /dev/null +++ b/dom/serviceworkers/test/test_install_event.html @@ -0,0 +1,143 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 94048 - test install event.</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function simpleRegister() { + var p = navigator.serviceWorker.register("worker.js", { scope: "./install_event" }); + return p; + } + + function nextRegister(reg) { + ok(reg instanceof ServiceWorkerRegistration, "reg should be a ServiceWorkerRegistration"); + var p = navigator.serviceWorker.register("install_event_worker.js", { scope: "./install_event" }); + return p.then(function(swr) { + ok(reg === swr, "register should resolve to the same registration object"); + var update_found_promise = new Promise(function(resolve, reject) { + swr.addEventListener('updatefound', function(e) { + ok(true, "Received onupdatefound"); + resolve(); + }); + }); + + var worker_activating = new Promise(function(res, reject) { + ok(swr.installing instanceof ServiceWorker, "There should be an installing worker if promise resolves."); + ok(swr.installing.state == "installing", "Installing worker's state should be 'installing'"); + swr.installing.onstatechange = function(e) { + if (e.target.state == "activating") { + e.target.onstatechange = null; + res(); + } + } + }); + + return Promise.all([update_found_promise, worker_activating]); + }, function(e) { + ok(false, "Unexpected Error in nextRegister! " + e); + }); + } + + function installError() { + // Silence worker errors so they don't cause the test to fail. + window.onerror = function(e) {} + return navigator.serviceWorker.register("install_event_error_worker.js", { scope: "./install_event" }) + .then(function(swr) { + ok(swr.installing instanceof ServiceWorker, "There should be an installing worker if promise resolves."); + ok(swr.installing.state == "installing", "Installing worker's state should be 'installing'"); + return new Promise(function(resolve, reject) { + swr.installing.onstatechange = function(e) { + ok(e.target.state == "redundant", "Installation of worker with error should fail."); + resolve(); + } + }); + }).then(function() { + return navigator.serviceWorker.getRegistration("./install_event").then(function(swr) { + var newest = swr.waiting || swr.active; + ok(newest, "Waiting or active worker should still exist"); + ok(newest.scriptURL.match(/install_event_worker.js$/), "Previous worker should remain the newest worker"); + }); + }); + } + + function testActive(worker) { + is(worker.state, "activating", "Should be activating"); + return new Promise(function(resolve, reject) { + worker.onstatechange = function(e) { + e.target.onstatechange = null; + is(e.target.state, "activated", "Activation of worker with error in activate event handler should still succeed."); + resolve(); + } + }); + } + + function activateErrorShouldSucceed() { + // Silence worker errors so they don't cause the test to fail. + window.onerror = function() { } + return navigator.serviceWorker.register("activate_event_error_worker.js", { scope: "./activate_error" }) + .then(function(swr) { + var p = new Promise(function(resolve, reject) { + ok(swr.installing.state == "installing", "activateErrorShouldSucceed(): Installing worker's state should be 'installing'"); + swr.installing.onstatechange = function(e) { + e.target.onstatechange = null; + if (swr.waiting) { + swr.waiting.onstatechange = function(event) { + event.target.onstatechange = null; + testActive(swr.active).then(resolve, reject); + } + } else { + testActive(swr.active).then(resolve, reject); + } + } + }); + + return p.then(function() { + return Promise.resolve(swr); + }); + }).then(function(swr) { + return swr.unregister(); + }); + } + + function unregister() { + return navigator.serviceWorker.getRegistration("./install_event").then(function(reg) { + return reg.unregister(); + }); + } + + function runTest() { + Promise.resolve() + .then(simpleRegister) + .then(nextRegister) + .then(installError) + .then(activateErrorShouldSucceed) + .then(unregister) + .then(function() { + SimpleTest.finish(); + }).catch(function(e) { + ok(false, "Some test failed with error " + e); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_install_event_gc.html b/dom/serviceworkers/test/test_install_event_gc.html new file mode 100644 index 0000000000..8b68b8ac47 --- /dev/null +++ b/dom/serviceworkers/test/test_install_event_gc.html @@ -0,0 +1,121 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test install event being GC'd before waitUntil fulfills</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script class="testbody" type="text/javascript"> +var script = 'blocking_install_event_worker.js'; +var scope = 'sw_clients/simple.html?install-event-gc'; +var registration; + +function register() { + return navigator.serviceWorker.register(script, { scope }) + .then(swr => registration = swr); +} + +function unregister() { + if (!registration) { + return; + } + return registration.unregister(); +} + +function waitForInstallEvent() { + return new Promise((resolve, reject) => { + navigator.serviceWorker.addEventListener('message', evt => { + if (evt.data.type === 'INSTALL_EVENT') { + resolve(); + } + }); + }); +} + +function gcWorker() { + return new Promise(function(resolve, reject) { + // We are able to trigger asynchronous garbage collection and cycle + // collection by emitting "child-cc-request" and "child-gc-request" + // observer notifications. The worker RuntimeService will translate + // these notifications into the appropriate operation on all known + // worker threads. + // + // In the failure case where GC/CC causes us to abort the installation, + // we will know something happened from the statechange event. + const statechangeHandler = evt => { + // Reject rather than resolving to avoid the possibility of us seeing + // an unrelated racing statechange somehow. Since in the success case we + // will still see a state change on termination, we do explicitly need to + // be removed on the success path. + ok(registration.installing, 'service worker is still installing?'); + reject(); + }; + registration.installing.addEventListener('statechange', statechangeHandler); + // In the success case since the service worker installation is effectively + // hung, we instead depend on sending a 'ping' message to the service worker + // and hearing it 'pong' back. Since we issue our postMessage after we + // trigger the GC/CC, our 'ping' will only be processed after the GC/CC and + // therefore the pong will also strictly occur after the cycle collection. + navigator.serviceWorker.addEventListener('message', evt => { + if (evt.data.type === 'pong') { + registration.installing.removeEventListener( + 'statechange', statechangeHandler); + resolve(); + } + }); + // At the current time, the service worker will exist in our same process + // and notifyObservers is synchronous. However, in the future, service + // workers may end up in a separate process and in that case it will be + // appropriate to use notifyObserversInParentProcess or something like it. + // (notifyObserversInParentProcess is a synchronous IPC call to the parent + // process's main thread. IPDL PContent::CycleCollect is an async message. + // Ordering will be maintained if the postMessage goes via PContent as well, + // but that seems unlikely.) + SpecialPowers.notifyObservers(null, 'child-gc-request'); + SpecialPowers.notifyObservers(null, 'child-cc-request'); + SpecialPowers.notifyObservers(null, 'child-gc-request'); + // (Only send the ping after we set the gc/cc/gc in motion.) + registration.installing.postMessage({ type: 'ping' }); + }); +} + +function terminateWorker() { + return SpecialPowers.pushPrefEnv({ + set: [ + ["dom.serviceWorkers.idle_timeout", 0], + ["dom.serviceWorkers.idle_extended_timeout", 0] + ] + }).then(_ => { + registration.installing.postMessage({ type: 'RESET_TIMER' }); + }); +} + +function runTest() { + Promise.all([ + waitForInstallEvent(), + register() + ]).then(_ => ok(registration.installing, 'service worker is installing')) + .then(gcWorker) + .then(_ => ok(registration.installing, 'service worker is still installing')) + .then(terminateWorker) + .catch(e => ok(false, e)) + .then(unregister) + .then(SimpleTest.finish); +} + +SimpleTest.waitForExplicitFinish(); +SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true], +]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_installation_simple.html b/dom/serviceworkers/test/test_installation_simple.html new file mode 100644 index 0000000000..69c9518ea0 --- /dev/null +++ b/dom/serviceworkers/test/test_installation_simple.html @@ -0,0 +1,208 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 930348 - test stub Navigator ServiceWorker utilities.</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function simpleRegister() { + var p = navigator.serviceWorker.register("worker.js", { scope: "simpleregister/" }); + ok(p instanceof Promise, "register() should return a Promise"); + return Promise.resolve(); + } + + function sameOriginWorker() { + p = navigator.serviceWorker.register("http://some-other-origin/worker.js"); + return p.then(function(w) { + ok(false, "Worker from different origin should fail"); + }, function(e) { + ok(e.name === "SecurityError", "Should fail with a SecurityError"); + }); + } + + function sameOriginScope() { + p = navigator.serviceWorker.register("worker.js", { scope: "http://www.example.com/" }); + return p.then(function(w) { + ok(false, "Worker controlling scope for different origin should fail"); + }, function(e) { + ok(e.name === "SecurityError", "Should fail with a SecurityError"); + }); + } + + function httpsOnly() { + return SpecialPowers.pushPrefEnv({'set': [["dom.serviceWorkers.testing.enabled", false]] }) + .then(function() { + return navigator.serviceWorker.register("/worker.js"); + }).then(function(w) { + ok(false, "non-HTTPS pages cannot register ServiceWorkers"); + }, function(e) { + ok(e.name === "TypeError", "navigator.serviceWorker should be undefined"); + }).then(function() { + return SpecialPowers.popPrefEnv(); + }); + } + + function realWorker() { + var p = navigator.serviceWorker.register("worker.js", { scope: "realworker" }); + return p.then(function(wr) { + ok(wr instanceof ServiceWorkerRegistration, "Register a ServiceWorker"); + + info(wr.scope); + ok(wr.scope == (new URL("realworker", document.baseURI)).href, "Scope should match"); + // active, waiting, installing should return valid worker instances + // because the registration is for the realworker scope, so the workers + // should be obtained for that scope and not for + // test_installation_simple.html + var worker = wr.installing; + ok(worker && wr.scope.match(/realworker$/) && + worker.scriptURL.match(/worker.js$/), "Valid worker instance should be available."); + return wr.unregister().then(function(success) { + ok(success, "The worker should be unregistered successfully"); + }, function(e) { + dump("Error unregistering the worker: " + e + "\n"); + }); + }, function(e) { + info("Error: " + e.name); + ok(false, "realWorker Registration should have succeeded!"); + }); + } + + function networkError404() { + return navigator.serviceWorker.register("404.js", { scope: "network_error/"}).then(function(w) { + ok(false, "404 response should fail with TypeError"); + }, function(e) { + ok(e.name === "TypeError", "404 response should fail with TypeError"); + }); + } + + function redirectError() { + return navigator.serviceWorker.register("redirect_serviceworker.sjs", { scope: "redirect_error/" }).then(function(swr) { + ok(false, "redirection should fail"); + }, function (e) { + ok(e.name === "SecurityError", "redirection should fail with SecurityError"); + }); + } + + function parseError() { + var p = navigator.serviceWorker.register("parse_error_worker.js", { scope: "parse_error/" }); + return p.then(function(wr) { + ok(false, "Registration should fail with parse error"); + return navigator.serviceWorker.getRegistration("parse_error/").then(function(swr) { + // See https://github.com/slightlyoff/ServiceWorker/issues/547 + is(swr, undefined, "A failed registration for a scope with no prior controllers should clear itself"); + }); + }, function(e) { + ok(e instanceof Error, "Registration should fail with parse error"); + }); + } + + // FIXME(nsm): test for parse error when Update step doesn't happen (directly from register). + + function updatefound() { + var frame = document.createElement("iframe"); + frame.setAttribute("id", "simpleregister-frame"); + frame.setAttribute("src", new URL("simpleregister/index.html", document.baseURI).href); + document.body.appendChild(frame); + var resolve, reject; + var p = new Promise(function(res, rej) { + resolve = res; + reject = rej; + }); + + var regPromise; + function continueTest() { + regPromise = navigator.serviceWorker.register( + "worker2.js", { scope: "simpleregister/" }); + } + + window.onmessage = function(e) { + if (e.data.type == "ready") { + continueTest(); + } else if (e.data.type == "finish") { + window.onmessage = null; + // We have to make frame navigate away, otherwise it will call + // MaybeStopControlling() when this document is unloaded. At that point + // the pref has been disabled, so the ServiceWorkerManager is not available. + frame.setAttribute("src", new URL("about:blank").href); + regPromise.then(function(reg) { + reg.unregister().then(function(success) { + ok(success, "The worker should be unregistered successfully"); + resolve(); + }, function(error) { + dump("Error unregistering the worker: " + error + "\n"); + }); + }); + } else if (e.data.type == "check") { + ok(e.data.status, e.data.msg); + } + } + return p; + } + + var readyPromiseResolved = false; + + function readyPromise() { + var frame = document.createElement("iframe"); + frame.setAttribute("id", "simpleregister-frame-ready"); + frame.setAttribute("src", new URL("simpleregister/ready.html", document.baseURI).href); + document.body.appendChild(frame); + + var channel = new MessageChannel(); + frame.addEventListener('load', function() { + frame.contentWindow.postMessage('your port!', '*', [channel.port2]); + }); + + channel.port1.onmessage = function() { + readyPromiseResolved = true; + } + + return Promise.resolve(); + } + + function checkReadyPromise() { + ok(readyPromiseResolved, "The ready promise has been resolved!"); + return Promise.resolve(); + } + + function runTest() { + simpleRegister() + .then(sameOriginWorker) + .then(sameOriginScope) + .then(httpsOnly) + .then(readyPromise) + .then(realWorker) + .then(networkError404) + .then(redirectError) + .then(parseError) + .then(updatefound) + .then(checkReadyPromise) + // put more tests here. + .then(function() { + SimpleTest.finish(); + }).catch(function(e) { + ok(false, "Some test failed with error " + e); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.testing.enabled", true], + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_match_all.html b/dom/serviceworkers/test/test_match_all.html new file mode 100644 index 0000000000..a1ee01507c --- /dev/null +++ b/dom/serviceworkers/test/test_match_all.html @@ -0,0 +1,83 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 982726 - test match_all not crashing</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> + // match_all_worker will call matchAll until the worker shuts down. + // Test passes if the browser doesn't crash on leaked promise objects. + var registration; + var content; + var iframe; + + function simpleRegister() { + return navigator.serviceWorker.register("match_all_worker.js", + { scope: "./sw_clients/" }) + .then((swr) => { + registration = swr; + return waitForState(swr.installing, 'activated', swr); + }); + } + + function closeAndUnregister() { + content.removeChild(iframe); + + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + } + + function openClient() { + var p = new Promise(function(resolve, reject) { + window.onmessage = function(e) { + if (e.data === "READY") { + resolve(); + } + } + }); + + content = document.getElementById("content"); + ok(content, "Parent exists."); + + iframe = document.createElement("iframe"); + iframe.setAttribute('src', "sw_clients/simple.html"); + content.appendChild(iframe); + + return p; + } + + function runTest() { + simpleRegister() + .then(openClient) + .then(closeAndUnregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(function() { + ok(true, "Didn't crash on resolving matchAll promises while worker shuts down."); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_match_all_advanced.html b/dom/serviceworkers/test/test_match_all_advanced.html new file mode 100644 index 0000000000..b4359511f3 --- /dev/null +++ b/dom/serviceworkers/test/test_match_all_advanced.html @@ -0,0 +1,102 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 982726 - Test matchAll with multiple clients</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> + var client_iframes = []; + var registration; + + function start() { + return navigator.serviceWorker.register("match_all_advanced_worker.js", + { scope: "./sw_clients/" }).then(function(swr) { + registration = swr; + return waitForState(swr.installing, 'activated'); + }).then(_ => { + window.onmessage = function (e) { + if (e.data === "READY") { + ok(registration.active, "Worker is active."); + registration.active.postMessage("RUN"); + } + } + }); + } + + function unregister() { + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + } + + + function testMatchAll() { + var p = new Promise(function(res, rej) { + navigator.serviceWorker.onmessage = function (e) { + ok(e.data === client_iframes.length, "MatchAll returned the correct number of clients."); + res(); + } + }); + + content = document.getElementById("content"); + ok(content, "Parent exists."); + + iframe = document.createElement("iframe"); + iframe.setAttribute('src', "sw_clients/service_worker_controlled.html"); + content.appendChild(iframe); + + client_iframes.push(iframe); + return p; + } + + function removeAndTest() { + content = document.getElementById("content"); + ok(content, "Parent exists."); + + content.removeChild(client_iframes.pop()); + content.removeChild(client_iframes.pop()); + + return testMatchAll(); + } + + function runTest() { + start() + .then(testMatchAll) + .then(testMatchAll) + .then(testMatchAll) + .then(removeAndTest) + .then(function(e) { + content = document.getElementById("content"); + while (client_iframes.length) { + content.removeChild(client_iframes.pop()); + } + }).then(unregister).catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(function() { + SimpleTest.finish(); + }); + + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_match_all_client_id.html b/dom/serviceworkers/test/test_match_all_client_id.html new file mode 100644 index 0000000000..0294c00aba --- /dev/null +++ b/dom/serviceworkers/test/test_match_all_client_id.html @@ -0,0 +1,95 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1058311 - Test matchAll client id </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> + var registration; + var clientURL = "match_all_client/match_all_client_id.html"; + function start() { + return navigator.serviceWorker.register("match_all_client_id_worker.js", + { scope: "./match_all_client/" }) + .then((swr) => { + registration = swr; + return waitForState(swr.installing, 'activated', swr); + }); + } + + function unregister() { + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + } + + function getMessageListener() { + return new Promise(function(res, rej) { + window.onmessage = function(e) { + ok(e.data, "Same client id for multiple calls."); + is(e.origin, "http://mochi.test:8888", "Event should have the correct origin"); + + if (!e.data) { + rej(); + return; + } + + info("DONE from: " + e.source); + res(); + } + }); + } + + function testNestedWindow() { + var p = getMessageListener(); + + var content = document.getElementById("content"); + ok(content, "Parent exists."); + + iframe = document.createElement("iframe"); + + content.appendChild(iframe); + iframe.setAttribute('src', clientURL); + + return p.then(() => content.removeChild(iframe)); + } + + function testAuxiliaryWindow() { + var p = getMessageListener(); + var w = window.open(clientURL); + + return p.then(() => w.close()); + } + + function runTest() { + info(window.opener == undefined); + start() + .then(testAuxiliaryWindow) + .then(testNestedWindow) + .then(unregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_match_all_client_properties.html b/dom/serviceworkers/test/test_match_all_client_properties.html new file mode 100644 index 0000000000..c8a0b448c2 --- /dev/null +++ b/dom/serviceworkers/test/test_match_all_client_properties.html @@ -0,0 +1,101 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1058311 - Test matchAll clients properties </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> + var registration; + var clientURL = "match_all_clients/match_all_controlled.html"; + function start() { + return navigator.serviceWorker.register("match_all_properties_worker.js", + { scope: "./match_all_clients/" }) + .then((swr) => { + registration = swr; + return waitForState(swr.installing, 'activated', swr); + }); + } + + function unregister() { + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + } + + function getMessageListener() { + return new Promise(function(res, rej) { + window.onmessage = function(e) { + if (e.data.message === undefined) { + info("rejecting promise"); + rej(); + return; + } + + ok(e.data.result, e.data.message); + + if (!e.data.result) { + rej(); + } + if (e.data.message == "DONE") { + info("DONE from: " + e.source); + res(); + } + } + }); + } + + function testNestedWindow() { + var p = getMessageListener(); + + var content = document.getElementById("content"); + ok(content, "Parent exists."); + + iframe = document.createElement("iframe"); + + content.appendChild(iframe); + iframe.setAttribute('src', clientURL); + + return p.then(() => content.removeChild(iframe)); + } + + function testAuxiliaryWindow() { + var p = getMessageListener(); + var w = window.open(clientURL); + + return p.then(() => w.close()); + } + + function runTest() { + info("catalin"); + info(window.opener == undefined); + start() + .then(testAuxiliaryWindow) + .then(testNestedWindow) + .then(unregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_navigationPreload_disable_crash.html b/dom/serviceworkers/test/test_navigationPreload_disable_crash.html new file mode 100644 index 0000000000..ea6439284d --- /dev/null +++ b/dom/serviceworkers/test/test_navigationPreload_disable_crash.html @@ -0,0 +1,52 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Failure to create a Promise shouldn't crash</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + async function runTest() { + const iframe = document.createElement('iframe'); + document.getElementById("content").appendChild(iframe); + + const serviceWorker = iframe.contentWindow.navigator.serviceWorker; + const worker = await iframe.contentWindow.navigator.serviceWorker.register("empty.js", {}); + + iframe.remove(); + + // We can't wait for this promise to settle, because the global's + // browsing context has been discarded when the iframe was removed. + // We're just checking if this call crashes, which would happen + // immediately, so ignoring the promise should be fine. + worker.navigationPreload.disable(); + ok(true, "navigationPreload.disable() failed but didn't crash."); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + // We can't call unregister on the worker after its browsing context has been + // discarded, so use SpecialPowers.removeAllServiceWorkerData. + SimpleTest.registerCleanupFunction(() => SpecialPowers.removeAllServiceWorkerData()); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.navigationPreload.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_navigator.html b/dom/serviceworkers/test/test_navigator.html new file mode 100644 index 0000000000..aaac04e926 --- /dev/null +++ b/dom/serviceworkers/test/test_navigator.html @@ -0,0 +1,40 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 930348 - test stub Navigator ServiceWorker utilities.</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function checkEnabled() { + ok(navigator.serviceWorker, "navigator.serviceWorker should exist when ServiceWorkers are enabled."); + ok(typeof navigator.serviceWorker.register === "function", "navigator.serviceWorker.register() should be a function."); + ok(typeof navigator.serviceWorker.getRegistration === "function", "navigator.serviceWorker.getAll() should be a function."); + ok(typeof navigator.serviceWorker.getRegistrations === "function", "navigator.serviceWorker.getAll() should be a function."); + ok(navigator.serviceWorker.ready instanceof Promise, "navigator.serviceWorker.ready should be a Promise."); + ok(navigator.serviceWorker.controller === null, "There should be no controller worker for an uncontrolled document."); + } + + SimpleTest.waitForExplicitFinish(); + + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, function() { + checkEnabled(); + SimpleTest.finish(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_nofetch_handler.html b/dom/serviceworkers/test/test_nofetch_handler.html new file mode 100644 index 0000000000..0725a68561 --- /dev/null +++ b/dom/serviceworkers/test/test_nofetch_handler.html @@ -0,0 +1,57 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Test for Bugs 1181127 and 1325101</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="error_reporting_helpers.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <meta http-equiv="Content-type" content="text/html;charset=UTF-8"> +</head> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1181127">Mozilla Bug 1181127</a> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1181127">Mozilla Bug 1325101</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> + +add_task(function setupPrefs() { + return SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + // Make sure the event handler during the install event persists. This ensures + // the reason for which the interception doesn't occur is because of the + // handlesFetch=false flag from ServiceWorkerInfo. + ["dom.serviceWorkers.idle_timeout", 299999], + ]}); +}); + +var iframeg; +function create_iframe(url) { + return new Promise(function(res) { + iframe = document.createElement('iframe'); + iframe.src = url; + iframe.onload = function() { res(iframe) } + document.body.appendChild(iframe); + iframeg = iframe; + }) +} + +add_task(async function test_nofetch_worker() { + let registration = await navigator.serviceWorker.register( + "nofetch_handler_worker.js", { scope: "./nofetch_handler_worker/"} ) + .then(swr => waitForState(swr.installing, 'activated', swr)); + + let iframe = await create_iframe("./nofetch_handler_worker/doesnt_exist.html"); + ok(!iframe.contentDocument.body.innerHTML.includes("intercepted"), "Request was not intercepted."); + + await SpecialPowers.popPrefEnv(); + await registration.unregister(); +}); +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_not_intercept_plugin.html b/dom/serviceworkers/test/test_not_intercept_plugin.html new file mode 100644 index 0000000000..4e7654deea --- /dev/null +++ b/dom/serviceworkers/test/test_not_intercept_plugin.html @@ -0,0 +1,75 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1187766 - Test loading plugins scenarios with fetch interception.</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> + SimpleTest.requestCompleteLog(); + + var registration; + function simpleRegister() { + var p = navigator.serviceWorker.register("./fetch/plugin/worker.js", { scope: "./fetch/plugin/" }); + return p.then(function(swr) { + registration = swr; + return waitForState(swr.installing, 'activated'); + }); + } + + function unregister() { + return registration.unregister().then(function(success) { + ok(success, "Service worker should be unregistered successfully"); + }, function(e) { + dump("SW unregistration error: " + e + "\n"); + }); + } + + function testPlugins() { + var p = new Promise(function(resolve, reject) { + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "done") { + window.onmessage = null; + w.close(); + resolve(); + } + } + }); + + var w = window.open("fetch/plugin/plugins.html"); + return p; + } + + function runTest() { + simpleRegister() + .then(testPlugins) + .then(unregister) + .then(function() { + SimpleTest.finish(); + }).catch(function(e) { + ok(false, "Some test failed with error " + e); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_notification_constructor_error.html b/dom/serviceworkers/test/test_notification_constructor_error.html new file mode 100644 index 0000000000..46d93e781f --- /dev/null +++ b/dom/serviceworkers/test/test_notification_constructor_error.html @@ -0,0 +1,51 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug XXXXXXX - Check that Notification constructor throws in ServiceWorkerGlobalScope</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/dom/notification/test/mochitest/MockServices.js"></script> + <script type="text/javascript" src="/tests/dom/notification/test/mochitest/NotificationTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function simpleRegister() { + return navigator.serviceWorker.register("notification_constructor_error.js", { scope: "notification_constructor_error/" }).then(function(swr) { + ok(false, "Registration should fail."); + }, function(e) { + is(e.name, 'TypeError', "Registration should fail with a TypeError."); + }); + } + + function runTest() { + MockServices.register(); + simpleRegister() + .then(function() { + MockServices.unregister(); + SimpleTest.finish(); + }).catch(function(e) { + ok(false, "Some test failed with error " + e); + MockServices.unregister(); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["notification.prompt.testing", true], + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_notification_get.html b/dom/serviceworkers/test/test_notification_get.html new file mode 100644 index 0000000000..44239b4f9e --- /dev/null +++ b/dom/serviceworkers/test/test_notification_get.html @@ -0,0 +1,213 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>ServiceWorkerRegistration.getNotifications() on main thread and worker thread.</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/dom/notification/test/mochitest/MockServices.js"></script> + <script type="text/javascript" src="/tests/dom/notification/test/mochitest/NotificationTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script type="text/javascript"> + + SimpleTest.requestFlakyTimeout("untriaged"); + + function testFrame(src) { + return new Promise(function(resolve, reject) { + var iframe = document.createElement("iframe"); + iframe.src = src; + window.callback = function(result) { + iframe.src = "about:blank"; + document.body.removeChild(iframe); + iframe = null; + SpecialPowers.exactGC(function() { + resolve(result); + }); + }; + document.body.appendChild(iframe); + }); + } + + function registerSW() { + return testFrame('notification/register.html').then(function() { + ok(true, "Registered service worker."); + }); + } + + function unregisterSW() { + return testFrame('notification/unregister.html').then(function() { + ok(true, "Unregistered service worker."); + }); + } + + // To check that the scope is respected when retrieving notifications. + function registerAlternateSWAndAddNotification() { + return testFrame('notification_alt/register.html').then(function() { + ok(true, "Registered alternate service worker."); + return navigator.serviceWorker.getRegistration("./notification_alt/").then(function(reg) { + return reg.showNotification("This is a notification_alt"); + }); + }); + } + + function unregisterAlternateSWAndAddNotification() { + return testFrame('notification_alt/unregister.html').then(function() { + ok(true, "unregistered alternate service worker."); + }); + } + + function testDismiss() { + // Dismissed persistent notifications should be removed from the + // notification list. + var alertsService = SpecialPowers.Cc["@mozilla.org/alerts-service;1"] + .getService(SpecialPowers.Ci.nsIAlertsService); + return navigator.serviceWorker.getRegistration("./notification/") + .then(function(reg) { + return reg.showNotification( + "This is a notification that will be closed", { tag: "dismiss" }) + .then(function() { + return reg; + }); + }).then(function(reg) { + return reg.getNotifications() + .then(function(notifications) { + is(notifications.length, 1, "There should be one visible notification"); + is(notifications[0].tag, "dismiss", "Tag should match"); + + // Simulate dismissing the notification by using the alerts service + // directly, instead of `Notification#close`. + var principal = SpecialPowers.wrap(document).nodePrincipal; + var id = principal.origin + "#tag:dismiss"; + alertsService.closeAlert(id, principal); + + return reg; + }); + }).then(function(reg) { + return reg.getNotifications(); + }).then(function(notifications) { + // Make sure dismissed notifications are no longer retrieved. + is(notifications.length, 0, "There should be no more stored notifications"); + }); + } + + function testGet() { + // Non persistent notifications will not show up in getNotification(). + var n = new Notification("Scope does not match"); + var options = NotificationTest.payload; + return navigator.serviceWorker.getRegistration("./notification/") + .then(function(reg) { + return reg.showNotification("This is a title", options) + .then(function() { + return reg; + }); + }).then(function(reg) { + return registerAlternateSWAndAddNotification().then(function() { + return reg; + }); + }).then(function(reg) { + return reg.getNotifications(); + }).then(function(notifications) { + is(notifications.length, 1, "There should be one stored notification"); + var notification = notifications[0]; + ok(notification instanceof Notification, "Should be a Notification"); + is(notification.title, "This is a title", "Title should match"); + for (var key in options) { + if (key === "data") { + ok(NotificationTest.customDataMatches(notification.data), + "data property should match"); + continue; + } + is(notification[key], options[key], key + " property should match"); + } + notification.close(); + }).then(function() { + return navigator.serviceWorker.getRegistration("./notification/").then(function(reg) { + return reg.getNotifications(); + }); + }).then(function(notifications) { + // Make sure closed notifications are no longer retrieved. + is(notifications.length, 0, "There should be no more stored notifications"); + }).catch(function(e) { + ok(false, "Something went wrong " + e.message); + }).then(unregisterAlternateSWAndAddNotification); + } + + function testGetWorker() { + todo(false, "navigator.serviceWorker is not available on workers yet"); + return Promise.resolve(); + } + + function waitForSWTests(reg, msg) { + return new Promise(function(resolve, reject) { + var content = document.getElementById("content"); + + iframe = document.createElement("iframe"); + + content.appendChild(iframe); + iframe.setAttribute('src', "notification/listener.html"); + + window.onmessage = function(e) { + if (e.data.type == 'status') { + ok(e.data.status, "Service worker test: " + e.data.msg); + } else if (e.data.type == 'finish') { + content.removeChild(iframe); + resolve(); + } + } + + iframe.onload = function(e) { + iframe.onload = null; + reg.active.postMessage(msg); + } + }); + } + + function testGetServiceWorker() { + return navigator.serviceWorker.getRegistration("./notification/") + .then(function(reg) { + return waitForSWTests(reg, 'create'); + }); + } + + // Create a Notification here, make sure ServiceWorker sees it. + function testAcrossThreads() { + return navigator.serviceWorker.getRegistration("./notification/") + .then(function(reg) { + return reg.showNotification("This is a title") + .then(function() { + return reg; + }); + }).then(function(reg) { + return waitForSWTests(reg, 'do-not-create'); + }); + } + + SimpleTest.waitForExplicitFinish(); + + MockServices.register(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.webnotifications.workers.enabled", true], + ["dom.webnotifications.serviceworker.enabled", true], + ["notification.prompt.testing", true], + ]}, function() { + registerSW() + .then(testGet) + .then(testGetWorker) + .then(testGetServiceWorker) + .then(testAcrossThreads) + .then(testDismiss) + .then(unregisterSW) + .then(function() { + MockServices.unregister(); + SimpleTest.finish(); + }); + }); +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_notification_openWindow.html b/dom/serviceworkers/test/test_notification_openWindow.html new file mode 100644 index 0000000000..830c9e03d2 --- /dev/null +++ b/dom/serviceworkers/test/test_notification_openWindow.html @@ -0,0 +1,91 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1578070</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="utils.js"></script> + <script type="text/javascript" src="/tests/dom/notification/test/mochitest/MockServices.js"></script> + <script type="text/javascript" src="/tests/dom/notification/test/mochitest/NotificationTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> +// eslint-disable-next-line mozilla/no-addtask-setup +add_task(async function setup() { + await SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.webnotifications.workers.enabled", true], + ["dom.webnotifications.serviceworker.enabled", true], + ["notification.prompt.testing", true], + ["dom.serviceWorkers.disable_open_click_delay", 1000], + ["dom.serviceWorkers.idle_timeout", 299999], + ["dom.serviceWorkers.idle_extended_timeout", 299999] + ]}); + + MockServices.register(); + SimpleTest.requestFlakyTimeout("Mock alert service dispatches show and click events."); + SimpleTest.registerCleanupFunction(() => { + MockServices.unregister(); + }); +}); + +add_task(async function test() { + info("Registering service worker."); + let swr = await navigator.serviceWorker.register("notification_openWindow_worker.js"); + await waitForState(swr.installing, "activated"); + + SimpleTest.registerCleanupFunction(async () => { + await swr.unregister(); + navigator.serviceWorker.onmessage = null; + }); + + for (let prefValue of [ + SpecialPowers.Ci.nsIBrowserDOMWindow.OPEN_CURRENTWINDOW, + SpecialPowers.Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW, + SpecialPowers.Ci.nsIBrowserDOMWindow.OPEN_NEWTAB, + ]) { + if (prefValue == SpecialPowers.Ci.nsIBrowserDOMWindow.OPEN_CURRENTWINDOW) { + // Let's open a new tab and focus on it. When the service + // worker notification is shown, the document will open in the focused tab. + // If we don't open a new tab, the document will be opened in the + // current test-runner tab and mess up the test setup. + window.open(""); + } + info(`Setting browser.link.open_newwindow to ${prefValue}.`); + await SpecialPowers.pushPrefEnv({ + set: [["browser.link.open_newwindow", prefValue]], + }); + + // The onclicknotification handler uses Clients.openWindow() to open a new + // window. This newly created window will attempt to open another window with + // Window.open() and some arbitrary URL. We crash before the second window + // finishes loading. + info("Showing notification."); + await swr.showNotification("notification"); + + info("Waiting for \"DONE\" from worker."); + await new Promise(resolve => { + navigator.serviceWorker.onmessage = event => { + if (event.data !== "DONE") { + ok(false, `Unexpected message from service worker: ${JSON.stringify(event.data)}`); + } + resolve(); + } + }); + + // If we make it here, then we didn't crash. + ok(true, "Didn't crash!"); + + navigator.serviceWorker.onmessage = null; + } +}); + +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_notificationclick-otherwindow.html b/dom/serviceworkers/test/test_notificationclick-otherwindow.html new file mode 100644 index 0000000000..a1ffb71c39 --- /dev/null +++ b/dom/serviceworkers/test/test_notificationclick-otherwindow.html @@ -0,0 +1,65 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=916893 +--> +<head> + <title>Bug 1114554 - Test ServiceWorkerGlobalScope.notificationclick event.</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/dom/notification/test/mochitest/MockServices.js"></script> + <script type="text/javascript" src="/tests/dom/notification/test/mochitest/NotificationTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1114554">Bug 1114554</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +<script src="utils.js"></script> +<script type="text/javascript"> + SimpleTest.requestFlakyTimeout("Mock alert service dispatches show and click events."); + + function testFrame(src) { + var iframe = document.createElement("iframe"); + iframe.src = src; + window.callback = function(result) { + window.callback = null; + document.body.removeChild(iframe); + iframe = null; + ok(result, "Got notificationclick event with correct data."); + MockServices.unregister(); + registration.unregister().then(function() { + SimpleTest.finish(); + }); + }; + document.body.appendChild(iframe); + } + + var registration; + + function runTest() { + MockServices.register(); + navigator.serviceWorker.register("notificationclick.js", { scope: "notificationclick-otherwindow.html" }).then(function(reg) { + registration = reg; + return waitForState(reg.installing, 'activated'); + }, function(e) { + ok(false, "registration should have passed!"); + }).then(() => { + testFrame('notificationclick-otherwindow.html'); + }); + }; + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.webnotifications.workers.enabled", true], + ["dom.webnotifications.serviceworker.enabled", true], + ["notification.prompt.testing", true], + ]}, runTest); +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_notificationclick.html b/dom/serviceworkers/test/test_notificationclick.html new file mode 100644 index 0000000000..f733a3703f --- /dev/null +++ b/dom/serviceworkers/test/test_notificationclick.html @@ -0,0 +1,66 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=916893 +--> +<head> + <title>Bug 1114554 - Test ServiceWorkerGlobalScope.notificationclick event.</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/dom/notification/test/mochitest/MockServices.js"></script> + <script type="text/javascript" src="/tests/dom/notification/test/mochitest/NotificationTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1114554">Bug 1114554</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +<script src="utils.js"></script> +<script type="text/javascript"> + SimpleTest.requestFlakyTimeout("Mock alert service dispatches show and click events."); + + function testFrame(src) { + var iframe = document.createElement("iframe"); + iframe.src = src; + window.callback = function(result) { + window.callback = null; + document.body.removeChild(iframe); + iframe = null; + ok(result, "Got notificationclick event with correct data."); + MockServices.unregister(); + registration.unregister().then(function() { + SimpleTest.finish(); + }); + }; + document.body.appendChild(iframe); + } + + var registration; + + function runTest() { + MockServices.register(); + navigator.serviceWorker.register("notificationclick.js", { scope: "notificationclick.html" }).then(function(reg) { + registration = reg; + return waitForState(reg.installing, 'activated'); + }, function(e) { + ok(false, "registration should have passed!"); + }).then(() => { + // Now that we know the document will be controlled, create the frame. + testFrame('notificationclick.html'); + }); + }; + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.webnotifications.workers.enabled", true], + ["dom.webnotifications.serviceworker.enabled", true], + ["notification.prompt.testing", true], + ]}, runTest); +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_notificationclick_focus.html b/dom/serviceworkers/test/test_notificationclick_focus.html new file mode 100644 index 0000000000..6a99112313 --- /dev/null +++ b/dom/serviceworkers/test/test_notificationclick_focus.html @@ -0,0 +1,66 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=916893 +--> +<head> + <title>Bug 1144660 - Test client.focus() permissions on notification click</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/dom/notification/test/mochitest/MockServices.js"></script> + <script type="text/javascript" src="/tests/dom/notification/test/mochitest/NotificationTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1114554">Bug 1114554</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +<script src="utils.js"></script> +<script type="text/javascript"> + SimpleTest.requestFlakyTimeout("Mock alert service dispatches show and click events."); + + function testFrame(src) { + var iframe = document.createElement("iframe"); + iframe.src = src; + window.callback = function(result) { + window.callback = null; + document.body.removeChild(iframe); + iframe = null; + ok(result, "All tests passed."); + MockServices.unregister(); + registration.unregister().then(function() { + SimpleTest.finish(); + }); + }; + document.body.appendChild(iframe); + } + + var registration; + + function runTest() { + MockServices.register(); + navigator.serviceWorker.register("notificationclick_focus.js", { scope: "notificationclick_focus.html" }).then(function(reg) { + registration = reg; + return waitForState(reg.installing, 'activated'); + }, function(e) { + ok(false, "registration should have passed!"); + }).then(() => { + testFrame('notificationclick_focus.html'); + }); + }; + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.webnotifications.workers.enabled", true], + ["dom.webnotifications.serviceworker.enabled", true], + ["notification.prompt.testing", true], + ["dom.serviceWorkers.disable_open_click_delay", 1000], + ]}, runTest); +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_notificationclose.html b/dom/serviceworkers/test/test_notificationclose.html new file mode 100644 index 0000000000..a1ae4f0a4e --- /dev/null +++ b/dom/serviceworkers/test/test_notificationclose.html @@ -0,0 +1,67 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1265841 +--> +<head> + <title>Bug 1265841 - Test ServiceWorkerGlobalScope.notificationclose event.</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/dom/notification/test/mochitest/MockServices.js"></script> + <script type="text/javascript" src="/tests/dom/notification/test/mochitest/NotificationTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1265841">Bug 1265841</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +<script src="utils.js"></script> +<script type="text/javascript"> + SimpleTest.requestFlakyTimeout("Mock alert service dispatches show, click, and close events."); + + function testFrame(src) { + var iframe = document.createElement("iframe"); + iframe.src = src; + window.callback = function(data) { + window.callback = null; + document.body.removeChild(iframe); + iframe = null; + ok(data.result, "Got notificationclose event with correct data."); + ok(!data.windowOpened, + "Shouldn't allow to openWindow in notificationclose"); + MockServices.unregister(); + registration.unregister().then(function() { + SimpleTest.finish(); + }); + }; + document.body.appendChild(iframe); + } + + var registration; + + function runTest() { + MockServices.register(); + navigator.serviceWorker.register("notificationclose.js", { scope: "notificationclose.html" }).then(function(reg) { + registration = reg; + return waitForState(reg.installing, 'activated'); + }, function(e) { + ok(false, "registration should have passed!"); + }).then(() => { + testFrame('notificationclose.html'); + }); + }; + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.webnotifications.workers.enabled", true], + ["dom.webnotifications.serviceworker.enabled", true], + ["notification.prompt.testing", true], + ]}, runTest); +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_onmessageerror.html b/dom/serviceworkers/test/test_onmessageerror.html new file mode 100644 index 0000000000..425b890951 --- /dev/null +++ b/dom/serviceworkers/test/test_onmessageerror.html @@ -0,0 +1,128 @@ +<!DOCTYPE html> +<html> + <head> + <title>Test onmessageerror event handlers</title> + </head> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="utils.js"></script> + <script> + /** + * Test that ServiceWorkerGlobalScope and ServiceWorkerContainer handle + * `messageerror` events, using a test helper class `StructuredCloneTester`. + * Intances of this class can be configured to fail to serialize or + * deserialize, as it's difficult to artificially create the case where an + * object successfully serializes but fails to deserialize (which can be + * caused by out-of-memory failures or the target global not supporting a + * serialized interface). + */ + + let registration = null; + let serviceWorker = null; + let serviceWorkerContainer = null; + const swScript = 'onmessageerror_worker.js'; + + add_task(async () => { + await SpecialPowers.pushPrefEnv({ + set: [ + ['dom.serviceWorkers.enabled', true], + ['dom.serviceWorkers.testing.enabled', true], + ['dom.testing.structuredclonetester.enabled', true], + ], + }); + + swContainer = navigator.serviceWorker; + + registration = await swContainer.register(swScript); + ok(registration, 'Service Worker regsisters'); + + serviceWorker = registration.installing; + await waitForState(serviceWorker, 'activated'); + }); // setup + + add_task(async () => { + const serializable = true; + const deserializable = true; + let sct = new StructuredCloneTester(serializable, deserializable); + + const p = new Promise((resolve, reject) => { + function onMessage(e) { + const expectedBehavior = 'Serializable and deserializable ' + + 'StructuredCloneTester serializes and deserializes'; + + is(e.data.received, 'message', expectedBehavior); + swContainer.removeEventListener('message', onMessage); + resolve(); + } + + swContainer.addEventListener('message', onMessage); + }); + + serviceWorker.postMessage({ serializable, deserializable, sct }); + + await p; + }); + + add_task(async () => { + const serializable = false; + // if it's not serializable, being deserializable or not doesn't matter + const deserializable = false; + let sct = new StructuredCloneTester(serializable, deserializable); + + try { + serviceWorker.postMessage({ serializable, deserializable, sct }); + ok(false, 'StructuredCloneTester serialization should have thrown -- ' + + 'this line should not have been reached.'); + } catch (e) { + const expectedBehavior = 'Unserializable StructuredCloneTester fails ' + + `to send, with exception name: ${e.name}`; + is(e.name, 'DataCloneError', expectedBehavior); + } + }); + + add_task(async () => { + const serializable = true; + const deserializable = false; + let sct = new StructuredCloneTester(serializable, deserializable); + + const p = new Promise((resolve, reject) => { + function onMessage(e) { + const expectedBehavior = 'ServiceWorkerGlobalScope handles ' + + 'messageerror events'; + + is(e.data.received, 'messageerror', expectedBehavior); + swContainer.removeEventListener('message', onMessage); + resolve(); + } + + swContainer.addEventListener('message', onMessage); + }); + + serviceWorker.postMessage({ serializable, deserializable, sct }); + + await p; + }); // test ServiceWorkerGlobalScope onmessageerror + + add_task(async () => { + const p = new Promise((resolve, reject) => { + function onMessageError(e) { + ok(true, 'ServiceWorkerContainer handles messageerror events'); + swContainer.removeEventListener('messageerror', onMessageError); + resolve(); + } + + swContainer.addEventListener('messageerror', onMessageError); + }); + + serviceWorker.postMessage('send-bad-message'); + + await p; + }); // test ServiceWorkerContainer onmessageerror + + add_task(async () => { + await SpecialPowers.popPrefEnv(); + ok(await registration.unregister(), 'Service Worker unregisters'); + }); // teardown + </script> + <body> + </body> +</html> diff --git a/dom/serviceworkers/test/test_opaque_intercept.html b/dom/serviceworkers/test/test_opaque_intercept.html new file mode 100644 index 0000000000..f0e40e0402 --- /dev/null +++ b/dom/serviceworkers/test/test_opaque_intercept.html @@ -0,0 +1,93 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 982726 - Test service worker post message </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + var registration; + function start() { + return navigator.serviceWorker.register("opaque_intercept_worker.js", + { scope: "./sw_clients/" }) + .then((swr) => registration = swr); + } + + function unregister() { + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + } + + + function testOpaqueIntercept(swr) { + var p = new Promise(function(res, rej) { + var ready = false; + var scriptLoaded = false; + window.onmessage = function(e) { + if (e.data === "READY") { + ok(!ready, "ready message should only be received once"); + ok(!scriptLoaded, "ready message should be received before script loaded"); + if (ready) { + res(); + return; + } + ready = true; + iframe.contentWindow.postMessage("REFRESH", "*"); + } else if (e.data === "SCRIPT_LOADED") { + ok(ready, "script loaded should be received after ready"); + ok(!scriptLoaded, "script loaded message should be received only once"); + scriptLoaded = true; + res(); + } + } + }); + + var content = document.getElementById("content"); + ok(content, "Parent exists."); + + var iframe = document.createElement("iframe"); + iframe.setAttribute('src', "sw_clients/refresher.html"); + content.appendChild(iframe); + + // Our service worker waits for us to finish installing. If it didn't do + // this, then loading our frame would race with it becoming active, + // possibly intercepting the first load of the iframe. This guarantees + // that our iframe will load first directly from the network. Note that + // refresher.html explicitly waits for the service worker to transition to + // active. + registration.installing.postMessage("ready"); + + return p.then(() => content.removeChild(iframe)); + } + + function runTest() { + start() + .then(testOpaqueIntercept) + .then(unregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true], + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_openWindow.html b/dom/serviceworkers/test/test_openWindow.html new file mode 100644 index 0000000000..448d143edd --- /dev/null +++ b/dom/serviceworkers/test/test_openWindow.html @@ -0,0 +1,112 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1172870 +--> +<head> + <title>Bug 1172870 - Test clients.openWindow</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="/tests/dom/notification/test/mochitest/MockServices.js"></script> + <script type="text/javascript" src="/tests/dom/notification/test/mochitest/NotificationTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1172870">Bug 1172870</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +</pre> +<script src="utils.js"></script> +<script type="text/javascript"> + SimpleTest.requestFlakyTimeout("Mock alert service dispatches show and click events."); + + function setup(ctx) { + MockServices.register(); + + return navigator.serviceWorker.register("openWindow_worker.js", {scope: "./"}) + .then(function(swr) { + ok(swr, "Registration successful"); + ctx.registration = swr; + return waitForState(swr.installing, 'activated', ctx); + }); + } + + function setupMessageHandler(ctx) { + return new Promise(function(res, rej) { + navigator.serviceWorker.onmessage = function(event) { + navigator.serviceWorker.onmessage = null; + for (i = 0; i < event.data.length; i++) { + ok(event.data[i].result, event.data[i].message); + } + res(ctx); + } + }); + } + + function testPopupNotAllowed(ctx) { + var p = setupMessageHandler(ctx); + ok(ctx.registration.active, "Worker is active."); + ctx.registration.active.postMessage("testNoPopup"); + + return p; + } + + function testPopupAllowed(ctx) { + var p = setupMessageHandler(ctx); + ctx.registration.showNotification("testPopup"); + + return p; + } + + function checkNumberOfWindows(ctx) { + return new Promise(function(res, rej) { + navigator.serviceWorker.onmessage = function(event) { + navigator.serviceWorker.onmessage = null; + for (i = 0; i < event.data.length; i++) { + ok(event.data[i].result, event.data[i].message); + } + res(ctx); + } + ctx.registration.active.postMessage("CHECK_NUMBER_OF_WINDOWS"); + }); + } + + function clear(ctx) { + MockServices.unregister(); + + return ctx.registration.unregister().then(function(result) { + ctx.registration = null; + ok(result, "Unregister was successful."); + }); + } + + function runTest() { + setup({}) + // Permission to allow popups persists for some time after a notification + // click event, so the order here is important. + .then(testPopupNotAllowed) + .then(testPopupAllowed) + .then(checkNumberOfWindows) + .then(clear) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.webnotifications.workers.enabled", true], + ["dom.webnotifications.serviceworker.enabled", true], + ["notification.prompt.testing", true], + ["dom.serviceWorkers.disable_open_click_delay", 1000], + ["dom.serviceWorkers.idle_timeout", 299999], + ["dom.serviceWorkers.idle_extended_timeout", 299999], + ["dom.securecontext.allowlist", "mochi.test,example.com"], + ]}, runTest); +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_origin_after_redirect.html b/dom/serviceworkers/test/test_origin_after_redirect.html new file mode 100644 index 0000000000..e4c0af23be --- /dev/null +++ b/dom/serviceworkers/test/test_origin_after_redirect.html @@ -0,0 +1,58 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test the origin of a redirected response from a service worker</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "/tests/dom/serviceworkers/test/fetch/origin/register.html"; + var win; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + win = window.open("/tests/dom/serviceworkers/test/fetch/origin/index.sjs", "mywindow", "width=100,height=100"); + } else if (e.data.status == "domain") { + is(e.data.data, "example.org", "Correct domain expected"); + } else if (e.data.status == "origin") { + is(e.data.data, "http://example.org", "Correct origin expected"); + } else if (e.data.status == "done") { + win.close(); + iframe.src = "/tests/dom/serviceworkers/test/fetch/origin/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + ok(true, "Test finished successfully"); + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true], + ["dom.security.https_first", false], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_origin_after_redirect_cached.html b/dom/serviceworkers/test/test_origin_after_redirect_cached.html new file mode 100644 index 0000000000..ca79581ccf --- /dev/null +++ b/dom/serviceworkers/test/test_origin_after_redirect_cached.html @@ -0,0 +1,58 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test the origin of a redirected response from a service worker</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "/tests/dom/serviceworkers/test/fetch/origin/register.html"; + var win; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + win = window.open("/tests/dom/serviceworkers/test/fetch/origin/index-cached.sjs", "mywindow", "width=100,height=100"); + } else if (e.data.status == "domain") { + is(e.data.data, "example.org", "Correct domain expected"); + } else if (e.data.status == "origin") { + is(e.data.data, "http://example.org", "Correct origin expected"); + } else if (e.data.status == "done") { + win.close(); + iframe.src = "/tests/dom/serviceworkers/test/fetch/origin/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + ok(true, "Test finished successfully"); + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true], + ["dom.security.https_first", false], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_origin_after_redirect_to_https.html b/dom/serviceworkers/test/test_origin_after_redirect_to_https.html new file mode 100644 index 0000000000..927a68ef3a --- /dev/null +++ b/dom/serviceworkers/test/test_origin_after_redirect_to_https.html @@ -0,0 +1,57 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test the origin of a redirected response from a service worker</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "/tests/dom/serviceworkers/test/fetch/origin/register.html"; + var win; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + win = window.open("/tests/dom/serviceworkers/test/fetch/origin/index-to-https.sjs", "mywindow", "width=100,height=100"); + } else if (e.data.status == "domain") { + is(e.data.data, "example.org", "Correct domain expected"); + } else if (e.data.status == "origin") { + is(e.data.data, "https://example.org", "Correct origin expected"); + } else if (e.data.status == "done") { + win.close(); + iframe.src = "/tests/dom/serviceworkers/test/fetch/origin/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + ok(true, "Test finished successfully"); + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_origin_after_redirect_to_https_cached.html b/dom/serviceworkers/test/test_origin_after_redirect_to_https_cached.html new file mode 100644 index 0000000000..29686e2302 --- /dev/null +++ b/dom/serviceworkers/test/test_origin_after_redirect_to_https_cached.html @@ -0,0 +1,57 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test the origin of a redirected response from a service worker</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + function runTest() { + iframe = document.querySelector("iframe"); + iframe.src = "/tests/dom/serviceworkers/test/fetch/origin/register.html"; + var win; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + win = window.open("/tests/dom/serviceworkers/test/fetch/origin/index-to-https-cached.sjs", "mywindow", "width=100,height=100"); + } else if (e.data.status == "domain") { + is(e.data.data, "example.org", "Correct domain expected"); + } else if (e.data.status == "origin") { + is(e.data.data, "https://example.org", "Correct origin expected"); + } else if (e.data.status == "done") { + win.close(); + iframe.src = "/tests/dom/serviceworkers/test/fetch/origin/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + window.onmessage = null; + ok(true, "Test finished successfully"); + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.caches.enabled", true], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_post_message.html b/dom/serviceworkers/test/test_post_message.html new file mode 100644 index 0000000000..b72f948dd6 --- /dev/null +++ b/dom/serviceworkers/test/test_post_message.html @@ -0,0 +1,80 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 982726 - Test service worker post message </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> + var magic_value = "MAGIC_VALUE_123"; + var registration; + function start() { + return navigator.serviceWorker.register("message_posting_worker.js", + { scope: "./sw_clients/" }) + .then(swr => waitForState(swr.installing, 'activated', swr)) + .then((swr) => registration = swr); + } + + function unregister() { + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + } + + + function testPostMessage(swr) { + var p = new Promise(function(res, rej) { + window.onmessage = function(e) { + if (e.data === "READY") { + swr.active.postMessage(magic_value); + } else if (e.data === magic_value) { + ok(true, "Worker posted the correct value."); + res(); + } else { + ok(false, "Wrong value. Expected: " + magic_value + + ", got: " + e.data); + res(); + } + } + }); + + var content = document.getElementById("content"); + ok(content, "Parent exists."); + + iframe = document.createElement("iframe"); + iframe.setAttribute('src', "sw_clients/service_worker_controlled.html"); + content.appendChild(iframe); + + return p.then(() => content.removeChild(iframe)); + } + + function runTest() { + start() + .then(testPostMessage) + .then(unregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_post_message_advanced.html b/dom/serviceworkers/test/test_post_message_advanced.html new file mode 100644 index 0000000000..580dfd3f07 --- /dev/null +++ b/dom/serviceworkers/test/test_post_message_advanced.html @@ -0,0 +1,109 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 982726 - Test service worker post message advanced </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> + var registration; + var base = ["string", true, 42]; + var blob = new Blob(["blob_content"]); + var file = new File(["file_content"], "file"); + var obj = { body : "object_content" }; + + function readBlob(blobToRead) { + return new Promise(function(resolve, reject) { + var reader = new FileReader(); + reader.onloadend = () => resolve(reader.result); + reader.readAsText(blobToRead); + }); + } + + function equals(v1, v2) { + return Promise.all([v1, v2]).then(function(val) { + ok(val[0] === val[1], "Values should match."); + }); + } + + function blob_equals(b1, b2) { + return equals(readBlob(b1), readBlob(b2)); + } + + function file_equals(f1, f2) { + return equals(f1.name, f2.name).then(blob_equals(f1, f2)); + } + + function obj_equals(o1, o2) { + return equals(o1.body, o2.body); + } + + function start() { + return navigator.serviceWorker.register("message_posting_worker.js", + { scope: "./sw_clients/" }) + .then(swr => waitForState(swr.installing, 'activated', swr)) + .then((swr) => registration = swr); + } + + function unregister() { + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + } + + function testPostMessageObject(object, test) { + var p = new Promise(function(res, rej) { + window.onmessage = function(e) { + if (e.data === "READY") { + registration.active.postMessage(object) + } else { + test(object, e.data).then(res); + } + } + }); + + var content = document.getElementById("content"); + ok(content, "Parent exists."); + + iframe = document.createElement("iframe"); + iframe.setAttribute('src', "sw_clients/service_worker_controlled.html"); + content.appendChild(iframe); + + return p.then(() => content.removeChild(iframe)); + } + + function runTest() { + start() + .then(testPostMessageObject.bind(this, base[0], equals)) + .then(testPostMessageObject.bind(this, base[1], equals)) + .then(testPostMessageObject.bind(this, base[2], equals)) + .then(testPostMessageObject.bind(this, blob, blob_equals)) + .then(testPostMessageObject.bind(this, file, file_equals)) + .then(testPostMessageObject.bind(this, obj, obj_equals)) + .then(unregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_post_message_source.html b/dom/serviceworkers/test/test_post_message_source.html new file mode 100644 index 0000000000..b72ebe3a7c --- /dev/null +++ b/dom/serviceworkers/test/test_post_message_source.html @@ -0,0 +1,66 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1142015 - Test service worker post message source </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + var magic_value = "MAGIC_VALUE_RANDOM"; + var registration; + function start() { + return navigator.serviceWorker.register("source_message_posting_worker.js", + { scope: "./nonexistent_scope/" }) + .then((swr) => registration = swr); + } + + function unregister() { + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + } + + + function testPostMessage(swr) { + var p = new Promise(function(res, rej) { + navigator.serviceWorker.onmessage = function(e) { + ok(e.data === magic_value, "Worker posted the correct value."); + res(); + } + }); + + ok(swr.installing, "Installing worker exists."); + swr.installing.postMessage(magic_value); + return p; + } + + + function runTest() { + start() + .then(testPostMessage) + .then(unregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_privateBrowsing.html b/dom/serviceworkers/test/test_privateBrowsing.html new file mode 100644 index 0000000000..6a10f375f9 --- /dev/null +++ b/dom/serviceworkers/test/test_privateBrowsing.html @@ -0,0 +1,103 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for ServiceWorker - Private Browsing</title> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"?> +</head> +<body> + +<script type="application/javascript"> +const {BrowserTestUtils} = ChromeUtils.import("resource://testing-common/BrowserTestUtils.jsm"); + +var mainWindow; + +var contentPage = "http://mochi.test:8888/chrome/dom/workers/test/empty.html"; +var workerScope = "http://mochi.test:8888/chrome/dom/serviceworkers/test/"; +var workerURL = workerScope + "worker.js"; + +function testOnWindow(aIsPrivate, aCallback) { + var win = mainWindow.OpenBrowserWindow({private: aIsPrivate}); + win.addEventListener("load", function() { + win.addEventListener("DOMContentLoaded", function onInnerLoad() { + if (win.content.location.href != contentPage) { + BrowserTestUtils.loadURI(win.gBrowser, contentPage); + return; + } + + win.removeEventListener("DOMContentLoaded", onInnerLoad, true); + SimpleTest.executeSoon(function() { aCallback(win); }); + }, true); + }, {capture: true, once: true}); +} + +function setupWindow() { + mainWindow = window.browsingContext.topChromeWindow; + runTest(); +} + +var wN; +var registration; +var wP; + +function testPrivateWindow() { + testOnWindow(true, function(aWin) { + wP = aWin; + ok(!wP.content.eval('"serviceWorker" in navigator'), "ServiceWorkers are not available for private windows"); + runTest(); + }); +} + +function doTests() { + testOnWindow(false, function(aWin) { + wN = aWin; + ok("serviceWorker" in wN.content.navigator, "ServiceWorkers are available for normal windows"); + + wN.content.navigator.serviceWorker.register(workerURL, + { scope: workerScope }) + .then(function(aRegistration) { + registration = aRegistration; + ok(registration, "Registering a service worker in a normal window should succeed"); + + // Bug 1255621: We should be able to load a controlled document in a private window. + testPrivateWindow(); + }, function(aError) { + ok(false, "Error registering worker in normal window: " + aError); + testPrivateWindow(); + }); + }); +} + +var steps = [ + setupWindow, + doTests +]; + +function cleanup() { + wN.close(); + wP.close(); + + SimpleTest.finish(); +} + +function runTest() { + if (!steps.length) { + registration.unregister().then(cleanup, cleanup); + + return; + } + + var step = steps.shift(); + step(); +} + +SimpleTest.waitForExplicitFinish(); +SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["browser.startup.page", 0], + ["browser.startup.homepage_override.mstone", "ignore"], +]}, runTest); + +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_register_base.html b/dom/serviceworkers/test/test_register_base.html new file mode 100644 index 0000000000..3a1f2f2621 --- /dev/null +++ b/dom/serviceworkers/test/test_register_base.html @@ -0,0 +1,34 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test that registering a service worker uses the docuemnt URI for the secure origin check</title> + <script type="text/javascript" src="http://mochi.test:8888/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="http://mochi.test:8888/tests/SimpleTest/test.css" /> + <base href="https://mozilla.org/"> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function runTest() { + ok(!("serviceWorker" in navigator), "ServiceWorkerContainer shouldn't be defined"); + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ]}, runTest); + }; +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_register_https_in_http.html b/dom/serviceworkers/test/test_register_https_in_http.html new file mode 100644 index 0000000000..096c3733a0 --- /dev/null +++ b/dom/serviceworkers/test/test_register_https_in_http.html @@ -0,0 +1,45 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1172948 - Test that registering a service worker from inside an HTTPS iframe embedded in an HTTP iframe doesn't work</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function runTest() { + var iframe = document.createElement("iframe"); + iframe.src = "https://example.com/tests/dom/serviceworkers/test/register_https.html"; + document.body.appendChild(iframe); + + window.onmessage = event => { + switch (event.data.type) { + case "ok": + ok(event.data.status, event.data.msg); + break; + case "done": + SimpleTest.finish(); + break; + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ]}, runTest); + }; +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_sandbox_intercept.html b/dom/serviceworkers/test/test_sandbox_intercept.html new file mode 100644 index 0000000000..2aa120994f --- /dev/null +++ b/dom/serviceworkers/test/test_sandbox_intercept.html @@ -0,0 +1,56 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1142727 - Test that sandboxed iframes are not intercepted</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"> +<iframe id="normal-frame"></iframe> +<iframe sandbox="allow-scripts" id="sandbox-frame"></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var normalFrame; + var sandboxFrame; + function runTest() { + normalFrame = document.getElementById("normal-frame"); + sandboxFrame = document.getElementById("sandbox-frame"); + normalFrame.src = "/tests/dom/serviceworkers/test/fetch/sandbox/register.html"; + window.onmessage = function(e) { + if (e.data.status == "ok") { + ok(e.data.result, e.data.message); + } else if (e.data.status == "registrationdone") { + normalFrame.src = "about:blank"; + sandboxFrame.src = "/tests/dom/serviceworkers/test/fetch/sandbox/index.html"; + } else if (e.data.status == "done") { + sandboxFrame.src = "about:blank"; + normalFrame.src = "/tests/dom/serviceworkers/test/fetch/sandbox/unregister.html"; + } else if (e.data.status == "unregistrationdone") { + normalFrame.src = "about:blank"; + window.onmessage = null; + ok(true, "Test finished successfully"); + SimpleTest.finish(); + } + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_sanitize.html b/dom/serviceworkers/test/test_sanitize.html new file mode 100644 index 0000000000..dd6bd42c8f --- /dev/null +++ b/dom/serviceworkers/test/test_sanitize.html @@ -0,0 +1,86 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1080109 - Clear ServiceWorker registrations for all domains</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function start() { + const Cc = SpecialPowers.Cc; + const Ci = SpecialPowers.Ci; + + function testNotIntercepted() { + testFrame("sanitize/frame.html").then(function(body) { + is(body, "FAIL", "Expected frame to not be controlled"); + // No need to unregister since that already happened. + navigator.serviceWorker.getRegistration("sanitize/foo").then(function(reg) { + ok(reg === undefined, "There should no longer be a valid registration"); + }, function(e) { + ok(false, "getRegistration() should not error"); + }).then(function(e) { + SimpleTest.finish(); + }); + }); + } + + registerSW().then(function() { + return testFrame("sanitize/frame.html").then(function(body) { + is(body, "intercepted", "Expected serviceworker to intercept request"); + }); + }).then(function() { + return navigator.serviceWorker.getRegistration("sanitize/foo"); + }).then(function(reg) { + reg.active.onstatechange = function(e) { + e.target.onstatechange = null; + is(e.target.state, "redundant", "On clearing data, serviceworker should become redundant"); + testNotIntercepted(); + }; + }).then(function() { + SpecialPowers.removeAllServiceWorkerData(); + }); + } + + function testFrame(src) { + return new Promise(function(resolve, reject) { + var iframe = document.createElement("iframe"); + iframe.src = src; + window.onmessage = function(message) { + window.onmessage = null; + iframe.src = "about:blank"; + document.body.removeChild(iframe); + iframe = null; + SpecialPowers.exactGC(function() { + resolve(message.data); + }); + }; + document.body.appendChild(iframe); + }); + } + + function registerSW() { + return testFrame("sanitize/register.html"); + } + + SimpleTest.waitForExplicitFinish(); + + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, function() { + start(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_sanitize_domain.html b/dom/serviceworkers/test/test_sanitize_domain.html new file mode 100644 index 0000000000..d0f5f7f69a --- /dev/null +++ b/dom/serviceworkers/test/test_sanitize_domain.html @@ -0,0 +1,89 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1080109 - Clear ServiceWorker registrations for specific domains</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function start() { + const Cc = SpecialPowers.Cc; + const Ci = SpecialPowers.Ci; + + function checkDomainRegistration(domain, exists) { + return testFrame("http://" + domain + "/tests/dom/serviceworkers/test/sanitize/example_check_and_unregister.html").then(function(body) { + if (body === "FAIL") { + ok(false, "Error acquiring registration or unregistering for " + domain); + } else { + if (exists) { + ok(body === true, "Expected " + domain + " to still have a registration."); + } else { + ok(body === false, "Expected " + domain + " to have no registration."); + } + } + }); + } + + registerSW().then(function() { + return testFrame("http://example.com/tests/dom/serviceworkers/test/sanitize/frame.html").then(function(body) { + is(body, "intercepted", "Expected serviceworker to intercept request"); + }); + }).then(function() { + return SpecialPowers.removeServiceWorkerDataForExampleDomain(); + }).then(function() { + return checkDomainRegistration("prefixexample.com", true /* exists */) + .then(function(e) { + return checkDomainRegistration("example.com", false /* exists */); + }).then(function(e) { + SimpleTest.finish(); + }); + }) + } + + function testFrame(src) { + return new Promise(function(resolve, reject) { + var iframe = document.createElement("iframe"); + iframe.src = src; + window.onmessage = function(message) { + window.onmessage = null; + iframe.src = "about:blank"; + document.body.removeChild(iframe); + iframe = null; + SpecialPowers.exactGC(function() { + resolve(message.data); + }); + }; + document.body.appendChild(iframe); + }); + } + + function registerSW() { + return testFrame("http://example.com/tests/dom/serviceworkers/test/sanitize/register.html") + .then(function(e) { + // Register for prefixexample.com and then ensure it does not get unregistered. + return testFrame("http://prefixexample.com/tests/dom/serviceworkers/test/sanitize/register.html"); + }); + } + + SimpleTest.waitForExplicitFinish(); + + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, function() { + start(); + }); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_scopes.html b/dom/serviceworkers/test/test_scopes.html new file mode 100644 index 0000000000..77e997766d --- /dev/null +++ b/dom/serviceworkers/test/test_scopes.html @@ -0,0 +1,143 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 984048 - Test scope glob matching.</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var scriptsAndScopes = [ + [ "worker.js", "./sub/dir/"], + [ "worker.js", "./sub/dir" ], + [ "worker.js", "./sub/dir.html" ], + [ "worker.js", "./sub/dir/a" ], + [ "worker.js", "./sub" ], + [ "worker.js", "./star*" ], // '*' has no special meaning + ]; + + function registerWorkers() { + var registerArray = []; + scriptsAndScopes.forEach(function(item) { + registerArray.push(navigator.serviceWorker.register(item[0], { scope: item[1] })); + }); + + // Check register()'s step 4 which uses script's url with "./" as the scope if no scope is passed. + // The other tests already check step 5. + registerArray.push(navigator.serviceWorker.register("scope/scope_worker.js")); + + // Check that SW cannot be registered for a scope "above" the script's location. + registerArray.push(new Promise(function(resolve, reject) { + navigator.serviceWorker.register("scope/scope_worker.js", { scope: "./" }) + .then(function() { + ok(false, "registration scope has to be inside service worker script scope."); + reject(); + }, function() { + ok(true, "registration scope has to be inside service worker script scope."); + resolve(); + }); + })); + return Promise.all(registerArray); + } + + function unregisterWorkers() { + var unregisterArray = []; + scriptsAndScopes.forEach(function(item) { + var p = navigator.serviceWorker.getRegistration(item[1]); + unregisterArray.push(p.then(function(reg) { + return reg.unregister(); + })); + }); + + unregisterArray.push(navigator.serviceWorker.getRegistration("scope/").then(function (reg) { + return reg.unregister(); + })); + + return Promise.all(unregisterArray); + } + + async function testScopes() { + function chromeScriptSource() { + /* eslint-env mozilla/chrome-script */ + + let swm = Cc["@mozilla.org/serviceworkers/manager;1"] + .getService(Ci.nsIServiceWorkerManager); + let secMan = Cc["@mozilla.org/scriptsecuritymanager;1"] + .getService(Ci.nsIScriptSecurityManager); + addMessageListener("getScope", (msg) => { + let principal = secMan.createContentPrincipalFromOrigin(msg.principal); + try { + return { scope: swm.getScopeForUrl(principal, msg.path) }; + } catch (e) { + return { exception: e.message }; + } + }); + } + + let chromeScript = SpecialPowers.loadChromeScript(chromeScriptSource); + let docPrincipal = SpecialPowers.wrap(document).nodePrincipal.spec; + + getScope = async (path) => { + let rv = await chromeScript.sendQuery("getScope", { principal: docPrincipal, path }); + if (rv.exception) + throw rv.exception; + return rv.scope; + }; + + var base = new URL(".", document.baseURI); + + function p(s) { + return base + s; + } + + async function fail(fn) { + try { + await getScope(p("index.html")); + ok(false, "No registration"); + } catch(e) { + ok(true, "No registration"); + } + } + + is(await getScope(p("sub.html")), p("sub"), "Scope should match"); + is(await getScope(p("sub/dir.html")), p("sub/dir.html"), "Scope should match"); + is(await getScope(p("sub/dir")), p("sub/dir"), "Scope should match"); + is(await getScope(p("sub/dir/foo")), p("sub/dir/"), "Scope should match"); + is(await getScope(p("sub/dir/afoo")), p("sub/dir/a"), "Scope should match"); + is(await getScope(p("star*wars")), p("star*"), "Scope should match"); + is(await getScope(p("scope/some_file.html")), p("scope/"), "Scope should match"); + await fail("index.html"); + await fail("sua.html"); + await fail("star/a.html"); + } + + function runTest() { + registerWorkers() + .then(testScopes) + .then(unregisterWorkers) + .then(function() { + SimpleTest.finish(); + }).catch(function(e) { + ok(false, "Some test failed with error " + e); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_script_loader_intercepted_js_cache.html b/dom/serviceworkers/test/test_script_loader_intercepted_js_cache.html new file mode 100644 index 0000000000..d0073705bb --- /dev/null +++ b/dom/serviceworkers/test/test_script_loader_intercepted_js_cache.html @@ -0,0 +1,224 @@ +<!DOCTYPE html> +<html> +<!-- https://bugzilla.mozilla.org/show_bug.cgi?id=1350359 --> +<!-- The JS bytecode cache is not supposed to be observable. To make it + observable, the ScriptLoader is instrumented to trigger events on the + script tag. These events are followed to reconstruct the code path taken by + the script loader and associate a simple name which is checked in these + test cases. +--> +<head> + <meta charset="utf-8"> + <title>Test for saving and loading bytecode in/from the necko cache</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="utils.js"></script> + <script type="application/javascript"> + + // This is the state machine of the trace events produced by the + // ScriptLoader. This state machine is used to give a name to each + // code path, such that we can assert each code path with a single word. + var scriptLoaderStateMachine = { + "scriptloader_load_source": { + "scriptloader_execute": { + "scriptloader_encode": { + "scriptloader_bytecode_saved": "bytecode_saved", + "scriptloader_bytecode_failed": "bytecode_failed" + }, + "scriptloader_no_encode": "source_exec" + } + }, + "scriptloader_load_bytecode": { + "scriptloader_fallback": { + // Replicate the top-level state machine without + // "scriptloader_load_bytecode" transition. + "scriptloader_load_source": { + "scriptloader_execute": { + "scriptloader_encode": { + "scriptloader_bytecode_saved": "fallback_bytecode_saved", + "scriptloader_bytecode_failed": "fallback_bytecode_failed" + }, + "scriptloader_no_encode": "fallback_source_exec" + } + } + }, + "scriptloader_execute": "bytecode_exec" + } + }; + + var gScript = SpecialPowers. + loadChromeScript('http://mochi.test:8888/tests/dom/serviceworkers/test/file_js_cache_cleanup.js'); + + function WaitForScriptTagEvent(url) { + var iframe = document.createElement("iframe"); + document.body.appendChild(iframe); + + var stateMachine = scriptLoaderStateMachine; + var stateHistory = []; + var stateMachineResolve, stateMachineReject; + var statePromise = new Promise((resolve, reject) => { + stateMachineResolve = resolve; + stateMachineReject = reject; + }); + var ping = 0; + + // Walk the script loader state machine with the emitted events. + function log_event(evt) { + // If we have multiple script tags in the loaded source, make sure + // we only watch a single one. + if (evt.target.id != "watchme") + return; + + dump("## ScriptLoader event: " + evt.type + "\n"); + stateHistory.push(evt.type) + if (typeof stateMachine == "object") + stateMachine = stateMachine[evt.type]; + if (typeof stateMachine == "string") { + // We arrived to a final state, report the name of it. + var result = stateMachine; + if (ping) { + result = `${result} & ping(=${ping})`; + } + stateMachineResolve(result); + } else if (stateMachine === undefined) { + // We followed an unknown transition, report the known history. + stateMachineReject(stateHistory); + } + } + + var iwin = iframe.contentWindow; + iwin.addEventListener("scriptloader_load_source", log_event); + iwin.addEventListener("scriptloader_load_bytecode", log_event); + iwin.addEventListener("scriptloader_generate_bytecode", log_event); + iwin.addEventListener("scriptloader_execute", log_event); + iwin.addEventListener("scriptloader_encode", log_event); + iwin.addEventListener("scriptloader_no_encode", log_event); + iwin.addEventListener("scriptloader_bytecode_saved", log_event); + iwin.addEventListener("scriptloader_bytecode_failed", log_event); + iwin.addEventListener("scriptloader_fallback", log_event); + iwin.addEventListener("ping", (evt) => { + ping += 1; + dump(`## Content event: ${evt.type} (=${ping})\n`); + }); + iframe.src = url; + + statePromise.then(() => { + document.body.removeChild(iframe); + }); + return statePromise; + } + + promise_test(async function() { + // Setting dom.expose_test_interfaces pref causes the + // nsScriptLoadRequest to fire event on script tags, with information + // about its internal state. The ScriptLoader source send events to + // trace these and resolve a promise with the path taken by the + // script loader. + // + // Setting dom.script_loader.bytecode_cache.strategy to -1 causes the + // nsScriptLoadRequest to force all the conditions necessary to make a + // script be saved as bytecode in the alternate data storage provided + // by the channel (necko cache). + await SpecialPowers.pushPrefEnv({set: [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ['dom.script_loader.bytecode_cache.enabled', true], + ['dom.expose_test_interfaces', true], + ['dom.script_loader.bytecode_cache.strategy', -1] + ]}); + + // Register the service worker that perform the pass-through fetch. + var registration = await navigator.serviceWorker + .register("fetch.js", {scope: "./"}); + let sw = registration.installing || registration.active; + + // wait for service worker be activated + await waitForState(sw, 'activated'); + + await testCheckTheJSBytecodeCache(); + await testSavebytecodeAfterTheInitializationOfThePage(); + await testDoNotSaveBytecodeOnCompilationErrors(); + + await registration.unregister(); + await teardown(); + }); + + function teardown() { + return new Promise((resolve, reject) => { + gScript.addMessageListener("teardown-complete", function teardownCompleteHandler() { + gScript.removeMessageListener("teardown-complete", teardownCompleteHandler); + gScript.destroy(); + resolve(); + }); + gScript.sendAsyncMessage("teardown"); + }); + } + + async function testCheckTheJSBytecodeCache() { + dump("## Test: Check the JS bytecode cache\n"); + + // Load the test page, and verify that the code path taken by the + // nsScriptLoadRequest corresponds to the code path which is loading a + // source and saving it as bytecode. + var stateMachineResult = WaitForScriptTagEvent("file_js_cache.html"); + assert_equals(await stateMachineResult, "bytecode_saved", + "[1] ScriptLoadRequest status after the first visit"); + + // Reload the same test page, and verify that the code path taken by + // the nsScriptLoadRequest corresponds to the code path which is + // loading bytecode and executing it. + stateMachineResult = WaitForScriptTagEvent("file_js_cache.html"); + assert_equals(await stateMachineResult, "bytecode_exec", + "[2] ScriptLoadRequest status after the second visit"); + + // Load another page which loads the same script with an SRI, while + // the cached bytecode does not have any. This should fallback to + // loading the source before saving the bytecode once more. + stateMachineResult = WaitForScriptTagEvent("file_js_cache_with_sri.html"); + assert_equals(await stateMachineResult, "fallback_bytecode_saved", + "[3] ScriptLoadRequest status after the SRI hash"); + + // Loading a page, which has the same SRI should verify the SRI and + // continue by executing the bytecode. + var stateMachineResult1 = WaitForScriptTagEvent("file_js_cache_with_sri.html"); + + // Loading a page which does not have a SRI while we have one in the + // cache should not change anything. We should also be able to load + // the cache simultanesouly. + var stateMachineResult2 = WaitForScriptTagEvent("file_js_cache.html"); + + assert_equals(await stateMachineResult1, "bytecode_exec", + "[4] ScriptLoadRequest status after same SRI hash"); + assert_equals(await stateMachineResult2, "bytecode_exec", + "[5] ScriptLoadRequest status after visit with no SRI"); + } + + async function testSavebytecodeAfterTheInitializationOfThePage() { + dump("## Test: Save bytecode after the initialization of the page"); + + // The test page add a new script which generate a "ping" event, which + // should be recorded before the bytecode is stored in the cache. + var stateMachineResult = + WaitForScriptTagEvent("file_js_cache_save_after_load.html"); + assert_equals(await stateMachineResult, "bytecode_saved & ping(=3)", + "Wait on all scripts to be executed"); + } + + async function testDoNotSaveBytecodeOnCompilationErrors() { + dump("## Test: Do not save bytecode on compilation errors"); + + // The test page loads a script which contains a syntax error, we should + // not attempt to encode any bytecode for it. + var stateMachineResult = + WaitForScriptTagEvent("file_js_cache_syntax_error.html"); + assert_equals(await stateMachineResult, "source_exec", + "Check the lack of bytecode encoding"); + } + + done(); + </script> +</head> +<body> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1350359">Mozilla Bug 1350359</a> +</body> +</html> diff --git a/dom/serviceworkers/test/test_self_update_worker.html b/dom/serviceworkers/test/test_self_update_worker.html new file mode 100644 index 0000000000..d6d4544dd9 --- /dev/null +++ b/dom/serviceworkers/test/test_self_update_worker.html @@ -0,0 +1,136 @@ +<!DOCTYPE HTML> +<html> +<!-- + Test that a self updating service worker can't keep running forever when the + script changes. + + - self_update_worker.sjs is a stateful server-side js script that returns a + SW script with a different version every time it's invoked. (version=1..n) + - The SW script will trigger an update when it reaches the activating state, + which, if not for the update delaying mechanism, would result in an iterative + cycle. + - We currently delay registration.update() calls originating from SWs not currently + controlling any clients. The delay is: 0s, 30s, 900s etc, but for the purpose of + this test, the delay is: 0s, infinite etc. + - We assert that the SW script never reaches version 3, meaning it will only + successfully update once. + - We give the worker reasonable time to self update by repeatedly registering + and unregistering an empty service worker. + --> +<head> + <title>Test for Bug 1432846</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="error_reporting_helpers.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <meta http-equiv="Content-type" content="text/html;charset=UTF-8"> +</head> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1432846">Mozilla Bug 1432846</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> +add_task(function setupPrefs() { + return SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}); +}); + +function activateDummyWorker() { + return navigator.serviceWorker.register("empty.js", + { scope: "./empty?random=" + Date.now() }) + .then(function(registration) { + var worker = registration.installing; + return waitForState(worker, 'activated', registration).then(function() { + ok(true, "got dummy!"); + return registration.unregister(); + }); + }); +} + +add_task(async function test_update() { + navigator.serviceWorker.onmessage = function(event) { + ok (event.data.version < 3, "Service worker updated too many times." + event.data.version); + } + + await SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.update_delay", 30000], + ["dom.serviceWorkers.idle_extended_timeout", 299999]]}); + + // clear version counter + await fetch("self_update_worker.sjs?clearcounter"); + + var worker; + let registration = await navigator.serviceWorker.register( + "self_update_worker.sjs", + { scope: "./test_self_update_worker.html?random=" + Date.now()}) + .then(function(reg) { + worker = reg.installing; + // We can't wait for 'activated' here, since it's possible for + // the update process to kill the worker before it activates. + // See: https://github.com/w3c/ServiceWorker/issues/1285 + return waitForState(worker, 'activating', reg); + }); + + // We need to wait a reasonable time to give the self updating worker a chance + // to change to a newer version. Register and activate an empty worker 5 times. + for (i = 0; i < 5; i++) { + await activateDummyWorker(); + } + + + await registration.unregister(); + await SpecialPowers.popPrefEnv(); + await SpecialPowers.popPrefEnv(); +}); + +// Test variant to ensure that we properly keep the timer alive by having a +// non-zero but small timer duration. In this case, the delay is simply our +// exponential growth rate of 30, so if we end up getting to version 4, that's +// okay and the test may need to be updated. +add_task(async function test_delay_update() { + let version; + navigator.serviceWorker.onmessage = function(event) { + ok (event.data.version <= 3, "Service worker updated too many times." + event.data.version); + version = event.data.version; + } + + await SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.update_delay", 1], + ["dom.serviceWorkers.idle_extended_timeout", 299999]]}); + + // clear version counter + await fetch("self_update_worker.sjs?clearcounter"); + + var worker; + let registration = await navigator.serviceWorker.register( + "self_update_worker.sjs", + { scope: "./test_self_update_worker.html?random=" + Date.now()}) + .then(function(reg) { + worker = reg.installing; + // We can't wait for 'activated' here, since it's possible for + // the update process to kill the worker before it activates. + // See: https://github.com/w3c/ServiceWorker/issues/1285 + return waitForState(worker, 'activating', reg); + }); + + // We need to wait a reasonable time to give the self updating worker a chance + // to change to a newer version. Register and activate an empty worker 5 times. + for (i = 0; i < 5; i++) { + await activateDummyWorker(); + } + + is(version, 3, "Service worker version should be 3."); + + await registration.unregister(); + await SpecialPowers.popPrefEnv(); + await SpecialPowers.popPrefEnv(); +}); +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_service_worker_allowed.html b/dom/serviceworkers/test/test_service_worker_allowed.html new file mode 100644 index 0000000000..a74379f383 --- /dev/null +++ b/dom/serviceworkers/test/test_service_worker_allowed.html @@ -0,0 +1,74 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test the Service-Worker-Allowed header</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<div id="content"></div> +<script class="testbody" type="text/javascript"> + var gTests = [ + "worker_scope_different.js", + "worker_scope_different2.js", + "worker_scope_too_deep.js", + ]; + + function testPermissiveHeader() { + // Make sure that this registration succeeds, as the prefix check should pass. + return navigator.serviceWorker.register("swa/worker_scope_too_narrow.js", {scope: "swa/"}) + .then(swr => { + ok(true, "Registration should finish successfully"); + return swr.unregister(); + }, err => { + ok(false, "Unexpected error when registering the service worker: " + err); + }); + } + + function testPreciseHeader() { + // Make sure that this registration succeeds, as the prefix check should pass + // given that we parse the use the full pathname from this URL.. + return navigator.serviceWorker.register("swa/worker_scope_precise.js", {scope: "swa/"}) + .then(swr => { + ok(true, "Registration should finish successfully"); + return swr.unregister(); + }, err => { + ok(false, "Unexpected error when registering the service worker: " + err); + }); + } + + function runTest() { + Promise.all(gTests.map(testName => { + return new Promise((resolve, reject) => { + // Make sure that registration fails. + navigator.serviceWorker.register("swa/" + testName, {scope: "swa/"}) + .then(reject, resolve); + }); + })).then(values => { + values.forEach(error => { + is(error.name, "SecurityError", "Registration should fail"); + }); + Promise.all([ + testPermissiveHeader(), + testPreciseHeader(), + ]).then(SimpleTest.finish, SimpleTest.finish); + }, (x) => { + ok(false, "Registration should not succeed, but it did"); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_serviceworker.html b/dom/serviceworkers/test/test_serviceworker.html new file mode 100644 index 0000000000..bfc5749405 --- /dev/null +++ b/dom/serviceworkers/test/test_serviceworker.html @@ -0,0 +1,79 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1137245 - Allow IndexedDB usage in ServiceWorkers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> + + var regisration; + function simpleRegister() { + return navigator.serviceWorker.register("service_worker.js", { + scope: 'service_worker_client.html' + }).then(swr => waitForState(swr.installing, 'activated', swr)); + } + + function unregister() { + return registration.unregister(); + } + + function testIndexedDBAvailable(sw) { + registration = sw; + var p = new Promise(function(resolve, reject) { + window.onmessage = function(e) { + if (e.data === "READY") { + sw.active.postMessage("GO"); + return; + } + + if (!("available" in e.data)) { + ok(false, "Something went wrong"); + reject(); + return; + } + + ok(e.data.available, "IndexedDB available in service worker."); + resolve(); + } + }); + + var content = document.getElementById("content"); + ok(content, "Parent exists."); + + iframe = document.createElement("iframe"); + iframe.setAttribute('src', "service_worker_client.html"); + content.appendChild(iframe); + + return p.then(() => content.removeChild(iframe)); + } + + function runTest() { + simpleRegister() + .then(testIndexedDBAvailable) + .then(unregister) + .then(SimpleTest.finish) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_serviceworker_header.html b/dom/serviceworkers/test/test_serviceworker_header.html new file mode 100644 index 0000000000..f607aeba3d --- /dev/null +++ b/dom/serviceworkers/test/test_serviceworker_header.html @@ -0,0 +1,41 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test that service worker scripts are fetched with a Service-Worker: script header</title> + <script type="text/javascript" src="http://mochi.test:8888/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="http://mochi.test:8888/tests/SimpleTest/test.css" /> + <base href="https://mozilla.org/"> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function runTest() { + navigator.serviceWorker.register("http://mochi.test:8888/tests/dom/serviceworkers/test/header_checker.sjs") + .then(reg => { + ok(true, "Register should succeed"); + reg.unregister().then(() => SimpleTest.finish()); + }, err => { + ok(false, "Register should not fail"); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.serviceWorkers.enabled", true], + ]}, runTest); + }; +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_serviceworker_interfaces.html b/dom/serviceworkers/test/test_serviceworker_interfaces.html new file mode 100644 index 0000000000..3b4ec19134 --- /dev/null +++ b/dom/serviceworkers/test/test_serviceworker_interfaces.html @@ -0,0 +1,116 @@ +<!-- Any copyright is dedicated to the Public Domain. + - http://creativecommons.org/publicdomain/zero/1.0/ --> +<!DOCTYPE HTML> +<html> +<head> + <title>Validate Interfaces Exposed to Service Workers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="text/javascript" src="../worker_driver.js"></script> +</head> +<body> +<script class="testbody" type="text/javascript"> + + function setupSW(registration) { + var iframe; + var worker = registration.installing || + registration.waiting || + registration.active; + window.onmessage = function(event) { + if (event.data.type == 'finish') { + iframe.remove(); + registration.unregister().then(function(success) { + ok(success, "The service worker should be unregistered successfully"); + + SimpleTest.finish(); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + SimpleTest.finish(); + }); + } else if (event.data.type == 'status') { + ok(event.data.status, event.data.msg); + + } else if (event.data.type == 'getPrefs') { + let result = {}; + event.data.prefs.forEach(function(pref) { + result[pref] = SpecialPowers.Services.prefs.getBoolPref(pref); + }); + worker.postMessage({ + type: 'returnPrefs', + prefs: event.data.prefs, + result + }); + + } else if (event.data.type == 'getHelperData') { + const { AppConstants } = SpecialPowers.ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" + ); + const isNightly = AppConstants.NIGHTLY_BUILD; + const isEarlyBetaOrEarlier = AppConstants.EARLY_BETA_OR_EARLIER; + const isRelease = AppConstants.RELEASE_OR_BETA; + const isDesktop = !/Mobile|Tablet/.test(navigator.userAgent); + const isMac = AppConstants.platform == "macosx"; + const isWindows = AppConstants.platform == "win"; + const isAndroid = AppConstants.platform == "android"; + const isLinux = AppConstants.platform == "linux"; + const isInsecureContext = !window.isSecureContext; + // Currently, MOZ_APP_NAME is always "fennec" for all mobile builds, so we can't use AppConstants for this + const isFennec = isAndroid && SpecialPowers.Cc["@mozilla.org/android/bridge;1"].getService(SpecialPowers.Ci.nsIAndroidBridge).isFennec; + + const result = { + isNightly, isEarlyBetaOrEarlier, isRelease, isDesktop, isMac, + isWindows, isAndroid, isLinux, isInsecureContext, isFennec + }; + + worker.postMessage({ + type: 'returnHelperData', result + }); + } + } + + worker.onerror = function(event) { + ok(false, 'Worker had an error: ' + event.data); + SimpleTest.finish(); + }; + + iframe = document.createElement("iframe"); + iframe.src = "message_receiver.html"; + iframe.onload = function() { + worker.postMessage({ script: "test_serviceworker_interfaces.js" }); + }; + document.body.appendChild(iframe); + } + + function runTest() { + navigator.serviceWorker.register("serviceworker_wrapper.js", {scope: "."}) + .then(setupSW); + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + // The handling of "dom.caches.enabled" here is a bit complicated. What we + // want to happen is that Cache is always enabled in service workers. So + // if service workers are disabled by default we want to force on both + // service workers and "dom.caches.enabled". But if service workers are + // enabled by default, we do not want to mess with the "dom.caches.enabled" + // value, since that would defeat the purpose of the test. Use a subframe + // to decide whether service workers are enabled by default, so we don't + // force creation of our own Navigator object before our prefs are set. + var prefs = [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]; + + var subframe = document.createElement("iframe"); + document.body.appendChild(subframe); + if (!("serviceWorker" in subframe.contentWindow.navigator)) { + prefs.push(["dom.caches.enabled", true]); + } + subframe.remove(); + + SpecialPowers.pushPrefEnv({"set": prefs}, runTest); + }; +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_serviceworker_interfaces.js b/dom/serviceworkers/test/test_serviceworker_interfaces.js new file mode 100644 index 0000000000..379b3faec9 --- /dev/null +++ b/dom/serviceworkers/test/test_serviceworker_interfaces.js @@ -0,0 +1,549 @@ +// This is a list of all interfaces that are exposed to workers. +// Please only add things to this list with great care and proper review +// from the associated module peers. + +// This file lists global interfaces we want exposed and verifies they +// are what we intend. Each entry in the arrays below can either be a +// simple string with the interface name, or an object with a 'name' +// property giving the interface name as a string, and additional +// properties which qualify the exposure of that interface. For example: +// +// [ +// "AGlobalInterface", +// { name: "ExperimentalThing", release: false }, +// { name: "ReallyExperimentalThing", nightly: true }, +// { name: "DesktopOnlyThing", desktop: true }, +// { name: "FancyControl", xbl: true }, +// { name: "DisabledEverywhere", disabled: true }, +// ]; +// +// See createInterfaceMap() below for a complete list of properties. + +// IMPORTANT: Do not change this list without review from +// a JavaScript Engine peer! +let wasmGlobalEntry = { + name: "WebAssembly", + insecureContext: true, + disabled: !getJSTestingFunctions().wasmIsSupportedByHardware(), +}; +let wasmGlobalInterfaces = [ + { name: "Module", insecureContext: true }, + { name: "Instance", insecureContext: true }, + { name: "Memory", insecureContext: true }, + { name: "Table", insecureContext: true }, + { name: "Global", insecureContext: true }, + { name: "CompileError", insecureContext: true }, + { name: "LinkError", insecureContext: true }, + { name: "RuntimeError", insecureContext: true }, + { name: "Function", insecureContext: true, nightly: true }, + { name: "Exception", insecureContext: true }, + { name: "Tag", insecureContext: true }, + { name: "compile", insecureContext: true }, + { name: "compileStreaming", insecureContext: true }, + { name: "instantiate", insecureContext: true }, + { name: "instantiateStreaming", insecureContext: true }, + { name: "validate", insecureContext: true }, +]; +// IMPORTANT: Do not change this list without review from +// a JavaScript Engine peer! +let ecmaGlobals = [ + "AggregateError", + "Array", + "ArrayBuffer", + "Atomics", + "Boolean", + "BigInt", + "BigInt64Array", + "BigUint64Array", + "DataView", + "Date", + "Error", + "EvalError", + "FinalizationRegistry", + "Float32Array", + "Float64Array", + "Function", + "Infinity", + "Int16Array", + "Int32Array", + "Int8Array", + "InternalError", + "Intl", + "JSON", + "Map", + "Math", + "NaN", + "Number", + "Object", + "Promise", + "Proxy", + "RangeError", + "ReferenceError", + "Reflect", + "RegExp", + "Set", + { + name: "SharedArrayBuffer", + crossOriginIsolated: true, + }, + "String", + "Symbol", + "SyntaxError", + "TypeError", + "Uint16Array", + "Uint32Array", + "Uint8Array", + "Uint8ClampedArray", + "URIError", + "WeakMap", + "WeakRef", + "WeakSet", + wasmGlobalEntry, + "decodeURI", + "decodeURIComponent", + "encodeURI", + "encodeURIComponent", + "escape", + "eval", + "globalThis", + "isFinite", + "isNaN", + "parseFloat", + "parseInt", + "undefined", + "unescape", +]; +// IMPORTANT: Do not change the list above without review from +// a JavaScript Engine peer! + +// IMPORTANT: Do not change the list below without review from a DOM peer! +let interfaceNamesInGlobalScope = [ + // IMPORTANT: Do not change this list without review from a DOM peer! + "AbortController", + // IMPORTANT: Do not change this list without review from a DOM peer! + "AbortSignal", + // IMPORTANT: Do not change this list without review from a DOM peer! + "Blob", + // IMPORTANT: Do not change this list without review from a DOM peer! + "BroadcastChannel", + // IMPORTANT: Do not change this list without review from a DOM peer! + "ByteLengthQueuingStrategy", + // IMPORTANT: Do not change this list without review from a DOM peer! + "Cache", + // IMPORTANT: Do not change this list without review from a DOM peer! + "CacheStorage", + // IMPORTANT: Do not change this list without review from a DOM peer! + "CanvasGradient", + // IMPORTANT: Do not change this list without review from a DOM peer! + "CanvasPattern", + // IMPORTANT: Do not change this list without review from a DOM peer! + "Client", + // IMPORTANT: Do not change this list without review from a DOM peer! + "Clients", + // IMPORTANT: Do not change this list without review from a DOM peer! + "CloseEvent", + // IMPORTANT: Do not change this list without review from a DOM peer! + "CountQueuingStrategy", + // IMPORTANT: Do not change this list without review from a DOM peer! + "Crypto", + // IMPORTANT: Do not change this list without review from a DOM peer! + "CustomEvent", + // IMPORTANT: Do not change this list without review from a DOM peer! + "Directory", + // IMPORTANT: Do not change this list without review from a DOM peer! + "DOMException", + // IMPORTANT: Do not change this list without review from a DOM peer! + "DOMMatrix", + // IMPORTANT: Do not change this list without review from a DOM peer! + "DOMMatrixReadOnly", + // IMPORTANT: Do not change this list without review from a DOM peer! + "DOMPoint", + // IMPORTANT: Do not change this list without review from a DOM peer! + "DOMPointReadOnly", + // IMPORTANT: Do not change this list without review from a DOM peer! + "DOMQuad", + // IMPORTANT: Do not change this list without review from a DOM peer! + "DOMRect", + // IMPORTANT: Do not change this list without review from a DOM peer! + "DOMRectReadOnly", + // IMPORTANT: Do not change this list without review from a DOM peer! + "DOMRequest", + // IMPORTANT: Do not change this list without review from a DOM peer! + "DOMStringList", + // IMPORTANT: Do not change this list without review from a DOM peer! + "ErrorEvent", + // IMPORTANT: Do not change this list without review from a DOM peer! + "Event", + // IMPORTANT: Do not change this list without review from a DOM peer! + "EventTarget", + // IMPORTANT: Do not change this list without review from a DOM peer! + "ExtendableEvent", + // IMPORTANT: Do not change this list without review from a DOM peer! + "ExtendableMessageEvent", + // IMPORTANT: Do not change this list without review from a DOM peer! + "FetchEvent", + // IMPORTANT: Do not change this list without review from a DOM peer! + "File", + // IMPORTANT: Do not change this list without review from a DOM peer! + "FileList", + // IMPORTANT: Do not change this list without review from a DOM peer! + "FileReader", + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "FileSystemDirectoryHandle", nightly: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "FileSystemFileHandle", nightly: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "FileSystemHandle", nightly: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "FileSystemWritableFileStream", nightly: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + "FontFace", + // IMPORTANT: Do not change this list without review from a DOM peer! + "FontFaceSet", + // IMPORTANT: Do not change this list without review from a DOM peer! + "FontFaceSetLoadEvent", + // IMPORTANT: Do not change this list without review from a DOM peer! + "FormData", + // IMPORTANT: Do not change this list without review from a DOM peer! + "Headers", + // IMPORTANT: Do not change this list without review from a DOM peer! + "IDBCursor", + // IMPORTANT: Do not change this list without review from a DOM peer! + "IDBCursorWithValue", + // IMPORTANT: Do not change this list without review from a DOM peer! + "IDBDatabase", + // IMPORTANT: Do not change this list without review from a DOM peer! + "IDBFactory", + // IMPORTANT: Do not change this list without review from a DOM peer! + "IDBIndex", + // IMPORTANT: Do not change this list without review from a DOM peer! + "IDBKeyRange", + // IMPORTANT: Do not change this list without review from a DOM peer! + "IDBObjectStore", + // IMPORTANT: Do not change this list without review from a DOM peer! + "IDBOpenDBRequest", + // IMPORTANT: Do not change this list without review from a DOM peer! + "IDBRequest", + // IMPORTANT: Do not change this list without review from a DOM peer! + "IDBTransaction", + // IMPORTANT: Do not change this list without review from a DOM peer! + "IDBVersionChangeEvent", + // IMPORTANT: Do not change this list without review from a DOM peer! + "ImageBitmap", + // IMPORTANT: Do not change this list without review from a DOM peer! + "ImageBitmapRenderingContext", + // IMPORTANT: Do not change this list without review from a DOM peer! + "ImageData", + // IMPORTANT: Do not change this list without review from a DOM peer! + "Lock", + // IMPORTANT: Do not change this list without review from a DOM peer! + "LockManager", + // IMPORTANT: Do not change this list without review from a DOM peer! + "MediaCapabilities", + // IMPORTANT: Do not change this list without review from a DOM peer! + "MediaCapabilitiesInfo", + // IMPORTANT: Do not change this list without review from a DOM peer! + "MessageChannel", + // IMPORTANT: Do not change this list without review from a DOM peer! + "MessageEvent", + // IMPORTANT: Do not change this list without review from a DOM peer! + "MessagePort", + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "NetworkInformation", disabled: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + "NavigationPreloadManager", + // IMPORTANT: Do not change this list without review from a DOM peer! + "Notification", + // IMPORTANT: Do not change this list without review from a DOM peer! + "NotificationEvent", + // IMPORTANT: Do not change this list without review from a DOM peer! + "OffscreenCanvas", + // IMPORTANT: Do not change this list without review from a DOM peer! + "OffscreenCanvasRenderingContext2D", + // IMPORTANT: Do not change this list without review from a DOM peer! + "Path2D", + // IMPORTANT: Do not change this list without review from a DOM peer! + "Performance", + // IMPORTANT: Do not change this list without review from a DOM peer! + "PerformanceEntry", + // IMPORTANT: Do not change this list without review from a DOM peer! + "PerformanceMark", + // IMPORTANT: Do not change this list without review from a DOM peer! + "PerformanceMeasure", + // IMPORTANT: Do not change this list without review from a DOM peer! + "PerformanceObserver", + // IMPORTANT: Do not change this list without review from a DOM peer! + "PerformanceObserverEntryList", + // IMPORTANT: Do not change this list without review from a DOM peer! + "PerformanceResourceTiming", + // IMPORTANT: Do not change this list without review from a DOM peer! + "PerformanceServerTiming", + // IMPORTANT: Do not change this list without review from a DOM peer! + "ProgressEvent", + // IMPORTANT: Do not change this list without review from a DOM peer! + "PromiseRejectionEvent", + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "PushEvent" }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "PushManager" }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "PushMessageData" }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "PushSubscription" }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "PushSubscriptionOptions" }, + // IMPORTANT: Do not change this list without review from a DOM peer! + "ReadableByteStreamController", + // IMPORTANT: Do not change this list without review from a DOM peer! + "ReadableStream", + // IMPORTANT: Do not change this list without review from a DOM peer! + "ReadableStreamBYOBReader", + // IMPORTANT: Do not change this list without review from a DOM peer! + "ReadableStreamBYOBRequest", + // IMPORTANT: Do not change this list without review from a DOM peer! + "ReadableStreamDefaultController", + // IMPORTANT: Do not change this list without review from a DOM peer! + "ReadableStreamDefaultReader", + // IMPORTANT: Do not change this list without review from a DOM peer! + "Request", + // IMPORTANT: Do not change this list without review from a DOM peer! + "Response", + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "Scheduler", nightly: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + "ServiceWorker", + // IMPORTANT: Do not change this list without review from a DOM peer! + "ServiceWorkerGlobalScope", + // IMPORTANT: Do not change this list without review from a DOM peer! + "ServiceWorkerRegistration", + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "StorageManager", fennec: false }, + // IMPORTANT: Do not change this list without review from a DOM peer! + "SubtleCrypto", + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "TaskController", nightly: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "TaskPriorityChangeEvent", nightly: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + { name: "TaskSignal", nightly: true }, + // IMPORTANT: Do not change this list without review from a DOM peer! + "TextDecoder", + // IMPORTANT: Do not change this list without review from a DOM peer! + "TextDecoderStream", + // IMPORTANT: Do not change this list without review from a DOM peer! + "TextEncoder", + // IMPORTANT: Do not change this list without review from a DOM peer! + "TextEncoderStream", + // IMPORTANT: Do not change this list without review from a DOM peer! + "TextMetrics", + // IMPORTANT: Do not change this list without review from a DOM peer! + "TransformStream", + // IMPORTANT: Do not change this list without review from a DOM peer! + "TransformStreamDefaultController", + // IMPORTANT: Do not change this list without review from a DOM peer! + "URL", + // IMPORTANT: Do not change this list without review from a DOM peer! + "URLSearchParams", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WebSocket", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WebGL2RenderingContext", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WebGLActiveInfo", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WebGLBuffer", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WebGLContextEvent", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WebGLFramebuffer", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WebGLProgram", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WebGLQuery", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WebGLRenderbuffer", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WebGLRenderingContext", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WebGLSampler", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WebGLShader", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WebGLShaderPrecisionFormat", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WebGLSync", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WebGLTexture", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WebGLTransformFeedback", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WebGLUniformLocation", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WebGLVertexArrayObject", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WindowClient", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WorkerGlobalScope", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WorkerLocation", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WorkerNavigator", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WritableStream", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WritableStreamDefaultController", + // IMPORTANT: Do not change this list without review from a DOM peer! + "WritableStreamDefaultWriter", + // IMPORTANT: Do not change this list without review from a DOM peer! + "clients", + // IMPORTANT: Do not change this list without review from a DOM peer! + "console", + // IMPORTANT: Do not change this list without review from a DOM peer! + "onactivate", + // IMPORTANT: Do not change this list without review from a DOM peer! + "onfetch", + // IMPORTANT: Do not change this list without review from a DOM peer! + "oninstall", + // IMPORTANT: Do not change this list without review from a DOM peer! + "onmessage", + // IMPORTANT: Do not change this list without review from a DOM peer! + "onmessageerror", + // IMPORTANT: Do not change this list without review from a DOM peer! + "onnotificationclick", + // IMPORTANT: Do not change this list without review from a DOM peer! + "onnotificationclose", + // IMPORTANT: Do not change this list without review from a DOM peer! + "onpush", + // IMPORTANT: Do not change this list without review from a DOM peer! + "onpushsubscriptionchange", + // IMPORTANT: Do not change this list without review from a DOM peer! + "registration", + // IMPORTANT: Do not change this list without review from a DOM peer! + "skipWaiting", + // IMPORTANT: Do not change this list without review from a DOM peer! +]; +// IMPORTANT: Do not change the list above without review from a DOM peer! + +// List of functions defined on the global by the test harness or this test +// file. +let testFunctions = [ + "ok", + "is", + "workerTestArrayEquals", + "workerTestDone", + "workerTestGetHelperData", + "workerTestGetStorageManager", + "entryDisabled", + "createInterfaceMap", + "runTest", +]; + +function entryDisabled( + entry, + { + isNightly, + isEarlyBetaOrEarlier, + isRelease, + isDesktop, + isAndroid, + isInsecureContext, + isFennec, + isCrossOriginIsolated, + } +) { + return ( + entry.nightly === !isNightly || + (entry.nightlyAndroid === !(isAndroid && isNightly) && isAndroid) || + (entry.nonReleaseAndroid === !(isAndroid && !isRelease) && isAndroid) || + entry.desktop === !isDesktop || + (entry.android === !isAndroid && + !entry.nonReleaseAndroid && + !entry.nightlyAndroid) || + entry.fennecOrDesktop === (isAndroid && !isFennec) || + entry.fennec === !isFennec || + entry.release === !isRelease || + entry.earlyBetaOrEarlier === !isEarlyBetaOrEarlier || + entry.crossOriginIsolated === !isCrossOriginIsolated || + entry.disabled + ); +} + +function createInterfaceMap(data, ...interfaceGroups) { + var interfaceMap = {}; + + function addInterfaces(interfaces) { + for (var entry of interfaces) { + if (typeof entry === "string") { + ok(!(entry in interfaceMap), "duplicate entry for " + entry); + interfaceMap[entry] = true; + } else { + ok(!(entry.name in interfaceMap), "duplicate entry for " + entry.name); + ok(!("pref" in entry), "Bogus pref annotation for " + entry.name); + if (entryDisabled(entry, data)) { + interfaceMap[entry.name] = false; + } else if (entry.optional) { + interfaceMap[entry.name] = "optional"; + } else { + interfaceMap[entry.name] = true; + } + } + } + } + + for (let interfaceGroup of interfaceGroups) { + addInterfaces(interfaceGroup); + } + + return interfaceMap; +} + +function runTest(parentName, parent, data, ...interfaceGroups) { + var interfaceMap = createInterfaceMap(data, ...interfaceGroups); + for (var name of Object.getOwnPropertyNames(parent)) { + // Ignore functions on the global that are part of the test (harness). + if (parent === self && testFunctions.includes(name)) { + continue; + } + ok( + interfaceMap[name] === "optional" || interfaceMap[name], + "If this is failing: DANGER, are you sure you want to expose the new interface " + + name + + " to all webpages as a property on " + + parentName + + "? Do not make a change to this file without a " + + " review from a DOM peer for that specific change!!! (or a JS peer for changes to ecmaGlobals)" + ); + delete interfaceMap[name]; + } + for (var name of Object.keys(interfaceMap)) { + if (interfaceMap[name] === "optional") { + delete interfaceMap[name]; + } else { + ok( + name in parent === interfaceMap[name], + name + + " should " + + (interfaceMap[name] ? "" : " NOT") + + " be defined on " + + parentName + ); + if (!interfaceMap[name]) { + delete interfaceMap[name]; + } + } + } + is( + Object.keys(interfaceMap).length, + 0, + "The following interface(s) are not enumerated: " + + Object.keys(interfaceMap).join(", ") + ); +} + +workerTestGetHelperData(function(data) { + runTest("self", self, data, ecmaGlobals, interfaceNamesInGlobalScope); + if (WebAssembly && !entryDisabled(wasmGlobalEntry, data)) { + runTest("WebAssembly", WebAssembly, data, wasmGlobalInterfaces); + } + workerTestDone(); +}); diff --git a/dom/serviceworkers/test/test_serviceworker_not_sharedworker.html b/dom/serviceworkers/test/test_serviceworker_not_sharedworker.html new file mode 100644 index 0000000000..33b4428e95 --- /dev/null +++ b/dom/serviceworkers/test/test_serviceworker_not_sharedworker.html @@ -0,0 +1,66 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1141274 - test that service workers and shared workers are separate</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<iframe></iframe> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + var iframe; + const SCOPE = "http://mochi.test:8888/tests/dom/serviceworkers/test/"; + function runTest() { + navigator.serviceWorker.ready.then(setupSW); + navigator.serviceWorker.register("serviceworker_not_sharedworker.js", + {scope: SCOPE}); + } + + var sw, worker; + function setupSW(registration) { + sw = registration.waiting || registration.active; + worker = new SharedWorker("serviceworker_not_sharedworker.js", SCOPE); + worker.port.start(); + iframe = document.querySelector("iframe"); + iframe.src = "message_receiver.html"; + iframe.onload = function() { + window.onmessage = function(e) { + is(e.data.result, "serviceworker", "We should be talking to a service worker"); + window.onmessage = null; + worker.port.onmessage = function(msg) { + is(msg.data.result, "sharedworker", "We should be talking to a shared worker"); + registration.unregister().then(function(success) { + ok(success, "unregister should succeed"); + SimpleTest.finish(); + }, function(ex) { + dump("Unregistering the SW failed with " + ex + "\n"); + SimpleTest.finish(); + }); + }; + worker.port.postMessage({msg: "whoareyou"}); + }; + sw.postMessage({msg: "whoareyou"}); + }; + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_serviceworkerinfo.xhtml b/dom/serviceworkers/test/test_serviceworkerinfo.xhtml new file mode 100644 index 0000000000..a3b0fe8e53 --- /dev/null +++ b/dom/serviceworkers/test/test_serviceworkerinfo.xhtml @@ -0,0 +1,114 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="Test for ServiceWorkerInfo" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="test();"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" src="chrome_helpers.js"/> + <script type="application/javascript"> + <![CDATA[ + + let IFRAME_URL = EXAMPLE_URL + "serviceworkerinfo_iframe.html"; + + function wait_for_active_worker(registration) { + ok(registration, "Registration is valid."); + return new Promise(function(res, rej) { + if (registration.activeWorker) { + res(registration); + return; + } + let listener = { + onChange: function() { + if (registration.activeWorker) { + registration.removeListener(listener); + res(registration); + } + } + } + registration.addListener(listener); + }); + } + + function test() { + SimpleTest.waitForExplicitFinish(); + + SpecialPowers.pushPrefEnv({'set': [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.idle_extended_timeout", 1000000], + ["dom.serviceWorkers.idle_timeout", 0], + ["dom.serviceWorkers.testing.enabled", true], + ]}, function () { + (async function() { + let iframe = $("iframe"); + let promise = new Promise(function (resolve) { + iframe.onload = function () { + resolve(); + }; + }); + iframe.src = IFRAME_URL; + await promise; + + info("Check that a service worker eventually shuts down."); + promise = Promise.all([ + waitForRegister(EXAMPLE_URL), + waitForServiceWorkerShutdown() + ]); + iframe.contentWindow.postMessage("register", "*"); + let [registration] = await promise; + + // Make sure the worker is active. + registration = await wait_for_active_worker(registration); + + let activeWorker = registration.activeWorker; + ok(activeWorker !== null, "Worker is not active!"); + ok(activeWorker.debugger === null); + + info("Attach a debugger to the service worker, and check that the " + + "service worker is restarted."); + activeWorker.attachDebugger(); + let workerDebugger = activeWorker.debugger; + ok(workerDebugger !== null); + + // Verify debugger properties + ok(workerDebugger.principal instanceof Ci.nsIPrincipal); + is(workerDebugger.url, EXAMPLE_URL + "worker.js"); + + info("Verify that getRegistrationByPrincipal return the same " + + "nsIServiceWorkerRegistrationInfo"); + let reg = swm.getRegistrationByPrincipal(workerDebugger.principal, + workerDebugger.url); + is(reg, registration); + + info("Check that getWorkerByID returns the same nsIWorkerDebugger"); + is(activeWorker, reg.getWorkerByID(workerDebugger.serviceWorkerID)); + + info("Detach the debugger from the service worker, and check that " + + "the service worker eventually shuts down again."); + promise = waitForServiceWorkerShutdown(); + activeWorker.detachDebugger(); + await promise; + ok(activeWorker.debugger === null); + + promise = waitForUnregister(EXAMPLE_URL); + iframe.contentWindow.postMessage("unregister", "*"); + registration = await promise; + + SimpleTest.finish(); + })(); + }); + } + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + <iframe id="iframe"></iframe> + </body> + <label id="test-result"/> +</window> diff --git a/dom/serviceworkers/test/test_serviceworkermanager.xhtml b/dom/serviceworkers/test/test_serviceworkermanager.xhtml new file mode 100644 index 0000000000..b118b7d3f1 --- /dev/null +++ b/dom/serviceworkers/test/test_serviceworkermanager.xhtml @@ -0,0 +1,79 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="Test for ServiceWorkerManager" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="test();"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" src="chrome_helpers.js"/> + <script type="application/javascript"> + <![CDATA[ + + let IFRAME_URL = EXAMPLE_URL + "serviceworkermanager_iframe.html"; + + function test() { + SimpleTest.waitForExplicitFinish(); + + SpecialPowers.pushPrefEnv({'set': [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, function () { + (async function() { + let registrations = swm.getAllRegistrations(); + is(registrations.length, 0); + + let iframe = $("iframe"); + let promise = waitForIframeLoad(iframe); + iframe.src = IFRAME_URL; + await promise; + + info("Check that the service worker manager notifies its listeners " + + "when a service worker is registered."); + promise = waitForRegister(EXAMPLE_URL); + iframe.contentWindow.postMessage("register", "*"); + let registration = await promise; + + registrations = swm.getAllRegistrations(); + is(registrations.length, 1); + is(registrations.queryElementAt(0, Ci.nsIServiceWorkerRegistrationInfo), + registration); + + info("Check that the service worker manager does not notify its " + + "listeners when a service worker is registered with the same " + + "scope as an existing registration."); + let listener = { + onRegister: function () { + ok(false, "Listener should not have been notified."); + } + }; + swm.addListener(listener); + iframe.contentWindow.postMessage("register", "*"); + + info("Check that the service worker manager notifies its listeners " + + "when a service worker is unregistered."); + promise = waitForUnregister(EXAMPLE_URL); + iframe.contentWindow.postMessage("unregister", "*"); + registration = await promise; + swm.removeListener(listener); + + registrations = swm.getAllRegistrations(); + is(registrations.length, 0); + + SimpleTest.finish(); + })(); + }); + } + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + <iframe id="iframe"></iframe> + </body> + <label id="test-result"/> +</window> diff --git a/dom/serviceworkers/test/test_serviceworkerregistrationinfo.xhtml b/dom/serviceworkers/test/test_serviceworkerregistrationinfo.xhtml new file mode 100644 index 0000000000..5b39350897 --- /dev/null +++ b/dom/serviceworkers/test/test_serviceworkerregistrationinfo.xhtml @@ -0,0 +1,155 @@ +<?xml version="1.0"?> +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<window title="Test for ServiceWorkerRegistrationInfo" + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + onload="test();"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + <script type="application/javascript" src="chrome_helpers.js"/> + <script type="application/javascript"> + <![CDATA[ + + let IFRAME_URL = EXAMPLE_URL + "serviceworkerregistrationinfo_iframe.html"; + + function test() { + SimpleTest.waitForExplicitFinish(); + + SpecialPowers.pushPrefEnv({'set': [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, function () { + (async function() { + let iframe = $("iframe"); + let promise = waitForIframeLoad(iframe); + iframe.src = IFRAME_URL; + await promise; + + // The change handler is not guaranteed to be called within the same + // tick of the event loop as the one in which the change happened. + // Because of this, the exact state of the service worker registration + // is only known until the handler returns. + // + // Because then-handlers are resolved asynchronously, the following + // checks are done using callbacks, which are called synchronously + // when then handler is called. These callbacks can return a promise, + // which is used to resolve the promise returned by the function. + + info("Check that a service worker registration notifies its " + + "listeners when its state changes."); + promise = waitForRegister(EXAMPLE_URL, function (registration) { + is(registration.scriptSpec, ""); + ok(registration.installingWorker === null); + ok(registration.waitingWorker === null); + ok(registration.activeWorker === null); + + return waitForServiceWorkerRegistrationChange(registration, function () { + // Got change event for updating (byte-check) + ok(registration.installingWorker === null); + ok(registration.waitingWorker === null); + ok(registration.activeWorker === null); + + return waitForServiceWorkerRegistrationChange(registration, function () { + is(registration.scriptSpec, EXAMPLE_URL + "worker.js"); + ok(registration.evaluatingWorker !== null); + ok(registration.installingWorker === null); + ok(registration.waitingWorker === null); + ok(registration.activeWorker === null); + + return waitForServiceWorkerRegistrationChange(registration, function () { + ok(registration.installingWorker !== null); + is(registration.installingWorker.scriptSpec, EXAMPLE_URL + "worker.js"); + ok(registration.waitingWorker === null); + ok(registration.activeWorker === null); + + return waitForServiceWorkerRegistrationChange(registration, function () { + ok(registration.installingWorker === null); + ok(registration.waitingWorker !== null); + ok(registration.activeWorker === null); + + return waitForServiceWorkerRegistrationChange(registration, function () { + // Activating + ok(registration.installingWorker === null); + ok(registration.waitingWorker === null); + ok(registration.activeWorker !== null); + + return waitForServiceWorkerRegistrationChange(registration, function () { + // Activated + ok(registration.installingWorker === null); + ok(registration.waitingWorker === null); + ok(registration.activeWorker !== null); + + return registration; + }); + }); + }); + }); + }); + }); + }); + iframe.contentWindow.postMessage("register", "*"); + let registration = await promise; + + promise = waitForServiceWorkerRegistrationChange(registration, function () { + // Got change event for updating (byte-check) + ok(registration.installingWorker === null); + ok(registration.waitingWorker === null); + ok(registration.activeWorker !== null); + + return waitForServiceWorkerRegistrationChange(registration, function () { + is(registration.scriptSpec, EXAMPLE_URL + "worker2.js"); + ok(registration.evaluatingWorker !== null); + + return waitForServiceWorkerRegistrationChange(registration, function () { + ok(registration.installingWorker !== null); + is(registration.installingWorker.scriptSpec, EXAMPLE_URL + "worker2.js"); + ok(registration.waitingWorker === null); + ok(registration.activeWorker !== null); + + return waitForServiceWorkerRegistrationChange(registration, function () { + ok(registration.installingWorker === null); + ok(registration.waitingWorker !== null); + ok(registration.activeWorker !== null); + + return waitForServiceWorkerRegistrationChange(registration, function () { + // Activating + ok(registration.installingWorker === null); + ok(registration.waitingWorker === null); + ok(registration.activeWorker !== null); + + return waitForServiceWorkerRegistrationChange(registration, function () { + // Activated + ok(registration.installingWorker === null); + ok(registration.waitingWorker === null); + ok(registration.activeWorker !== null); + + return registration; + }); + }); + }); + }); + }); + }); + iframe.contentWindow.postMessage("register", "*"); + await promise; + + iframe.contentWindow.postMessage("unregister", "*"); + await waitForUnregister(EXAMPLE_URL); + + SimpleTest.finish(); + })(); + }); + } + + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display:none;"></div> + <pre id="test"></pre> + <iframe id="iframe"></iframe> + </body> + <label id="test-result"/> +</window> diff --git a/dom/serviceworkers/test/test_skip_waiting.html b/dom/serviceworkers/test/test_skip_waiting.html new file mode 100644 index 0000000000..6147ad6b38 --- /dev/null +++ b/dom/serviceworkers/test/test_skip_waiting.html @@ -0,0 +1,86 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1131352 - Add ServiceWorkerGlobalScope skipWaiting()</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> + var registration, iframe, content; + + function start() { + return navigator.serviceWorker.register("worker.js", + {scope: "./skip_waiting_scope/"}); + } + + async function waitForActivated(swr) { + registration = swr; + await waitForState(registration.installing, "activated") + + iframe = document.createElement("iframe"); + iframe.setAttribute("src", "skip_waiting_scope/index.html"); + + content = document.getElementById("content"); + content.appendChild(iframe); + + await new Promise(resolve => iframe.onload = resolve); + } + + function checkWhetherItSkippedWaiting() { + var promise = new Promise(function(resolve, reject) { + window.onmessage = function (evt) { + if (evt.data.event === "controllerchange") { + ok(evt.data.controllerScriptURL.match("skip_waiting_installed_worker"), + "The controller changed after skiping the waiting step"); + resolve(); + } else { + ok(false, "Wrong value. Somenting went wrong"); + resolve(); + } + }; + }); + + navigator.serviceWorker.register("skip_waiting_installed_worker.js", + {scope: "./skip_waiting_scope/"}) + .then(swr => { + registration = swr; + }); + + return promise; + } + + function clean() { + content.removeChild(iframe); + + return registration.unregister(); + } + + function runTest() { + start() + .then(waitForActivated) + .then(checkWhetherItSkippedWaiting) + .then(clean) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_streamfilter.html b/dom/serviceworkers/test/test_streamfilter.html new file mode 100644 index 0000000000..7367fb8b84 --- /dev/null +++ b/dom/serviceworkers/test/test_streamfilter.html @@ -0,0 +1,207 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title> + Test StreamFilter-monitored responses for ServiceWorker-intercepted requests + </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="/tests/SimpleTest/ExtensionTestUtils.js"></script> + <link rel="stylesheet" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<script> +// eslint-disable-next-line mozilla/no-addtask-setup +add_task(async function setup() { + SimpleTest.waitForExplicitFinish(); + + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ], + }); + + const registration = await navigator.serviceWorker.register( + "streamfilter_worker.js" + ); + + SimpleTest.registerCleanupFunction(async function unregisterRegistration() { + await registration.unregister(); + }); + + await new Promise(resolve => { + const serviceWorker = registration.installing; + + serviceWorker.onstatechange = () => { + if (serviceWorker.state == "activated") { + resolve(); + } + }; + }); + + ok(navigator.serviceWorker.controller, "Page is controlled"); +}); + +async function getExtension() { + let extension = ExtensionTestUtils.loadExtension({ + manifest: { + permissions: ["webRequest", "webRequestBlocking", "<all_urls>"], + }, + + // This WebExtension only proxies a response's data through a StreamFilter; + // it doesn't modify the data itself in any way. + background() { + class FilterWrapper { + constructor(requestId) { + const filter = browser.webRequest.filterResponseData(requestId); + const arrayBuffers = []; + + filter.onstart = () => { + browser.test.sendMessage("start"); + }; + + filter.ondata = ({ data }) => { + arrayBuffers.push(data); + }; + + filter.onstop = () => { + browser.test.sendMessage("stop"); + new Blob(arrayBuffers).arrayBuffer().then(buffer => { + filter.write(buffer); + filter.close(); + }); + }; + + filter.onerror = () => { + // We only ever expect a redirect error here. + browser.test.assertEq(filter.error, "ServiceWorker fallback redirection"); + browser.test.sendMessage("error"); + }; + } + } + + browser.webRequest.onBeforeRequest.addListener( + details => { + new FilterWrapper(details.requestId); + }, + { + urls: ["<all_urls>"], + types: ["xmlhttprequest"], + }, + ["blocking"] + ); + }, + }); + + await extension.startup(); + return extension; +} + +const streamFilterServerUrl = `${location.origin}/tests/dom/serviceworkers/test/streamfilter_server.sjs`; + +const requestUrlForServerQueryString = "syntheticResponse=0"; + +// streamfilter_server.sjs is expected to respond to a request to this URL. +const requestUrlForServer = `${streamFilterServerUrl}?${requestUrlForServerQueryString}`; + +const requestUrlForServiceWorkerQueryString = "syntheticResponse=1"; + +// streamfilter_worker.js is expected to respond to a request to this URL. +const requestUrlForServiceWorker = `${streamFilterServerUrl}?${requestUrlForServiceWorkerQueryString}`; + +// startNetworkerRequestFn must be a function that, when called, starts a +// network request and returns a promise that resolves after the request +// completes (or fails). This function will return the value that that promise +// resolves with (or throw if it rejects). +async function observeFilteredNetworkRequest(startNetworkRequestFn, promises) { + const networkRequestPromise = startNetworkRequestFn(); + await Promise.all(promises); + return networkRequestPromise; +} + +// Returns a promise that resolves with the XHR's response text. +function callXHR(requestUrl, promises) { + return observeFilteredNetworkRequest(() => { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.onload = () => { + resolve(xhr.responseText); + }; + xhr.onerror = reject; + xhr.open("GET", requestUrl); + xhr.send(); + }); + }, promises); +} + +// Returns a promise that resolves with the Fetch's response text. +function callFetch(requestUrl, promises) { + return observeFilteredNetworkRequest(() => { + return fetch(requestUrl).then(response => response.text()); + }, promises); +} + +// The expected response text is always the query string (without the leading +// "?") of the request URL. +add_task(async function callXhrExpectServerResponse() { + info(`Performing XHR at ${requestUrlForServer}...`); + let extension = await getExtension(); + is( + await callXHR(requestUrlForServer, [ + extension.awaitMessage("start"), + extension.awaitMessage("error"), + extension.awaitMessage("stop"), + ]), + requestUrlForServerQueryString, + "Server-supplied response for XHR completed successfully" + ); + await extension.unload(); +}); + +add_task(async function callXhrExpectServiceWorkerResponse() { + info(`Performing XHR at ${requestUrlForServiceWorker}...`); + let extension = await getExtension(); + is( + await callXHR(requestUrlForServiceWorker, [ + extension.awaitMessage("start"), + extension.awaitMessage("stop"), + ]), + requestUrlForServiceWorkerQueryString, + "ServiceWorker-supplied response for XHR completed successfully" + ); + await extension.unload(); +}); + +add_task(async function callFetchExpectServerResponse() { + info(`Performing Fetch at ${requestUrlForServer}...`); + let extension = await getExtension(); + is( + await callFetch(requestUrlForServer, [ + extension.awaitMessage("start"), + extension.awaitMessage("error"), + extension.awaitMessage("stop"), + ]), + requestUrlForServerQueryString, + "Server-supplied response for Fetch completed successfully" + ); + await extension.unload(); +}); + +add_task(async function callFetchExpectServiceWorkerResponse() { + info(`Performing Fetch at ${requestUrlForServiceWorker}...`); + let extension = await getExtension(); + is( + await callFetch(requestUrlForServiceWorker, [ + extension.awaitMessage("start"), + extension.awaitMessage("stop"), + ]), + requestUrlForServiceWorkerQueryString, + "ServiceWorker-supplied response for Fetch completed successfully" + ); + await extension.unload(); +}); +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_strict_mode_warning.html b/dom/serviceworkers/test/test_strict_mode_warning.html new file mode 100644 index 0000000000..4df0d1a380 --- /dev/null +++ b/dom/serviceworkers/test/test_strict_mode_warning.html @@ -0,0 +1,42 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1170550 - test registration of service worker scripts with a strict mode warning</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function runTest() { + navigator.serviceWorker + .register("strict_mode_warning.js", {scope: "strict_mode_warning"}) + .then((reg) => { + ok(true, "Registration should not fail for warnings"); + return reg.unregister(); + }) + .then(() => { + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + onload = function() { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, runTest); + }; +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_third_party_iframes.html b/dom/serviceworkers/test/test_third_party_iframes.html new file mode 100644 index 0000000000..90e9dadfa8 --- /dev/null +++ b/dom/serviceworkers/test/test_third_party_iframes.html @@ -0,0 +1,263 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <meta http-equiv="Content-type" content="text/html;charset=UTF-8"> + <title>Bug 1152899 - Disallow the interception of third-party iframes using service workers when the third-party cookie preference is set</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script class="testbody" type="text/javascript"> + +var chromeScript; +chromeScript = SpecialPowers.loadChromeScript(_ => { + /* eslint-env mozilla/chrome-script */ + Services.clearData.deleteData(Ci.nsIClearDataService.CLEAR_ALL, value => resolve()); +}); + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestLongerTimeout(2); + +let index = 0; +function next() { + info("Step " + index); + if (index >= steps.length) { + SimpleTest.finish(); + return; + } + try { + let i = index++; + steps[i](); + } catch(ex) { + ok(false, "Caught exception", ex); + } +} + +onload = next; + +let iframe; +let proxyWindow; +let basePath = "/tests/dom/serviceworkers/test/thirdparty/"; +let origin = window.location.protocol + "//" + window.location.host; +let thirdPartyOrigin = "https://example.com"; + +function loadIframe() { + let message = { + source: "parent", + href: origin + basePath + "iframe2.html" + }; + iframe.contentWindow.postMessage(message, "*"); +} + +function loadThirdPartyIframe() { + let message = { + source: "parent", + href: thirdPartyOrigin + basePath + "iframe2.html" + } + iframe.contentWindow.postMessage(message, "*"); +} + +function runTest(aExpectedResponses) { + // Let's use a proxy window to have the new cookie policy applied. + proxyWindow = window.open("window_party_iframes.html"); + proxyWindow.onload = _ => { + iframe = proxyWindow.document.querySelector("iframe"); + iframe.src = thirdPartyOrigin + basePath + "register.html"; + let responsesIndex = 0; + window.onmessage = function(e) { + let status = e.data.status; + let expected = aExpectedResponses[responsesIndex]; + if (status == expected.status) { + ok(true, "Received expected " + expected.status); + if (expected.next) { + expected.next(); + } + } else { + ok(false, "Expected " + expected.status + " got " + status); + } + responsesIndex++; + }; + } +} + +// Verify that we can register and intercept a 3rd party iframe with +// the given cookie policy. +function testShouldIntercept(behavior, done) { + SpecialPowers.pushPrefEnv({"set": [ + ["network.cookie.cookieBehavior", behavior], + ]}, function() { + runTest([{ + status: "ok" + }, { + status: "registrationdone", + next() { + iframe.src = origin + basePath + "iframe1.html"; + } + }, { + status: "iframeloaded", + next: loadIframe + }, { + status: "networkresponse", + }, { + status: "worker-networkresponse", + next: loadThirdPartyIframe + }, { + status: "swresponse", + }, { + status: "worker-swresponse", + next() { + iframe.src = thirdPartyOrigin + basePath + "unregister.html"; + } + }, { + status: "controlled", + }, { + status: "unregistrationdone", + next() { + window.onmessage = null; + proxyWindow.close(); + ok(true, "Test finished successfully"); + done(); + } + }]); + }); +} + +// Verify that we cannot register a service worker in a 3rd party +// iframe with the given cookie policy. +function testShouldNotRegister(behavior, done) { + SpecialPowers.pushPrefEnv({"set": [ + ["network.cookie.cookieBehavior", behavior], + ]}, function() { + runTest([{ + status: "registrationfailed", + next() { + iframe.src = origin + basePath + "iframe1.html"; + } + }, { + status: "iframeloaded", + next: loadIframe + }, { + status: "networkresponse", + }, { + status: "worker-networkresponse", + next: loadThirdPartyIframe + }, { + status: "networkresponse", + }, { + status: "worker-networkresponse", + next() { + window.onmessage = null; + proxyWindow.close(); + ok(true, "Test finished successfully"); + done(); + } + }]); + }); +} + +// Verify that if a service worker is already registered a 3rd +// party iframe will still not be intercepted with the given cookie +// policy. +function testShouldNotIntercept(behavior, done) { + SpecialPowers.pushPrefEnv({"set": [ + ["network.cookie.cookieBehavior", BEHAVIOR_ACCEPT], + ]}, function() { + runTest([{ + status: "ok" + }, { + status: "registrationdone", + next() { + SpecialPowers.pushPrefEnv({"set": [ + ["network.cookie.cookieBehavior", behavior], + ]}, function() { + proxyWindow.close(); + proxyWindow = window.open("window_party_iframes.html"); + proxyWindow.onload = _ => { + iframe = proxyWindow.document.querySelector("iframe"); + iframe.src = origin + basePath + "iframe1.html"; + } + }); + } + }, { + status: "iframeloaded", + next: loadIframe + }, { + status: "networkresponse", + }, { + status: "worker-networkresponse", + next: loadThirdPartyIframe + }, { + status: "networkresponse", + }, { + status: "worker-networkresponse", + next() { + iframe.src = thirdPartyOrigin + basePath + "unregister.html"; + } + }, { + status: "uncontrolled", + }, { + status: "getregistrationfailed", + next() { + SpecialPowers.pushPrefEnv({"set": [ + ["network.cookie.cookieBehavior", BEHAVIOR_ACCEPT], + ]}, function() { + proxyWindow.close(); + proxyWindow = window.open("window_party_iframes.html"); + proxyWindow.onload = _ => { + iframe = proxyWindow.document.querySelector("iframe"); + iframe.src = thirdPartyOrigin + basePath + "unregister.html"; + } + }); + } + }, { + status: "controlled", + }, { + status: "unregistrationdone", + next() { + window.onmessage = null; + proxyWindow.close(); + ok(true, "Test finished successfully"); + done(); + } + }]); + }); +} + +const BEHAVIOR_ACCEPT = 0; +const BEHAVIOR_REJECTFOREIGN = 1; +const BEHAVIOR_REJECT = 2; +const BEHAVIOR_LIMITFOREIGN = 3; + +let steps = [() => { + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["browser.dom.window.dump.enabled", true], + ["network.cookie.cookieBehavior", BEHAVIOR_ACCEPT], + ]}, next); +}, () => { + testShouldNotRegister(BEHAVIOR_REJECTFOREIGN, next); +}, () => { + testShouldNotIntercept(BEHAVIOR_REJECTFOREIGN, next); +}, () => { + testShouldNotRegister(BEHAVIOR_REJECT, next); +}, () => { + testShouldNotIntercept(BEHAVIOR_REJECT, next); +}, () => { + testShouldNotRegister(BEHAVIOR_LIMITFOREIGN, next); +}, () => { + testShouldNotIntercept(BEHAVIOR_LIMITFOREIGN, next); +}, () => { + testShouldIntercept(BEHAVIOR_ACCEPT, next); +}]; + + +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_unregister.html b/dom/serviceworkers/test/test_unregister.html new file mode 100644 index 0000000000..959378f03a --- /dev/null +++ b/dom/serviceworkers/test/test_unregister.html @@ -0,0 +1,137 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 984048 - Test unregister</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + function simpleRegister() { + return navigator.serviceWorker.register("worker.js", { scope: "unregister/" }).then(function(swr) { + if (swr.installing) { + return new Promise(function(resolve, reject) { + swr.installing.onstatechange = function(e) { + if (swr.waiting) { + swr.waiting.onstatechange = function(event) { + if (swr.active) { + resolve(); + } else if (swr.waiting && swr.waiting.state == "redundant") { + reject("Should not go into redundant"); + } + } + } else { + if (swr.active) { + resolve(); + } else { + reject("No waiting and no active!"); + } + } + } + }); + } else { + return Promise.reject("Installing should be non-null"); + } + }); + } + + function testControlled() { + var testPromise = new Promise(function(res, rej) { + window.onmessage = function(e) { + if (!("controlled" in e.data)) { + ok(false, "Something went wrong."); + rej(); + return; + } + + ok(e.data.controlled, "New window should be controlled."); + res(); + } + }) + + var div = document.getElementById("content"); + ok(div, "Parent exists"); + + var ifr = document.createElement("iframe"); + ifr.setAttribute('src', "unregister/index.html"); + div.appendChild(ifr); + + return testPromise.then(function() { + div.removeChild(ifr); + }); + } + + function unregister() { + return navigator.serviceWorker.getRegistration("unregister/") + .then(function(reg) { + if (!reg) { + info("Registration already removed"); + return; + } + + info("getRegistration() succeeded " + reg.scope); + return reg.unregister().then(function(v) { + ok(v, "Unregister should resolve to true"); + }, function(e) { + ok(false, "Unregister failed with " + e.name); + }); + }); + } + + function testUncontrolled() { + var testPromise = new Promise(function(res, rej) { + window.onmessage = function(e) { + if (!("controlled" in e.data)) { + ok(false, "Something went wrong."); + rej(); + return; + } + + ok(!e.data.controlled, "New window should not be controlled."); + res(); + } + }); + + var div = document.getElementById("content"); + ok(div, "Parent exists"); + + var ifr = document.createElement("iframe"); + ifr.setAttribute('src', "unregister/index.html"); + div.appendChild(ifr); + + return testPromise.then(function() { + div.removeChild(ifr); + }); + } + + function runTest() { + simpleRegister() + .then(testControlled) + .then(unregister) + .then(testUncontrolled) + .then(function() { + SimpleTest.finish(); + }).catch(function(e) { + ok(false, "Some test failed with error " + e); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_unresolved_fetch_interception.html b/dom/serviceworkers/test/test_unresolved_fetch_interception.html new file mode 100644 index 0000000000..7182b0fb86 --- /dev/null +++ b/dom/serviceworkers/test/test_unresolved_fetch_interception.html @@ -0,0 +1,95 @@ +<!DOCTYPE HTML> +<html> +<!-- + Test that an unresolved respondWith promise will reset the channel when + the service worker is terminated due to idling, and that appropriate error + messages are logged for both the termination of the serice worker and the + resetting of the channel. + --> +<head> + <title>Test for Bug 1188545</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="error_reporting_helpers.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <meta http-equiv="Content-type" content="text/html;charset=UTF-8"> +</head> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1188545">Mozilla Bug 118845</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> +// (This doesn't really need to be its own task, but it allows the actual test +// case to be self-contained.) +add_task(function setupPrefs() { + return SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}); +}); + +add_task(async function grace_timeout_termination_with_interrupted_intercept() { + // Setup timeouts so that the service worker will go into grace timeout after + // a zero-length idle timeout. + await SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.idle_timeout", 0], + ["dom.serviceWorkers.idle_extended_timeout", 299999]]}); + + let registration = await navigator.serviceWorker.register( + "unresolved_fetch_worker.js", { scope: "./"} ); + await waitForState(registration.installing, "activated"); + ok(navigator.serviceWorker.controller, "Controlled"); // double check! + + // We want to make sure the SW is active and processing the fetch before we + // try and kill it. It sends us a message when it has done so. + let waitForFetchActive = new Promise((resolve) => { + navigator.serviceWorker.onmessage = resolve; + }); + + // Issue a fetch which the SW will respondWith() a never resolved promise. + // The fetch, however, will terminate when the SW is killed, so check that. + let hangingFetch = fetch("does_not_exist.html") + .then(() => { ok(false, "should have rejected "); }, + () => { ok(true, "hung fetch terminates when worker dies"); }); + + await waitForFetchActive; + + let expectedMessage = expect_console_message( + // Termination error + "ServiceWorkerGraceTimeoutTermination", + [make_absolute_url("./")], + // The interception failure error generated by the RespondWithHandler + // destructor when it notices it didn't get a response before being + // destroyed. It logs via the intercepted channel nsIConsoleReportCollector + // that is eventually flushed to our document and its console. + "InterceptionFailedWithURL", + [make_absolute_url("does_not_exist.html")] + ); + + // Zero out the grace timeout too so the worker will get terminated after two + // zero-length timer firings. Note that we need to do something to get the + // SW to renew its keepalive for this to actually cause the timers to be + // rescheduled... + await SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.idle_extended_timeout", 0]]}); + // ...which we do by postMessaging it. + navigator.serviceWorker.controller.postMessage("doomity doom doom"); + + // Now wait for signs that the worker was terminated by the fetch failing. + await hangingFetch; + + // The worker should now be dead and the error logged, wait/assert. + await wait_for_expected_message(expectedMessage); + + // roll back all of our test case specific preferences and otherwise cleanup + await SpecialPowers.popPrefEnv(); + await SpecialPowers.popPrefEnv(); + await registration.unregister(); +}); +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_workerUnregister.html b/dom/serviceworkers/test/test_workerUnregister.html new file mode 100644 index 0000000000..d0bc1d6ce4 --- /dev/null +++ b/dom/serviceworkers/test/test_workerUnregister.html @@ -0,0 +1,81 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 982728 - Test ServiceWorkerGlobalScope.unregister</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<div id="container"></div> +<script class="testbody" type="text/javascript"> + + function simpleRegister() { + return navigator.serviceWorker.register("worker_unregister.js", { scope: "unregister/" }).then(function(swr) { + if (swr.installing) { + return new Promise(function(resolve, reject) { + swr.installing.onstatechange = function(e) { + if (swr.waiting) { + swr.waiting.onstatechange = function(event) { + if (swr.active) { + resolve(); + } else if (swr.waiting && swr.waiting.state == "redundant") { + reject("Should not go into redundant"); + } + } + } else { + if (swr.active) { + resolve(); + } else { + reject("No waiting and no active!"); + } + } + } + }); + } else { + return Promise.reject("Installing should be non-null"); + } + }); + } + + function waitForMessages(sw) { + var p = new Promise(function(resolve, reject) { + window.onmessage = function(e) { + if (e.data === "DONE") { + ok(true, "The worker has unregistered itself"); + } else if (e.data === "ERROR") { + ok(false, "The worker has unregistered itself"); + } else if (e.data === "FINISH") { + resolve(); + } + } + }); + + var frame = document.createElement("iframe"); + frame.setAttribute("src", "unregister/unregister.html"); + document.body.appendChild(frame); + + return p; + } + + function runTest() { + simpleRegister().then(waitForMessages).catch(function(e) { + ok(false, "Something went wrong."); + }).then(function() { + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_workerUpdate.html b/dom/serviceworkers/test/test_workerUpdate.html new file mode 100644 index 0000000000..015e6bb4ae --- /dev/null +++ b/dom/serviceworkers/test/test_workerUpdate.html @@ -0,0 +1,63 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1065366 - Test ServiceWorkerGlobalScope.update</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<div id="container"></div> +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> + + function simpleRegister() { + return navigator.serviceWorker.register("worker_update.js", { scope: "workerUpdate/" }) + .then(swr => waitForState(swr.installing, 'activated', swr)); + } + + var registration; + function waitForMessages(sw) { + registration = sw; + var p = new Promise(function(resolve, reject) { + window.onmessage = function(e) { + if (e.data === "FINISH") { + ok(true, "The worker has updated itself"); + resolve(); + } else if (e.data === "FAIL") { + ok(false, "The worker failed to update itself"); + resolve(); + } + } + }); + + var frame = document.createElement("iframe"); + frame.setAttribute("src", "workerUpdate/update.html"); + document.body.appendChild(frame); + + return p; + } + + function runTest() { + simpleRegister().then(waitForMessages).catch(function(e) { + ok(false, "Something went wrong."); + }).then(function() { + return registration.unregister(); + }).then(function() { + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true] + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_worker_reference_gc_timeout.html b/dom/serviceworkers/test/test_worker_reference_gc_timeout.html new file mode 100644 index 0000000000..cf04e13f2e --- /dev/null +++ b/dom/serviceworkers/test/test_worker_reference_gc_timeout.html @@ -0,0 +1,76 @@ +<!DOCTYPE HTML> +<html> +<!-- + --> +<head> + <title>Test for Bug 1317266</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script src="error_reporting_helpers.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <meta http-equiv="Content-type" content="text/html;charset=UTF-8"> +</head> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1317266">Mozilla Bug 1317266</a> +<p id="display"></p> +<div id="content" style="display: none"> + +</div> +<pre id="test"> +</pre> + +<script class="testbody" type="text/javascript"> +SimpleTest.requestFlakyTimeout("Forcing a race with the cycle collector."); + +add_task(function setupPrefs() { + return SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}); +}); + +add_task(async function test_worker_ref_gc() { + let registration = await navigator.serviceWorker.register( + "lazy_worker.js", { scope: "./lazy_worker_scope_timeout"} ) + .then(function(reg) { + SpecialPowers.exactGC(); + var worker = reg.installing; + return new Promise(function(resolve) { + worker.addEventListener('statechange', function() { + info("state is " + worker.state + "\n"); + SpecialPowers.exactGC(); + if (worker.state === 'activated') { + resolve(reg); + } + }); + }); + }); + ok(true, "Got activated event!"); + + await registration.unregister(); +}); + +add_task(async function test_worker_ref_gc_ready_promise() { + let wait_active = navigator.serviceWorker.ready.then(function(reg) { + SpecialPowers.exactGC(); + ok(reg.active, "Got active worker."); + ok(reg.active.state === "activating", "Worker is in activating state"); + return new Promise(function(res) { + reg.active.onstatechange = function(e) { + reg.active.onstatechange = null; + ok(reg.active.state === "activated", "Worker was activated"); + res(); + } + }); + }); + + let registration = await navigator.serviceWorker.register( + "lazy_worker.js", { scope: "."} ); + await wait_active; + await registration.unregister(); +}); + +add_task(async function cleanup() { + await SpecialPowers.popPrefEnv(); +}); +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/test_workerupdatefoundevent.html b/dom/serviceworkers/test/test_workerupdatefoundevent.html new file mode 100644 index 0000000000..1c3ced13bd --- /dev/null +++ b/dom/serviceworkers/test/test_workerupdatefoundevent.html @@ -0,0 +1,91 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1131327 - Test ServiceWorkerRegistration.onupdatefound on ServiceWorker</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"></div> +<pre id="test"></pre> +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> + var registration; + var promise; + + async function start() { + registration = await navigator.serviceWorker.register("worker_updatefoundevent.js", + { scope: "./updatefoundevent.html" }) + await waitForState(registration.installing, 'activated'); + + content = document.getElementById("content"); + iframe = document.createElement("iframe"); + content.appendChild(iframe); + iframe.setAttribute("src", "./updatefoundevent.html"); + + await new Promise(function(resolve) { iframe.onload = resolve; }); + ok(iframe.contentWindow.navigator.serviceWorker.controller, "Controlled client."); + + return Promise.resolve(); + + } + + function startWaitForUpdateFound() { + registration.onupdatefound = function(e) { + } + + promise = new Promise(function(resolve, reject) { + window.onmessage = function(e) { + + if (e.data == "finish") { + ok(true, "Received updatefound"); + resolve(); + } + } + }); + + return Promise.resolve(); + } + + function registerNext() { + return navigator.serviceWorker.register("worker_updatefoundevent2.js", + { scope: "./updatefoundevent.html" }); + } + + function waitForUpdateFound() { + return promise; + } + + function unregister() { + window.onmessage = null; + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }); + } + + function runTest() { + start() + .then(startWaitForUpdateFound) + .then(registerNext) + .then(waitForUpdateFound) + .then(unregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/test_xslt.html b/dom/serviceworkers/test/test_xslt.html new file mode 100644 index 0000000000..a955c843ac --- /dev/null +++ b/dom/serviceworkers/test/test_xslt.html @@ -0,0 +1,117 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 1182113 - Test service worker XSLT interception</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"></div> +<pre id="test"></pre> +<script src="utils.js"></script> +<script class="testbody" type="text/javascript"> + var registration; + var worker; + + function start() { + return navigator.serviceWorker.register("xslt_worker.js", + { scope: "./" }) + .then((swr) => { + registration = swr; + + // Ensure the registration is active before continuing + return waitForState(swr.installing, 'activated'); + }); + } + + function unregister() { + return registration.unregister().then(function(result) { + ok(result, "Unregister should return true."); + }, function(e) { + dump("Unregistering the SW failed with " + e + "\n"); + }); + } + + function getXmlString(xmlObject) { + serializer = new XMLSerializer(); + return serializer.serializeToString(iframe.contentDocument); + } + + function synthetic() { + content = document.getElementById("content"); + ok(content, "parent exists."); + + iframe = document.createElement("iframe"); + content.appendChild(iframe); + + iframe.setAttribute('src', "xslt/test.xml"); + + var p = new Promise(function(res, rej) { + iframe.onload = function(e) { + dump("Set request mode\n"); + registration.active.postMessage("synthetic"); + xmlString = getXmlString(iframe.contentDocument); + ok(!xmlString.includes("Error"), "Load synthetic cross origin XSLT should be allowed"); + res(); + }; + }); + + return p; + } + + function cors() { + var p = new Promise(function(res, rej) { + iframe.onload = function(e) { + xmlString = getXmlString(iframe.contentDocument); + ok(!xmlString.includes("Error"), "Load CORS cross origin XSLT should be allowed"); + res(); + }; + }); + + registration.active.postMessage("cors"); + iframe.setAttribute('src', "xslt/test.xml"); + + return p; + } + + function opaque() { + var p = new Promise(function(res, rej) { + iframe.onload = function(e) { + xmlString = getXmlString(iframe.contentDocument); + ok(xmlString.includes("Error"), "Load opaque cross origin XSLT should not be allowed"); + res(); + }; + }); + + registration.active.postMessage("opaque"); + iframe.setAttribute('src', "xslt/test.xml"); + + return p; + } + + function runTest() { + start() + .then(synthetic) + .then(opaque) + .then(cors) + .then(unregister) + .catch(function(e) { + ok(false, "Some test failed with error " + e); + }).then(SimpleTest.finish); + } + + SimpleTest.waitForExplicitFinish(); + SpecialPowers.pushPrefEnv({"set": [ + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ]}, runTest); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/thirdparty/iframe1.html b/dom/serviceworkers/test/thirdparty/iframe1.html new file mode 100644 index 0000000000..e8982d306a --- /dev/null +++ b/dom/serviceworkers/test/thirdparty/iframe1.html @@ -0,0 +1,42 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<html> +<head> + <meta http-equiv="Content-type" content="text/html;charset=UTF-8"> + <title>SW third party iframe test</title> + + <script type="text/javascript"> + function messageListener(event) { + let message = event.data; + + dump("got message " + JSON.stringify(message) + "\n"); + if (message.source == "parent") { + document.getElementById("iframe2").src = message.href; + } + else if (message.source == "iframe") { + parent.postMessage(event.data, "*"); + } else if (message.source == "worker") { + parent.postMessage(event.data, "*"); + } + } + </script> + +</head> + +<body> + <script> + onload = function() { + window.addEventListener('message', messageListener); + let message = { + source: "iframe", + status: "iframeloaded", + } + parent.postMessage(message, "*"); + } + </script> + <iframe id="iframe2"></iframe> +</body> + +</html> diff --git a/dom/serviceworkers/test/thirdparty/iframe2.html b/dom/serviceworkers/test/thirdparty/iframe2.html new file mode 100644 index 0000000000..8013899195 --- /dev/null +++ b/dom/serviceworkers/test/thirdparty/iframe2.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<script> + window.parent.postMessage({ + source: "iframe", + status: "networkresponse" + }, "*"); + var w = new Worker('worker.js'); + w.onmessage = function(evt) { + window.parent.postMessage({ + source: 'worker', + status: evt.data, + }, '*'); + }; +</script> diff --git a/dom/serviceworkers/test/thirdparty/register.html b/dom/serviceworkers/test/thirdparty/register.html new file mode 100644 index 0000000000..b166acb8a4 --- /dev/null +++ b/dom/serviceworkers/test/thirdparty/register.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<script> + function ok(v, msg) { + window.parent.postMessage({status: "ok", result: !!v, message: msg}, "*"); + } + + var isDone = false; + function done(reg) { + if (!isDone) { + ok(reg.waiting || reg.active, + "Either active or waiting worker should be available."); + window.parent.postMessage({status: "registrationdone"}, "*"); + isDone = true; + } + } + + navigator.serviceWorker.register("sw.js", {scope: "."}) + .then(function(registration) { + if (registration.installing) { + registration.installing.onstatechange = function(e) { + done(registration); + }; + } else { + done(registration); + } + }).catch(function(e) { + window.parent.postMessage({status: "registrationfailed"}, "*"); + }); +</script> diff --git a/dom/serviceworkers/test/thirdparty/sw.js b/dom/serviceworkers/test/thirdparty/sw.js new file mode 100644 index 0000000000..ea844e8f2b --- /dev/null +++ b/dom/serviceworkers/test/thirdparty/sw.js @@ -0,0 +1,33 @@ +self.addEventListener("fetch", function(event) { + dump("fetch " + event.request.url + "\n"); + if (event.request.url.includes("iframe2.html")) { + var body = + "<script>" + + "window.parent.postMessage({" + + "source: 'iframe', status: 'swresponse'" + + "}, '*');" + + "var w = new Worker('worker.js');" + + "w.onmessage = function(evt) {" + + "window.parent.postMessage({" + + "source: 'worker'," + + "status: evt.data," + + "}, '*');" + + "};" + + "</script>"; + event.respondWith( + new Response(body, { + headers: { "Content-Type": "text/html" }, + }) + ); + return; + } + if (event.request.url.includes("worker.js")) { + var body = "self.postMessage('worker-swresponse');"; + event.respondWith( + new Response(body, { + headers: { "Content-Type": "application/javascript" }, + }) + ); + return; + } +}); diff --git a/dom/serviceworkers/test/thirdparty/unregister.html b/dom/serviceworkers/test/thirdparty/unregister.html new file mode 100644 index 0000000000..65b29d5648 --- /dev/null +++ b/dom/serviceworkers/test/thirdparty/unregister.html @@ -0,0 +1,19 @@ +<!DOCTYPE html> +<script> + if (navigator.serviceWorker.controller) { + window.parent.postMessage({status: "controlled"}, "*"); + } else { + window.parent.postMessage({status: "uncontrolled"}, "*"); + } + + navigator.serviceWorker.getRegistration(".").then(function(registration) { + if(!registration) { + return; + } + registration.unregister().then(() => { + window.parent.postMessage({status: "unregistrationdone"}, "*"); + }); + }).catch(function(e) { + window.parent.postMessage({status: "getregistrationfailed"}, "*"); + }); +</script> diff --git a/dom/serviceworkers/test/thirdparty/worker.js b/dom/serviceworkers/test/thirdparty/worker.js new file mode 100644 index 0000000000..bbdc608cde --- /dev/null +++ b/dom/serviceworkers/test/thirdparty/worker.js @@ -0,0 +1 @@ +self.postMessage("worker-networkresponse"); diff --git a/dom/serviceworkers/test/unregister/index.html b/dom/serviceworkers/test/unregister/index.html new file mode 100644 index 0000000000..36cac9fcf6 --- /dev/null +++ b/dom/serviceworkers/test/unregister/index.html @@ -0,0 +1,26 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Bug 984048 - Test unregister</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"></pre> +<script class="testbody" type="text/javascript"> + + if (!parent) { + info("unregister/index.html should not to be launched directly!"); + } + + parent.postMessage({ controlled: !!navigator.serviceWorker.controller }, "*"); +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/unregister/unregister.html b/dom/serviceworkers/test/unregister/unregister.html new file mode 100644 index 0000000000..42633ca343 --- /dev/null +++ b/dom/serviceworkers/test/unregister/unregister.html @@ -0,0 +1,21 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test worker::unregister</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script type="text/javascript"> + + navigator.serviceWorker.onmessage = function(e) { parent.postMessage(e.data, "*"); } + navigator.serviceWorker.controller.postMessage("GO"); + +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/unresolved_fetch_worker.js b/dom/serviceworkers/test/unresolved_fetch_worker.js new file mode 100644 index 0000000000..80d39b037a --- /dev/null +++ b/dom/serviceworkers/test/unresolved_fetch_worker.js @@ -0,0 +1,18 @@ +var keepPromiseAlive; +onfetch = function(event) { + event.waitUntil( + clients.matchAll().then(clients => { + clients.forEach(client => { + client.postMessage("continue"); + }); + }) + ); + + // Never resolve, and keep it alive on our global so it can't get GC'ed and + // make this test weird and intermittent. + event.respondWith((keepPromiseAlive = new Promise(function(res, rej) {}))); +}; + +addEventListener("activate", function(event) { + event.waitUntil(clients.claim()); +}); diff --git a/dom/serviceworkers/test/update_worker.sjs b/dom/serviceworkers/test/update_worker.sjs new file mode 100644 index 0000000000..44782a2732 --- /dev/null +++ b/dom/serviceworkers/test/update_worker.sjs @@ -0,0 +1,12 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +function handleRequest(request, response) { + // This header is necessary for making this script able to be loaded. + response.setHeader("Content-Type", "application/javascript"); + + var body = "/* " + Date.now() + " */"; + response.write(body); +} diff --git a/dom/serviceworkers/test/updatefoundevent.html b/dom/serviceworkers/test/updatefoundevent.html new file mode 100644 index 0000000000..78088c7cd0 --- /dev/null +++ b/dom/serviceworkers/test/updatefoundevent.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html> +<head> + <title>Bug 1131327 - Test ServiceWorkerRegistration.onupdatefound on ServiceWorker</title> +</head> +<body> +<script> + navigator.serviceWorker.onmessage = function(e) { + dump("NSM iframe got message " + e.data + "\n"); + window.parent.postMessage(e.data, "*"); + }; +</script> +</body> diff --git a/dom/serviceworkers/test/utils.js b/dom/serviceworkers/test/utils.js new file mode 100644 index 0000000000..28be239593 --- /dev/null +++ b/dom/serviceworkers/test/utils.js @@ -0,0 +1,136 @@ +function waitForState(worker, state, context) { + return new Promise(resolve => { + function onStateChange() { + if (worker.state === state) { + worker.removeEventListener("statechange", onStateChange); + resolve(context); + } + } + + // First add an event listener, so we won't miss any change that happens + // before we check the current state. + worker.addEventListener("statechange", onStateChange); + + // Now check if the worker is already in the desired state. + onStateChange(); + }); +} + +/** + * Helper for browser tests to issue register calls from the content global and + * wait for the SW to progress to the active state, as most tests desire. + * From the ContentTask.spawn, use via + * `content.wrappedJSObject.registerAndWaitForActive`. + */ +async function registerAndWaitForActive(script, maybeScope) { + console.log("...calling register"); + let opts = undefined; + if (maybeScope) { + opts = { scope: maybeScope }; + } + const reg = await navigator.serviceWorker.register(script, opts); + // Unless registration resurrection happens, the SW should be in the + // installing slot. + console.log("...waiting for activation"); + await waitForState(reg.installing, "activated", reg); + console.log("...activated!"); + return reg; +} + +/** + * Helper to create an iframe with the given URL and return the first + * postMessage payload received. This is intended to be used when creating + * cross-origin iframes. + * + * A promise will be returned that resolves with the payload of the postMessage + * call. + */ +function createIframeAndWaitForMessage(url) { + const iframe = document.createElement("iframe"); + document.body.appendChild(iframe); + return new Promise(resolve => { + window.addEventListener( + "message", + event => { + resolve(event.data); + }, + { once: true } + ); + iframe.src = url; + }); +} + +/** + * Helper to create a nested iframe into the iframe created by + * createIframeAndWaitForMessage(). + * + * A promise will be returned that resolves with the payload of the postMessage + * call. + */ +function createNestedIframeAndWaitForMessage(url) { + const iframe = document.getElementsByTagName("iframe")[0]; + iframe.contentWindow.postMessage("create nested iframe", "*"); + return new Promise(resolve => { + window.addEventListener( + "message", + event => { + resolve(event.data); + }, + { once: true } + ); + }); +} + +async function unregisterAll() { + const registrations = await navigator.serviceWorker.getRegistrations(); + for (const reg of registrations) { + await reg.unregister(); + } +} + +/** + * Make a blob that contains random data and therefore shouldn't compress all + * that well. + */ +function makeRandomBlob(size) { + const arr = new Uint8Array(size); + let offset = 0; + /** + * getRandomValues will only provide a maximum of 64k of data at a time and + * will error if we ask for more, so using a while loop for get a random value + * which much larger than 64k. + * https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues#exceptions + */ + while (offset < size) { + const nextSize = Math.min(size - offset, 65536); + window.crypto.getRandomValues(new Uint8Array(arr.buffer, offset, nextSize)); + offset += nextSize; + } + return new Blob([arr], { type: "application/octet-stream" }); +} + +async function fillStorage(cacheBytes, idbBytes) { + // ## Fill Cache API Storage + const cache = await caches.open("filler"); + await cache.put("fill", new Response(makeRandomBlob(cacheBytes))); + + // ## Fill IDB + const storeName = "filler"; + let db = await new Promise((resolve, reject) => { + let openReq = indexedDB.open("filler", 1); + openReq.onerror = event => { + reject(event.target.error); + }; + openReq.onsuccess = event => { + resolve(event.target.result); + }; + openReq.onupgradeneeded = event => { + const useDB = event.target.result; + useDB.onerror = error => { + reject(error); + }; + const store = useDB.createObjectStore(storeName); + store.put({ blob: makeRandomBlob(idbBytes) }, "filler-blob"); + }; + }); +} diff --git a/dom/serviceworkers/test/window_party_iframes.html b/dom/serviceworkers/test/window_party_iframes.html new file mode 100644 index 0000000000..abeea4449b --- /dev/null +++ b/dom/serviceworkers/test/window_party_iframes.html @@ -0,0 +1,18 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <meta http-equiv="Content-type" content="text/html;charset=UTF-8"> +</head> +<body> +<iframe></iframe> +<script> +window.onmessage = e => { + opener.postMessage(e.data, "*"); +} +</script> +</body> +</html> diff --git a/dom/serviceworkers/test/worker.js b/dom/serviceworkers/test/worker.js new file mode 100644 index 0000000000..2aba167d18 --- /dev/null +++ b/dom/serviceworkers/test/worker.js @@ -0,0 +1 @@ +// empty worker, always succeed! diff --git a/dom/serviceworkers/test/worker2.js b/dom/serviceworkers/test/worker2.js new file mode 100644 index 0000000000..3072d0817f --- /dev/null +++ b/dom/serviceworkers/test/worker2.js @@ -0,0 +1 @@ +// worker2.js diff --git a/dom/serviceworkers/test/worker3.js b/dom/serviceworkers/test/worker3.js new file mode 100644 index 0000000000..449fc2f976 --- /dev/null +++ b/dom/serviceworkers/test/worker3.js @@ -0,0 +1 @@ +// worker3.js diff --git a/dom/serviceworkers/test/workerUpdate/update.html b/dom/serviceworkers/test/workerUpdate/update.html new file mode 100644 index 0000000000..666e213d14 --- /dev/null +++ b/dom/serviceworkers/test/workerUpdate/update.html @@ -0,0 +1,23 @@ +<!-- + Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ +--> +<!DOCTYPE HTML> +<html> +<head> + <title>Test worker::update</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<script type="text/javascript"> + + navigator.serviceWorker.onmessage = function(e) { parent.postMessage(e.data, "*"); } + navigator.serviceWorker.ready.then(function() { + navigator.serviceWorker.controller.postMessage("GO"); + }); + +</script> +</pre> +</body> +</html> diff --git a/dom/serviceworkers/test/worker_unregister.js b/dom/serviceworkers/test/worker_unregister.js new file mode 100644 index 0000000000..a2b45245b4 --- /dev/null +++ b/dom/serviceworkers/test/worker_unregister.js @@ -0,0 +1,22 @@ +onmessage = function(e) { + clients.matchAll().then(function(c) { + if (c.length === 0) { + // We cannot proceed. + return; + } + + registration + .unregister() + .then( + function() { + c[0].postMessage("DONE"); + }, + function() { + c[0].postMessage("ERROR"); + } + ) + .then(function() { + c[0].postMessage("FINISH"); + }); + }); +}; diff --git a/dom/serviceworkers/test/worker_update.js b/dom/serviceworkers/test/worker_update.js new file mode 100644 index 0000000000..5c83b9cd9e --- /dev/null +++ b/dom/serviceworkers/test/worker_update.js @@ -0,0 +1,25 @@ +// For now this test only calls update to verify that our registration +// job queueing works properly when called from the worker thread. We should +// test actual update scenarios with a SJS test. +onmessage = function(e) { + self.registration + .update() + .then(function(v) { + return v instanceof ServiceWorkerRegistration ? "FINISH" : "FAIL"; + }) + .catch(function(ex) { + return "FAIL"; + }) + .then(function(result) { + clients.matchAll().then(function(c) { + if (!c.length) { + dump( + "!!!!!!!!!!! WORKER HAS NO CLIENTS TO FINISH TEST !!!!!!!!!!!!\n" + ); + return; + } + + c[0].postMessage(result); + }); + }); +}; diff --git a/dom/serviceworkers/test/worker_updatefoundevent.js b/dom/serviceworkers/test/worker_updatefoundevent.js new file mode 100644 index 0000000000..cb6c6fa862 --- /dev/null +++ b/dom/serviceworkers/test/worker_updatefoundevent.js @@ -0,0 +1,20 @@ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +registration.onupdatefound = function(e) { + clients.matchAll().then(function(clients) { + if (!clients.length) { + // We don't control any clients when the first update event is fired + // because we haven't reached the 'activated' state. + return; + } + + if (registration.scope.match(/updatefoundevent\.html$/)) { + clients[0].postMessage("finish"); + } else { + dump("Scope did not match"); + } + }); +}; diff --git a/dom/serviceworkers/test/worker_updatefoundevent2.js b/dom/serviceworkers/test/worker_updatefoundevent2.js new file mode 100644 index 0000000000..da4c592aad --- /dev/null +++ b/dom/serviceworkers/test/worker_updatefoundevent2.js @@ -0,0 +1 @@ +// Not useful. diff --git a/dom/serviceworkers/test/xslt/test.xml b/dom/serviceworkers/test/xslt/test.xml new file mode 100644 index 0000000000..83c7776339 --- /dev/null +++ b/dom/serviceworkers/test/xslt/test.xml @@ -0,0 +1,6 @@ +<?xml version="1.0"?> +<?xml-stylesheet type="text/xsl" href="test.xsl"?> +<result> + <Title>Example</Title> + <Error>Error</Error> +</result> diff --git a/dom/serviceworkers/test/xslt/xslt.sjs b/dom/serviceworkers/test/xslt/xslt.sjs new file mode 100644 index 0000000000..db681ab500 --- /dev/null +++ b/dom/serviceworkers/test/xslt/xslt.sjs @@ -0,0 +1,12 @@ +function handleRequest(request, response) { + response.setHeader("Content-Type", "application/xslt+xml", false); + response.setHeader("Access-Control-Allow-Origin", "*"); + + var body = request.queryString; + if (!body) { + response.setStatusLine(null, 500, "Invalid querystring"); + return; + } + + response.write(unescape(body)); +} diff --git a/dom/serviceworkers/test/xslt_worker.js b/dom/serviceworkers/test/xslt_worker.js new file mode 100644 index 0000000000..d3c4383f22 --- /dev/null +++ b/dom/serviceworkers/test/xslt_worker.js @@ -0,0 +1,58 @@ +var testType = "synthetic"; + +var xslt = + '<?xml version="1.0"?> ' + + '<xsl:stylesheet version="1.0"' + + ' xmlns:xsl="http://www.w3.org/1999/XSL/Transform">' + + ' <xsl:template match="node()|@*">' + + " <xsl:copy>" + + ' <xsl:apply-templates select="node()|@*"/>' + + " </xsl:copy>" + + " </xsl:template>" + + ' <xsl:template match="Error"/>' + + "</xsl:stylesheet>"; + +onfetch = function(event) { + if (event.request.url.includes("test.xsl")) { + if (testType == "synthetic") { + if (event.request.mode != "cors") { + event.respondWith(Response.error()); + return; + } + + event.respondWith( + Promise.resolve( + new Response(xslt, { + headers: { "Content-Type": "application/xslt+xml" }, + }) + ) + ); + } else if (testType == "cors") { + if (event.request.mode != "cors") { + event.respondWith(Response.error()); + return; + } + + var url = + "http://example.com/tests/dom/serviceworkers/test/xslt/xslt.sjs?" + + escape(xslt); + event.respondWith(fetch(url, { mode: "cors" })); + } else if (testType == "opaque") { + if (event.request.mode != "cors") { + event.respondWith(Response.error()); + return; + } + + var url = + "http://example.com/tests/dom/serviceworkers/test/xslt/xslt.sjs?" + + escape(xslt); + event.respondWith(fetch(url, { mode: "no-cors" })); + } else { + event.respondWith(Response.error()); + } + } +}; + +onmessage = function(event) { + testType = event.data; +}; |