diff --git a/package.json b/package.json index 41d8fc852e1a0..5f6ac9c9013ec 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-correct-devtools-protocol-package.ts b/scripts/ensure-correct-devtools-protocol-package.ts index 2d6a4a1d1b7db..1c94dbccbaaae 100644 --- a/scripts/ensure-correct-devtools-protocol-package.ts +++ b/scripts/ensure-correct-devtools-protocol-package.ts @@ -66,7 +66,7 @@ const output = execSync(command, { encoding: 'utf8', }); -const bestRevisionFromNpm = output.split(' ')[1].replace(/'|\n/g, ''); +const bestRevisionFromNpm = output.split(' ')[1]!.replace(/'|\n/g, ''); if (currentProtocolPackageInstalledVersion !== bestRevisionFromNpm) { console.log(`ERROR: bad devtools-protocol revision detected: 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/scripts/test-ts-definition-files.ts b/scripts/test-ts-definition-files.ts index 7e4e83737218a..42f4b7b02c105 100644 --- a/scripts/test-ts-definition-files.ts +++ b/scripts/test-ts-definition-files.ts @@ -10,9 +10,9 @@ const EXPECTED_ERRORS = new Map([ "bad.ts(6,35): error TS2551: Property 'launh' does not exist on type", "bad.ts(8,29): error TS2551: Property 'devics' does not exist on type", 'bad.ts(12,39): error TS2554: Expected 0 arguments, but got 1.', - "bad.ts(20,5): error TS2345: Argument of type '(divElem: number) => any' is not assignable to parameter of type 'EvaluateFn", + "bad.ts(20,5): error TS2345: Argument of type '(divElem: number) => any' is not assignable to parameter of type 'EvaluateFn'.", "bad.ts(20,34): error TS2339: Property 'innerText' does not exist on type 'number'.", - "bad.ts(24,45): error TS2344: Type '(x: number) => string' does not satisfy the constraint 'EvaluateFn'.", + "bad.ts(24,45): error TS2344: Type '(x: number) => string' does not satisfy the constraint 'EvaluateFn'.", "bad.ts(27,34): error TS2339: Property 'innerText' does not exist on type 'number'.", ], ], @@ -39,7 +39,7 @@ const EXPECTED_ERRORS = new Map([ "bad.js(7,29): error TS2551: Property 'devics' does not exist on type", 'bad.js(11,39): error TS2554: Expected 0 arguments, but got 1.', "bad.js(15,9): error TS2322: Type 'ElementHandle | null' is not assignable to type 'ElementHandle'", - "bad.js(22,5): error TS2345: Argument of type '(divElem: number) => any' is not assignable to parameter of type 'EvaluateFn'.", + "bad.js(22,5): error TS2345: Argument of type '(divElem: number) => any' is not assignable to parameter of type 'EvaluateFn'.", "bad.js(22,26): error TS2339: Property 'innerText' does not exist on type 'number'.", ], ], @@ -73,7 +73,7 @@ const EXPECTED_ERRORS = new Map([ ]); const PROJECT_FOLDERS = [...EXPECTED_ERRORS.keys()]; -if (!process.env.CI) { +if (!process.env['CI']) { console.log(`IMPORTANT: this script assumes you have compiled Puppeteer and its types file before running. Make sure you have run: => npm run tsc && npm run generate-d-ts @@ -107,7 +107,7 @@ function packPuppeteer() { const tar = packPuppeteer(); const tarPath = path.join(process.cwd(), tar); -function compileAndCatchErrors(projectLocation) { +function compileAndCatchErrors(projectLocation: string) { const { status, stdout, stderr } = spawnSync('npm', ['run', 'compile'], { cwd: projectLocation, encoding: 'utf-8', @@ -159,7 +159,7 @@ function testProject(folder: string) { } ); - if (status > 0) { + if (status) { console.error( 'Installing the tar file unexpectedly failed', stdout, 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..d9e58cc33ea82 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,7 +88,7 @@ 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); @@ -88,7 +98,7 @@ const waitFor = async ( domWorld: DOMWorld, selector: string, options: WaitForSelectorOptions -): Promise> => { +): Promise | null> => { const binding: PageBinding = { name: 'ariaQuerySelector', pptrFunction: async (selector: string) => { @@ -98,7 +108,10 @@ const waitFor = async ( }, }; return domWorld.waitForSelectorInPage( - (_: Element, selector: string) => globalThis.ariaQuerySelector(selector), + (_: Element, selector: string) => + ( + globalThis as unknown as { ariaQuerySelector(selector: string): void } + ).ariaQuerySelector(selector), selector, options, binding diff --git a/src/common/Browser.ts b/src/common/Browser.ts index ae440208dca70..bf8cb39f8b7b3 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 ((await predicate(target)) && !isResolved) { + 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..2d1069176d685 100644 --- a/src/common/DOMWorld.ts +++ b/src/common/DOMWorld.ts @@ -500,38 +500,38 @@ export class DOMWorld { options: { delay?: number; button?: MouseButton; clickCount?: number } ): Promise { const handle = await this.$(selector); - assert(handle, 'No node found for selector: ' + selector); - await handle!.click(options); - await handle!.dispose(); + assert(handle, `No element found for selector: ${selector}`); + 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(); + assert(handle, `No element found for selector: ${selector}`); + 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(); + assert(handle, `No element found for selector: ${selector}`); + 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(); + assert(handle, `No element found for selector: ${selector}`); + 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(); + assert(handle, `No element found for selector: ${selector}`); + await handle.tap(); + await handle.dispose(); } async type( @@ -540,9 +540,9 @@ export class DOMWorld { options?: { delay: number } ): Promise { const handle = await this.$(selector); - assert(handle, 'No node found for selector: ' + selector); - await handle!.type(text, options); - await handle!.dispose(); + assert(handle, `No element found for selector: ${selector}`); + await handle.type(text, options); + await handle.dispose(); } async waitForSelector( @@ -551,9 +551,7 @@ export class DOMWorld { ): Promise { const { updatedSelector, queryHandler } = getQueryHandlerAndSelector(selector); - if (!queryHandler.waitFor) { - throw new Error('Query handler does not support waitFor'); - } + assert(queryHandler.waitFor, 'Query handler does not support waiting'); return queryHandler.waitFor(this, updatedSelector, options); } @@ -970,7 +968,7 @@ async function waitForPredicatePageFunction( root: Element | Document | null, predicateBody: string, predicateAcceptsContextElement: boolean, - polling: string, + polling: 'raf' | 'mutation' | number, timeout: number, ...args: unknown[] ): Promise { @@ -978,9 +976,14 @@ async function waitForPredicatePageFunction( const predicate = new Function('...args', predicateBody); let timedOut = false; if (timeout) setTimeout(() => (timedOut = true), timeout); - if (polling === 'raf') return await pollRaf(); - if (polling === 'mutation') return await pollMutation(); - if (typeof polling === 'number') return await pollInterval(polling); + switch (polling) { + case 'raf': + return await pollRaf(); + case 'mutation': + return await pollMutation(); + default: + return await pollInterval(polling); + } /** * @returns {!Promise<*>} @@ -1023,7 +1026,7 @@ async function waitForPredicatePageFunction( await onRaf(); return result; - async function onRaf(): Promise { + async function onRaf(): Promise { if (timedOut) { fulfill(); return; @@ -1042,7 +1045,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..08924bc346bf8 100644 --- a/src/common/EvalTypes.ts +++ b/src/common/EvalTypes.ts @@ -19,7 +19,9 @@ import { JSHandle, ElementHandle } from './JSHandle.js'; /** * @public */ -export type EvaluateFn = string | ((arg1: T, ...args: any[]) => any); +export type EvaluateFn = + | string + | ((arg1: T, ...args: U[]) => V); /** * @public */ @@ -47,7 +49,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..68464351c9aa5 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); + assert(connection); + const session = connection.session(event.sessionId); + assert(session); 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 @@ -324,6 +330,7 @@ export class FrameManager extends EventEmitter { } mainFrame(): Frame { + assert(this._mainFrame, 'Requesting main frame too early!'); return this._mainFrame; } @@ -341,7 +348,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 +359,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 +396,7 @@ export class FrameManager extends EventEmitter { } // Update frame payload. + assert(frame); frame._navigated(framePayload); this.emit(FrameManagerEmittedEvents.FrameNavigated, frame); @@ -445,10 +454,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 +650,7 @@ export class Frame { * @internal */ _frameManager: FrameManager; - private _parentFrame?: Frame; + private _parentFrame: Frame | null; /** * @internal */ @@ -668,11 +678,11 @@ export class Frame { /** * @internal */ - _mainWorld: DOMWorld; + _mainWorld!: DOMWorld; /** * @internal */ - _secondaryWorld: DOMWorld; + _secondaryWorld!: DOMWorld; /** * @internal */ @@ -680,7 +690,7 @@ export class Frame { /** * @internal */ - _client: CDPSession; + _client!: CDPSession; /** * @internal @@ -692,7 +702,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 +1467,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..96954f717e405 100644 --- a/src/common/JSHandle.ts +++ b/src/common/JSHandle.ts @@ -38,10 +38,10 @@ import { MouseButton } from './Input.js'; * @public */ export interface BoxModel { - content: Array<{ x: number; y: number }>; - padding: Array<{ x: number; y: number }>; - border: Array<{ x: number; y: number }>; - margin: Array<{ x: number; y: number }>; + content: Point[]; + padding: Point[]; + border: Point[]; + margin: Point[]; width: number; height: number; } @@ -49,15 +49,7 @@ export interface BoxModel { /** * @public */ -export interface BoundingBox { - /** - * the x coordinate of the element in pixels. - */ - x: number; - /** - * the y coordinate of the element in pixels. - */ - y: number; +export interface BoundingBox extends Point { /** * the width of the element in pixels. */ @@ -90,11 +82,8 @@ export function createJSHandle( return new JSHandle(context, context._client, remoteObject); } -const applyOffsetsToQuad = ( - quad: Array<{ x: number; y: number }>, - offsetX: number, - offsetY: number -) => quad.map((part) => ({ x: part.x + offsetX, y: part.y + offsetY })); +const applyOffsetsToQuad = (quad: Point[], offsetX: number, offsetY: number) => + quad.map((part) => ({ x: part.x + offsetX, y: part.y + offsetY })); /** * Represents an in-page JavaScript object. JSHandles can be created with the @@ -202,8 +191,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 +223,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 +392,7 @@ export class ElementHandle< } = {} ): Promise { const frame = this._context.frame(); + assert(frame); const secondaryContext = await frame._secondaryWorld.executionContext(); const adoptedRoot = await secondaryContext._adoptElementHandle(this); const handle = await frame._secondaryWorld.waitForSelector(selector, { @@ -475,6 +466,7 @@ export class ElementHandle< } = {} ): Promise { const frame = this._context.frame(); + assert(frame); const secondaryContext = await frame._secondaryWorld.executionContext(); const adoptedRoot = await secondaryContext._adoptElementHandle(this); xpath = xpath.startsWith('//') ? '.' + xpath : xpath; @@ -494,7 +486,7 @@ export class ElementHandle< return result; } - asElement(): ElementHandle | null { + override asElement(): ElementHandle | null { return this; } @@ -511,46 +503,47 @@ export class ElementHandle< } private async _scrollIntoViewIfNeeded(): Promise { - const error = await this.evaluate< - ( + const error = await this.evaluate( + async ( element: Element, pageJavascriptEnabled: boolean - ) => Promise - >(async (element, pageJavascriptEnabled) => { - if (!element.isConnected) return 'Node is detached from document'; - if (element.nodeType !== Node.ELEMENT_NODE) - return 'Node is not of type HTMLElement'; - // force-scroll if page's javascript is disabled. - if (!pageJavascriptEnabled) { - element.scrollIntoView({ - block: 'center', - inline: 'center', - // @ts-expect-error Chrome still supports behavior: instant but - // it's not in the spec so TS shouts We don't want to make this - // breaking change in Puppeteer yet so we'll ignore the line. - behavior: 'instant', + ): Promise => { + if (!element.isConnected) return 'Node is detached from document'; + if (element.nodeType !== Node.ELEMENT_NODE) + return 'Node is not of type HTMLElement'; + // force-scroll if page's javascript is disabled. + if (!pageJavascriptEnabled) { + element.scrollIntoView({ + block: 'center', + inline: 'center', + // @ts-expect-error Chrome still supports behavior: instant but + // it's not in the spec so TS shouts We don't want to make this + // breaking change in Puppeteer yet so we'll ignore the line. + behavior: 'instant', + }); + return false; + } + const visibleRatio = await new Promise((resolve) => { + const observer = new IntersectionObserver((entries) => { + resolve(entries[0]!.intersectionRatio); + observer.disconnect(); + }); + observer.observe(element); }); + if (visibleRatio !== 1.0) { + element.scrollIntoView({ + block: 'center', + inline: 'center', + // @ts-expect-error Chrome still supports behavior: instant but + // it's not in the spec so TS shouts We don't want to make this + // breaking change in Puppeteer yet so we'll ignore the line. + behavior: 'instant', + }); + } return false; - } - const visibleRatio = await new Promise((resolve) => { - const observer = new IntersectionObserver((entries) => { - resolve(entries[0].intersectionRatio); - observer.disconnect(); - }); - observer.observe(element); - }); - if (visibleRatio !== 1.0) { - element.scrollIntoView({ - block: 'center', - inline: 'center', - // @ts-expect-error Chrome still supports behavior: instant but - // it's not in the spec so TS shouts We don't want to make this - // breaking change in Puppeteer yet so we'll ignore the line. - behavior: 'instant', - }); - } - return false; - }, this._page.isJavaScriptEnabled()); + }, + this._page.isJavaScriptEnabled() + ); if (error) throw new Error(error); } @@ -560,14 +553,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 +571,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 +606,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; @@ -657,20 +651,20 @@ export class ElementHandle< .catch((error) => debugError(error)); } - private _fromProtocolQuad(quad: number[]): Array<{ x: number; y: number }> { + private _fromProtocolQuad(quad: number[]): Point[] { 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]! }, ]; } private _intersectQuadWithViewport( - quad: Array<{ x: number; y: number }>, + quad: Point[], width: number, height: number - ): Array<{ x: number; y: number }> { + ): Point[] { return quad.map((point) => ({ x: Math.min(Math.max(point.x, 0), width), y: Math.min(Math.max(point.y, 0), height), @@ -789,7 +783,7 @@ export class ElementHandle< throw new Error('Element is not a