summaryrefslogtreecommitdiffstats
path: root/testing/marionette/navigate.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /testing/marionette/navigate.js
parentInitial commit. (diff)
downloadfirefox-upstream.tar.xz
firefox-upstream.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/marionette/navigate.js')
-rw-r--r--testing/marionette/navigate.js414
1 files changed, 414 insertions, 0 deletions
diff --git a/testing/marionette/navigate.js b/testing/marionette/navigate.js
new file mode 100644
index 0000000000..4392045e93
--- /dev/null
+++ b/testing/marionette/navigate.js
@@ -0,0 +1,414 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+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",
+ EventDispatcher:
+ "chrome://marionette/content/actors/MarionetteEventsParent.jsm",
+ Log: "chrome://marionette/content/log.js",
+ MarionettePrefs: "chrome://marionette/content/prefs.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.
+ *
+ * @param {URL} current
+ * URL the browser is currently visiting.
+ * @param {Object} options
+ * @param {BrowsingContext=} options.browsingContext
+ * The current browsing context. Needed for targets of _parent and _top.
+ * @param {URL=} options.future
+ * Destination URL, if known.
+ * @param {target=} options.target
+ * Link target, if known.
+ *
+ * @return {boolean}
+ * Full page load would be expected if future is followed.
+ *
+ * @throws TypeError
+ * If <code>current</code> is not defined, or any of
+ * <code>current</code> or <code>future</code> are invalid URLs.
+ */
+navigate.isLoadEventExpected = function(current, options = {}) {
+ const { browsingContext, future, target } = options;
+
+ if (typeof current == "undefined") {
+ throw new TypeError("Expected at least one URL");
+ }
+
+ if (["_parent", "_top"].includes(target) && !browsingContext) {
+ throw new TypeError(
+ "Expected browsingContext when target is _parent or _top"
+ );
+ }
+
+ // Don't wait if the navigation happens in a different browsing context
+ if (
+ target === "_blank" ||
+ (target === "_parent" && browsingContext.parent) ||
+ (target === "_top" && browsingContext.top != browsingContext)
+ ) {
+ return false;
+ }
+
+ // Assume we will go somewhere exciting
+ if (typeof future == "undefined") {
+ return true;
+ }
+
+ // Assume javascript:<whatever> will modify the current document
+ // but this is not an entirely safe assumption to make,
+ // considering it could be used to set window.location
+ if (future.protocol == "javascript:") {
+ return false;
+ }
+
+ // If hashes are present and identical
+ if (
+ current.href.includes("#") &&
+ future.href.includes("#") &&
+ current.hash === future.hash
+ ) {
+ return false;
+ }
+
+ 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 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 {
+ browsingContextFn = driver.getBrowsingContext.bind(driver),
+ loadEventExpected = true,
+ requireBeforeUnload = true,
+ } = options;
+
+ const chromeWindow = browsingContextFn().topChromeWindow;
+ const pageLoadStrategy = driver.capabilities.get("pageLoadStrategy");
+
+ // Return immediately if no load event is expected
+ if (!loadEventExpected || pageLoadStrategy === PageLoadStrategy.None) {
+ await callback();
+ return Promise.resolve();
+ }
+
+ let rejectNavigation;
+ let resolveNavigation;
+
+ let seenBeforeUnload = false;
+ let seenUnload = false;
+
+ let unloadTimer;
+
+ const checkDone = ({ finished, error }) => {
+ if (finished) {
+ if (error) {
+ rejectNavigation(error);
+ } else {
+ resolveNavigation();
+ }
+ }
+ };
+
+ 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 }, message) => {
+ let data = MarionettePrefs.useActors ? message : json;
+
+ if (MarionettePrefs.useActors) {
+ // Only care about navigation events from the actor of the current frame.
+ // Bug 1674329: Always use the currently active browsing context,
+ // and not the original one to not cause hangs for remoteness changes.
+ if (data.browsingContext != browsingContextFn()) {
+ return;
+ }
+ } else if (
+ data.browsingContext.browserId != browsingContextFn().browserId
+ ) {
+ return;
+ }
+
+ logger.trace(truncate`Received event ${data.type} for ${data.documentURI}`);
+
+ switch (data.type) {
+ case "beforeunload":
+ seenBeforeUnload = true;
+ 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, data);
+ checkDone(result);
+ break;
+ }
+ };
+
+ // In the case when the currently selected frame is closed,
+ // there will be no further load events. Stop listening immediately.
+ const onBrowsingContextDiscarded = (subject, topic) => {
+ // With the currentWindowGlobal gone the browsing context hasn't been
+ // replaced due to a remoteness change but closed.
+ if (subject == browsingContextFn() && !subject.currentWindowGlobal) {
+ logger.trace(
+ "Canceled page load listener " +
+ `because browsing context with id ${subject.id} 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 });
+ };
+
+ chromeWindow.addEventListener("TabClose", onUnload);
+ chromeWindow.addEventListener("unload", onUnload);
+ driver.dialogObserver.add(onDialogOpened);
+ Services.obs.addObserver(
+ onBrowsingContextDiscarded,
+ "browsing-context-discarded"
+ );
+
+ if (MarionettePrefs.useActors) {
+ EventDispatcher.on("page-load", onNavigation);
+ } else {
+ driver.mm.addMessageListener(
+ "Marionette:NavigationEvent",
+ onNavigation,
+ true
+ );
+ }
+
+ return new TimedPromise(
+ async (resolve, reject) => {
+ rejectNavigation = reject;
+ resolveNavigation = resolve;
+
+ try {
+ await callback();
+
+ // 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
+ );
+ }
+ } catch (e) {
+ // Executing the callback above could destroy the actor pair before the
+ // command returns. Such an error has to be ignored.
+ if (e.name !== "AbortError") {
+ checkDone({ finished: true, error: e });
+ }
+ }
+ },
+ {
+ timeout: driver.timeouts.pageLoad,
+ }
+ ).finally(() => {
+ // Clean-up all registered listeners and timers
+ Services.obs.removeObserver(
+ onBrowsingContextDiscarded,
+ "browsing-context-discarded"
+ );
+ chromeWindow.removeEventListener("TabClose", onUnload);
+ chromeWindow.removeEventListener("unload", onUnload);
+ driver.dialogObserver?.remove(onDialogOpened);
+ unloadTimer?.cancel();
+
+ if (MarionettePrefs.useActors) {
+ EventDispatcher.off("page-load", onNavigation);
+ } else {
+ driver.mm.removeMessageListener(
+ "Marionette:NavigationEvent",
+ onNavigation,
+ true
+ );
+ }
+ });
+};