appium-remote-debugger
Version:
Appium proxy for Remote Debugger protocol
277 lines • 11.8 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DEFAULT_PAGE_READINESS_TIMEOUT_MS = void 0;
exports.frameDetached = frameDetached;
exports.cancelPageLoad = cancelPageLoad;
exports.isPageLoadingCompleted = isPageLoadingCompleted;
exports.waitForDom = waitForDom;
exports.checkPageIsReady = checkPageIsReady;
exports.navToUrl = navToUrl;
const utils_1 = require("../utils");
const events_1 = require("./events");
const support_1 = require("@appium/support");
const lodash_1 = __importDefault(require("lodash"));
const bluebird_1 = __importStar(require("bluebird"));
const property_accessors_1 = require("./property-accessors");
exports.DEFAULT_PAGE_READINESS_TIMEOUT_MS = 20 * 1000;
const PAGE_READINESS_CHECK_INTERVAL_MS = 50;
/**
* pageLoadStrategy in WebDriver definitions.
*/
const PAGE_LOAD_STRATEGY = Object.freeze({
EAGER: 'eager',
NONE: 'none',
NORMAL: 'normal',
});
/**
* Emits a frame detached event when a frame is detached from the page.
* This is typically called by the RPC client when receiving a Page.frameDetached event.
*/
function frameDetached() {
this.emit(events_1.events.EVENT_FRAMES_DETACHED);
}
/**
* Cancels the current page load operation by unregistering from page readiness
* notifications and canceling any pending page load delay.
*/
function cancelPageLoad() {
this.log.debug('Unregistering from page readiness notifications');
(0, property_accessors_1.setPageLoading)(this, false);
(0, property_accessors_1.getPageLoadDelay)(this)?.cancel();
}
/**
* Determines if the current readyState indicates that page loading is completed
* based on the configured page load strategy.
*
* @param readyState - The document readyState value ('loading', 'interactive', or 'complete').
* @returns True if the page load is considered complete for the current strategy:
* - 'eager': returns true when readyState is not 'loading'
* - 'none': always returns true
* - 'normal' (default): returns true only when readyState is 'complete'
*/
function isPageLoadingCompleted(readyState) {
const pageLoadStrategy = lodash_1.default.toLower((0, property_accessors_1.getPageLoadStartegy)(this));
switch (pageLoadStrategy) {
case PAGE_LOAD_STRATEGY.EAGER:
// This could include 'interactive' or 'complete'
return readyState !== 'loading';
case PAGE_LOAD_STRATEGY.NONE:
return true;
case PAGE_LOAD_STRATEGY.NORMAL:
default:
return readyState === 'complete';
}
}
/**
* Waits for the DOM to be ready by periodically checking the page readiness state.
* Uses exponential backoff for retry intervals and respects the configured page load
* strategy and timeout settings.
*
* @param startPageLoadTimer - Optional timer instance to use for tracking elapsed time.
* If not provided, a new timer will be created and started.
*/
async function waitForDom(startPageLoadTimer) {
const readinessTimeoutMs = this.pageLoadMs;
this.log.debug(`Waiting up to ${readinessTimeoutMs}ms for the page to be ready`);
const timer = startPageLoadTimer ?? new support_1.timing.Timer().start();
let isPageLoading = true;
(0, property_accessors_1.setPageLoading)(this, true);
(0, property_accessors_1.setPageLoadDelay)(this, support_1.util.cancellableDelay(readinessTimeoutMs));
const pageReadinessPromise = bluebird_1.default.resolve((async () => {
let retry = 0;
while (isPageLoading) {
// if we are ready, or we've spend too much time on this
const elapsedMs = timer.getDuration().asMilliSeconds;
// exponential retry
const intervalMs = Math.min(PAGE_READINESS_CHECK_INTERVAL_MS * Math.pow(2, retry), readinessTimeoutMs - elapsedMs);
await bluebird_1.default.delay(intervalMs);
// we can get this called in the middle of trying to find a new app
if (!(0, property_accessors_1.getAppIdKey)(this)) {
this.log.debug('Not connected to an application. Ignoring page readiess check');
return;
}
if (!isPageLoading) {
return;
}
const maxWaitMs = (readinessTimeoutMs - elapsedMs) * 0.95;
if (await this.checkPageIsReady(maxWaitMs)) {
if (isPageLoading) {
this.log.debug(`Page is ready in ${elapsedMs}ms`);
isPageLoading = false;
}
return;
}
if (elapsedMs > readinessTimeoutMs) {
this.log.info(`Timed out after ${readinessTimeoutMs}ms of waiting for the page readiness. Continuing anyway`);
isPageLoading = false;
return;
}
retry++;
}
})());
const cancellationPromise = bluebird_1.default.resolve((async () => {
try {
await (0, property_accessors_1.getPageLoadDelay)(this);
}
catch { }
})());
try {
await bluebird_1.default.any([cancellationPromise, pageReadinessPromise]);
}
finally {
isPageLoading = false;
(0, property_accessors_1.setPageLoading)(this, false);
(0, property_accessors_1.setPageLoadDelay)(this, bluebird_1.default.resolve());
}
}
/**
* Checks if the current page is ready by executing a JavaScript command to
* retrieve the document readyState and evaluating it against the page load strategy.
*
* @param timeoutMs - Optional timeout in milliseconds for the readyState check.
* If not provided, uses the configured page ready timeout.
* @returns A promise that resolves to true if the page is ready according to
* the page load strategy, false otherwise or if the check times out.
*/
async function checkPageIsReady(timeoutMs) {
const readyCmd = 'document.readyState;';
const actualTimeoutMs = timeoutMs ?? (0, property_accessors_1.getPageReadyTimeout)(this);
try {
const readyState = await bluebird_1.default.resolve(this.execute(readyCmd)).timeout(actualTimeoutMs);
this.log.debug(JSON.stringify({
readyState,
pageLoadStrategy: (0, property_accessors_1.getPageLoadStartegy)(this) ?? PAGE_LOAD_STRATEGY.NORMAL,
}));
return this.isPageLoadingCompleted(readyState);
}
catch (err) {
if (err instanceof bluebird_1.TimeoutError) {
this.log.debug(`Page readiness check timed out after ${actualTimeoutMs}ms`);
}
else {
this.log.warn(`Page readiness check has failed. Original error: ${err.message}`);
}
return false;
}
}
/**
* Navigates to a new URL and waits for the page to be ready.
* Validates the URL format, waits for the page to be available, sets window.location.href
* via Runtime.evaluate (Page.navigate was removed from WebKit Inspector protocol), and
* monitors for the Page.loadEventFired event or timeout.
*
* @param url - The URL to navigate to. Must be a valid URL format.
* @throws TypeError if the provided URL is not a valid URL format.
*/
async function navToUrl(url) {
const { appIdKey, pageIdKey } = (0, utils_1.checkParams)({
appIdKey: (0, property_accessors_1.getAppIdKey)(this),
pageIdKey: (0, property_accessors_1.getPageIdKey)(this),
});
const rpcClient = this.requireRpcClient();
try {
new URL(url);
}
catch {
throw new TypeError(`'${url}' is not a valid URL`);
}
this.log.debug(`Navigating to new URL: '${url}'`);
(0, property_accessors_1.setNavigatingToPage)(this, true);
await rpcClient.waitForPage(appIdKey, pageIdKey);
const readinessTimeoutMs = this.pageLoadMs;
let onPageLoaded;
let onPageLoadedTimeout;
(0, property_accessors_1.setPageLoadDelay)(this, support_1.util.cancellableDelay(readinessTimeoutMs));
(0, property_accessors_1.setPageLoading)(this, true);
let isPageLoading = true;
const start = new support_1.timing.Timer().start();
const pageReadinessPromise = new bluebird_1.default((resolve) => {
onPageLoadedTimeout = setTimeout(() => {
if (isPageLoading) {
isPageLoading = false;
this.log.info(`Timed out after ${start.getDuration().asMilliSeconds.toFixed(0)}ms of waiting ` +
`for the ${url} page readiness. Continuing anyway`);
}
return resolve();
}, readinessTimeoutMs);
onPageLoaded = () => {
if (isPageLoading) {
isPageLoading = false;
this.log.debug(`The page ${url} is ready in ${start.getDuration().asMilliSeconds.toFixed(0)}ms`);
}
if (onPageLoadedTimeout) {
clearTimeout(onPageLoadedTimeout);
onPageLoadedTimeout = null;
}
return resolve();
};
// https://chromedevtools.github.io/devtools-protocol/tot/Page/#event-loadEventFired
rpcClient.once('Page.loadEventFired', onPageLoaded);
// Page.navigate was removed from the WebKit Inspector protocol since iOS 26.4
// See https://github.com/appium/appium/issues/21976
rpcClient.send('Runtime.evaluate', {
expression: `window.location.href = ${JSON.stringify(url)};`,
appIdKey,
pageIdKey,
});
});
const cancellationPromise = bluebird_1.default.resolve((async () => {
try {
await (0, property_accessors_1.getPageLoadDelay)(this);
}
catch { }
})());
try {
await bluebird_1.default.any([cancellationPromise, pageReadinessPromise]);
}
finally {
(0, property_accessors_1.setPageLoading)(this, false);
isPageLoading = false;
(0, property_accessors_1.setNavigatingToPage)(this, false);
(0, property_accessors_1.setPageLoadDelay)(this, bluebird_1.default.resolve());
if (onPageLoadedTimeout && pageReadinessPromise.isFulfilled()) {
clearTimeout(onPageLoadedTimeout);
onPageLoadedTimeout = null;
}
if (onPageLoaded) {
rpcClient.off('Page.loadEventFired', onPageLoaded);
}
}
}
//# sourceMappingURL=navigate.js.map