summaryrefslogtreecommitdiffstats
path: root/dom/serviceworkers/test/browser_head.js
diff options
context:
space:
mode:
Diffstat (limited to 'dom/serviceworkers/test/browser_head.js')
-rw-r--r--dom/serviceworkers/test/browser_head.js318
1 files changed, 318 insertions, 0 deletions
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");
+ });
+}