1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
7 ChromeUtils.defineESModuleGetters(lazy, {
8 error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
10 "chrome://remote/content/marionette/actors/MarionetteEventsParent.sys.mjs",
11 Log: "chrome://remote/content/shared/Log.sys.mjs",
13 "chrome://remote/content/shared/webdriver/Capabilities.sys.mjs",
14 ProgressListener: "chrome://remote/content/shared/Navigate.sys.mjs",
15 TimedPromise: "chrome://remote/content/marionette/sync.sys.mjs",
16 truncate: "chrome://remote/content/shared/Format.sys.mjs",
19 ChromeUtils.defineLazyGetter(lazy, "logger", () =>
20 lazy.Log.get(lazy.Log.TYPES.MARIONETTE)
23 // Timeouts used to check if a new navigation has been initiated.
24 const TIMEOUT_BEFOREUNLOAD_EVENT = 200;
25 const TIMEOUT_UNLOAD_EVENT = 5000;
28 export const navigate = {};
31 * Checks the value of readyState for the current page
32 * load activity, and resolves the command if the load
33 * has been finished. It also takes care of the selected
36 * @param {PageLoadStrategy} pageLoadStrategy
37 * Strategy when navigation is considered as finished.
38 * @param {object} eventData
39 * @param {string} eventData.documentURI
40 * Current document URI of the document.
41 * @param {string} eventData.readyState
42 * Current ready state of the document.
45 * True if the page load has been finished.
47 function checkReadyState(pageLoadStrategy, eventData = {}) {
48 const { documentURI, readyState } = eventData;
50 const result = { error: null, finished: false };
54 if (documentURI.startsWith("about:certerror")) {
55 result.error = new lazy.error.InsecureCertificateError();
56 result.finished = true;
57 } else if (/about:.*(error)\?/.exec(documentURI)) {
58 result.error = new lazy.error.UnknownError(
59 `Reached error page: ${documentURI}`
61 result.finished = true;
63 // Return early with a page load strategy of eager, and also
64 // special-case about:blocked pages which should be treated as
65 // non-error pages but do not raise a pageshow event. about:blank
66 // is also treaded specifically here, because it gets temporary
67 // loaded for new content processes, and we only want to rely on
68 // complete loads for it.
70 (pageLoadStrategy === lazy.PageLoadStrategy.Eager &&
71 documentURI != "about:blank") ||
72 /about:blocked\?/.exec(documentURI)
74 result.finished = true;
79 result.finished = true;
87 * Determines if we expect to get a DOM load event (DOMContentLoaded)
88 * on navigating to the <code>future</code> URL.
90 * @param {URL} current
91 * URL the browser is currently visiting.
92 * @param {object} options
93 * @param {BrowsingContext=} options.browsingContext
94 * The current browsing context. Needed for targets of _parent and _top.
95 * @param {URL=} options.future
96 * Destination URL, if known.
97 * @param {target=} options.target
98 * Link target, if known.
101 * Full page load would be expected if future is followed.
104 * If <code>current</code> is not defined, or any of
105 * <code>current</code> or <code>future</code> are invalid URLs.
107 navigate.isLoadEventExpected = function (current, options = {}) {
108 const { browsingContext, future, target } = options;
110 if (typeof current == "undefined") {
111 throw new TypeError("Expected at least one URL");
114 if (["_parent", "_top"].includes(target) && !browsingContext) {
116 "Expected browsingContext when target is _parent or _top"
120 // Don't wait if the navigation happens in a different browsing context
122 target === "_blank" ||
123 (target === "_parent" && browsingContext.parent) ||
124 (target === "_top" && browsingContext.top != browsingContext)
129 // Assume we will go somewhere exciting
130 if (typeof future == "undefined") {
134 // Assume javascript:<whatever> will modify the current document
135 // but this is not an entirely safe assumption to make,
136 // considering it could be used to set window.location
137 if (future.protocol == "javascript:") {
141 // If hashes are present and identical
143 current.href.includes("#") &&
144 future.href.includes("#") &&
145 current.hash === future.hash
154 * Load the given URL in the specified browsing context.
156 * @param {CanonicalBrowsingContext} browsingContext
157 * Browsing context to load the URL into.
158 * @param {string} url
159 * URL to navigate to.
161 navigate.navigateTo = async function (browsingContext, url) {
163 loadFlags: Ci.nsIWebNavigation.LOAD_FLAGS_IS_LINK,
164 triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
165 // Fake user activation.
166 hasValidUserGestureActivation: true,
168 browsingContext.fixupAndLoadURIString(url, opts);
174 * @param {CanonicalBrowsingContext} browsingContext
175 * Browsing context to refresh.
177 navigate.refresh = async function (browsingContext) {
178 const flags = Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
179 browsingContext.reload(flags);
183 * Execute a callback and wait for a possible navigation to complete
185 * @param {GeckoDriver} driver
186 * Reference to driver instance.
187 * @param {Function} callback
188 * Callback to execute that might trigger a navigation.
189 * @param {object} options
190 * @param {BrowsingContext=} options.browsingContext
191 * Browsing context to observe. Defaults to the current browsing context.
192 * @param {boolean=} options.loadEventExpected
193 * If false, return immediately and don't wait for
194 * the navigation to be completed. Defaults to true.
195 * @param {boolean=} options.requireBeforeUnload
196 * If false and no beforeunload event is fired, abort waiting
197 * for the navigation. Defaults to true.
199 navigate.waitForNavigationCompleted = async function waitForNavigationCompleted(
205 browsingContextFn = driver.getBrowsingContext.bind(driver),
206 loadEventExpected = true,
207 requireBeforeUnload = true,
210 const browsingContext = browsingContextFn();
211 const chromeWindow = browsingContext.topChromeWindow;
212 const pageLoadStrategy = driver.currentSession.pageLoadStrategy;
214 // Return immediately if no load event is expected
215 if (!loadEventExpected) {
217 return Promise.resolve();
220 // When not waiting for page load events, do not return until the navigation has actually started.
221 if (pageLoadStrategy === lazy.PageLoadStrategy.None) {
222 const listener = new lazy.ProgressListener(browsingContext.webProgress, {
223 resolveWhenStarted: true,
224 waitForExplicitStart: true,
226 const navigated = listener.start();
227 navigated.finally(() => {
228 if (listener.isStarted) {
237 return Promise.resolve();
240 let rejectNavigation;
241 let resolveNavigation;
243 let browsingContextChanged = false;
244 let seenBeforeUnload = false;
245 let seenUnload = false;
249 const checkDone = ({ finished, error }) => {
252 rejectNavigation(error);
259 const onPromptClosed = (_, data) => {
260 if (data.detail.promptType === "beforeunload" && !data.detail.accepted) {
261 // If a beforeunload prompt is dismissed there will be no navigation.
263 `Canceled page load listener because a beforeunload prompt was dismissed`
265 checkDone({ finished: true });
269 const onPromptOpened = (_, data) => {
270 if (data.prompt.promptType === "beforeunload") {
271 // WebDriver HTTP basically doesn't know anything about beforeunload
272 // prompts. As such we always ignore the prompt opened event.
277 `Canceled page load listener because a ${data.prompt.promptType} prompt opened`
279 checkDone({ finished: true });
282 const onTimer = () => {
283 // For the command "Element Click" we want to detect a potential navigation
284 // as early as possible. The `beforeunload` event is an indication for that
285 // but could still cause the navigation to get aborted by the user. As such
286 // wait a bit longer for the `unload` event to happen (only when the page
287 // load strategy is `none`), which usually will occur pretty soon after
290 // Note that with WebDriver BiDi enabled the `beforeunload` prompts might
291 // not get implicitly accepted, so lets keep the timer around until we know
292 // that it is really not required.
293 if (seenBeforeUnload) {
294 seenBeforeUnload = false;
295 unloadTimer.initWithCallback(
297 TIMEOUT_UNLOAD_EVENT,
298 Ci.nsITimer.TYPE_ONE_SHOT
301 // If no page unload has been detected, ensure to properly stop
302 // the load listener, and return from the currently active command.
303 } else if (!seenUnload) {
305 "Canceled page load listener because no navigation " +
308 checkDone({ finished: true });
312 const onNavigation = (eventName, data) => {
313 const browsingContext = browsingContextFn();
315 // Ignore events from other browsing contexts than the selected one.
316 if (data.browsingContext != browsingContext) {
321 lazy.truncate`[${data.browsingContext.id}] Received event ${data.type} for ${data.documentURI}`
326 seenBeforeUnload = true;
335 checkDone({ finished: true });
338 case "DOMContentLoaded":
340 // Don't require an unload event when a top-level browsing context
342 if (!seenUnload && !browsingContextChanged) {
345 const result = checkReadyState(pageLoadStrategy, data);
352 // In the case when the currently selected frame is closed,
353 // there will be no further load events. Stop listening immediately.
354 const onBrowsingContextDiscarded = (subject, topic, why) => {
355 // If the BrowsingContext is being discarded to be replaced by another
356 // context, we don't want to stop waiting for the pageload to complete, as
357 // we will continue listening to the newly created context.
358 if (subject == browsingContextFn() && why != "replace") {
360 "Canceled page load listener " +
361 `because browsing context with id ${subject.id} has been removed`
363 checkDone({ finished: true });
367 // Detect changes to the top-level browsing context to not
368 // necessarily require an unload event.
369 const onBrowsingContextChanged = event => {
370 if (event.target === driver.curBrowser.contentBrowser) {
371 browsingContextChanged = true;
375 const onUnload = () => {
377 "Canceled page load listener " +
378 "because the top-browsing context has been closed"
380 checkDone({ finished: true });
383 chromeWindow.addEventListener("TabClose", onUnload);
384 chromeWindow.addEventListener("unload", onUnload);
385 driver.curBrowser.tabBrowser?.addEventListener(
386 "XULFrameLoaderCreated",
387 onBrowsingContextChanged
389 driver.promptListener.on("closed", onPromptClosed);
390 driver.promptListener.on("opened", onPromptOpened);
391 Services.obs.addObserver(
392 onBrowsingContextDiscarded,
393 "browsing-context-discarded"
396 lazy.EventDispatcher.on("page-load", onNavigation);
398 return new lazy.TimedPromise(
399 async (resolve, reject) => {
400 rejectNavigation = reject;
401 resolveNavigation = resolve;
406 // Certain commands like clickElement can cause a navigation. Setup a timer
407 // to check if a "beforeunload" event has been emitted within the given
408 // time frame. If not resolve the Promise.
409 if (!requireBeforeUnload) {
410 unloadTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
411 unloadTimer.initWithCallback(
413 TIMEOUT_BEFOREUNLOAD_EVENT,
414 Ci.nsITimer.TYPE_ONE_SHOT
418 // Executing the callback above could destroy the actor pair before the
419 // command returns. Such an error has to be ignored.
420 if (e.name !== "AbortError") {
421 checkDone({ finished: true, error: e });
426 errorMessage: "Navigation timed out",
427 timeout: driver.currentSession.timeouts.pageLoad,
430 // Clean-up all registered listeners and timers
431 Services.obs.removeObserver(
432 onBrowsingContextDiscarded,
433 "browsing-context-discarded"
435 chromeWindow.removeEventListener("TabClose", onUnload);
436 chromeWindow.removeEventListener("unload", onUnload);
437 driver.curBrowser.tabBrowser?.removeEventListener(
438 "XULFrameLoaderCreated",
439 onBrowsingContextChanged
441 driver.promptListener?.off("closed", onPromptClosed);
442 driver.promptListener?.off("opened", onPromptOpened);
443 unloadTimer?.cancel();
445 lazy.EventDispatcher.off("page-load", onNavigation);