Bug 1612831 - [marionette] Move navigation commands to parent process. r=marionette-reviewers,maja_zf

Differential Revision: https://phabricator.services.mozilla.com/D80622
This commit is contained in:
Henrik Skupin 2020-09-15 04:53:24 +00:00
Родитель a64de8bf5f
Коммит 2866bc388a
7 изменённых файлов: 512 добавлений и 738 удалений

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

@ -110,6 +110,10 @@ this.GeckoDriver = function(server) {
this.sessionID = null;
this.wins = new browser.Windows();
this.browsers = {};
// Maps permanentKey to browsing context id: WeakMap.<Object, number>
this._browserIds = new WeakMap();
// points to current browser
this.curBrowser = null;
// top-most chrome window
@ -119,9 +123,6 @@ this.GeckoDriver = function(server) {
this.chromeBrowsingContext = null;
this.contentBrowsingContext = null;
this.observing = null;
this._browserIds = new WeakMap();
// Use content context by default
this.context = Context.Content;
@ -269,6 +270,8 @@ GeckoDriver.prototype.init = function() {
this.mm.addMessageListener("Marionette:ListenersAttached", this);
this.mm.addMessageListener("Marionette:Register", this);
this.mm.addMessageListener("Marionette:switchedToFrame", this);
this.mm.addMessageListener("Marionette:NavigationEvent", this);
this.mm.addMessageListener("Marionette:Unloaded", this, true);
};
GeckoDriver.prototype.uninit = function() {
@ -276,6 +279,8 @@ GeckoDriver.prototype.uninit = function() {
this.mm.removeMessageListener("Marionette:ListenersAttached", this);
this.mm.removeMessageListener("Marionette:Register", this);
this.mm.removeMessageListener("Marionette:switchedToFrame", this);
this.mm.removeMessageListener("Marionette:NavigationEvent", this);
this.mm.removeMessageListener("Marionette:Unloaded", this);
};
/**
@ -1188,29 +1193,14 @@ GeckoDriver.prototype.navigateTo = async function(cmd) {
const currentURL = await this._getCurrentURL();
const loadEventExpected = navigate.isLoadEventExpected(currentURL, validURL);
const navigated = this.listener.navigateTo({
url: validURL.href,
loadEventExpected,
pageTimeout: this.timeouts.pageLoad,
});
// If a process change of the frame script interrupts our page load, this
// will never return. We need to re-issue this request to correctly poll for
// readyState and send errors.
this.curBrowser.pendingCommands.push(() => {
let parameters = {
// TODO(ato): Bug 1242595
commandID: this.listener.activeMessageId,
pageTimeout: this.timeouts.pageLoad,
startTime: new Date().getTime(),
};
this.curBrowser.messageManager.sendAsyncMessage(
"Marionette:waitForPageLoaded",
parameters
);
});
await navigated;
const browsingContext = this.getBrowsingContext({ context: Context.Content });
await navigate.waitForNavigationCompleted(
this,
() => {
navigate.navigateTo(browsingContext, validURL);
},
{ loadEventExpected }
);
this.curBrowser.contentBrowser.focus();
};
@ -1308,32 +1298,16 @@ GeckoDriver.prototype.goBack = async function() {
assert.open(this.curBrowser);
await this._handleUserPrompts();
const browsingContext = this.getBrowsingContext({ context: Context.Content });
// If there is no history, just return
if (!this.curBrowser.contentBrowser.webNavigation.canGoBack) {
if (!browsingContext.top.embedderElement?.canGoBack) {
return;
}
let lastURL = await this._getCurrentURL();
let goBack = this.listener.goBack({ pageTimeout: this.timeouts.pageLoad });
// If a process change of the frame script interrupts our page load, this
// will never return. We need to re-issue this request to correctly poll for
// readyState and send errors.
this.curBrowser.pendingCommands.push(() => {
let parameters = {
// TODO(ato): Bug 1242595
commandID: this.listener.activeMessageId,
lastSeenURL: lastURL.href,
pageTimeout: this.timeouts.pageLoad,
startTime: new Date().getTime(),
};
this.curBrowser.messageManager.sendAsyncMessage(
"Marionette:waitForPageLoaded",
parameters
);
await navigate.waitForNavigationCompleted(this, () => {
browsingContext.goBack();
});
await goBack;
};
/**
@ -1352,34 +1326,16 @@ GeckoDriver.prototype.goForward = async function() {
assert.open(this.curBrowser);
await this._handleUserPrompts();
const browsingContext = this.getBrowsingContext({ context: Context.Content });
// If there is no history, just return
if (!this.curBrowser.contentBrowser.webNavigation.canGoForward) {
if (!browsingContext.top.embedderElement?.canGoForward) {
return;
}
let lastURL = await this._getCurrentURL();
let goForward = this.listener.goForward({
pageTimeout: this.timeouts.pageLoad,
await navigate.waitForNavigationCompleted(this, () => {
browsingContext.goForward();
});
// If a process change of the frame script interrupts our page load, this
// will never return. We need to re-issue this request to correctly poll for
// readyState and send errors.
this.curBrowser.pendingCommands.push(() => {
let parameters = {
// TODO(ato): Bug 1242595
commandID: this.listener.activeMessageId,
lastSeenURL: lastURL.href,
pageTimeout: this.timeouts.pageLoad,
startTime: new Date().getTime(),
};
this.curBrowser.messageManager.sendAsyncMessage(
"Marionette:waitForPageLoaded",
parameters
);
});
await goForward;
};
/**
@ -1398,25 +1354,13 @@ GeckoDriver.prototype.refresh = async function() {
assert.open(this.getCurrentWindow());
await this._handleUserPrompts();
let refresh = this.listener.refresh({ pageTimeout: this.timeouts.pageLoad });
// We need to move to the top frame before navigating
await this.listener.switchToFrame();
// If a process change of the frame script interrupts our page load, this
// will never return. We need to re-issue this request to correctly poll for
// readyState and send errors.
this.curBrowser.pendingCommands.push(() => {
let parameters = {
// TODO(ato): Bug 1242595
commandID: this.listener.activeMessageId,
pageTimeout: this.timeouts.pageLoad,
startTime: new Date().getTime(),
};
this.curBrowser.messageManager.sendAsyncMessage(
"Marionette:waitForPageLoaded",
parameters
);
const browsingContext = this.getBrowsingContext({ context: Context.Content });
await navigate.waitForNavigationCompleted(this, () => {
navigate.refresh(browsingContext);
});
await refresh;
};
/**
@ -2197,28 +2141,19 @@ GeckoDriver.prototype.clickElement = async function(cmd) {
break;
case Context.Content:
let click = this.listener.clickElement({
webElRef: webEl.toJSON(),
pageTimeout: this.timeouts.pageLoad,
});
const target = await this.listener.getElementAttribute(webEl, "target");
// If a process change of the frame script interrupts our page load,
// this will never return. We need to re-issue this request to correctly
// poll for readyState and send errors.
this.curBrowser.pendingCommands.push(() => {
let parameters = {
// TODO(ato): Bug 1242595
commandID: this.listener.activeMessageId,
pageTimeout: this.timeouts.pageLoad,
startTime: new Date().getTime(),
};
this.curBrowser.messageManager.sendAsyncMessage(
"Marionette:waitForPageLoaded",
parameters
);
});
await click;
await navigate.waitForNavigationCompleted(
this,
async () => {
await this.listener.clickElement(webEl);
},
{
browsingContext: this.getBrowsingContext(),
requireBeforeUnload: false,
loadEventExpected: target !== "_blank",
}
);
break;
default:
@ -2982,13 +2917,6 @@ GeckoDriver.prototype.deleteSession = function() {
this.chromeBrowsingContext = null;
this.contentBrowsingContext = null;
if (this.observing !== null) {
for (let topic in this.observing) {
Services.obs.removeObserver(this.observing[topic], topic);
}
this.observing = null;
}
if (this.dialogObserver) {
this.dialogObserver.cleanup();
this.dialogObserver = null;

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

@ -292,6 +292,13 @@ class TestNavigate(BaseNavigationTestCase):
self.marionette.navigate("about:blank")
def test_about_newtab(self):
with self.marionette.using_prefs({"browser.newtabpage.enabled": True}):
self.marionette.navigate("about:newtab")
self.marionette.navigate(self.test_page_remote)
self.marionette.find_element(By.ID, "testDiv")
@run_if_manage_instance("Only runnable if Marionette manages the instance")
def test_focus_after_navigation(self):
self.marionette.restart()

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

@ -4,8 +4,8 @@
from __future__ import absolute_import, print_function
from marionette_driver.errors import InvalidArgumentException, UnsupportedOperationException
from marionette_harness import MarionetteTestCase
from marionette_driver.errors import UnsupportedOperationException
from marionette_harness import MarionetteTestCase, skip
class TestReftest(MarionetteTestCase):
@ -30,6 +30,7 @@ class TestReftest(MarionetteTestCase):
super(TestReftest, self).tearDown()
@skip("Bug 1648444 - Unexpected page unload when refreshing about:blank")
def test_basic(self):
self.marionette._send_message("reftest:setup", {"screenshot": "unexpected"})
rv = self.marionette._send_message("reftest:run",

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

@ -43,6 +43,7 @@ XPCOMUtils.defineLazyModuleGetters(this, {
XPCOMUtils.defineLazyGetter(this, "logger", () => Log.getWithPrefix(contentId));
XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]);
const contentFrameMessageManager = this;
const contentId = content.docShell.browsingContext.id;
const curContainer = {
@ -90,405 +91,6 @@ const eventObservers = new ContentEventObserverService(
sendAsyncMessage.bind(this)
);
/**
* The load listener singleton helps to keep track of active page load
* activities, and can be used by any command which might cause a navigation
* to happen. In the specific case of a process change of the frame script it
* allows to continue observing the current page load.
*/
const loadListener = {
commandID: null,
seenBeforeUnload: false,
seenUnload: false,
timeout: null,
timerPageLoad: null,
timerPageUnload: null,
/**
* Start listening for page unload/load events.
*
* @param {number} commandID
* ID of the currently handled message between the driver and
* listener.
* @param {number} timeout
* Timeout in seconds the method has to wait for the page being
* finished loading.
* @param {number} startTime
* Unix timestap when the navitation request got triggered.
* @param {boolean=} waitForUnloaded
* If true wait for page unload events, otherwise only for page
* load events.
*/
start(commandID, timeout, startTime, waitForUnloaded = true) {
this.commandID = commandID;
this.timeout = timeout;
this.seenBeforeUnload = false;
this.seenUnload = false;
this.timerPageLoad = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
this.timerPageUnload = null;
// In case the frame script has been moved to a differnt process,
// wait the remaining time
timeout = startTime + timeout - new Date().getTime();
if (timeout <= 0) {
this.notify(this.timerPageLoad);
return;
}
if (waitForUnloaded) {
addEventListener("beforeunload", this, true);
addEventListener("hashchange", this, true);
addEventListener("pagehide", this, true);
addEventListener("popstate", this, true);
addEventListener("unload", this, true);
Services.obs.addObserver(this, "outer-window-destroyed");
} else {
// The frame script has been moved to a differnt content process.
// Due to the time it takes to re-register the browser in Marionette,
// it can happen that page load events are missed before the listeners
// are getting attached again. By checking the document readyState the
// command can return immediately if the page load is already done.
let readyState = content.document.readyState;
let documentURI = content.document.documentURI;
logger.trace(truncate`Check readyState ${readyState} for ${documentURI}`);
// If the page load has already finished, don't setup listeners and
// timers but return immediatelly.
if (this.handleReadyState(readyState, documentURI)) {
return;
}
addEventListener("DOMContentLoaded", loadListener, true);
addEventListener("pageshow", loadListener, true);
}
this.timerPageLoad.initWithCallback(
this,
timeout,
Ci.nsITimer.TYPE_ONE_SHOT
);
},
/**
* Stop listening for page unload/load events.
*/
stop() {
if (this.timerPageLoad) {
this.timerPageLoad.cancel();
}
if (this.timerPageUnload) {
this.timerPageUnload.cancel();
}
removeEventListener("beforeunload", this, true);
removeEventListener("hashchange", this, true);
removeEventListener("pagehide", this, true);
removeEventListener("popstate", this, true);
removeEventListener("DOMContentLoaded", this, true);
removeEventListener("pageshow", this, true);
removeEventListener("unload", this, true);
// In case the observer was added before the frame script has been moved
// to a different process, it will no longer be available. Exceptions can
// be ignored.
try {
Services.obs.removeObserver(this, "outer-window-destroyed");
} catch (e) {}
},
/**
* Callback for registered DOM events.
*/
handleEvent(event) {
// Only care about events from the currently selected browsing context,
// whereby some of those do not bubble up to the window.
if (
event.target != curContainer.frame &&
event.target != curContainer.frame.document
) {
return;
}
let location = event.target.documentURI || event.target.location.href;
logger.trace(truncate`Received DOM event ${event.type} for ${location}`);
switch (event.type) {
case "beforeunload":
this.seenBeforeUnload = true;
break;
case "unload":
this.seenUnload = true;
break;
case "pagehide":
this.seenUnload = true;
removeEventListener("hashchange", this, true);
removeEventListener("pagehide", this, true);
removeEventListener("popstate", this, true);
// Now wait until the target page has been loaded
addEventListener("DOMContentLoaded", this, true);
addEventListener("pageshow", this, true);
break;
case "hashchange":
case "popstate":
this.stop();
sendOk(this.commandID);
break;
case "DOMContentLoaded":
case "pageshow":
this.handleReadyState(
event.target.readyState,
event.target.documentURI
);
break;
}
},
/**
* Checks the value of readyState for the current page
* load activity, and resolves the command if the load
* has been finished. It also takes care of the selected
* page load strategy.
*
* @param {string} readyState
* Current ready state of the document.
* @param {string} documentURI
* Current document URI of the document.
*
* @return {boolean}
* True if the page load has been finished.
*/
handleReadyState(readyState, documentURI) {
let finished = false;
switch (readyState) {
case "interactive":
if (documentURI.startsWith("about:certerror")) {
this.stop();
sendError(new error.InsecureCertificateError(), this.commandID);
finished = true;
} else if (/about:.*(error)\?/.exec(documentURI)) {
this.stop();
sendError(
new error.UnknownError(`Reached error page: ${documentURI}`),
this.commandID
);
finished = true;
// Return early with a page load strategy of eager, and also
// special-case about:blocked pages which should be treated as
// non-error pages but do not raise a pageshow event. about:blank
// is also treaded specifically here, because it gets temporary
// loaded for new content processes, and we only want to rely on
// complete loads for it.
} else if (
(capabilities.get("pageLoadStrategy") === PageLoadStrategy.Eager &&
documentURI != "about:blank") ||
/about:blocked\?/.exec(documentURI)
) {
this.stop();
sendOk(this.commandID);
finished = true;
}
break;
case "complete":
this.stop();
sendOk(this.commandID);
finished = true;
break;
}
return finished;
},
/**
* Callback for navigation timeout timer.
*/
notify(timer) {
switch (timer) {
case this.timerPageUnload:
// In the case when a document has a beforeunload handler
// registered, the currently active command will return immediately
// due to the modal dialog observer in proxy.js.
//
// Otherwise the timeout waiting for the document to start
// navigating is increased by 5000 ms to ensure a possible load
// event is not missed. In the common case such an event should
// occur pretty soon after beforeunload, and we optimise for this.
if (this.seenBeforeUnload) {
this.seenBeforeUnload = null;
this.timerPageUnload.initWithCallback(
this,
5000,
Ci.nsITimer.TYPE_ONE_SHOT
);
// If no page unload has been detected, ensure to properly stop
// the load listener, and return from the currently active command.
} else if (!this.seenUnload) {
logger.debug(
"Canceled page load listener because no navigation " +
"has been detected"
);
this.stop();
sendOk(this.commandID);
}
break;
case this.timerPageLoad:
this.stop();
sendError(
new error.TimeoutError(
`Timeout loading page after ${this.timeout}ms`
),
this.commandID
);
break;
}
},
observe(subject, topic) {
logger.trace(`Received observer notification ${topic}`);
const winId = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
const bc = BrowsingContext.get(curContainer.id);
switch (topic) {
// In the case when the currently selected frame is closed,
// there will be no further load events. Stop listening immediately.
case "outer-window-destroyed":
if (bc.window.windowUtils.deprecatedOuterWindowID == winId) {
this.stop();
sendOk(this.commandID);
}
break;
}
},
/**
* Continue to listen for page load events after the frame script has been
* moved to a different content process.
*
* @param {number} commandID
* ID of the currently handled message between the driver and
* listener.
* @param {number} timeout
* Timeout in milliseconds the method has to wait for the page
* being finished loading.
* @param {number} startTime
* Unix timestap when the navitation request got triggered.
*/
waitForLoadAfterFramescriptReload(commandID, timeout, startTime) {
this.start(commandID, timeout, startTime, false);
},
/**
* Use a trigger callback to initiate a page load, and attach listeners if
* a page load is expected.
*
* @param {function} trigger
* Callback that triggers the page load.
* @param {number} commandID
* ID of the currently handled message between the driver and listener.
* @param {number} pageTimeout
* Timeout in milliseconds the method has to wait for the page
* finished loading.
* @param {boolean=} loadEventExpected
* Optional flag, which indicates that navigate has to wait for the page
* finished loading.
* @param {string=} url
* Optional URL, which is used to check if a page load is expected.
*/
async navigate(
trigger,
commandID,
timeout,
loadEventExpected = true,
useUnloadTimer = false
) {
// Only wait if the page load strategy is not `none`
loadEventExpected =
loadEventExpected &&
capabilities.get("pageLoadStrategy") !== PageLoadStrategy.None;
if (loadEventExpected) {
let startTime = new Date().getTime();
this.start(commandID, timeout, startTime, true);
}
await trigger();
try {
if (!loadEventExpected) {
sendOk(commandID);
return;
}
// If requested setup a timer to detect a possible page load
if (useUnloadTimer) {
this.timerPageUnload = Cc["@mozilla.org/timer;1"].createInstance(
Ci.nsITimer
);
this.timerPageUnload.initWithCallback(
this,
200,
Ci.nsITimer.TYPE_ONE_SHOT
);
}
} catch (e) {
if (loadEventExpected) {
this.stop();
}
sendError(e, commandID);
}
},
};
/**
* Called when listener is first started up. The listener sends its
* unique window ID and its current URI to the actor. If the actor returns
* an ID, we start the listeners. Otherwise, nothing happens.
*/
function registerSelf() {
logger.trace("Frame script loaded");
curContainer.frame = content;
sandboxes.clear();
legacyactions.mouseEventsOnly = false;
action.inputStateMap = new Map();
action.inputsToCancel = [];
let reply = sendSyncMessage("Marionette:Register", {
frameId: contentId,
});
if (reply.length == 0) {
logger.error("No reply from Marionette:Register");
return;
}
if (reply[0].frameId === contentId) {
logger.trace("Frame script registered");
startListeners();
sendAsyncMessage("Marionette:ListenersAttached", {
frameId: contentId,
});
}
}
// Eventually we will not have a closure for every single command,
// but use a generic dispatch for all listener commands.
//
@ -524,6 +126,7 @@ function dispatch(fn) {
};
}
let clickElementFn = dispatch(clickElement);
let getActiveElementFn = dispatch(getActiveElement);
let getBrowsingContextIdFn = dispatch(getBrowsingContextId);
let getCurrentUrlFn = dispatch(getCurrentUrl);
@ -553,10 +156,11 @@ let sendKeysToElementFn = dispatch(sendKeysToElement);
let reftestWaitFn = dispatch(reftestWait);
function startListeners() {
eventDispatcher.enable();
addMessageListener("Marionette:actionChain", actionChainFn);
addMessageListener("Marionette:cancelRequest", cancelRequest);
addMessageListener("Marionette:clearElement", clearElementFn);
addMessageListener("Marionette:clickElement", clickElement);
addMessageListener("Marionette:clickElement", clickElementFn);
addMessageListener("Marionette:Deregister", deregister);
addMessageListener("Marionette:DOM:AddEventListener", domAddEventListener);
addMessageListener(
@ -581,15 +185,11 @@ function startListeners() {
);
addMessageListener("Marionette:getPageSource", getPageSourceFn);
addMessageListener("Marionette:getScreenshotRect", getScreenshotRectFn);
addMessageListener("Marionette:goBack", goBack);
addMessageListener("Marionette:goForward", goForward);
addMessageListener("Marionette:isElementDisplayed", isElementDisplayedFn);
addMessageListener("Marionette:isElementEnabled", isElementEnabledFn);
addMessageListener("Marionette:isElementSelected", isElementSelectedFn);
addMessageListener("Marionette:multiAction", multiActionFn);
addMessageListener("Marionette:navigateTo", navigateTo);
addMessageListener("Marionette:performActions", performActionsFn);
addMessageListener("Marionette:refresh", refresh);
addMessageListener("Marionette:reftestWait", reftestWaitFn);
addMessageListener("Marionette:releaseActions", releaseActionsFn);
addMessageListener("Marionette:sendKeysToElement", sendKeysToElementFn);
@ -598,14 +198,14 @@ function startListeners() {
addMessageListener("Marionette:switchToFrame", switchToFrame);
addMessageListener("Marionette:switchToParentFrame", switchToParentFrame);
addMessageListener("Marionette:switchToShadowRoot", switchToShadowRootFn);
addMessageListener("Marionette:waitForPageLoaded", waitForPageLoaded);
}
function deregister() {
eventDispatcher.disable();
removeMessageListener("Marionette:actionChain", actionChainFn);
removeMessageListener("Marionette:cancelRequest", cancelRequest);
removeMessageListener("Marionette:clearElement", clearElementFn);
removeMessageListener("Marionette:clickElement", clickElement);
removeMessageListener("Marionette:clickElement", clickElementFn);
removeMessageListener("Marionette:Deregister", deregister);
removeMessageListener("Marionette:execute", executeFn);
removeMessageListener("Marionette:executeInSandbox", executeInSandboxFn);
@ -634,15 +234,11 @@ function deregister() {
);
removeMessageListener("Marionette:getPageSource", getPageSourceFn);
removeMessageListener("Marionette:getScreenshotRect", getScreenshotRectFn);
removeMessageListener("Marionette:goBack", goBack);
removeMessageListener("Marionette:goForward", goForward);
removeMessageListener("Marionette:isElementDisplayed", isElementDisplayedFn);
removeMessageListener("Marionette:isElementEnabled", isElementEnabledFn);
removeMessageListener("Marionette:isElementSelected", isElementSelectedFn);
removeMessageListener("Marionette:multiAction", multiActionFn);
removeMessageListener("Marionette:navigateTo", navigateTo);
removeMessageListener("Marionette:performActions", performActionsFn);
removeMessageListener("Marionette:refresh", refresh);
removeMessageListener("Marionette:releaseActions", releaseActionsFn);
removeMessageListener("Marionette:sendKeysToElement", sendKeysToElementFn);
removeMessageListener("Marionette:Session:Delete", deleteSession);
@ -650,7 +246,6 @@ function deregister() {
removeMessageListener("Marionette:switchToFrame", switchToFrame);
removeMessageListener("Marionette:switchToParentFrame", switchToParentFrame);
removeMessageListener("Marionette:switchToShadowRoot", switchToShadowRootFn);
removeMessageListener("Marionette:waitForPageLoaded", waitForPageLoaded);
}
function deleteSession() {
@ -1070,149 +665,6 @@ function multiAction(args, maxLen) {
setDispatch(concurrentEvent, pendingTouches);
}
/**
* Cancel the polling and remove the event listener associated with a
* current navigation request in case we're interupted by an onbeforeunload
* handler and navigation doesn't complete.
*/
function cancelRequest() {
loadListener.stop();
}
/**
* This implements the latter part of a get request (for the case we need
* to resume one when the frame script has been moved to a different content
* process in the middle of a navigate request). This is most of of the work
* of a navigate request, but doesn't assume DOMContentLoaded is yet to fire.
*
* @param {number} commandID
* ID of the currently handled message between the driver and
* listener.
* @param {number} pageTimeout
* Timeout in seconds the method has to wait for the page being
* finished loading.
* @param {number} startTime
* Unix timestap when the navitation request got triggered.
*/
function waitForPageLoaded(msg) {
let { commandID, pageTimeout, startTime } = msg.json;
loadListener.waitForLoadAfterFramescriptReload(
commandID,
pageTimeout,
startTime
);
}
/**
* Navigate to the given URL. The operation will be performed on the
* current browsing context, which means it handles the case where we
* navigate within an iframe. All other navigation is handled by the driver
* (in chrome space).
*/
async function navigateTo(msg) {
let { commandID, pageTimeout, url, loadEventExpected } = msg.json;
try {
await loadListener.navigate(
() => {
curContainer.frame.location = url;
},
commandID,
pageTimeout,
loadEventExpected
);
} catch (e) {
sendError(e, commandID);
}
}
/**
* Cause the browser to traverse one step backward in the joint history
* of the current browsing context.
*
* @param {number} commandID
* ID of the currently handled message between the driver and
* listener.
* @param {number} pageTimeout
* Timeout in milliseconds the method has to wait for the page being
* finished loading.
*/
async function goBack(msg) {
let { commandID, pageTimeout } = msg.json;
try {
await loadListener.navigate(
() => {
curContainer.frame.history.back();
},
commandID,
pageTimeout
);
} catch (e) {
sendError(e, commandID);
}
}
/**
* Cause the browser to traverse one step forward in the joint history
* of the current browsing context.
*
* @param {number} commandID
* ID of the currently handled message between the driver and
* listener.
* @param {number} pageTimeout
* Timeout in milliseconds the method has to wait for the page being
* finished loading.
*/
async function goForward(msg) {
let { commandID, pageTimeout } = msg.json;
try {
await loadListener.navigate(
() => {
curContainer.frame.history.forward();
},
commandID,
pageTimeout
);
} catch (e) {
sendError(e, commandID);
}
}
/**
* Causes the browser to reload the page in in current top-level browsing
* context.
*
* @param {number} commandID
* ID of the currently handled message between the driver and
* listener.
* @param {number} pageTimeout
* Timeout in milliseconds the method has to wait for the page being
* finished loading.
*/
async function refresh(msg) {
let { commandID, pageTimeout } = msg.json;
try {
// We need to move to the top frame before navigating
curContainer.frame = content;
sendSyncMessage("Marionette:switchedToFrame", {
browsingContextId: curContainer.id,
});
await loadListener.navigate(
() => {
curContainer.frame.location.reload(true);
},
commandID,
pageTimeout
);
} catch (e) {
sendError(e, commandID);
}
}
/**
* Get source of the current browsing context's DOM.
*/
@ -1289,45 +741,15 @@ function getCurrentUrl() {
/**
* Send click event to element.
*
* @param {number} commandID
* ID of the currently handled message between the driver and
* listener.
* @param {WebElement} webElRef
* Reference to the web element to click.
* @param {number} pageTimeout
* Timeout in milliseconds the method has to wait for the page being
* finished loading.
* @param {WebElement} el
* Element to click.
*/
async function clickElement(msg) {
let { commandID, webElRef, pageTimeout } = msg.json;
try {
let webEl = WebElement.fromJSON(webElRef);
let el = seenEls.get(webEl, curContainer.frame);
let loadEventExpected = true;
let target = getElementAttribute(el, "target");
if (target === "_blank") {
loadEventExpected = false;
}
await loadListener.navigate(
() => {
return interaction.clickElement(
el,
capabilities.get("moz:accessibilityChecks"),
capabilities.get("moz:webdriverClick")
);
},
commandID,
pageTimeout,
loadEventExpected,
true
);
} catch (e) {
sendError(e, commandID);
}
function clickElement(el) {
return interaction.clickElement(
el,
capabilities.get("moz:accessibilityChecks"),
capabilities.get("moz:webdriverClick")
);
}
function getElementAttribute(el, name) {
@ -1828,5 +1250,135 @@ function domRemoveEventListener(msg) {
eventObservers.remove(msg.json.type);
}
const eventDispatcher = {
enabled: false,
enable() {
if (this.enabled) {
return;
}
addEventListener("unload", this, false);
addEventListener("beforeunload", this, true);
addEventListener("pagehide", this, true);
addEventListener("popstate", this, true);
addEventListener("DOMContentLoaded", this, true);
addEventListener("hashchange", this, true);
addEventListener("pageshow", this, true);
Services.obs.addObserver(this, "webnavigation-destroy");
this.enabled = true;
},
disable() {
if (!this.enabled) {
return;
}
removeEventListener("unload", this, false);
removeEventListener("beforeunload", this, true);
removeEventListener("pagehide", this, true);
removeEventListener("popstate", this, true);
removeEventListener("DOMContentLoaded", this, true);
removeEventListener("hashchange", this, true);
removeEventListener("pageshow", this, true);
// In case the observer was added before the frame script has been moved
// to a different process, it will no longer be available. Exceptions can
// be ignored.
try {
Services.obs.removeObserver(this, "webnavigation-destroy");
} catch (e) {}
this.enabled = false;
},
handleEvent(event) {
const { target, type } = event;
// An unload event indicates that the framescript died because of a process
// change, or that the tab / window has been closed.
if (type === "unload" && target === contentFrameMessageManager) {
logger.trace(`Frame script unloaded`);
sendAsyncMessage("Marionette:Unloaded", {
browsingContext: content.docShell.browsingContext,
});
return;
}
// Only care about events from the currently selected browsing context,
// whereby some of those do not bubble up to the window.
if (![curContainer.frame, curContainer.frame.document].includes(target)) {
return;
}
if (type === "pagehide") {
// The content window has been replaced. Immediately register the page
// load events again so that we don't miss possible load events
addEventListener("DOMContentLoaded", this, true);
addEventListener("pageshow", this, true);
}
sendAsyncMessage("Marionette:NavigationEvent", {
browsingContext: content.docShell.browsingContext,
documentURI: target.documentURI,
readyState: target.readyState,
type,
});
},
observe(subject, topic) {
subject.QueryInterface(Ci.nsIDocShell);
const browsingContext = subject.browsingContext;
const isFrame = browsingContext !== subject.browsingContext.top;
// The currently selected iframe has been closed
if (isFrame && browsingContext.id === curContainer.id) {
logger.trace(`Frame with id ${browsingContext.id} got removed`);
sendAsyncMessage("Marionette:FrameRemoved", {
browsingContextId: browsingContext.id,
});
}
},
};
/**
* Called when listener is first started up. The listener sends its
* unique window ID and its current URI to the actor. If the actor returns
* an ID, we start the listeners. Otherwise, nothing happens.
*/
function registerSelf() {
logger.trace("Frame script loaded");
curContainer.frame = content;
sandboxes.clear();
legacyactions.mouseEventsOnly = false;
action.inputStateMap = new Map();
action.inputsToCancel = [];
let reply = sendSyncMessage("Marionette:Register", {
frameId: contentId,
});
if (reply.length == 0) {
logger.error("No reply from Marionette:Register");
return;
}
if (reply[0].frameId === contentId) {
startListeners();
sendAsyncMessage("Marionette:ListenersAttached", {
frameId: contentId,
});
}
}
// Call register self when we get loaded
registerSelf();

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

@ -6,9 +6,85 @@
const EXPORTED_SYMBOLS = ["navigate"];
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
XPCOMUtils.defineLazyModuleGetters(this, {
error: "chrome://marionette/content/error.js",
Log: "chrome://marionette/content/log.js",
modal: "chrome://marionette/content/modal.js",
PageLoadStrategy: "chrome://marionette/content/capabilities.js",
TimedPromise: "chrome://marionette/content/sync.js",
truncate: "chrome://marionette/content/format.js",
});
XPCOMUtils.defineLazyGetter(this, "logger", Log.get);
// Timeouts used to check if a new navigation has been initiated.
const TIMEOUT_BEFOREUNLOAD_EVENT = 200;
const TIMEOUT_UNLOAD_EVENT = 5000;
/** @namespace */
this.navigate = {};
/**
* Checks the value of readyState for the current page
* load activity, and resolves the command if the load
* has been finished. It also takes care of the selected
* page load strategy.
*
* @param {PageLoadStrategy} pageLoadStrategy
* Strategy when navigation is considered as finished.
* @param {object} eventData
* @param {string} eventData.documentURI
* Current document URI of the document.
* @param {string} eventData.readyState
* Current ready state of the document.
*
* @return {boolean}
* True if the page load has been finished.
*/
function checkReadyState(pageLoadStrategy, eventData = {}) {
const { documentURI, readyState } = eventData;
const result = { error: null, finished: false };
switch (readyState) {
case "interactive":
if (documentURI.startsWith("about:certerror")) {
result.error = new error.InsecureCertificateError();
result.finished = true;
} else if (/about:.*(error)\?/.exec(documentURI)) {
result.error = new error.UnknownError(
`Reached error page: ${documentURI}`
);
result.finished = true;
// Return early with a page load strategy of eager, and also
// special-case about:blocked pages which should be treated as
// non-error pages but do not raise a pageshow event. about:blank
// is also treaded specifically here, because it gets temporary
// loaded for new content processes, and we only want to rely on
// complete loads for it.
} else if (
(pageLoadStrategy === PageLoadStrategy.Eager &&
documentURI != "about:blank") ||
/about:blocked\?/.exec(documentURI)
) {
result.finished = true;
}
break;
case "complete":
result.finished = true;
break;
}
return result;
}
/**
* Determines if we expect to get a DOM load event (DOMContentLoaded)
* on navigating to the <code>future</code> URL.
@ -54,3 +130,228 @@ navigate.isLoadEventExpected = function(current, future = undefined) {
return true;
};
/**
* Load the given URL in the specified browsing context.
*
* @param {CanonicalBrowsingContext} browsingContext
* Browsing context to load the URL into.
* @param {string} url
* URL to navigate to.
*/
navigate.navigateTo = async function(browsingContext, url) {
const opts = {
loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_IS_LINK,
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
};
browsingContext.loadURI(url, opts);
};
/**
* Reload the page.
*
* @param {CanonicalBrowsingContext} browsingContext
* Browsing context to refresh.
*/
navigate.refresh = async function(browsingContext) {
const flags = Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
browsingContext.reload(flags);
};
/**
* Execute a callback and wait for a possible navigation to complete
*
* @param {GeckoDriver} driver
* Reference to driver instance.
* @param {Function} callback
* Callback to execute that might trigger a navigation.
* @param {Object} options
* @param {BrowsingContext=} browsingContext
* Browsing context to observe. Defaults to the current top-level
* browsing context.
* @param {boolean=} loadEventExpected
* If false, return immediately and don't wait for
* the navigation to be completed. Defaults to true.
* @param {boolean=} requireBeforeUnload
* If false and no beforeunload event is fired, abort waiting
* for the navigation. Defaults to true.
*/
navigate.waitForNavigationCompleted = async function waitForNavigationCompleted(
driver,
callback,
options = {}
) {
const {
browsingContext = driver.getBrowsingContext({ top: true }),
loadEventExpected = true,
requireBeforeUnload = true,
} = options;
const pageLoadStrategy = driver.capabilities.get("pageLoadStrategy");
const chromeWindow = browsingContext.topChromeWindow;
// Return immediately if no load event is expected
if (!loadEventExpected || pageLoadStrategy === PageLoadStrategy.None) {
return Promise.resolve();
}
return new TimedPromise(
async (resolve, reject) => {
const frameRemovedMessage = "Marionette:FrameRemoved";
const navigationMessage = "Marionette:NavigationEvent";
let seenBeforeUnload = false;
let seenUnload = false;
let unloadTimer;
const checkDone = ({ finished, error }) => {
if (finished) {
chromeWindow.removeEventListener("TabClose", onUnload);
chromeWindow.removeEventListener("unload", onUnload);
driver.dialogObserver.remove(onDialogOpened);
driver.mm.removeMessageListener(
frameRemovedMessage,
onFrameRemoved,
true
);
driver.mm.removeMessageListener(
navigationMessage,
onNavigation,
true
);
unloadTimer?.cancel();
if (error) {
reject(error);
} else {
resolve();
}
}
};
const onDialogOpened = (action, dialog, win) => {
// Only care about modals of the currently selected window.
if (win !== chromeWindow) {
return;
}
if (action === modal.ACTION_OPENED) {
logger.trace("Canceled page load listener because a dialog opened");
checkDone({ finished: true });
}
};
const onTimer = timer => {
// In the case when a document has a beforeunload handler
// registered, the currently active command will return immediately
// due to the modal dialog observer in proxy.js.
//
// Otherwise the timeout waiting for the document to start
// navigating is increased by 5000 ms to ensure a possible load
// event is not missed. In the common case such an event should
// occur pretty soon after beforeunload, and we optimise for this.
if (seenBeforeUnload) {
seenBeforeUnload = false;
unloadTimer.initWithCallback(
onTimer,
TIMEOUT_UNLOAD_EVENT,
Ci.nsITimer.TYPE_ONE_SHOT
);
// If no page unload has been detected, ensure to properly stop
// the load listener, and return from the currently active command.
} else if (!seenUnload) {
logger.trace(
"Canceled page load listener because no navigation " +
"has been detected"
);
checkDone({ finished: true });
}
};
const onNavigation = ({ json }) => {
if (json.browsingContext.browserId != browsingContext.browserId) {
return;
}
logger.trace(
truncate`Received message ${json.type} for ${json.documentURI}`
);
switch (json.type) {
case "beforeunload":
seenBeforeUnload = true;
seenUnload = false;
break;
case "pagehide":
seenUnload = true;
break;
case "hashchange":
case "popstate":
checkDone({ finished: true });
break;
case "DOMContentLoaded":
case "pageshow":
if (!seenUnload) {
return;
}
const result = checkReadyState(pageLoadStrategy, json);
checkDone(result);
break;
}
};
// In the case when the currently selected frame is closed,
// there will be no further load events. Stop listening immediately.
const onFrameRemoved = ({ json }) => {
if (json.browsingContextId != browsingContext.id) {
return;
}
logger.trace(
"Canceled page load listener because current frame has been removed"
);
checkDone({ finished: true });
};
const onUnload = event => {
logger.trace(
"Canceled page load listener " +
"because the top-browsing context has been closed"
);
checkDone({ finished: true });
};
// Certain commands like clickElement can cause a navigation. Setup a timer
// to check if a "beforeunload" event has been emitted within the given
// time frame. If not resolve the Promise.
if (!requireBeforeUnload) {
unloadTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
unloadTimer.initWithCallback(
onTimer,
TIMEOUT_BEFOREUNLOAD_EVENT,
Ci.nsITimer.TYPE_ONE_SHOT
);
}
chromeWindow.addEventListener("TabClose", onUnload);
chromeWindow.addEventListener("unload", onUnload);
driver.dialogObserver.add(onDialogOpened);
driver.mm.addMessageListener(frameRemovedMessage, onFrameRemoved, true);
driver.mm.addMessageListener(navigationMessage, onNavigation, true);
try {
await callback();
} catch (e) {
checkDone({ finished: true, error: e });
}
},
{
timeout: driver.timeouts.pageLoad,
}
);
};

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

@ -20,6 +20,7 @@ XPCOMUtils.defineLazyModuleGetters(this, {
capture: "chrome://marionette/content/capture.js",
error: "chrome://marionette/content/error.js",
Log: "chrome://marionette/content/log.js",
navigate: "chrome://marionette/content/navigate.js",
print: "chrome://marionette/content/print.js",
});
@ -124,11 +125,9 @@ reftest.Runner = class {
if (Services.appinfo.OS == "Android") {
logger.debug("Using current window");
reftestWin = this.parentWindow;
await this.driver.listener.navigateTo({
commandID: this.driver.listener.activeMessageId,
pageTimeout: timeout,
url: "about:blank",
loadEventExpected: true,
await navigate.waitForNavigationCompleted(this.driver, () => {
const browsingContext = this.driver.getBrowsingContext();
navigate.navigateTo(browsingContext, "about:blank");
});
} else {
logger.debug("Using separate window");
@ -616,14 +615,14 @@ max-width: ${width}px; max-height: ${height}px`;
}
async loadTestUrl(win, url, timeout) {
const browsingContext = this.driver.getBrowsingContext({ top: true });
logger.debug(`Starting load of ${url}`);
let navigateOpts = {
commandId: this.driver.listener.activeMessageId,
pageTimeout: timeout,
};
if (this.lastURL === url) {
logger.debug(`Refreshing page`);
await this.driver.listener.refresh(navigateOpts);
await navigate.waitForNavigationCompleted(this.driver, () => {
navigate.refresh(browsingContext);
});
} else {
// HACK: DocumentLoadListener currently doesn't know how to
// process-switch loads in a non-tabbed <browser>. We need to manually
@ -632,14 +631,14 @@ max-width: ${width}px; max-height: ${height}px`;
//
// See bug 1636169.
this.updateBrowserRemotenessByURL(win.gBrowser, url);
navigate.navigateTo(browsingContext, url);
navigateOpts.url = url;
navigateOpts.loadEventExpected = false;
await this.driver.listener.navigateTo(navigateOpts);
this.lastURL = url;
}
this.ensureFocus(win);
// TODO: Move all the wait logic into the parent process (bug 1648444)
await this.driver.listener.reftestWait(url, this.useRemoteTabs);
}

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

@ -50,20 +50,6 @@ function executeSoon(func) {
Services.tm.dispatchToMainThread(func);
}
/**
* @callback Condition
*
* @param {function(*)} resolve
* To be called when the condition has been met. Will return the
* resolved value.
* @param {function} reject
* To be called when the condition has not been met. Will cause
* the condition to be revaluated or time out.
*
* @return {*}
* The value from calling ``resolve``.
*/
/**
* Runs a Promise-like function off the main thread until it is resolved
* through ``resolve`` or ``rejected`` callbacks. The function is