diff --git a/package.json b/package.json index 6bf865e716372..de18f097e808e 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "generate-api-docs-for-testing": "commonmark docs/api.md > docs/api.html", "clean-lib": "rimraf lib", "build": "npm run tsc && npm run generate-d-ts && npm run generate-esm-package-json", - "tsc": "npm run clean-lib && tsc --version && (npm run tsc-cjs & npm run tsc-esm) && (npm run tsc-compat-cjs & npm run tsc-compat-esm)", + "tsc": "npm run clean-lib && tsc --version && (npm run tsc-cjs && npm run tsc-esm) && (npm run tsc-compat-cjs && npm run tsc-compat-esm)", "tsc-cjs": "tsc -b src/tsconfig.cjs.json", "tsc-esm": "tsc -b src/tsconfig.esm.json", "tsc-compat-cjs": "tsc -b compat/cjs/tsconfig.json", @@ -107,6 +107,7 @@ "@types/rimraf": "3.0.2", "@types/sinon": "10.0.11", "@types/tar-fs": "2.0.1", + "@types/unbzip2-stream": "1.4.0", "@types/ws": "8.5.3", "@typescript-eslint/eslint-plugin": "5.23.0", "@typescript-eslint/parser": "5.22.0", diff --git a/scripts/ensure-pinned-deps.ts b/scripts/ensure-pinned-deps.ts index e51c8c60f047c..ea0d1fc8cbc99 100644 --- a/scripts/ensure-pinned-deps.ts +++ b/scripts/ensure-pinned-deps.ts @@ -21,7 +21,7 @@ const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies }; const invalidDeps = new Map(); for (const [depKey, depValue] of Object.entries(allDeps)) { - if (/[0-9]/.test(depValue[0])) { + if (/[0-9]/.test(depValue[0]!)) { continue; } diff --git a/src/common/Accessibility.ts b/src/common/Accessibility.ts index cf9a37fc5c284..062c3c9eb055f 100644 --- a/src/common/Accessibility.ts +++ b/src/common/Accessibility.ts @@ -180,10 +180,10 @@ export class Accessibility { */ public async snapshot( options: SnapshotOptions = {} - ): Promise { + ): Promise { const { interestingOnly = true, root = null } = options; const { nodes } = await this._client.send('Accessibility.getFullAXTree'); - let backendNodeId = null; + let backendNodeId: number | undefined; if (root) { const { node } = await this._client.send('DOM.describeNode', { objectId: root._remoteObject.objectId, @@ -191,19 +191,19 @@ export class Accessibility { backendNodeId = node.backendNodeId; } const defaultRoot = AXNode.createTree(nodes); - let needle = defaultRoot; + let needle: AXNode | null = defaultRoot; if (backendNodeId) { needle = defaultRoot.find( (node) => node.payload.backendDOMNodeId === backendNodeId ); if (!needle) return null; } - if (!interestingOnly) return this.serializeTree(needle)[0]; + if (!interestingOnly) return this.serializeTree(needle)[0] ?? null; const interestingNodes = new Set(); this.collectInterestingNodes(interestingNodes, defaultRoot, false); if (!interestingNodes.has(needle)) return null; - return this.serializeTree(needle, interestingNodes)[0]; + return this.serializeTree(needle, interestingNodes)[0] ?? null; } private serializeTree( @@ -496,7 +496,7 @@ class AXNode { nodeById.set(payload.nodeId, new AXNode(payload)); for (const node of nodeById.values()) { for (const childId of node.payload.childIds || []) - node.children.push(nodeById.get(childId)); + node.children.push(nodeById.get(childId)!); } return nodeById.values().next().value; } diff --git a/src/common/AriaQueryHandler.ts b/src/common/AriaQueryHandler.ts index fd06cfe2b2114..0187ec1a6bedb 100644 --- a/src/common/AriaQueryHandler.ts +++ b/src/common/AriaQueryHandler.ts @@ -19,6 +19,7 @@ import { ElementHandle, JSHandle } from './JSHandle.js'; import { Protocol } from 'devtools-protocol'; import { CDPSession } from './Connection.js'; import { DOMWorld, PageBinding, WaitForSelectorOptions } from './DOMWorld.js'; +import { assert } from './assert.js'; async function queryAXTree( client: CDPSession, @@ -32,7 +33,8 @@ async function queryAXTree( role, }); const filteredNodes: Protocol.Accessibility.AXNode[] = nodes.filter( - (node: Protocol.Accessibility.AXNode) => node.role.value !== 'StaticText' + (node: Protocol.Accessibility.AXNode) => + !node.role || node.role.value !== 'StaticText' ); return filteredNodes; } @@ -43,6 +45,13 @@ const knownAttributes = new Set(['name', 'role']); const attributeRegexp = /\[\s*(?\w+)\s*=\s*(?"|')(?\\.|.*?(?=\k))\k\s*\]/g; +type ARIAQueryOption = { name?: string; role?: string }; +function isKnownAttribute( + attribute: string +): attribute is keyof ARIAQueryOption { + return knownAttributes.has(attribute); +} + /* * The selectors consist of an accessible name to query for and optionally * further aria attributes on the form `[=]`. @@ -53,15 +62,16 @@ const attributeRegexp = * - 'label' queries for elements with name 'label' and any role. * - '[name=""][role="button"]' queries for elements with no name and role 'button'. */ -type ariaQueryOption = { name?: string; role?: string }; -function parseAriaSelector(selector: string): ariaQueryOption { - const queryOptions: ariaQueryOption = {}; +function parseAriaSelector(selector: string): ARIAQueryOption { + const queryOptions: ARIAQueryOption = {}; const defaultName = selector.replace( attributeRegexp, - (_, attribute: string, quote: string, value: string) => { + (_, attribute: string, _quote: string, value: string) => { attribute = attribute.trim(); - if (!knownAttributes.has(attribute)) - throw new Error(`Unknown aria attribute "${attribute}" in selector`); + assert( + isKnownAttribute(attribute), + `Unknown aria attribute "${attribute}" in selector` + ); queryOptions[attribute] = normalizeValue(value); return ''; } @@ -78,17 +88,21 @@ const queryOne = async ( const exeCtx = element.executionContext(); const { name, role } = parseAriaSelector(selector); const res = await queryAXTree(exeCtx._client, element, name, role); - if (res.length < 1) { + if (!res[0] || !res[0].backendDOMNodeId) { return null; } return exeCtx._adoptBackendNodeId(res[0].backendDOMNodeId); }; +declare global { + function ariaQuerySelector(selector: string): void; +} + const waitFor = async ( domWorld: DOMWorld, selector: string, options: WaitForSelectorOptions -): Promise> => { +): Promise | null> => { const binding: PageBinding = { name: 'ariaQuerySelector', pptrFunction: async (selector: string) => { diff --git a/src/common/Browser.ts b/src/common/Browser.ts index ae440208dca70..481490122d37c 100644 --- a/src/common/Browser.ts +++ b/src/common/Browser.ts @@ -246,7 +246,7 @@ export class Browser extends EventEmitter { private _connection: Connection; private _closeCallback: BrowserCloseCallback; private _targetFilterCallback: TargetFilterCallback; - private _isPageTargetCallback: IsPageTargetCallback; + private _isPageTargetCallback!: IsPageTargetCallback; private _defaultContext: BrowserContext; private _contexts: Map; private _screenshotTaskQueue: TaskQueue; @@ -572,33 +572,24 @@ export class Browser extends EventEmitter { ): Promise { const { timeout = 30000 } = options; let resolve: (value: Target | PromiseLike) => void; + let isResolved = false; const targetPromise = new Promise((x) => (resolve = x)); this.on(BrowserEmittedEvents.TargetCreated, check); this.on(BrowserEmittedEvents.TargetChanged, check); try { if (!timeout) return await targetPromise; - return await helper.waitWithTimeout( - Promise.race([ - targetPromise, - (async () => { - for (const target of this.targets()) { - if (await predicate(target)) { - return target; - } - } - await targetPromise; - })(), - ]), - 'target', - timeout - ); + this.targets().forEach(check); + return await helper.waitWithTimeout(targetPromise, 'target', timeout); } finally { - this.removeListener(BrowserEmittedEvents.TargetCreated, check); - this.removeListener(BrowserEmittedEvents.TargetChanged, check); + this.off(BrowserEmittedEvents.TargetCreated, check); + this.off(BrowserEmittedEvents.TargetChanged, check); } async function check(target: Target): Promise { - if (await predicate(target)) resolve(target); + if (!isResolved && (await predicate(target))) { + isResolved = true; + resolve(target); + } } } diff --git a/src/common/BrowserConnector.ts b/src/common/BrowserConnector.ts index 99add8d6e3be1..b3f2b3ba84664 100644 --- a/src/common/BrowserConnector.ts +++ b/src/common/BrowserConnector.ts @@ -93,7 +93,7 @@ export const connectToBrowser = async ( 'Exactly one of browserWSEndpoint, browserURL or transport must be passed to puppeteer.connect' ); - let connection = null; + let connection!: Connection; if (transport) { connection = new Connection('', transport, slowMo); } else if (browserWSEndpoint) { @@ -117,7 +117,7 @@ export const connectToBrowser = async ( browserContextIds, ignoreHTTPSErrors, defaultViewport, - null, + undefined, () => connection.send('Browser.close').catch(debugError), targetFilter, isPageTarget @@ -138,9 +138,11 @@ async function getWSEndpoint(browserURL: string): Promise { const data = await result.json(); return data.webSocketDebuggerUrl; } catch (error) { - error.message = - `Failed to fetch browser webSocket URL from ${endpointURL}: ` + - error.message; + if (error instanceof Error) { + error.message = + `Failed to fetch browser webSocket URL from ${endpointURL}: ` + + error.message; + } throw error; } } diff --git a/src/common/BrowserWebSocketTransport.ts b/src/common/BrowserWebSocketTransport.ts index 9d0e5c4592f74..0ebddb8a62016 100644 --- a/src/common/BrowserWebSocketTransport.ts +++ b/src/common/BrowserWebSocketTransport.ts @@ -41,8 +41,6 @@ export class BrowserWebSocketTransport implements ConnectionTransport { }); // Silently ignore all errors - we don't know what to do with them. this._ws.addEventListener('error', () => {}); - this.onmessage = null; - this.onclose = null; } send(message: string): void { diff --git a/src/common/ConsoleMessage.ts b/src/common/ConsoleMessage.ts index a14828c90be38..c31de8ceb609d 100644 --- a/src/common/ConsoleMessage.ts +++ b/src/common/ConsoleMessage.ts @@ -111,7 +111,7 @@ export class ConsoleMessage { * @returns The location of the console message. */ location(): ConsoleMessageLocation { - return this._stackTraceLocations.length ? this._stackTraceLocations[0] : {}; + return this._stackTraceLocations[0] ?? {}; } /** diff --git a/src/common/Coverage.ts b/src/common/Coverage.ts index 4f94f6916c62f..a77c4d1739ad5 100644 --- a/src/common/Coverage.ts +++ b/src/common/Coverage.ts @@ -395,10 +395,12 @@ export class CSSCoverage { }); } - const coverage = []; + const coverage: CoverageEntry[] = []; for (const styleSheetId of this._stylesheetURLs.keys()) { const url = this._stylesheetURLs.get(styleSheetId); + assert(url); const text = this._stylesheetSources.get(styleSheetId); + assert(text); const ranges = convertToDisjointRanges( styleSheetIdToCoverage.get(styleSheetId) || [] ); @@ -432,16 +434,19 @@ function convertToDisjointRanges( }); const hitCountStack = []; - const results = []; + const results: Array<{ + start: number; + end: number; + }> = []; let lastOffset = 0; // Run scanning line to intersect all ranges. for (const point of points) { if ( hitCountStack.length && lastOffset < point.offset && - hitCountStack[hitCountStack.length - 1] > 0 + hitCountStack[hitCountStack.length - 1]! > 0 ) { - const lastResult = results.length ? results[results.length - 1] : null; + const lastResult = results[results.length - 1]; if (lastResult && lastResult.end === lastOffset) lastResult.end = point.offset; else results.push({ start: lastOffset, end: point.offset }); diff --git a/src/common/DOMWorld.ts b/src/common/DOMWorld.ts index f647524a4c5cb..e709f82313904 100644 --- a/src/common/DOMWorld.ts +++ b/src/common/DOMWorld.ts @@ -501,37 +501,37 @@ export class DOMWorld { ): Promise { const handle = await this.$(selector); assert(handle, 'No node found for selector: ' + selector); - await handle!.click(options); - await handle!.dispose(); + await handle.click(options); + await handle.dispose(); } async focus(selector: string): Promise { const handle = await this.$(selector); assert(handle, 'No node found for selector: ' + selector); - await handle!.focus(); - await handle!.dispose(); + await handle.focus(); + await handle.dispose(); } async hover(selector: string): Promise { const handle = await this.$(selector); assert(handle, 'No node found for selector: ' + selector); - await handle!.hover(); - await handle!.dispose(); + await handle.hover(); + await handle.dispose(); } async select(selector: string, ...values: string[]): Promise { const handle = await this.$(selector); assert(handle, 'No node found for selector: ' + selector); - const result = await handle!.select(...values); - await handle!.dispose(); + const result = await handle.select(...values); + await handle.dispose(); return result; } async tap(selector: string): Promise { const handle = await this.$(selector); assert(handle, 'No node found for selector: ' + selector); - await handle!.tap(); - await handle!.dispose(); + await handle.tap(); + await handle.dispose(); } async type( @@ -541,8 +541,8 @@ export class DOMWorld { ): Promise { const handle = await this.$(selector); assert(handle, 'No node found for selector: ' + selector); - await handle!.type(text, options); - await handle!.dispose(); + await handle.type(text, options); + await handle.dispose(); } async waitForSelector( @@ -981,6 +981,7 @@ async function waitForPredicatePageFunction( if (polling === 'raf') return await pollRaf(); if (polling === 'mutation') return await pollMutation(); if (typeof polling === 'number') return await pollInterval(polling); + assert(false); /** * @returns {!Promise<*>} @@ -1023,7 +1024,7 @@ async function waitForPredicatePageFunction( await onRaf(); return result; - async function onRaf(): Promise { + async function onRaf(): Promise { if (timedOut) { fulfill(); return; @@ -1042,7 +1043,7 @@ async function waitForPredicatePageFunction( await onTimeout(); return result; - async function onTimeout(): Promise { + async function onTimeout(): Promise { if (timedOut) { fulfill(); return; diff --git a/src/common/Debug.ts b/src/common/Debug.ts index 105319870462b..433de847c2c0c 100644 --- a/src/common/Debug.ts +++ b/src/common/Debug.ts @@ -16,6 +16,11 @@ import { isNode } from '../environment.js'; +declare global { + // eslint-disable-next-line no-var + var __PUPPETEER_DEBUG: string; +} + /** * A debug function that can be used in any environment. * @@ -60,7 +65,7 @@ export const debug = (prefix: string): ((...args: unknown[]) => void) => { } return (...logArgs: unknown[]): void => { - const debugLevel = globalThis.__PUPPETEER_DEBUG as string; + const debugLevel = globalThis.__PUPPETEER_DEBUG; if (!debugLevel) return; const everythingShouldBeLogged = debugLevel === '*'; diff --git a/src/common/EvalTypes.ts b/src/common/EvalTypes.ts index b400f44b5fb47..38433178a6f81 100644 --- a/src/common/EvalTypes.ts +++ b/src/common/EvalTypes.ts @@ -47,7 +47,7 @@ export type Serializable = | string | boolean | null - | BigInt + | bigint | JSONArray | JSONObject; diff --git a/src/common/ExecutionContext.ts b/src/common/ExecutionContext.ts index 631b236a173c8..4ab47b527288f 100644 --- a/src/common/ExecutionContext.ts +++ b/src/common/ExecutionContext.ts @@ -53,7 +53,7 @@ export class ExecutionContext { /** * @internal */ - _world: DOMWorld; + _world?: DOMWorld; /** * @internal */ @@ -69,7 +69,7 @@ export class ExecutionContext { constructor( client: CDPSession, contextPayload: Protocol.Runtime.ExecutionContextDescription, - world: DOMWorld + world?: DOMWorld ) { this._client = client; this._world = world; @@ -277,12 +277,10 @@ export class ExecutionContext { ? helper.valueFromRemoteObject(remoteObject) : createJSHandle(this, remoteObject); - /** - * @param {*} arg - * @returns {*} - * @this {ExecutionContext} - */ - function convertArgument(this: ExecutionContext, arg: unknown): unknown { + function convertArgument( + this: ExecutionContext, + arg: unknown + ): Protocol.Runtime.CallArgument { if (typeof arg === 'bigint') // eslint-disable-line valid-typeof return { unserializableValue: `${arg.toString()}n` }; @@ -364,7 +362,7 @@ export class ExecutionContext { * @internal */ async _adoptBackendNodeId( - backendNodeId: Protocol.DOM.BackendNodeId + backendNodeId?: Protocol.DOM.BackendNodeId ): Promise { const { object } = await this._client.send('DOM.resolveNode', { backendNodeId: backendNodeId, diff --git a/src/common/FrameManager.ts b/src/common/FrameManager.ts index f3dcc32488375..b77da70c5e611 100644 --- a/src/common/FrameManager.ts +++ b/src/common/FrameManager.ts @@ -73,7 +73,7 @@ export class FrameManager extends EventEmitter { private _frames = new Map(); private _contextIdToContext = new Map(); private _isolatedWorlds = new Set(); - private _mainFrame: Frame; + private _mainFrame!: Frame; constructor( client: CDPSession, @@ -163,8 +163,9 @@ export class FrameManager extends EventEmitter { } catch (error) { // The target might have been closed before the initialization finished. if ( - error.message.includes('Target closed') || - error.message.includes('Session closed') + error instanceof Error && + (error.message.includes('Target closed') || + error.message.includes('Session closed')) ) { return; } @@ -212,7 +213,7 @@ export class FrameManager extends EventEmitter { async function navigate( client: CDPSession, url: string, - referrer: string, + referrer: string | undefined, frameId: string ): Promise { try { @@ -225,7 +226,10 @@ export class FrameManager extends EventEmitter { ? new Error(`${response.errorText} at ${url}`) : null; } catch (error) { - return error; + if (error instanceof Error) { + return error; + } + throw error; } } } @@ -261,9 +265,10 @@ export class FrameManager extends EventEmitter { } const frame = this._frames.get(event.targetInfo.targetId); - const session = Connection.fromSession(this._client).session( - event.sessionId - ); + const connection = Connection.fromSession(this._client); + if (!connection) return; + const session = connection.session(event.sessionId); + if (!session) return; if (frame) frame._updateClient(session); this.setupEventListeners(session); await this.initialize(session); @@ -272,6 +277,7 @@ export class FrameManager extends EventEmitter { private async _onDetachedFromTarget( event: Protocol.Target.DetachedFromTargetEvent ) { + if (!event.targetId) return; const frame = this._frames.get(event.targetId); if (frame && frame.isOOPFrame()) { // When an OOP iframe is removed from the page, it @@ -341,7 +347,7 @@ export class FrameManager extends EventEmitter { parentFrameId?: string ): void { if (this._frames.has(frameId)) { - const frame = this._frames.get(frameId); + const frame = this._frames.get(frameId)!; if (session && frame.isOOPFrame()) { // If an OOP iframes becomes a normal iframe again // it is first attached to the parent page before @@ -352,6 +358,7 @@ export class FrameManager extends EventEmitter { } assert(parentFrameId); const parentFrame = this._frames.get(parentFrameId); + assert(parentFrame); const frame = new Frame(this, parentFrame, frameId, session); this._frames.set(frame._id, frame); this.emit(FrameManagerEmittedEvents.FrameAttached, frame); @@ -388,6 +395,7 @@ export class FrameManager extends EventEmitter { } // Update frame payload. + assert(frame); frame._navigated(framePayload); this.emit(FrameManagerEmittedEvents.FrameNavigated, frame); @@ -445,10 +453,11 @@ export class FrameManager extends EventEmitter { contextPayload: Protocol.Runtime.ExecutionContextDescription, session: CDPSession ): void { - const auxData = contextPayload.auxData as { frameId?: string }; - const frameId = auxData ? auxData.frameId : null; - const frame = this._frames.get(frameId) || null; - let world = null; + const auxData = contextPayload.auxData as { frameId?: string } | undefined; + const frameId = auxData && auxData.frameId; + const frame = + typeof frameId === 'string' ? this._frames.get(frameId) : undefined; + let world: DOMWorld | undefined; if (frame) { // Only care about execution contexts created for the current session. if (frame._client !== session) return; @@ -640,7 +649,7 @@ export class Frame { * @internal */ _frameManager: FrameManager; - private _parentFrame?: Frame; + private _parentFrame: Frame | null; /** * @internal */ @@ -668,11 +677,11 @@ export class Frame { /** * @internal */ - _mainWorld: DOMWorld; + _mainWorld!: DOMWorld; /** * @internal */ - _secondaryWorld: DOMWorld; + _secondaryWorld!: DOMWorld; /** * @internal */ @@ -680,7 +689,7 @@ export class Frame { /** * @internal */ - _client: CDPSession; + _client!: CDPSession; /** * @internal @@ -692,7 +701,7 @@ export class Frame { client: CDPSession ) { this._frameManager = frameManager; - this._parentFrame = parentFrame; + this._parentFrame = parentFrame ?? null; this._url = ''; this._id = frameId; this._detached = false; @@ -1457,7 +1466,7 @@ function assertNoLegacyNavigationOptions(options: { 'ERROR: networkIdleInflight option is no longer supported.' ); assert( - options.waitUntil !== 'networkidle', + options['waitUntil'] !== 'networkidle', 'ERROR: "networkidle" option is no longer supported. Use "networkidle2" instead' ); } diff --git a/src/common/HTTPRequest.ts b/src/common/HTTPRequest.ts index cd57d3eb308ed..bbec19b811993 100644 --- a/src/common/HTTPRequest.ts +++ b/src/common/HTTPRequest.ts @@ -185,8 +185,8 @@ export class HTTPRequest { this._interceptHandlers = []; this._initiator = event.initiator; - for (const key of Object.keys(event.request.headers)) - this._headers[key.toLowerCase()] = event.request.headers[key]; + for (const [key, value] of Object.entries(event.request.headers)) + this._headers[key.toLowerCase()] = value; } /** diff --git a/src/common/HTTPResponse.ts b/src/common/HTTPResponse.ts index ea785afb00a23..69783ab604bfe 100644 --- a/src/common/HTTPResponse.ts +++ b/src/common/HTTPResponse.ts @@ -88,8 +88,9 @@ export class HTTPResponse { this._status = extraInfo ? extraInfo.statusCode : responsePayload.status; const headers = extraInfo ? extraInfo.headers : responsePayload.headers; - for (const key of Object.keys(headers)) - this._headers[key.toLowerCase()] = headers[key]; + for (const [key, value] of Object.entries(headers)) { + this._headers[key.toLowerCase()] = value; + } this._securityDetails = responsePayload.securityDetails ? new SecurityDetails(responsePayload.securityDetails) diff --git a/src/common/JSHandle.ts b/src/common/JSHandle.ts index 99c338ea81c9c..e2f715c8f8b76 100644 --- a/src/common/JSHandle.ts +++ b/src/common/JSHandle.ts @@ -114,7 +114,7 @@ const applyOffsetsToQuad = ( * * @public */ -export class JSHandle { +export class JSHandle { /** * @internal */ @@ -202,8 +202,8 @@ export class JSHandle { */ async getProperty(propertyName: string): Promise { const objectHandle = await this.evaluateHandle( - (object: Element, propertyName: string) => { - const result = { __proto__: null }; + (object: Element, propertyName: keyof Element) => { + const result: Record = { __proto__: null }; result[propertyName] = object[propertyName]; return result; }, @@ -234,13 +234,14 @@ export class JSHandle { * ``` */ async getProperties(): Promise> { + assert(this._remoteObject.objectId); const response = await this._client.send('Runtime.getProperties', { objectId: this._remoteObject.objectId, ownProperties: true, }); const result = new Map(); for (const property of response.result) { - if (!property.enumerable) continue; + if (!property.enumerable || !property.value) continue; result.set(property.name, createJSHandle(this._context, property.value)); } return result; @@ -402,6 +403,7 @@ export class ElementHandle< } = {} ): Promise { const frame = this._context.frame(); + if (!frame) return null; const secondaryContext = await frame._secondaryWorld.executionContext(); const adoptedRoot = await secondaryContext._adoptElementHandle(this); const handle = await frame._secondaryWorld.waitForSelector(selector, { @@ -475,6 +477,7 @@ export class ElementHandle< } = {} ): Promise { const frame = this._context.frame(); + if (!frame) return null; const secondaryContext = await frame._secondaryWorld.executionContext(); const adoptedRoot = await secondaryContext._adoptElementHandle(this); xpath = xpath.startsWith('//') ? '.' + xpath : xpath; @@ -494,7 +497,7 @@ export class ElementHandle< return result; } - asElement(): ElementHandle | null { + override asElement(): ElementHandle | null { return this; } @@ -534,7 +537,7 @@ export class ElementHandle< } const visibleRatio = await new Promise((resolve) => { const observer = new IntersectionObserver((entries) => { - resolve(entries[0].intersectionRatio); + resolve(entries[0]!.intersectionRatio); observer.disconnect(); }); observer.observe(element); @@ -560,14 +563,15 @@ export class ElementHandle< ): Promise<{ offsetX: number; offsetY: number }> { let offsetX = 0; let offsetY = 0; - while (frame.parentFrame()) { - const parent = frame.parentFrame(); - if (!frame.isOOPFrame()) { - frame = parent; + let currentFrame: Frame | null = frame; + while (currentFrame && currentFrame.parentFrame()) { + const parent = currentFrame.parentFrame(); + if (!currentFrame.isOOPFrame() || !parent) { + currentFrame = parent; continue; } const { backendNodeId } = await parent._client.send('DOM.getFrameOwner', { - frameId: frame._id, + frameId: currentFrame._id, }); const result = await parent._client.send('DOM.getBoxModel', { backendNodeId: backendNodeId, @@ -577,9 +581,9 @@ export class ElementHandle< } const contentBoxQuad = result.model.content; const topLeftCorner = this._fromProtocolQuad(contentBoxQuad)[0]; - offsetX += topLeftCorner.x; - offsetY += topLeftCorner.y; - frame = parent; + offsetX += topLeftCorner!.x; + offsetY += topLeftCorner!.y; + currentFrame = parent; } return { offsetX, offsetY }; } @@ -612,7 +616,7 @@ export class ElementHandle< .filter((quad) => computeQuadArea(quad) > 1); if (!quads.length) throw new Error('Node is either not clickable or not an HTMLElement'); - const quad = quads[0]; + const quad = quads[0]!; if (offset) { // Return the point of the first quad identified by offset. let minX = Number.MAX_SAFE_INTEGER; @@ -659,10 +663,10 @@ export class ElementHandle< private _fromProtocolQuad(quad: number[]): Array<{ x: number; y: number }> { return [ - { x: quad[0], y: quad[1] }, - { x: quad[2], y: quad[3] }, - { x: quad[4], y: quad[5] }, - { x: quad[6], y: quad[7] }, + { x: quad[0]!, y: quad[1]! }, + { x: quad[2]!, y: quad[3]! }, + { x: quad[4]!, y: quad[5]! }, + { x: quad[6]!, y: quad[7]! }, ]; } @@ -789,7 +793,7 @@ export class ElementHandle< throw new Error('Element is not a