Source code

Revision control

Copy as Markdown

Other Tools

/**
* 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:
**/
// 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)
.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");
});
}