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 const EXPORTED_SYMBOLS = [
9 "waitForInitialNavigationCompleted",
12 const { XPCOMUtils } = ChromeUtils.import(
13 "resource://gre/modules/XPCOMUtils.jsm"
16 XPCOMUtils.defineLazyModuleGetters(this, {
17 Log: "chrome://remote/content/shared/Log.jsm",
18 truncate: "chrome://remote/content/shared/Format.jsm",
21 XPCOMUtils.defineLazyGetter(this, "logger", () =>
22 Log.get(Log.TYPES.REMOTE_AGENT)
25 // Used to keep weak references of webProgressListeners alive.
26 const webProgressListeners = new Set();
29 * Wait until the initial load of the given WebProgress is done.
31 * @param {WebProgress} webProgress
32 * The WebProgress instance to observe.
33 * @param {Object=} options
34 * @param {Boolean=} options.resolveWhenStarted
35 * Flag to indicate that the Promise has to be resolved when the
36 * page load has been started. Otherwise wait until the page has
37 * finished loading. Defaults to `false`.
40 * Promise which resolves when the page load is in the expected state.
42 * - {nsIURI} currentURI The current URI of the page
43 * - {nsIURI} targetURI Target URI of the navigation
45 async function waitForInitialNavigationCompleted(webProgress, options = {}) {
46 const { resolveWhenStarted = false } = options;
48 const browsingContext = webProgress.browsingContext;
50 // Start the listener right away to avoid race conditions.
51 const listener = new ProgressListener(webProgress, { resolveWhenStarted });
52 const navigated = listener.start();
54 // Right after a browsing context has been attached it could happen that
55 // no window global has been set yet. Consider this as nothing has been
58 if (browsingContext.currentWindowGlobal) {
59 isInitial = browsingContext.currentWindowGlobal.isInitialDocument;
62 // If the current document is not the initial "about:blank" and is also
63 // no longer loading, assume the navigation is done and return.
64 if (!isInitial && !listener.isLoadingDocument) {
66 truncate`[${browsingContext.id}] Document already finished loading: ${browsingContext.currentURI?.spec}`
69 // Will resolve the navigated promise.
76 currentURI: listener.currentURI,
77 targetURI: listener.targetURI,
82 * WebProgressListener to observe for page loads.
84 class ProgressListener {
87 #waitForExplicitStart;
96 * Create a new WebProgressListener instance.
98 * @param {WebProgress} webProgress
99 * The web progress to attach the listener to.
100 * @param {Object=} options
101 * @param {Boolean=} options.resolveWhenStarted
102 * Flag to indicate that the Promise has to be resolved when the
103 * page load has been started. Otherwise wait until the page has
104 * finished loading. Defaults to `false`.
105 * @param {Number=} options.unloadTimeout
106 * Time to allow before the page gets unloaded. Defaults to 200ms.
107 * @param {Boolean=} options.waitForExplicitStart
108 * Flag to indicate that the Promise can only resolve after receiving a
109 * STATE_START state change. In other words, if the webProgress is already
110 * navigating, the Promise will only resolve for the next navigation.
111 * Defaults to `false`.
113 constructor(webProgress, options = {}) {
115 resolveWhenStarted = false,
117 waitForExplicitStart = false,
120 this.#resolveWhenStarted = resolveWhenStarted;
121 this.#unloadTimeout = unloadTimeout;
122 this.#waitForExplicitStart = waitForExplicitStart;
123 this.#webProgress = webProgress;
125 this.#resolve = null;
126 this.#seenStartFlag = false;
127 this.#targetURI = null;
128 this.#unloadTimer = null;
131 get browsingContext() {
132 return this.#webProgress.browsingContext;
136 return this.#webProgress.browsingContext.currentURI;
140 return this.#targetURI;
143 get isLoadingDocument() {
144 return this.#webProgress.isLoadingDocument;
147 #checkLoadingState(request, options = {}) {
148 const { isStart = false, isStop = false } = options;
150 if (isStart && !this.#seenStartFlag) {
151 this.#seenStartFlag = true;
153 this.#targetURI = this.#getTargetURI(request);
156 truncate`[${this.browsingContext.id}] ${this.constructor.name} state=start: ${this.targetURI?.spec}`
159 if (this.#unloadTimer) {
160 this.#unloadTimer.cancel();
161 this.#unloadTimer = null;
164 if (this.#resolveWhenStarted) {
165 // Return immediately when the load should not be awaited.
171 if (isStop && this.#seenStartFlag) {
173 truncate`[${this.browsingContext.id}] ${this.constructor.name} state=stop: ${this.currentURI.spec}`
180 #getTargetURI(request) {
182 return request.QueryInterface(Ci.nsIChannel).originalURI;
188 onStateChange(progress, request, flag, status) {
189 this.#checkLoadingState(request, {
190 isStart: flag & Ci.nsIWebProgressListener.STATE_START,
191 isStop: flag & Ci.nsIWebProgressListener.STATE_STOP,
196 * Start observing web progress changes.
199 * A promise that will resolve when the navigation has been finished.
203 throw new Error(`Progress listener already started`);
206 if (this.#webProgress.isLoadingDocument) {
207 this.#targetURI = this.#getTargetURI(this.#webProgress.documentRequest);
209 if (this.#resolveWhenStarted) {
210 // Resolve immediately when the page is already loading and there
211 // is no requirement to wait for it to finish.
212 return Promise.resolve();
216 const promise = new Promise(resolve => (this.#resolve = resolve));
218 // Enable all state notifications to get informed about an upcoming load
219 // as early as possible.
220 this.#webProgress.addProgressListener(
222 Ci.nsIWebProgress.NOTIFY_STATE_ALL
225 webProgressListeners.add(this);
227 if (this.#webProgress.isLoadingDocument && !this.#waitForExplicitStart) {
228 this.#checkLoadingState(this.#webProgress.documentRequest, {
232 // If the document is not loading yet wait some time for the navigation
234 this.#unloadTimer = Cc["@mozilla.org/timer;1"].createInstance(
237 this.#unloadTimer.initWithCallback(
240 truncate`[${this.browsingContext.id}] No navigation detected: ${this.currentURI?.spec}`
242 // Assume the target is the currently loaded URI.
243 this.#targetURI = this.currentURI;
247 Ci.nsITimer.TYPE_ONE_SHOT
255 * Stop observing web progress changes.
258 if (!this.#resolve) {
259 throw new Error(`Progress listener not yet started`);
262 this.#unloadTimer?.cancel();
263 this.#unloadTimer = null;
265 this.#webProgress.removeProgressListener(
267 Ci.nsIWebProgress.NOTIFY_STATE_ALL
269 webProgressListeners.delete(this);
271 if (!this.#targetURI) {
272 // If no target URI has been set yet it should be the current URI
273 this.#targetURI = this.browsingContext.currentURI;
277 this.#resolve = null;
281 return `[object ${this.constructor.name}]`;
284 get QueryInterface() {
285 return ChromeUtils.generateQI([
286 "nsIWebProgressListener",
287 "nsISupportsWeakReference",