Bug 1736576 - Add support for multiple files in scripting.executeScript(). r=robwu

Instead of trying to make `_execute()` in `ext-tabs-base.js` accept
options for both the Scripting and Tabs APIs, we introduced an
`execute()` function in `parent/ext-scripting.js` that is similar to
`_execute()` but specific to the Scripting API options.

Differential Revision: https://phabricator.services.mozilla.com/D133872
This commit is contained in:
William Durand 2022-01-18 16:33:57 +00:00
parent 0eb950d9a0
commit a718b52a24
3 changed files with 199 additions and 14 deletions

View file

@ -8,6 +8,70 @@
var { ExtensionError } = ExtensionUtils;
/**
* Inserts a script in the given tab, and returns a promise which resolves when
* the operation has completed.
*
* @param {TabBase} tab
* The tab in which to perform the injection.
* @param {BaseContext} context
* The extension context for which to perform the injection.
* @param {Object} details
* The details object, specifying what to inject, where, and when.
* Derived from the ScriptInjection type.
* @param {string} kind
* The kind of data being injected. Possible choices: "js".
* @param {string} method
* The name of the method which was called to trigger the injection.
* Used to generate appropriate error messages on failure.
*
* @returns {Promise}
* Resolves to the result of the execution, once it has completed.
*/
const execute = (tab, context, details, kind, method) => {
let options = {
jsPaths: [],
extensionId: context.extension.id,
};
// TODO: Bug 1750765 - Add test coverage for this option.
options.hasActiveTabPermission = tab.hasActiveTabPermission;
options.matches = tab.extension.allowedOrigins.patterns.map(
host => host.pattern
);
if (details.code) {
options[`${kind}Code`] = details.code;
}
if (details.files) {
for (const file of details.files) {
let url = context.uri.resolve(file);
if (!tab.extension.isExtensionURL(url)) {
return Promise.reject({
message: "Files to be injected must be within the extension",
});
}
options[`${kind}Paths`].push(url);
}
}
// TODO: Bug 1736574 - Add support for multiple frame IDs and `allFrames`.
if (details.frameId) {
options.frameID = details.frameId;
}
options.runAt = "document_idle";
options.wantReturnValue = true;
// TODO: Bug 1736579 - Configure options for CSS injection, e.g., `cssPaths`
// and `cssOrigin`.
// This function is derived from `_execute()` in `parent/ext-tabs-base.js`,
// make sure to keep both in sync when relevant.
return tab.queryContent("Execute", options);
};
this.scripting = class extends ExtensionAPI {
getAPI(context) {
const { extension } = context;
@ -20,19 +84,11 @@ this.scripting = class extends ExtensionAPI {
let tab = tabManager.get(tabId);
let executeScriptDetails = {
code: null,
file: null,
runAt: "document_idle",
};
if (details.files) {
// TODO bug 1736576: Support more than one file.
executeScriptDetails.file = details.files[0];
} else {
let executeDetails = {
// Defined in `child/ext-scripting.js`.
executeScriptDetails.code = details.codeToExecute;
}
code: details.codeToExecute,
files: details.files,
};
const promises = [];
@ -42,9 +98,9 @@ this.scripting = class extends ExtensionAPI {
}
for (const frameId of frameIds) {
const details = { ...executeDetails, frameId };
promises.push(
tab
.executeScript(context, { ...executeScriptDetails, frameId })
execute(tab, context, details, "js", "executeScript")
// We return `null` when the result value is falsey.
.then(results => ({ frameId, result: results[0] || null }))
.catch(error => ({ frameId, result: null, error }))

View file

@ -808,6 +808,10 @@ class TabBase {
}
options.wantReturnValue = true;
// The scripting API (defined in `parent/ext-scripting.js`) has its own
// `execute()` function that calls `queryContent()` as well. Make sure to
// keep both in sync when relevant.
return this.queryContent("Execute", options);
}

View file

@ -685,6 +685,131 @@ add_task(async function test_executeScript_with_iframe_srcdoc() {
await extension.unload();
});
add_task(async function test_executeScript_with_multiple_files() {
let extension = makeExtension({
async background() {
const tabs = await browser.tabs.query({ active: true });
browser.test.assertEq(1, tabs.length, "expected 1 tab");
const results = await browser.scripting.executeScript({
target: { tabId: tabs[0].id },
files: ["1.js", "2.js"],
});
browser.test.assertEq(
1,
results.length,
"got expected number of results"
);
browser.test.assertEq(
"value from 2.js",
results[0].result,
"got the expected result"
);
browser.test.assertEq(0, results[0].frameId, "got the expected frameId");
browser.test.notifyPass("execute-script");
},
files: {
"1.js": function() {
return "value from 1.js";
},
"2.js": function() {
return "value from 2.js";
},
},
});
await extension.startup();
await extension.awaitFinish("execute-script");
await extension.unload();
});
add_task(async function test_executeScript_with_multiple_files_and_an_error() {
let tab = await AppTestDelegate.openNewForegroundTab(
window,
"https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_contains_iframe.html",
true
);
let extension = makeExtension({
async background() {
const tabs = await browser.tabs.query({ active: true });
browser.test.assertEq(1, tabs.length, "expected 1 tab");
const results = await browser.scripting.executeScript({
target: { tabId: tabs[0].id },
files: ["1.js", "2.js"],
});
browser.test.assertEq(
1,
results.length,
"got expected number of results"
);
browser.test.assertEq(null, results[0].result, "got the expected result");
browser.test.assertEq(0, results[0].frameId, "got the expected frameId");
browser.test.notifyPass("execute-script");
},
files: {
"1.js": function() {
throw new Error(`Thrown at ${location.pathname.split("/").pop()}`);
},
"2.js": function() {
return "value from 2.js";
},
},
});
consoleMonitor.start([{ message: /Thrown at file_contains_iframe/ }]);
await extension.startup();
await extension.awaitFinish("execute-script");
await extension.unload();
await consoleMonitor.finished();
await AppTestDelegate.removeTab(window, tab);
});
add_task(async function test_executeScript_with_file_not_in_extension() {
let tab = await AppTestDelegate.openNewForegroundTab(
window,
"https://test1.example.com/tests/toolkit/components/extensions/test/mochitest/file_contains_iframe.html",
true
);
let extension = makeExtension({
async background() {
const tabs = await browser.tabs.query({ active: true });
browser.test.assertEq(1, tabs.length, "expected 1 tab");
await browser.scripting.executeScript({
target: { tabId: tabs[0].id },
files: ["https://example.com/script.js"],
});
browser.test.notifyPass("execute-script");
},
});
consoleMonitor.start([{
message: /Files to be injected must be within the extension/,
}]);
await extension.startup();
await extension.awaitFinish("execute-script");
await extension.unload();
await consoleMonitor.finished();
await AppTestDelegate.removeTab(window, tab);
});
</script>
</body>