"use strict"; /* * Copyright 2024 Google LLC. * Copyright (c) Microsoft Corporation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ Object.defineProperty(exports, "__esModule", { value: true }); exports.NavigationTracker = exports.NavigationState = exports.NavigationResult = void 0; const protocol_js_1 = require("../../../protocol/protocol.js"); const Deferred_js_1 = require("../../../utils/Deferred.js"); const log_js_1 = require("../../../utils/log.js"); const time_js_1 = require("../../../utils/time.js"); const urlHelpers_js_1 = require("../../../utils/urlHelpers.js"); const uuid_js_1 = require("../../../utils/uuid.js"); class NavigationResult { eventName; message; constructor(eventName, message) { this.eventName = eventName; this.message = message; } } exports.NavigationResult = NavigationResult; class NavigationState { navigationId = (0, uuid_js_1.uuidv4)(); #browsingContextId; #started = false; #finished = new Deferred_js_1.Deferred(); url; loaderId; #isInitial; #eventManager; committed = new Deferred_js_1.Deferred(); isFragmentNavigation; get finished() { return this.#finished; } constructor(url, browsingContextId, isInitial, eventManager) { this.#browsingContextId = browsingContextId; this.url = url; this.#isInitial = isInitial; this.#eventManager = eventManager; } navigationInfo() { return { context: this.#browsingContextId, navigation: this.navigationId, timestamp: (0, time_js_1.getTimestamp)(), url: this.url, }; } start() { if ( // Initial navigation should not be reported. !this.#isInitial && // No need in reporting started navigation twice. !this.#started && // No need for reporting fragment navigations. Step 13 vs step 16 of the spec: // https://html.spec.whatwg.org/#beginning-navigation:webdriver-bidi-navigation-started !this.isFragmentNavigation) { this.#eventManager.registerEvent({ type: 'event', method: protocol_js_1.ChromiumBidi.BrowsingContext.EventNames.NavigationStarted, params: this.navigationInfo(), }, this.#browsingContextId); } this.#started = true; } #finish(navigationResult) { this.#started = true; if (!this.#isInitial && !this.#finished.isFinished && navigationResult.eventName !== "browsingContext.load" /* NavigationEventName.Load */) { this.#eventManager.registerEvent({ type: 'event', method: navigationResult.eventName, params: this.navigationInfo(), }, this.#browsingContextId); } this.#finished.resolve(navigationResult); } frameNavigated() { this.committed.resolve(); if (!this.#isInitial) { this.#eventManager.registerEvent({ type: 'event', method: protocol_js_1.ChromiumBidi.BrowsingContext.EventNames.NavigationCommitted, params: this.navigationInfo(), }, this.#browsingContextId); } } fragmentNavigated() { this.committed.resolve(); this.#finish(new NavigationResult("browsingContext.fragmentNavigated" /* NavigationEventName.FragmentNavigated */)); } load() { this.#finish(new NavigationResult("browsingContext.load" /* NavigationEventName.Load */)); } fail(message) { this.#finish(new NavigationResult(this.committed.isFinished ? "browsingContext.navigationAborted" /* NavigationEventName.NavigationAborted */ : "browsingContext.navigationFailed" /* NavigationEventName.NavigationFailed */, message)); } } exports.NavigationState = NavigationState; /** * Keeps track of navigations. Details: http://go/webdriver:bidi-navigation */ class NavigationTracker { #eventManager; #logger; #loaderIdToNavigationsMap = new Map(); #browsingContextId; /** * Last committed navigation is committed, but is not guaranteed to be finished, as it * can still wait for `load` or `DOMContentLoaded` events. */ #lastCommittedNavigation; /** * Pending navigation is a navigation that is started but not yet committed. */ #pendingNavigation; // Flags if the initial navigation to `about:blank` is in progress. #isInitialNavigation = true; constructor(url, browsingContextId, eventManager, logger) { this.#browsingContextId = browsingContextId; this.#eventManager = eventManager; this.#logger = logger; this.#isInitialNavigation = true; // The initial navigation is always committed. this.#lastCommittedNavigation = new NavigationState(url, browsingContextId, (0, urlHelpers_js_1.urlMatchesAboutBlank)(url), this.#eventManager); } /** * Returns current started ongoing navigation. It can be either a started pending * navigation, or one is already navigated. */ get currentNavigationId() { if (this.#pendingNavigation?.isFragmentNavigation === false) { // Use pending navigation if it is started and it is not a fragment navigation. return this.#pendingNavigation.navigationId; } // If the pending navigation is a fragment one, or if it is not exists, the last // committed navigation should be used. return this.#lastCommittedNavigation.navigationId; } /** * Flags if the current navigation relates to the initial to `about:blank` navigation. */ get isInitialNavigation() { return this.#isInitialNavigation; } /** * Url of the last navigated navigation. */ get url() { return this.#lastCommittedNavigation.url; } /** * Creates a pending navigation e.g. when navigation command is called. Required to * provide navigation id before the actual navigation is started. It will be used when * navigation started. Can be aborted, failed, fragment navigated, or became a current * navigation. */ createPendingNavigation(url, canBeInitialNavigation = false) { this.#logger?.(log_js_1.LogType.debug, 'createCommandNavigation'); this.#isInitialNavigation = canBeInitialNavigation && this.#isInitialNavigation && (0, urlHelpers_js_1.urlMatchesAboutBlank)(url); this.#pendingNavigation?.fail('navigation canceled by concurrent navigation'); const navigation = new NavigationState(url, this.#browsingContextId, this.#isInitialNavigation, this.#eventManager); this.#pendingNavigation = navigation; return navigation; } dispose() { this.#pendingNavigation?.fail('navigation canceled by context disposal'); this.#lastCommittedNavigation.fail('navigation canceled by context disposal'); } // Update the current url. onTargetInfoChanged(url) { this.#logger?.(log_js_1.LogType.debug, `onTargetInfoChanged ${url}`); this.#lastCommittedNavigation.url = url; } #getNavigationForFrameNavigated(url, loaderId) { if (this.#loaderIdToNavigationsMap.has(loaderId)) { return this.#loaderIdToNavigationsMap.get(loaderId); } if (this.#pendingNavigation !== undefined && this.#pendingNavigation.loaderId === undefined) { // This can be a pending navigation to `about:blank` created by a command. Use the // pending navigation in this case. return this.#pendingNavigation; } // Create a new pending navigation. return this.createPendingNavigation(url, true); } /** * @param {string} unreachableUrl indicated the navigation is actually failed. */ frameNavigated(url, loaderId, unreachableUrl) { this.#logger?.(log_js_1.LogType.debug, `frameNavigated ${url}`); if (unreachableUrl !== undefined) { // The navigation failed. const navigation = this.#loaderIdToNavigationsMap.get(loaderId) ?? this.#pendingNavigation ?? this.createPendingNavigation(unreachableUrl, true); navigation.url = unreachableUrl; navigation.start(); navigation.fail('the requested url is unreachable'); return; } const navigation = this.#getNavigationForFrameNavigated(url, loaderId); if (navigation !== this.#lastCommittedNavigation) { // Even though the `lastCommittedNavigation` is navigated, it still can be waiting // for `load` or `DOMContentLoaded` events. this.#lastCommittedNavigation.fail('navigation canceled by concurrent navigation'); } navigation.url = url; navigation.loaderId = loaderId; this.#loaderIdToNavigationsMap.set(loaderId, navigation); navigation.start(); navigation.frameNavigated(); this.#lastCommittedNavigation = navigation; if (this.#pendingNavigation === navigation) { this.#pendingNavigation = undefined; } } navigatedWithinDocument(url, navigationType) { this.#logger?.(log_js_1.LogType.debug, `navigatedWithinDocument ${url}, ${navigationType}`); // Current navigation URL should be updated. this.#lastCommittedNavigation.url = url; if (navigationType !== 'fragment') { // TODO: check for other navigation types, like `javascript`. return; } // There is no way to map `navigatedWithinDocument` to a specific navigation. Consider // it is the pending navigation, if it is a fragment one. const fragmentNavigation = this.#pendingNavigation?.isFragmentNavigation === true ? this.#pendingNavigation : new NavigationState(url, this.#browsingContextId, false, this.#eventManager); // Finish ongoing navigation. fragmentNavigation.fragmentNavigated(); if (fragmentNavigation === this.#pendingNavigation) { this.#pendingNavigation = undefined; } } /** * Required to mark navigation as fully complete. * TODO: navigation should be complete when it became the current one on * `Page.frameNavigated` or on navigating command finished with a new loader Id. */ loadPageEvent(loaderId) { this.#logger?.(log_js_1.LogType.debug, 'loadPageEvent'); // Even if it was an initial navigation, it is finished. this.#isInitialNavigation = false; this.#loaderIdToNavigationsMap.get(loaderId)?.load(); } /** * Fail navigation due to navigation command failed. */ failNavigation(navigation, errorText) { this.#logger?.(log_js_1.LogType.debug, 'failCommandNavigation'); navigation.fail(errorText); } /** * Updates the navigation's `loaderId` and sets it as current one, if it is a * cross-document navigation. */ navigationCommandFinished(navigation, loaderId) { this.#logger?.(log_js_1.LogType.debug, `finishCommandNavigation ${navigation.navigationId}, ${loaderId}`); if (loaderId !== undefined) { navigation.loaderId = loaderId; this.#loaderIdToNavigationsMap.set(loaderId, navigation); } navigation.isFragmentNavigation = loaderId === undefined; } frameStartedNavigating(url, loaderId, navigationType) { this.#logger?.(log_js_1.LogType.debug, `frameStartedNavigating ${url}, ${loaderId}`); if (this.#pendingNavigation && this.#pendingNavigation?.loaderId !== undefined && this.#pendingNavigation?.loaderId !== loaderId) { // If there is a pending navigation with loader id set, but not equal to the new // loader id, cancel pending navigation. this.#pendingNavigation?.fail('navigation canceled by concurrent navigation'); this.#pendingNavigation = undefined; } if (this.#loaderIdToNavigationsMap.has(loaderId)) { const existingNavigation = this.#loaderIdToNavigationsMap.get(loaderId); // Navigation can be changed from `sameDocument` to `differentDocument`. existingNavigation.isFragmentNavigation = NavigationTracker.#isFragmentNavigation(navigationType); this.#pendingNavigation = existingNavigation; return; } const pendingNavigation = this.#pendingNavigation ?? this.createPendingNavigation(url, true); this.#loaderIdToNavigationsMap.set(loaderId, pendingNavigation); pendingNavigation.isFragmentNavigation = NavigationTracker.#isFragmentNavigation(navigationType); pendingNavigation.url = url; pendingNavigation.loaderId = loaderId; pendingNavigation.start(); } static #isFragmentNavigation(navigationType) { // Page.frameStartedNavigating.navigationType can be one of the following values: // reload, reloadBypassingCache, restore, restoreWithPost, historySameDocument, // historyDifferentDocument, sameDocument, differentDocument. // https://chromedevtools.github.io/devtools-protocol/tot/Page/#event-frameStartedNavigating return ['historySameDocument', 'sameDocument'].includes(navigationType); } /** * If there is a navigation with the loaderId equals to the network request id, it means * that the navigation failed. */ networkLoadingFailed(loaderId, errorText) { this.#loaderIdToNavigationsMap.get(loaderId)?.fail(errorText); } } exports.NavigationTracker = NavigationTracker; //# sourceMappingURL=NavigationTracker.js.map