Bug 1597879 - Implement Page.addScriptToEvaluateOnNewDocument; r=remote-protocol-reviewers,whimboo,ato

Differential Revision: https://phabricator.services.mozilla.com/D55334

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Maja Frydrychowicz 2019-12-11 20:49:46 +00:00
Родитель bfe666563e
Коммит ab6bc896fd
4 изменённых файлов: 187 добавлений и 2 удалений

Просмотреть файл

@ -80,6 +80,10 @@ class ContextObserver {
this.emit("context-destroyed", { frameId }); this.emit("context-destroyed", { frameId });
this.emit("frame-navigated", { frameId, window }); this.emit("frame-navigated", { frameId, window });
this.emit("context-created", { windowId: id, window }); this.emit("context-created", { windowId: id, window });
// Delay script-loaded to allow context cleanup to happen first
Services.tm.dispatchToMainThread(() => {
this.emit("script-loaded");
});
break; break;
case "pageshow": case "pageshow":
// `persisted` is true when this is about a page being resurected from BF Cache // `persisted` is true when this is about a page being resurected from BF Cache
@ -89,6 +93,7 @@ class ContextObserver {
// XXX(ochameau) we might have to emit FrameNavigate here to properly handle BF Cache // XXX(ochameau) we might have to emit FrameNavigate here to properly handle BF Cache
// scenario in Page domain events // scenario in Page domain events
this.emit("context-created", { windowId: id, window }); this.emit("context-created", { windowId: id, window });
this.emit("script-loaded");
break; break;
case "pagehide": case "pagehide":

Просмотреть файл

@ -6,14 +6,25 @@
var EXPORTED_SYMBOLS = ["Page"]; var EXPORTED_SYMBOLS = ["Page"];
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
const { ContentProcessDomain } = ChromeUtils.import( const { ContentProcessDomain } = ChromeUtils.import(
"chrome://remote/content/domains/ContentProcessDomain.jsm" "chrome://remote/content/domains/ContentProcessDomain.jsm"
); );
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { UnsupportedError } = ChromeUtils.import( const { UnsupportedError } = ChromeUtils.import(
"chrome://remote/content/Error.jsm" "chrome://remote/content/Error.jsm"
); );
XPCOMUtils.defineLazyServiceGetter(
this,
"uuidGen",
"@mozilla.org/uuid-generator;1",
"nsIUUIDGenerator"
);
const { const {
LOAD_FLAGS_BYPASS_CACHE, LOAD_FLAGS_BYPASS_CACHE,
LOAD_FLAGS_BYPASS_PROXY, LOAD_FLAGS_BYPASS_PROXY,
@ -26,12 +37,19 @@ class Page extends ContentProcessDomain {
this.enabled = false; this.enabled = false;
this.lifecycleEnabled = false; this.lifecycleEnabled = false;
// script id => { source, worldName }
this.scriptsToEvaluateOnLoad = new Map();
this.worldsToEvaluateOnLoad = new Set();
this._onFrameNavigated = this._onFrameNavigated.bind(this); this._onFrameNavigated = this._onFrameNavigated.bind(this);
this._onScriptLoaded = this._onScriptLoaded.bind(this);
this.contextObserver.on("script-loaded", this._onScriptLoaded);
} }
destructor() { destructor() {
this.setLifecycleEventsEnabled({ enabled: false }); this.setLifecycleEventsEnabled({ enabled: false });
this.contextObserver.off("script-loaded", this._onScriptLoaded);
this.disable(); this.disable();
super.destructor(); super.destructor();
@ -124,7 +142,33 @@ class Page extends ContentProcessDomain {
}; };
} }
addScriptToEvaluateOnNewDocument() {} /**
* Enqueues given script to be evaluated in every frame upon creation
*
* If `worldName` is specified, creates an execution context with the given name
* and evaluates given script in it.
*
* At this time, queued scripts do not get evaluated, hence `source` is marked as
* "unsupported".
*
* @param {Object} options
* @param {string} options.source (not supported)
* @param {string=} options.worldName
* @return {string} Page.ScriptIdentifier
*/
addScriptToEvaluateOnNewDocument(options = {}) {
const { source, worldName } = options;
if (worldName) {
this.worldsToEvaluateOnLoad.add(worldName);
}
const identifier = uuidGen
.generateUUID()
.toString()
.slice(1, -1);
this.scriptsToEvaluateOnLoad.set(identifier, { worldName, source });
return { identifier };
}
/** /**
* Creates an isolated world for the given frame. * Creates an isolated world for the given frame.
@ -186,6 +230,20 @@ class Page extends ContentProcessDomain {
}); });
} }
_onScriptLoaded(name) {
const Runtime = this.session.domains.get("Runtime");
for (const world of this.worldsToEvaluateOnLoad) {
Runtime._onContextCreated("context-created", {
windowId: this.content.windowUtils.currentInnerWindowID,
window: this.content,
isDefault: false,
contextName: world,
contextType: "isolated",
});
}
// TODO evaluate each onNewDoc script in the appropriate world
}
emitLifecycleEvent(frameId, loaderId, name, timestamp) { emitLifecycleEvent(frameId, loaderId, name, timestamp) {
if (this.lifecycleEnabled) { if (this.lifecycleEnabled) {
this.emit("Page.lifecycleEvent", { frameId, loaderId, name, timestamp }); this.emit("Page.lifecycleEvent", { frameId, loaderId, name, timestamp });

Просмотреть файл

@ -16,5 +16,6 @@ support-files =
[browser_javascriptDialog_otherTarget.js] [browser_javascriptDialog_otherTarget.js]
[browser_javascriptDialog_prompt.js] [browser_javascriptDialog_prompt.js]
[browser_lifecycleEvent.js] [browser_lifecycleEvent.js]
[browser_scriptToEvaluateOnNewDocument.js]
[browser_reload.js] [browser_reload.js]
[browser_runtimeEvents.js] [browser_runtimeEvents.js]

Просмотреть файл

@ -0,0 +1,121 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test Page.addScriptToEvaluateOnNewDocument and Page.removeScriptToEvaluateOnNewDocument
const DOC = toDataURL("default-test-page");
const WORLD = "testWorld";
// TODO Bug 1601695 - Add support for `source` parameter
add_task(async function addScript({ Page, Runtime }) {
await loadURL(DOC);
const { identifier: id1 } = await Page.addScriptToEvaluateOnNewDocument({
source: "1 + 1;",
});
is(typeof id1, "string", "Script id should be a string");
ok(id1.length > 0, "Script id is non-empty");
const { identifier: id2 } = await Page.addScriptToEvaluateOnNewDocument({
source: "1 + 1;",
});
ok(id2.length > 0, "Script id is non-empty");
isnot(id1, id2, "Two scripts should have different ids");
await Runtime.enable();
// flush event for DOC default context
await Runtime.executionContextCreated();
await checkIsolatedContextAfterLoad(Runtime, toDataURL("<p>Hello"), []);
});
add_task(async function addScriptAfterNavigation({ Page }) {
await loadURL(DOC);
const { identifier: id1 } = await Page.addScriptToEvaluateOnNewDocument({
source: "1 + 1;",
});
is(typeof id1, "string", "Script id should be a string");
ok(id1.length > 0, "Script id is non-empty");
await loadURL(toDataURL("<p>Hello"));
const { identifier: id2 } = await Page.addScriptToEvaluateOnNewDocument({
source: "1 + 2;",
});
ok(id2.length > 0, "Script id is non-empty");
isnot(id1, id2, "Two scripts should have different ids");
});
add_task(async function addWithIsolatedWorldAndNavigate({ Page, Runtime }) {
await Runtime.enable();
const { frameId } = await Page.navigate({ url: DOC });
await Page.addScriptToEvaluateOnNewDocument({
source: "1 + 1;",
worldName: WORLD,
});
const isolatedId = await Page.createIsolatedWorld({
frameId,
worldName: WORLD,
grantUniversalAccess: true,
});
const contexts = await checkIsolatedContextAfterLoad(
Runtime,
toDataURL("<p>Next")
);
isnot(contexts[1].id, isolatedId, "The context has a new id");
});
add_task(async function addWithIsolatedWorldNavigateTwice({ Page, Runtime }) {
await Runtime.enable();
await Page.addScriptToEvaluateOnNewDocument({
source: "1 + 1;",
worldName: WORLD,
});
await checkIsolatedContextAfterLoad(Runtime, DOC);
await checkIsolatedContextAfterLoad(Runtime, toDataURL("<p>Hello"));
});
add_task(async function addTwoScriptsWithIsolatedWorld({ Page, Runtime }) {
await Runtime.enable();
const names = [WORLD, "A_whole_new_world"];
await Page.addScriptToEvaluateOnNewDocument({
source: "1 + 1;",
worldName: names[0],
});
await Page.addScriptToEvaluateOnNewDocument({
source: "1 + 8;",
worldName: names[1],
});
await checkIsolatedContextAfterLoad(Runtime, DOC, names);
});
async function checkIsolatedContextAfterLoad(Runtime, url, names = [WORLD]) {
// At least the default context will get created
const expected = names.length + 1;
const contextsCreated = new Promise(resolve => {
const ctx = [];
const unsubscribe = Runtime.executionContextCreated(payload => {
ctx.push(payload.context);
info(
`Runtime.executionContextCreated: ${payload.context.auxData.type}` +
`\n\turl ${payload.context.origin}`
);
if (ctx.length > expected) {
unsubscribe();
resolve(ctx);
}
});
timeoutPromise(1000).then(() => {
unsubscribe();
resolve(ctx);
});
});
await loadURL(url);
const contexts = await contextsCreated;
is(contexts.length, expected, "Expected number of contexts got created");
is(contexts[0].auxData.isDefault, true, "Got default context");
is(contexts[0].auxData.type, "default", "Got default context");
is(contexts[0].name, "", "Get context with empty name");
names.forEach((name, index) => {
is(contexts[index + 1].name, name, "Get context with expected name");
is(contexts[index + 1].auxData.isDefault, false, "Got isolated context");
is(contexts[index + 1].auxData.type, "isolated", "Got isolated context");
});
return contexts;
}