зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
a64de8bf5f
Коммит
2866bc388a
|
@ -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
|
||||
|
|
Загрузка…
Ссылка в новой задаче