Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

"use strict";
const PROCESS_COUNT_PREF = "dom.ipc.processCount";
const { createAppInfo } = AddonTestUtils;
AddonTestUtils.init(this);
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "49");
const server = createHttpServer();
server.registerDirectory("/data/", do_get_file("data"));
const BASE_URL = `http://localhost:${server.identity.primaryPort}/data`;
add_task(async function setup_test_environment() {
// Start with one content process so that we can increase the number
// later and test the behavior of a fresh content process.
Services.prefs.setIntPref(PROCESS_COUNT_PREF, 1);
// Grant the optional permissions requested, without prompting.
Services.prefs.setBoolPref(
"extensions.webextOptionalPermissionPrompts",
false
);
registerCleanupFunction(() => {
Services.prefs.clearUserPref("extensions.webextOptionalPermissionPrompts");
});
});
// Test that there is no userScripts API namespace when the manifest doesn't include a user_scripts
// property.
add_task(async function test_userScripts_manifest_property_required() {
function background() {
browser.test.assertEq(
undefined,
browser.userScripts,
"userScripts API namespace should be undefined in the extension page"
);
browser.test.sendMessage("background-page:done");
}
async function contentScript() {
browser.test.assertEq(
undefined,
browser.userScripts,
"userScripts API namespace should be undefined in the content script"
);
browser.test.sendMessage("content-script:done");
}
let extension = ExtensionTestUtils.loadExtension({
background,
manifest: {
permissions: ["http://*/*/file_sample.html"],
content_scripts: [
{
js: ["content_script.js"],
run_at: "document_start",
},
],
},
files: {
"content_script.js": contentScript,
},
});
await extension.startup();
await extension.awaitMessage("background-page:done");
let url = `${BASE_URL}/file_sample.html`;
let contentPage = await ExtensionTestUtils.loadContentPage(url);
await extension.awaitMessage("content-script:done");
await extension.unload();
await contentPage.close();
});
// Test that userScripts can only matches origins that are subsumed by the extension permissions,
// and that more origins can be allowed by requesting an optional permission.
add_task(async function test_userScripts_matches_denied() {
async function background() {
async function registerUserScriptWithMatches(matches) {
const scripts = await browser.userScripts.register({
js: [{ code: "" }],
matches,
});
await scripts.unregister();
}
// These matches are supposed to be denied until the extension has been granted the
// <all_urls> origin permission.
const testMatches = [
"<all_urls>",
];
browser.test.onMessage.addListener(async msg => {
if (msg === "test-denied-matches") {
for (let testMatch of testMatches) {
await browser.test.assertRejects(
registerUserScriptWithMatches([testMatch]),
/Permission denied to register a user script for/,
"Got the expected rejection when the extension permission does not subsume the userScript matches"
);
}
} else if (msg === "grant-all-urls") {
await browser.permissions.request({ origins: ["<all_urls>"] });
} else if (msg === "test-allowed-matches") {
for (let testMatch of testMatches) {
try {
await registerUserScriptWithMatches([testMatch]);
} catch (err) {
browser.test.fail(
`Unexpected rejection ${err} on matching ${JSON.stringify(
testMatch
)}`
);
}
}
} else {
browser.test.fail(`Received an unexpected ${msg} test message`);
}
browser.test.sendMessage(`${msg}:done`);
});
browser.test.sendMessage("background-ready");
}
let extension = ExtensionTestUtils.loadExtension({
manifest: {
permissions: ["http://localhost/*"],
optional_permissions: ["<all_urls>"],
user_scripts: {},
},
background,
});
await extension.startup();
await extension.awaitMessage("background-ready");
// Test that the matches not subsumed by the extension permissions are being denied.
extension.sendMessage("test-denied-matches");
await extension.awaitMessage("test-denied-matches:done");
// Grant the optional <all_urls> permission.
await withHandlingUserInput(extension, async () => {
extension.sendMessage("grant-all-urls");
await extension.awaitMessage("grant-all-urls:done");
});
// Test that all the matches are now subsumed by the extension permissions.
extension.sendMessage("test-allowed-matches");
await extension.awaitMessage("test-allowed-matches:done");
await extension.unload();
});
// Test that userScripts sandboxes:
// - can be registered/unregistered from an extension page (and they are registered on both new and
// existing processes).
// - have no WebExtensions APIs available
// - are able to access the target window and document
add_task(async function test_userScripts_no_webext_apis() {
async function background() {
const sharedCode = {
code: 'console.log("js code shared by multiple userScripts");',
};
const userScriptOptions = {
js: [
sharedCode,
{
code: `
window.addEventListener("load", () => {
const webextAPINamespaces = this.browser ? Object.keys(this.browser) : undefined;
document.body.innerHTML = "userScript loaded - " + JSON.stringify(webextAPINamespaces);
}, {once: true});
`,
},
],
runAt: "document_start",
matches,
scriptMetadata: {
name: "test-user-script",
arrayProperty: ["el1"],
objectProperty: { nestedProp: "nestedValue" },
nullProperty: null,
},
};
let script = await browser.userScripts.register(userScriptOptions);
// Unregister and then register the same js code again, to verify that the last registered
// userScript doesn't get assigned a revoked blob url (otherwise Extensioncontent.sys.mjs
// ScriptCache raises an error because it fails to compile the revoked blob url and the user
// script will never be loaded).
script.unregister();
script = await browser.userScripts.register(userScriptOptions);
browser.test.onMessage.addListener(async msg => {
if (msg !== "register-new-script") {
return;
}
await script.unregister();
await browser.userScripts.register({
...userScriptOptions,
scriptMetadata: { name: "test-new-script" },
js: [
sharedCode,
{
code: `
window.addEventListener("load", () => {
const webextAPINamespaces = this.browser ? Object.keys(this.browser) : undefined;
document.body.innerHTML = "new userScript loaded - " + JSON.stringify(webextAPINamespaces);
}, {once: true});
`,
},
],
});
browser.test.sendMessage("script-registered");
});
const scriptToRemove = await browser.userScripts.register({
js: [
sharedCode,
{
code: `
window.addEventListener("load", () => {
document.body.innerHTML = "unexpected unregistered userScript loaded";
}, {once: true});
`,
},
],
runAt: "document_start",
matches,
scriptMetadata: {
name: "user-script-to-remove",
},
});
browser.test.assertTrue(
"unregister" in script,
"Got an unregister method on the userScript API object"
);
// Remove the last registered user script.
await scriptToRemove.unregister();
browser.test.sendMessage("background-ready");
}
let extensionData = {
manifest: {
user_scripts: {},
},
background,
};
let extension = ExtensionTestUtils.loadExtension(extensionData);
await extension.startup();
await extension.awaitMessage("background-ready");
let url = `${BASE_URL}/file_sample.html?testpage=1`;
let contentPage = await ExtensionTestUtils.loadContentPage(url);
let result = await contentPage.spawn([], async () => {
return {
textContent: this.content.document.body.textContent,
url: this.content.location.href,
readyState: this.content.document.readyState,
};
});
Assert.deepEqual(
result,
{
textContent: "userScript loaded - undefined",
url,
readyState: "complete",
},
"The userScript executed on the expected url and no access to the WebExtensions APIs"
);
info("Test content script are correctly created on a newly created process");
await extension.sendMessage("register-new-script");
await extension.awaitMessage("script-registered");
// Update the process count preference, so that we can test that the newly registered user script
// is propagated as expected into the newly created process.
Services.prefs.setIntPref(PROCESS_COUNT_PREF, 2);
const url2 = `${BASE_URL}/file_sample.html?testpage=2`;
let contentPage2 = await ExtensionTestUtils.loadContentPage(url2, {
remote: true,
});
let result2 = await contentPage2.spawn([], async () => {
return {
textContent: this.content.document.body.textContent,
url: this.content.location.href,
readyState: this.content.document.readyState,
};
});
Assert.deepEqual(
result2,
{
textContent: "new userScript loaded - undefined",
url: url2,
readyState: "complete",
},
"The userScript executed on the expected url and no access to the WebExtensions APIs"
);
await contentPage.close();
await contentPage2.close();
await extension.unload();
});
// This test verify that a cached script is still able to catch the document
// while it is still loading (when we do not block the document parsing as
// we do for a non cached script).
add_task(async function test_cached_userScript_on_document_start() {
function apiScript() {
browser.userScripts.onBeforeScript.addListener(script => {
script.defineGlobals({
sendTestMessage(name, params) {
return browser.test.sendMessage(name, params);
},
});
});
}
async function background() {
function userScript() {
this.sendTestMessage("user-script-loaded", {
url: window.location.href,
documentReadyState: document.readyState,
});
}
await browser.userScripts.register({
js: [
{
code: `(${userScript})();`,
},
],
runAt: "document_start",
});
browser.test.sendMessage("user-script-registered");
}
let extension = ExtensionTestUtils.loadExtension({
manifest: {
user_scripts: {
api_script: "api-script.js",
// The following is an unexpected manifest property, that we expect to be ignored and
// to not prevent the test extension from being installed and run as expected.
unexpected_manifest_key: "test-unexpected-key",
},
},
background,
files: {
"api-script.js": apiScript,
},
});
ExtensionTestUtils.failOnSchemaWarnings(false);
await extension.startup();
ExtensionTestUtils.failOnSchemaWarnings(true);
await extension.awaitMessage("user-script-registered");
let url = `${BASE_URL}/file_sample.html`;
let contentPage = await ExtensionTestUtils.loadContentPage(url);
let msg = await extension.awaitMessage("user-script-loaded");
Assert.deepEqual(
msg,
{
url,
documentReadyState: "loading",
},
"Got the expected url and document.readyState from a non cached user script"
);
// Reload the page and check that the cached content script is still able to
// run on document_start.
await contentPage.loadURL(url);
let msgFromCached = await extension.awaitMessage("user-script-loaded");
Assert.deepEqual(
msgFromCached,
{
url,
documentReadyState: "loading",
},
"Got the expected url and document.readyState from a cached user script"
);
await contentPage.close();
await extension.unload();
});
add_task(async function test_userScripts_pref_disabled() {
async function run_userScript_on_pref_disabled_test() {
async function background() {
let promise = (async () => {
await browser.userScripts.register({
js: [
{
code: "throw new Error('This userScripts should not be registered')",
},
],
runAt: "document_start",
matches: ["<all_urls>"],
});
})();
await browser.test.assertRejects(
promise,
/userScripts APIs are currently experimental/,
"Got the expected error from userScripts.register when the userScripts API is disabled"
);
browser.test.sendMessage("background-page:done");
}
async function contentScript() {
let promise = (async () => {
browser.userScripts.onBeforeScript.addListener(() => {});
})();
await browser.test.assertRejects(
promise,
/userScripts APIs are currently experimental/,
"Got the expected error from userScripts.onBeforeScript when the userScripts API is disabled"
);
browser.test.sendMessage("content-script:done");
}
let extension = ExtensionTestUtils.loadExtension({
background,
manifest: {
permissions: ["http://*/*/file_sample.html"],
user_scripts: { api_script: "" },
content_scripts: [
{
js: ["content_script.js"],
run_at: "document_start",
},
],
},
files: {
"content_script.js": contentScript,
},
});
await extension.startup();
await extension.awaitMessage("background-page:done");
let url = `${BASE_URL}/file_sample.html`;
let contentPage = await ExtensionTestUtils.loadContentPage(url);
await extension.awaitMessage("content-script:done");
await extension.unload();
await contentPage.close();
}
await runWithPrefs(
[["extensions.webextensions.userScripts.enabled", false]],
run_userScript_on_pref_disabled_test
);
});
// This test verify that userScripts.onBeforeScript API Event is not available without
// a "user_scripts.api_script" property in the manifest.
add_task(async function test_user_script_api_script_required() {
let extension = ExtensionTestUtils.loadExtension({
manifest: {
content_scripts: [
{
js: ["content_script.js"],
run_at: "document_start",
},
],
user_scripts: {},
},
files: {
"content_script.js": function () {
browser.test.assertEq(
undefined,
browser.userScripts && browser.userScripts.onBeforeScript,
"Got an undefined onBeforeScript property as expected"
);
browser.test.sendMessage("no-onBeforeScript:done");
},
},
});
await extension.startup();
let url = `${BASE_URL}/file_sample.html`;
let contentPage = await ExtensionTestUtils.loadContentPage(url);
await extension.awaitMessage("no-onBeforeScript:done");
await extension.unload();
await contentPage.close();
});
add_task(async function test_scriptMetaData() {
function getTestCases(isUserScriptsRegister) {
return [
// When scriptMetadata is not set (or undefined), it is treated as if it were null.
// In the API script, the metadata is then expected to be null.
isUserScriptsRegister ? undefined : null,
// Falsey
null,
"",
false,
0,
// Truthy
true,
1,
"non-empty string",
// Objects
["some array with value"],
{ "some object": "with value" },
];
}
async function background() {
for (let scriptMetadata of getTestCases(true)) {
await browser.userScripts.register({
js: [{ file: "userscript.js" }],
runAt: "document_end",
scriptMetadata,
});
}
browser.test.sendMessage("background-page:done");
}
function apiScript() {
let testCases = getTestCases(false);
let i = 0;
browser.userScripts.onBeforeScript.addListener(script => {
script.defineGlobals({
checkMetadata() {
let expectation = testCases[i];
let metadata = script.metadata;
if (typeof expectation === "object" && expectation !== null) {
// Non-primitive values cannot be compared with assertEq,
// so serialize both and just verify that they are equal.
expectation = JSON.stringify(expectation);
metadata = JSON.stringify(script.metadata);
}
browser.test.assertEq(
expectation,
metadata,
`Expected metadata at call ${i}`
);
if (++i === testCases.length) {
browser.test.sendMessage("apiscript:done");
}
},
});
});
}
let extension = ExtensionTestUtils.loadExtension({
background: `${getTestCases};(${background})()`,
manifest: {
permissions: ["http://*/*/file_sample.html"],
user_scripts: {
api_script: "apiscript.js",
},
},
files: {
"apiscript.js": `${getTestCases};(${apiScript})()`,
"userscript.js": "checkMetadata();",
},
});
await extension.startup();
await extension.awaitMessage("background-page:done");
const pageUrl = `${BASE_URL}/file_sample.html`;
info(`Load content page: ${pageUrl}`);
const page = await ExtensionTestUtils.loadContentPage(pageUrl);
await extension.awaitMessage("apiscript:done");
await page.close();
await extension.unload();
});
add_task(async function test_userScriptOptions_js_property_required() {
function background() {
const userScriptOptions = {
runAt: "document_start",
};
browser.test.assertThrows(
() => browser.userScripts.register(userScriptOptions),
/Type error for parameter userScriptOptions \(Property \"js\" is required\)/,
"Got the expected error from userScripts.register when js property is missing"
);
browser.test.sendMessage("done");
}
let extension = ExtensionTestUtils.loadExtension({
background,
manifest: {
permissions: ["http://*/*/file_sample.html"],
user_scripts: {},
},
});
await extension.startup();
await extension.awaitMessage("done");
await extension.unload();
});
add_task(async function test_userScripts_are_unregistered_on_unload() {
let extension = ExtensionTestUtils.loadExtension({
manifest: {
permissions: ["http://*/*/file_sample.html"],
user_scripts: {
api_script: "api_script.js",
},
},
files: {
"userscript.js": "",
"extpage.html": `<!DOCTYPE html><script src="extpage.js"></script>`,
"extpage.js": async function extPage() {
await browser.userScripts.register({
js: [{ file: "userscript.js" }],
});
browser.test.sendMessage("user-script-registered");
},
},
});
await extension.startup();
equal(
// In order to read the `registeredContentScripts` map, we need to access
// the extension embedded in the `ExtensionWrapper` first.
extension.extension.registeredContentScripts.size,
0,
"no user scripts registered yet"
);
const url = `moz-extension://${extension.uuid}/extpage.html`;
info(`loading extension page: ${url}`);
const page = await ExtensionTestUtils.loadContentPage(url);
info("waiting for the user script to be registered");
await extension.awaitMessage("user-script-registered");
equal(
extension.extension.registeredContentScripts.size,
1,
"got registered user scripts in the extension content scripts map"
);
await page.close();
equal(
extension.extension.registeredContentScripts.size,
0,
"user scripts unregistered from the extension content scripts map"
);
await extension.unload();
});