From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 21:33:14 +0200 Subject: Adding upstream version 115.7.0esr. Signed-off-by: Daniel Baumann --- dom/serviceworkers/test/browser_head.js | 318 ++++++++++++++++++++++++++++++++ 1 file changed, 318 insertions(+) create mode 100644 dom/serviceworkers/test/browser_head.js (limited to 'dom/serviceworkers/test/browser_head.js') diff --git a/dom/serviceworkers/test/browser_head.js b/dom/serviceworkers/test/browser_head.js new file mode 100644 index 0000000000..78e4d327ec --- /dev/null +++ b/dom/serviceworkers/test/browser_head.js @@ -0,0 +1,318 @@ +/** + * 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: +// +// 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"); + }); +} -- cgit v1.2.3