feat: use strict typescript (#8401)

This commit is contained in:
jrandolf 2022-05-31 16:34:16 +02:00 committed by GitHub
parent f67bfb7a6c
commit b4e751f29c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 703 additions and 563 deletions

View File

@ -49,7 +49,7 @@
"generate-api-docs-for-testing": "commonmark docs/api.md > docs/api.html", "generate-api-docs-for-testing": "commonmark docs/api.md > docs/api.html",
"clean-lib": "rimraf lib", "clean-lib": "rimraf lib",
"build": "npm run tsc && npm run generate-d-ts && npm run generate-esm-package-json", "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-cjs": "tsc -b src/tsconfig.cjs.json",
"tsc-esm": "tsc -b src/tsconfig.esm.json", "tsc-esm": "tsc -b src/tsconfig.esm.json",
"tsc-compat-cjs": "tsc -b compat/cjs/tsconfig.json", "tsc-compat-cjs": "tsc -b compat/cjs/tsconfig.json",
@ -107,6 +107,7 @@
"@types/rimraf": "3.0.2", "@types/rimraf": "3.0.2",
"@types/sinon": "10.0.11", "@types/sinon": "10.0.11",
"@types/tar-fs": "2.0.1", "@types/tar-fs": "2.0.1",
"@types/unbzip2-stream": "1.4.0",
"@types/ws": "8.5.3", "@types/ws": "8.5.3",
"@typescript-eslint/eslint-plugin": "5.23.0", "@typescript-eslint/eslint-plugin": "5.23.0",
"@typescript-eslint/parser": "5.22.0", "@typescript-eslint/parser": "5.22.0",

View File

@ -66,7 +66,7 @@ const output = execSync(command, {
encoding: 'utf8', encoding: 'utf8',
}); });
const bestRevisionFromNpm = output.split(' ')[1].replace(/'|\n/g, ''); const bestRevisionFromNpm = output.split(' ')[1]!.replace(/'|\n/g, '');
if (currentProtocolPackageInstalledVersion !== bestRevisionFromNpm) { if (currentProtocolPackageInstalledVersion !== bestRevisionFromNpm) {
console.log(`ERROR: bad devtools-protocol revision detected: console.log(`ERROR: bad devtools-protocol revision detected:

View File

@ -21,7 +21,7 @@ const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies };
const invalidDeps = new Map<string, string>(); const invalidDeps = new Map<string, string>();
for (const [depKey, depValue] of Object.entries(allDeps)) { for (const [depKey, depValue] of Object.entries(allDeps)) {
if (/[0-9]/.test(depValue[0])) { if (/[0-9]/.test(depValue[0]!)) {
continue; continue;
} }

View File

@ -10,9 +10,9 @@ const EXPECTED_ERRORS = new Map<string, string[]>([
"bad.ts(6,35): error TS2551: Property 'launh' does not exist on type", "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(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(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<HTMLAnchorElement>", "bad.ts(20,5): error TS2345: Argument of type '(divElem: number) => any' is not assignable to parameter of type 'EvaluateFn<HTMLAnchorElement, any, any>'.",
"bad.ts(20,34): error TS2339: Property 'innerText' does not exist on type 'number'.", "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<HTMLAnchorElement>'.", "bad.ts(24,45): error TS2344: Type '(x: number) => string' does not satisfy the constraint 'EvaluateFn<HTMLAnchorElement, any, any>'.",
"bad.ts(27,34): error TS2339: Property 'innerText' does not exist on type 'number'.", "bad.ts(27,34): error TS2339: Property 'innerText' does not exist on type 'number'.",
], ],
], ],
@ -39,7 +39,7 @@ const EXPECTED_ERRORS = new Map<string, string[]>([
"bad.js(7,29): error TS2551: Property 'devics' does not exist on type", "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(11,39): error TS2554: Expected 0 arguments, but got 1.',
"bad.js(15,9): error TS2322: Type 'ElementHandle<HTMLElement> | null' is not assignable to type 'ElementHandle<HTMLElement>'", "bad.js(15,9): error TS2322: Type 'ElementHandle<HTMLElement> | null' is not assignable to type 'ElementHandle<HTMLElement>'",
"bad.js(22,5): error TS2345: Argument of type '(divElem: number) => any' is not assignable to parameter of type 'EvaluateFn<HTMLElement>'.", "bad.js(22,5): error TS2345: Argument of type '(divElem: number) => any' is not assignable to parameter of type 'EvaluateFn<HTMLElement, any, any>'.",
"bad.js(22,26): error TS2339: Property 'innerText' does not exist on type 'number'.", "bad.js(22,26): error TS2339: Property 'innerText' does not exist on type 'number'.",
], ],
], ],
@ -73,7 +73,7 @@ const EXPECTED_ERRORS = new Map<string, string[]>([
]); ]);
const PROJECT_FOLDERS = [...EXPECTED_ERRORS.keys()]; const PROJECT_FOLDERS = [...EXPECTED_ERRORS.keys()];
if (!process.env.CI) { if (!process.env['CI']) {
console.log(`IMPORTANT: this script assumes you have compiled Puppeteer console.log(`IMPORTANT: this script assumes you have compiled Puppeteer
and its types file before running. Make sure you have run: and its types file before running. Make sure you have run:
=> npm run tsc && npm run generate-d-ts => npm run tsc && npm run generate-d-ts
@ -107,7 +107,7 @@ function packPuppeteer() {
const tar = packPuppeteer(); const tar = packPuppeteer();
const tarPath = path.join(process.cwd(), tar); const tarPath = path.join(process.cwd(), tar);
function compileAndCatchErrors(projectLocation) { function compileAndCatchErrors(projectLocation: string) {
const { status, stdout, stderr } = spawnSync('npm', ['run', 'compile'], { const { status, stdout, stderr } = spawnSync('npm', ['run', 'compile'], {
cwd: projectLocation, cwd: projectLocation,
encoding: 'utf-8', encoding: 'utf-8',
@ -159,7 +159,7 @@ function testProject(folder: string) {
} }
); );
if (status > 0) { if (status) {
console.error( console.error(
'Installing the tar file unexpectedly failed', 'Installing the tar file unexpectedly failed',
stdout, stdout,

View File

@ -180,10 +180,10 @@ export class Accessibility {
*/ */
public async snapshot( public async snapshot(
options: SnapshotOptions = {} options: SnapshotOptions = {}
): Promise<SerializedAXNode> { ): Promise<SerializedAXNode | null> {
const { interestingOnly = true, root = null } = options; const { interestingOnly = true, root = null } = options;
const { nodes } = await this._client.send('Accessibility.getFullAXTree'); const { nodes } = await this._client.send('Accessibility.getFullAXTree');
let backendNodeId = null; let backendNodeId: number | undefined;
if (root) { if (root) {
const { node } = await this._client.send('DOM.describeNode', { const { node } = await this._client.send('DOM.describeNode', {
objectId: root._remoteObject.objectId, objectId: root._remoteObject.objectId,
@ -191,19 +191,19 @@ export class Accessibility {
backendNodeId = node.backendNodeId; backendNodeId = node.backendNodeId;
} }
const defaultRoot = AXNode.createTree(nodes); const defaultRoot = AXNode.createTree(nodes);
let needle = defaultRoot; let needle: AXNode | null = defaultRoot;
if (backendNodeId) { if (backendNodeId) {
needle = defaultRoot.find( needle = defaultRoot.find(
(node) => node.payload.backendDOMNodeId === backendNodeId (node) => node.payload.backendDOMNodeId === backendNodeId
); );
if (!needle) return null; if (!needle) return null;
} }
if (!interestingOnly) return this.serializeTree(needle)[0]; if (!interestingOnly) return this.serializeTree(needle)[0] ?? null;
const interestingNodes = new Set<AXNode>(); const interestingNodes = new Set<AXNode>();
this.collectInterestingNodes(interestingNodes, defaultRoot, false); this.collectInterestingNodes(interestingNodes, defaultRoot, false);
if (!interestingNodes.has(needle)) return null; if (!interestingNodes.has(needle)) return null;
return this.serializeTree(needle, interestingNodes)[0]; return this.serializeTree(needle, interestingNodes)[0] ?? null;
} }
private serializeTree( private serializeTree(
@ -496,7 +496,7 @@ class AXNode {
nodeById.set(payload.nodeId, new AXNode(payload)); nodeById.set(payload.nodeId, new AXNode(payload));
for (const node of nodeById.values()) { for (const node of nodeById.values()) {
for (const childId of node.payload.childIds || []) for (const childId of node.payload.childIds || [])
node.children.push(nodeById.get(childId)); node.children.push(nodeById.get(childId)!);
} }
return nodeById.values().next().value; return nodeById.values().next().value;
} }

View File

@ -19,6 +19,7 @@ import { ElementHandle, JSHandle } from './JSHandle.js';
import { Protocol } from 'devtools-protocol'; import { Protocol } from 'devtools-protocol';
import { CDPSession } from './Connection.js'; import { CDPSession } from './Connection.js';
import { DOMWorld, PageBinding, WaitForSelectorOptions } from './DOMWorld.js'; import { DOMWorld, PageBinding, WaitForSelectorOptions } from './DOMWorld.js';
import { assert } from './assert.js';
async function queryAXTree( async function queryAXTree(
client: CDPSession, client: CDPSession,
@ -32,7 +33,8 @@ async function queryAXTree(
role, role,
}); });
const filteredNodes: Protocol.Accessibility.AXNode[] = nodes.filter( 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; return filteredNodes;
} }
@ -43,6 +45,13 @@ const knownAttributes = new Set(['name', 'role']);
const attributeRegexp = const attributeRegexp =
/\[\s*(?<attribute>\w+)\s*=\s*(?<quote>"|')(?<value>\\.|.*?(?=\k<quote>))\k<quote>\s*\]/g; /\[\s*(?<attribute>\w+)\s*=\s*(?<quote>"|')(?<value>\\.|.*?(?=\k<quote>))\k<quote>\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 * The selectors consist of an accessible name to query for and optionally
* further aria attributes on the form `[<attribute>=<value>]`. * further aria attributes on the form `[<attribute>=<value>]`.
@ -53,15 +62,16 @@ const attributeRegexp =
* - 'label' queries for elements with name 'label' and any role. * - 'label' queries for elements with name 'label' and any role.
* - '[name=""][role="button"]' queries for elements with no name and role 'button'. * - '[name=""][role="button"]' queries for elements with no name and role 'button'.
*/ */
type ariaQueryOption = { name?: string; role?: string }; function parseAriaSelector(selector: string): ARIAQueryOption {
function parseAriaSelector(selector: string): ariaQueryOption { const queryOptions: ARIAQueryOption = {};
const queryOptions: ariaQueryOption = {};
const defaultName = selector.replace( const defaultName = selector.replace(
attributeRegexp, attributeRegexp,
(_, attribute: string, quote: string, value: string) => { (_, attribute: string, _quote: string, value: string) => {
attribute = attribute.trim(); attribute = attribute.trim();
if (!knownAttributes.has(attribute)) assert(
throw new Error(`Unknown aria attribute "${attribute}" in selector`); isKnownAttribute(attribute),
`Unknown aria attribute "${attribute}" in selector`
);
queryOptions[attribute] = normalizeValue(value); queryOptions[attribute] = normalizeValue(value);
return ''; return '';
} }
@ -78,7 +88,7 @@ const queryOne = async (
const exeCtx = element.executionContext(); const exeCtx = element.executionContext();
const { name, role } = parseAriaSelector(selector); const { name, role } = parseAriaSelector(selector);
const res = await queryAXTree(exeCtx._client, element, name, role); const res = await queryAXTree(exeCtx._client, element, name, role);
if (res.length < 1) { if (!res[0] || !res[0].backendDOMNodeId) {
return null; return null;
} }
return exeCtx._adoptBackendNodeId(res[0].backendDOMNodeId); return exeCtx._adoptBackendNodeId(res[0].backendDOMNodeId);
@ -88,7 +98,7 @@ const waitFor = async (
domWorld: DOMWorld, domWorld: DOMWorld,
selector: string, selector: string,
options: WaitForSelectorOptions options: WaitForSelectorOptions
): Promise<ElementHandle<Element>> => { ): Promise<ElementHandle<Element> | null> => {
const binding: PageBinding = { const binding: PageBinding = {
name: 'ariaQuerySelector', name: 'ariaQuerySelector',
pptrFunction: async (selector: string) => { pptrFunction: async (selector: string) => {
@ -98,7 +108,10 @@ const waitFor = async (
}, },
}; };
return domWorld.waitForSelectorInPage( return domWorld.waitForSelectorInPage(
(_: Element, selector: string) => globalThis.ariaQuerySelector(selector), (_: Element, selector: string) =>
(
globalThis as unknown as { ariaQuerySelector(selector: string): void }
).ariaQuerySelector(selector),
selector, selector,
options, options,
binding binding

View File

@ -246,7 +246,7 @@ export class Browser extends EventEmitter {
private _connection: Connection; private _connection: Connection;
private _closeCallback: BrowserCloseCallback; private _closeCallback: BrowserCloseCallback;
private _targetFilterCallback: TargetFilterCallback; private _targetFilterCallback: TargetFilterCallback;
private _isPageTargetCallback: IsPageTargetCallback; private _isPageTargetCallback!: IsPageTargetCallback;
private _defaultContext: BrowserContext; private _defaultContext: BrowserContext;
private _contexts: Map<string, BrowserContext>; private _contexts: Map<string, BrowserContext>;
private _screenshotTaskQueue: TaskQueue; private _screenshotTaskQueue: TaskQueue;
@ -572,33 +572,24 @@ export class Browser extends EventEmitter {
): Promise<Target> { ): Promise<Target> {
const { timeout = 30000 } = options; const { timeout = 30000 } = options;
let resolve: (value: Target | PromiseLike<Target>) => void; let resolve: (value: Target | PromiseLike<Target>) => void;
let isResolved = false;
const targetPromise = new Promise<Target>((x) => (resolve = x)); const targetPromise = new Promise<Target>((x) => (resolve = x));
this.on(BrowserEmittedEvents.TargetCreated, check); this.on(BrowserEmittedEvents.TargetCreated, check);
this.on(BrowserEmittedEvents.TargetChanged, check); this.on(BrowserEmittedEvents.TargetChanged, check);
try { try {
if (!timeout) return await targetPromise; if (!timeout) return await targetPromise;
return await helper.waitWithTimeout<Target>( this.targets().forEach(check);
Promise.race([ return await helper.waitWithTimeout(targetPromise, 'target', timeout);
targetPromise,
(async () => {
for (const target of this.targets()) {
if (await predicate(target)) {
return target;
}
}
await targetPromise;
})(),
]),
'target',
timeout
);
} finally { } finally {
this.removeListener(BrowserEmittedEvents.TargetCreated, check); this.off(BrowserEmittedEvents.TargetCreated, check);
this.removeListener(BrowserEmittedEvents.TargetChanged, check); this.off(BrowserEmittedEvents.TargetChanged, check);
} }
async function check(target: Target): Promise<void> { async function check(target: Target): Promise<void> {
if (await predicate(target)) resolve(target); if ((await predicate(target)) && !isResolved) {
isResolved = true;
resolve(target);
}
} }
} }

View File

@ -93,7 +93,7 @@ export const connectToBrowser = async (
'Exactly one of browserWSEndpoint, browserURL or transport must be passed to puppeteer.connect' 'Exactly one of browserWSEndpoint, browserURL or transport must be passed to puppeteer.connect'
); );
let connection = null; let connection!: Connection;
if (transport) { if (transport) {
connection = new Connection('', transport, slowMo); connection = new Connection('', transport, slowMo);
} else if (browserWSEndpoint) { } else if (browserWSEndpoint) {
@ -117,7 +117,7 @@ export const connectToBrowser = async (
browserContextIds, browserContextIds,
ignoreHTTPSErrors, ignoreHTTPSErrors,
defaultViewport, defaultViewport,
null, undefined,
() => connection.send('Browser.close').catch(debugError), () => connection.send('Browser.close').catch(debugError),
targetFilter, targetFilter,
isPageTarget isPageTarget
@ -138,9 +138,11 @@ async function getWSEndpoint(browserURL: string): Promise<string> {
const data = await result.json(); const data = await result.json();
return data.webSocketDebuggerUrl; return data.webSocketDebuggerUrl;
} catch (error) { } catch (error) {
error.message = if (error instanceof Error) {
`Failed to fetch browser webSocket URL from ${endpointURL}: ` + error.message =
error.message; `Failed to fetch browser webSocket URL from ${endpointURL}: ` +
error.message;
}
throw error; throw error;
} }
} }

View File

@ -41,8 +41,6 @@ export class BrowserWebSocketTransport implements ConnectionTransport {
}); });
// Silently ignore all errors - we don't know what to do with them. // Silently ignore all errors - we don't know what to do with them.
this._ws.addEventListener('error', () => {}); this._ws.addEventListener('error', () => {});
this.onmessage = null;
this.onclose = null;
} }
send(message: string): void { send(message: string): void {

View File

@ -111,7 +111,7 @@ export class ConsoleMessage {
* @returns The location of the console message. * @returns The location of the console message.
*/ */
location(): ConsoleMessageLocation { location(): ConsoleMessageLocation {
return this._stackTraceLocations.length ? this._stackTraceLocations[0] : {}; return this._stackTraceLocations[0] ?? {};
} }
/** /**

View File

@ -395,10 +395,12 @@ export class CSSCoverage {
}); });
} }
const coverage = []; const coverage: CoverageEntry[] = [];
for (const styleSheetId of this._stylesheetURLs.keys()) { for (const styleSheetId of this._stylesheetURLs.keys()) {
const url = this._stylesheetURLs.get(styleSheetId); const url = this._stylesheetURLs.get(styleSheetId);
assert(url);
const text = this._stylesheetSources.get(styleSheetId); const text = this._stylesheetSources.get(styleSheetId);
assert(text);
const ranges = convertToDisjointRanges( const ranges = convertToDisjointRanges(
styleSheetIdToCoverage.get(styleSheetId) || [] styleSheetIdToCoverage.get(styleSheetId) || []
); );
@ -432,16 +434,19 @@ function convertToDisjointRanges(
}); });
const hitCountStack = []; const hitCountStack = [];
const results = []; const results: Array<{
start: number;
end: number;
}> = [];
let lastOffset = 0; let lastOffset = 0;
// Run scanning line to intersect all ranges. // Run scanning line to intersect all ranges.
for (const point of points) { for (const point of points) {
if ( if (
hitCountStack.length && hitCountStack.length &&
lastOffset < point.offset && 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) if (lastResult && lastResult.end === lastOffset)
lastResult.end = point.offset; lastResult.end = point.offset;
else results.push({ start: lastOffset, end: point.offset }); else results.push({ start: lastOffset, end: point.offset });

View File

@ -500,38 +500,38 @@ export class DOMWorld {
options: { delay?: number; button?: MouseButton; clickCount?: number } options: { delay?: number; button?: MouseButton; clickCount?: number }
): Promise<void> { ): Promise<void> {
const handle = await this.$(selector); const handle = await this.$(selector);
assert(handle, 'No node found for selector: ' + selector); assert(handle, `No element found for selector: ${selector}`);
await handle!.click(options); await handle.click(options);
await handle!.dispose(); await handle.dispose();
} }
async focus(selector: string): Promise<void> { async focus(selector: string): Promise<void> {
const handle = await this.$(selector); const handle = await this.$(selector);
assert(handle, 'No node found for selector: ' + selector); assert(handle, `No element found for selector: ${selector}`);
await handle!.focus(); await handle.focus();
await handle!.dispose(); await handle.dispose();
} }
async hover(selector: string): Promise<void> { async hover(selector: string): Promise<void> {
const handle = await this.$(selector); const handle = await this.$(selector);
assert(handle, 'No node found for selector: ' + selector); assert(handle, `No element found for selector: ${selector}`);
await handle!.hover(); await handle.hover();
await handle!.dispose(); await handle.dispose();
} }
async select(selector: string, ...values: string[]): Promise<string[]> { async select(selector: string, ...values: string[]): Promise<string[]> {
const handle = await this.$(selector); const handle = await this.$(selector);
assert(handle, 'No node found for selector: ' + selector); assert(handle, `No element found for selector: ${selector}`);
const result = await handle!.select(...values); const result = await handle.select(...values);
await handle!.dispose(); await handle.dispose();
return result; return result;
} }
async tap(selector: string): Promise<void> { async tap(selector: string): Promise<void> {
const handle = await this.$(selector); const handle = await this.$(selector);
assert(handle, 'No node found for selector: ' + selector); assert(handle, `No element found for selector: ${selector}`);
await handle!.tap(); await handle.tap();
await handle!.dispose(); await handle.dispose();
} }
async type( async type(
@ -540,9 +540,9 @@ export class DOMWorld {
options?: { delay: number } options?: { delay: number }
): Promise<void> { ): Promise<void> {
const handle = await this.$(selector); const handle = await this.$(selector);
assert(handle, 'No node found for selector: ' + selector); assert(handle, `No element found for selector: ${selector}`);
await handle!.type(text, options); await handle.type(text, options);
await handle!.dispose(); await handle.dispose();
} }
async waitForSelector( async waitForSelector(
@ -551,9 +551,7 @@ export class DOMWorld {
): Promise<ElementHandle | null> { ): Promise<ElementHandle | null> {
const { updatedSelector, queryHandler } = const { updatedSelector, queryHandler } =
getQueryHandlerAndSelector(selector); getQueryHandlerAndSelector(selector);
if (!queryHandler.waitFor) { assert(queryHandler.waitFor, 'Query handler does not support waiting');
throw new Error('Query handler does not support waitFor');
}
return queryHandler.waitFor(this, updatedSelector, options); return queryHandler.waitFor(this, updatedSelector, options);
} }
@ -970,7 +968,7 @@ async function waitForPredicatePageFunction(
root: Element | Document | null, root: Element | Document | null,
predicateBody: string, predicateBody: string,
predicateAcceptsContextElement: boolean, predicateAcceptsContextElement: boolean,
polling: string, polling: 'raf' | 'mutation' | number,
timeout: number, timeout: number,
...args: unknown[] ...args: unknown[]
): Promise<unknown> { ): Promise<unknown> {
@ -978,9 +976,14 @@ async function waitForPredicatePageFunction(
const predicate = new Function('...args', predicateBody); const predicate = new Function('...args', predicateBody);
let timedOut = false; let timedOut = false;
if (timeout) setTimeout(() => (timedOut = true), timeout); if (timeout) setTimeout(() => (timedOut = true), timeout);
if (polling === 'raf') return await pollRaf(); switch (polling) {
if (polling === 'mutation') return await pollMutation(); case 'raf':
if (typeof polling === 'number') return await pollInterval(polling); return await pollRaf();
case 'mutation':
return await pollMutation();
default:
return await pollInterval(polling);
}
/** /**
* @returns {!Promise<*>} * @returns {!Promise<*>}
@ -1023,7 +1026,7 @@ async function waitForPredicatePageFunction(
await onRaf(); await onRaf();
return result; return result;
async function onRaf(): Promise<unknown> { async function onRaf(): Promise<void> {
if (timedOut) { if (timedOut) {
fulfill(); fulfill();
return; return;
@ -1042,7 +1045,7 @@ async function waitForPredicatePageFunction(
await onTimeout(); await onTimeout();
return result; return result;
async function onTimeout(): Promise<unknown> { async function onTimeout(): Promise<void> {
if (timedOut) { if (timedOut) {
fulfill(); fulfill();
return; return;

View File

@ -16,6 +16,11 @@
import { isNode } from '../environment.js'; 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. * 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 => { return (...logArgs: unknown[]): void => {
const debugLevel = globalThis.__PUPPETEER_DEBUG as string; const debugLevel = globalThis.__PUPPETEER_DEBUG;
if (!debugLevel) return; if (!debugLevel) return;
const everythingShouldBeLogged = debugLevel === '*'; const everythingShouldBeLogged = debugLevel === '*';

View File

@ -19,7 +19,9 @@ import { JSHandle, ElementHandle } from './JSHandle.js';
/** /**
* @public * @public
*/ */
export type EvaluateFn<T = any> = string | ((arg1: T, ...args: any[]) => any); export type EvaluateFn<T = any, U = any, V = any> =
| string
| ((arg1: T, ...args: U[]) => V);
/** /**
* @public * @public
*/ */
@ -47,7 +49,7 @@ export type Serializable =
| string | string
| boolean | boolean
| null | null
| BigInt | bigint
| JSONArray | JSONArray
| JSONObject; | JSONObject;

View File

@ -53,7 +53,7 @@ export class ExecutionContext {
/** /**
* @internal * @internal
*/ */
_world: DOMWorld; _world?: DOMWorld;
/** /**
* @internal * @internal
*/ */
@ -69,7 +69,7 @@ export class ExecutionContext {
constructor( constructor(
client: CDPSession, client: CDPSession,
contextPayload: Protocol.Runtime.ExecutionContextDescription, contextPayload: Protocol.Runtime.ExecutionContextDescription,
world: DOMWorld world?: DOMWorld
) { ) {
this._client = client; this._client = client;
this._world = world; this._world = world;
@ -277,12 +277,10 @@ export class ExecutionContext {
? helper.valueFromRemoteObject(remoteObject) ? helper.valueFromRemoteObject(remoteObject)
: createJSHandle(this, remoteObject); : createJSHandle(this, remoteObject);
/** function convertArgument(
* @param {*} arg this: ExecutionContext,
* @returns {*} arg: unknown
* @this {ExecutionContext} ): Protocol.Runtime.CallArgument {
*/
function convertArgument(this: ExecutionContext, arg: unknown): unknown {
if (typeof arg === 'bigint') if (typeof arg === 'bigint')
// eslint-disable-line valid-typeof // eslint-disable-line valid-typeof
return { unserializableValue: `${arg.toString()}n` }; return { unserializableValue: `${arg.toString()}n` };
@ -364,7 +362,7 @@ export class ExecutionContext {
* @internal * @internal
*/ */
async _adoptBackendNodeId( async _adoptBackendNodeId(
backendNodeId: Protocol.DOM.BackendNodeId backendNodeId?: Protocol.DOM.BackendNodeId
): Promise<ElementHandle> { ): Promise<ElementHandle> {
const { object } = await this._client.send('DOM.resolveNode', { const { object } = await this._client.send('DOM.resolveNode', {
backendNodeId: backendNodeId, backendNodeId: backendNodeId,

View File

@ -73,7 +73,7 @@ export class FrameManager extends EventEmitter {
private _frames = new Map<string, Frame>(); private _frames = new Map<string, Frame>();
private _contextIdToContext = new Map<string, ExecutionContext>(); private _contextIdToContext = new Map<string, ExecutionContext>();
private _isolatedWorlds = new Set<string>(); private _isolatedWorlds = new Set<string>();
private _mainFrame: Frame; private _mainFrame?: Frame;
constructor( constructor(
client: CDPSession, client: CDPSession,
@ -163,8 +163,9 @@ export class FrameManager extends EventEmitter {
} catch (error) { } catch (error) {
// The target might have been closed before the initialization finished. // The target might have been closed before the initialization finished.
if ( if (
error.message.includes('Target closed') || error instanceof Error &&
error.message.includes('Session closed') (error.message.includes('Target closed') ||
error.message.includes('Session closed'))
) { ) {
return; return;
} }
@ -212,7 +213,7 @@ export class FrameManager extends EventEmitter {
async function navigate( async function navigate(
client: CDPSession, client: CDPSession,
url: string, url: string,
referrer: string, referrer: string | undefined,
frameId: string frameId: string
): Promise<Error | null> { ): Promise<Error | null> {
try { try {
@ -225,7 +226,10 @@ export class FrameManager extends EventEmitter {
? new Error(`${response.errorText} at ${url}`) ? new Error(`${response.errorText} at ${url}`)
: null; : null;
} catch (error) { } 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 frame = this._frames.get(event.targetInfo.targetId);
const session = Connection.fromSession(this._client).session( const connection = Connection.fromSession(this._client);
event.sessionId assert(connection);
); const session = connection.session(event.sessionId);
assert(session);
if (frame) frame._updateClient(session); if (frame) frame._updateClient(session);
this.setupEventListeners(session); this.setupEventListeners(session);
await this.initialize(session); await this.initialize(session);
@ -272,6 +277,7 @@ export class FrameManager extends EventEmitter {
private async _onDetachedFromTarget( private async _onDetachedFromTarget(
event: Protocol.Target.DetachedFromTargetEvent event: Protocol.Target.DetachedFromTargetEvent
) { ) {
if (!event.targetId) return;
const frame = this._frames.get(event.targetId); const frame = this._frames.get(event.targetId);
if (frame && frame.isOOPFrame()) { if (frame && frame.isOOPFrame()) {
// When an OOP iframe is removed from the page, it // When an OOP iframe is removed from the page, it
@ -324,6 +330,7 @@ export class FrameManager extends EventEmitter {
} }
mainFrame(): Frame { mainFrame(): Frame {
assert(this._mainFrame, 'Requesting main frame too early!');
return this._mainFrame; return this._mainFrame;
} }
@ -341,7 +348,7 @@ export class FrameManager extends EventEmitter {
parentFrameId?: string parentFrameId?: string
): void { ): void {
if (this._frames.has(frameId)) { if (this._frames.has(frameId)) {
const frame = this._frames.get(frameId); const frame = this._frames.get(frameId)!;
if (session && frame.isOOPFrame()) { if (session && frame.isOOPFrame()) {
// If an OOP iframes becomes a normal iframe again // If an OOP iframes becomes a normal iframe again
// it is first attached to the parent page before // it is first attached to the parent page before
@ -352,6 +359,7 @@ export class FrameManager extends EventEmitter {
} }
assert(parentFrameId); assert(parentFrameId);
const parentFrame = this._frames.get(parentFrameId); const parentFrame = this._frames.get(parentFrameId);
assert(parentFrame);
const frame = new Frame(this, parentFrame, frameId, session); const frame = new Frame(this, parentFrame, frameId, session);
this._frames.set(frame._id, frame); this._frames.set(frame._id, frame);
this.emit(FrameManagerEmittedEvents.FrameAttached, frame); this.emit(FrameManagerEmittedEvents.FrameAttached, frame);
@ -388,6 +396,7 @@ export class FrameManager extends EventEmitter {
} }
// Update frame payload. // Update frame payload.
assert(frame);
frame._navigated(framePayload); frame._navigated(framePayload);
this.emit(FrameManagerEmittedEvents.FrameNavigated, frame); this.emit(FrameManagerEmittedEvents.FrameNavigated, frame);
@ -445,10 +454,11 @@ export class FrameManager extends EventEmitter {
contextPayload: Protocol.Runtime.ExecutionContextDescription, contextPayload: Protocol.Runtime.ExecutionContextDescription,
session: CDPSession session: CDPSession
): void { ): void {
const auxData = contextPayload.auxData as { frameId?: string }; const auxData = contextPayload.auxData as { frameId?: string } | undefined;
const frameId = auxData ? auxData.frameId : null; const frameId = auxData && auxData.frameId;
const frame = this._frames.get(frameId) || null; const frame =
let world = null; typeof frameId === 'string' ? this._frames.get(frameId) : undefined;
let world: DOMWorld | undefined;
if (frame) { if (frame) {
// Only care about execution contexts created for the current session. // Only care about execution contexts created for the current session.
if (frame._client !== session) return; if (frame._client !== session) return;
@ -640,7 +650,7 @@ export class Frame {
* @internal * @internal
*/ */
_frameManager: FrameManager; _frameManager: FrameManager;
private _parentFrame?: Frame; private _parentFrame: Frame | null;
/** /**
* @internal * @internal
*/ */
@ -668,11 +678,11 @@ export class Frame {
/** /**
* @internal * @internal
*/ */
_mainWorld: DOMWorld; _mainWorld!: DOMWorld;
/** /**
* @internal * @internal
*/ */
_secondaryWorld: DOMWorld; _secondaryWorld!: DOMWorld;
/** /**
* @internal * @internal
*/ */
@ -680,7 +690,7 @@ export class Frame {
/** /**
* @internal * @internal
*/ */
_client: CDPSession; _client!: CDPSession;
/** /**
* @internal * @internal
@ -692,7 +702,7 @@ export class Frame {
client: CDPSession client: CDPSession
) { ) {
this._frameManager = frameManager; this._frameManager = frameManager;
this._parentFrame = parentFrame; this._parentFrame = parentFrame ?? null;
this._url = ''; this._url = '';
this._id = frameId; this._id = frameId;
this._detached = false; this._detached = false;
@ -1457,7 +1467,7 @@ function assertNoLegacyNavigationOptions(options: {
'ERROR: networkIdleInflight option is no longer supported.' 'ERROR: networkIdleInflight option is no longer supported.'
); );
assert( assert(
options.waitUntil !== 'networkidle', options['waitUntil'] !== 'networkidle',
'ERROR: "networkidle" option is no longer supported. Use "networkidle2" instead' 'ERROR: "networkidle" option is no longer supported. Use "networkidle2" instead'
); );
} }

View File

@ -185,8 +185,8 @@ export class HTTPRequest {
this._interceptHandlers = []; this._interceptHandlers = [];
this._initiator = event.initiator; this._initiator = event.initiator;
for (const key of Object.keys(event.request.headers)) for (const [key, value] of Object.entries(event.request.headers))
this._headers[key.toLowerCase()] = event.request.headers[key]; this._headers[key.toLowerCase()] = value;
} }
/** /**

View File

@ -88,8 +88,9 @@ export class HTTPResponse {
this._status = extraInfo ? extraInfo.statusCode : responsePayload.status; this._status = extraInfo ? extraInfo.statusCode : responsePayload.status;
const headers = extraInfo ? extraInfo.headers : responsePayload.headers; const headers = extraInfo ? extraInfo.headers : responsePayload.headers;
for (const key of Object.keys(headers)) for (const [key, value] of Object.entries(headers)) {
this._headers[key.toLowerCase()] = headers[key]; this._headers[key.toLowerCase()] = value;
}
this._securityDetails = responsePayload.securityDetails this._securityDetails = responsePayload.securityDetails
? new SecurityDetails(responsePayload.securityDetails) ? new SecurityDetails(responsePayload.securityDetails)

View File

@ -38,10 +38,10 @@ import { MouseButton } from './Input.js';
* @public * @public
*/ */
export interface BoxModel { export interface BoxModel {
content: Array<{ x: number; y: number }>; content: Point[];
padding: Array<{ x: number; y: number }>; padding: Point[];
border: Array<{ x: number; y: number }>; border: Point[];
margin: Array<{ x: number; y: number }>; margin: Point[];
width: number; width: number;
height: number; height: number;
} }
@ -49,15 +49,7 @@ export interface BoxModel {
/** /**
* @public * @public
*/ */
export interface BoundingBox { export interface BoundingBox extends Point {
/**
* the x coordinate of the element in pixels.
*/
x: number;
/**
* the y coordinate of the element in pixels.
*/
y: number;
/** /**
* the width of the element in pixels. * the width of the element in pixels.
*/ */
@ -90,11 +82,8 @@ export function createJSHandle(
return new JSHandle(context, context._client, remoteObject); return new JSHandle(context, context._client, remoteObject);
} }
const applyOffsetsToQuad = ( const applyOffsetsToQuad = (quad: Point[], offsetX: number, offsetY: number) =>
quad: Array<{ x: number; y: number }>, quad.map((part) => ({ x: part.x + offsetX, y: part.y + offsetY }));
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 * Represents an in-page JavaScript object. JSHandles can be created with the
@ -202,8 +191,8 @@ export class JSHandle<HandleObjectType = unknown> {
*/ */
async getProperty(propertyName: string): Promise<JSHandle> { async getProperty(propertyName: string): Promise<JSHandle> {
const objectHandle = await this.evaluateHandle( const objectHandle = await this.evaluateHandle(
(object: Element, propertyName: string) => { (object: Element, propertyName: keyof Element) => {
const result = { __proto__: null }; const result: Record<string, unknown> = { __proto__: null };
result[propertyName] = object[propertyName]; result[propertyName] = object[propertyName];
return result; return result;
}, },
@ -234,13 +223,14 @@ export class JSHandle<HandleObjectType = unknown> {
* ``` * ```
*/ */
async getProperties(): Promise<Map<string, JSHandle>> { async getProperties(): Promise<Map<string, JSHandle>> {
assert(this._remoteObject.objectId);
const response = await this._client.send('Runtime.getProperties', { const response = await this._client.send('Runtime.getProperties', {
objectId: this._remoteObject.objectId, objectId: this._remoteObject.objectId,
ownProperties: true, ownProperties: true,
}); });
const result = new Map<string, JSHandle>(); const result = new Map<string, JSHandle>();
for (const property of response.result) { 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)); result.set(property.name, createJSHandle(this._context, property.value));
} }
return result; return result;
@ -402,6 +392,7 @@ export class ElementHandle<
} = {} } = {}
): Promise<ElementHandle | null> { ): Promise<ElementHandle | null> {
const frame = this._context.frame(); const frame = this._context.frame();
assert(frame);
const secondaryContext = await frame._secondaryWorld.executionContext(); const secondaryContext = await frame._secondaryWorld.executionContext();
const adoptedRoot = await secondaryContext._adoptElementHandle(this); const adoptedRoot = await secondaryContext._adoptElementHandle(this);
const handle = await frame._secondaryWorld.waitForSelector(selector, { const handle = await frame._secondaryWorld.waitForSelector(selector, {
@ -475,6 +466,7 @@ export class ElementHandle<
} = {} } = {}
): Promise<ElementHandle | null> { ): Promise<ElementHandle | null> {
const frame = this._context.frame(); const frame = this._context.frame();
assert(frame);
const secondaryContext = await frame._secondaryWorld.executionContext(); const secondaryContext = await frame._secondaryWorld.executionContext();
const adoptedRoot = await secondaryContext._adoptElementHandle(this); const adoptedRoot = await secondaryContext._adoptElementHandle(this);
xpath = xpath.startsWith('//') ? '.' + xpath : xpath; xpath = xpath.startsWith('//') ? '.' + xpath : xpath;
@ -494,7 +486,7 @@ export class ElementHandle<
return result; return result;
} }
asElement(): ElementHandle<ElementType> | null { override asElement(): ElementHandle<ElementType> | null {
return this; return this;
} }
@ -511,46 +503,47 @@ export class ElementHandle<
} }
private async _scrollIntoViewIfNeeded(): Promise<void> { private async _scrollIntoViewIfNeeded(): Promise<void> {
const error = await this.evaluate< const error = await this.evaluate(
( async (
element: Element, element: Element,
pageJavascriptEnabled: boolean pageJavascriptEnabled: boolean
) => Promise<string | false> ): Promise<string | false> => {
>(async (element, pageJavascriptEnabled) => { if (!element.isConnected) return 'Node is detached from document';
if (!element.isConnected) return 'Node is detached from document'; if (element.nodeType !== Node.ELEMENT_NODE)
if (element.nodeType !== Node.ELEMENT_NODE) return 'Node is not of type HTMLElement';
return 'Node is not of type HTMLElement'; // force-scroll if page's javascript is disabled.
// force-scroll if page's javascript is disabled. if (!pageJavascriptEnabled) {
if (!pageJavascriptEnabled) { element.scrollIntoView({
element.scrollIntoView({ block: 'center',
block: 'center', inline: 'center',
inline: 'center', // @ts-expect-error Chrome still supports behavior: instant but
// @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
// 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.
// breaking change in Puppeteer yet so we'll ignore the line. behavior: 'instant',
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; return false;
} },
const visibleRatio = await new Promise((resolve) => { this._page.isJavaScriptEnabled()
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());
if (error) throw new Error(error); if (error) throw new Error(error);
} }
@ -560,14 +553,15 @@ export class ElementHandle<
): Promise<{ offsetX: number; offsetY: number }> { ): Promise<{ offsetX: number; offsetY: number }> {
let offsetX = 0; let offsetX = 0;
let offsetY = 0; let offsetY = 0;
while (frame.parentFrame()) { let currentFrame: Frame | null = frame;
const parent = frame.parentFrame(); while (currentFrame && currentFrame.parentFrame()) {
if (!frame.isOOPFrame()) { const parent = currentFrame.parentFrame();
frame = parent; if (!currentFrame.isOOPFrame() || !parent) {
currentFrame = parent;
continue; continue;
} }
const { backendNodeId } = await parent._client.send('DOM.getFrameOwner', { const { backendNodeId } = await parent._client.send('DOM.getFrameOwner', {
frameId: frame._id, frameId: currentFrame._id,
}); });
const result = await parent._client.send('DOM.getBoxModel', { const result = await parent._client.send('DOM.getBoxModel', {
backendNodeId: backendNodeId, backendNodeId: backendNodeId,
@ -577,9 +571,9 @@ export class ElementHandle<
} }
const contentBoxQuad = result.model.content; const contentBoxQuad = result.model.content;
const topLeftCorner = this._fromProtocolQuad(contentBoxQuad)[0]; const topLeftCorner = this._fromProtocolQuad(contentBoxQuad)[0];
offsetX += topLeftCorner.x; offsetX += topLeftCorner!.x;
offsetY += topLeftCorner.y; offsetY += topLeftCorner!.y;
frame = parent; currentFrame = parent;
} }
return { offsetX, offsetY }; return { offsetX, offsetY };
} }
@ -612,7 +606,7 @@ export class ElementHandle<
.filter((quad) => computeQuadArea(quad) > 1); .filter((quad) => computeQuadArea(quad) > 1);
if (!quads.length) if (!quads.length)
throw new Error('Node is either not clickable or not an HTMLElement'); throw new Error('Node is either not clickable or not an HTMLElement');
const quad = quads[0]; const quad = quads[0]!;
if (offset) { if (offset) {
// Return the point of the first quad identified by offset. // Return the point of the first quad identified by offset.
let minX = Number.MAX_SAFE_INTEGER; let minX = Number.MAX_SAFE_INTEGER;
@ -657,20 +651,20 @@ export class ElementHandle<
.catch((error) => debugError(error)); .catch((error) => debugError(error));
} }
private _fromProtocolQuad(quad: number[]): Array<{ x: number; y: number }> { private _fromProtocolQuad(quad: number[]): Point[] {
return [ return [
{ x: quad[0], y: quad[1] }, { x: quad[0]!, y: quad[1]! },
{ x: quad[2], y: quad[3] }, { x: quad[2]!, y: quad[3]! },
{ x: quad[4], y: quad[5] }, { x: quad[4]!, y: quad[5]! },
{ x: quad[6], y: quad[7] }, { x: quad[6]!, y: quad[7]! },
]; ];
} }
private _intersectQuadWithViewport( private _intersectQuadWithViewport(
quad: Array<{ x: number; y: number }>, quad: Point[],
width: number, width: number,
height: number height: number
): Array<{ x: number; y: number }> { ): Point[] {
return quad.map((point) => ({ return quad.map((point) => ({
x: Math.min(Math.max(point.x, 0), width), x: Math.min(Math.max(point.x, 0), width),
y: Math.min(Math.max(point.y, 0), height), y: Math.min(Math.max(point.y, 0), height),
@ -789,7 +783,7 @@ export class ElementHandle<
throw new Error('Element is not a <select> element.'); throw new Error('Element is not a <select> element.');
const options = Array.from(element.options); const options = Array.from(element.options);
element.value = undefined; element.value = '';
for (const option of options) { for (const option of options) {
option.selected = values.includes(option.value); option.selected = values.includes(option.value);
if (option.selected && !element.multiple) break; if (option.selected && !element.multiple) break;
@ -843,7 +837,7 @@ export class ElementHandle<
try { try {
await fs.promises.access(resolvedPath, fs.constants.R_OK); await fs.promises.access(resolvedPath, fs.constants.R_OK);
} catch (error) { } catch (error) {
if (error.code === 'ENOENT') if (error && (error as NodeJS.ErrnoException).code === 'ENOENT')
throw new Error(`${filePath} does not exist or is not readable`); throw new Error(`${filePath} does not exist or is not readable`);
} }
@ -952,10 +946,10 @@ export class ElementHandle<
const { offsetX, offsetY } = await this._getOOPIFOffsets(this._frame); const { offsetX, offsetY } = await this._getOOPIFOffsets(this._frame);
const quad = result.model.border; const quad = result.model.border;
const x = Math.min(quad[0], quad[2], quad[4], quad[6]); const x = Math.min(quad[0]!, quad[2]!, quad[4]!, quad[6]!);
const y = Math.min(quad[1], quad[3], quad[5], quad[7]); const y = Math.min(quad[1]!, quad[3]!, quad[5]!, quad[7]!);
const width = Math.max(quad[0], quad[2], quad[4], quad[6]) - x; const width = Math.max(quad[0]!, quad[2]!, quad[4]!, quad[6]!) - x;
const height = Math.max(quad[1], quad[3], quad[5], quad[7]) - y; const height = Math.max(quad[1]!, quad[3]!, quad[5]!, quad[7]!) - y;
return { x: x + offsetX, y: y + offsetY, width, height }; return { x: x + offsetX, y: y + offsetY, width, height };
} }
@ -1014,11 +1008,11 @@ export class ElementHandle<
assert(boundingBox, 'Node is either not visible or not an HTMLElement'); assert(boundingBox, 'Node is either not visible or not an HTMLElement');
const viewport = this._page.viewport(); const viewport = this._page.viewport();
assert(viewport);
if ( if (
viewport && boundingBox.width > viewport.width ||
(boundingBox.width > viewport.width || boundingBox.height > viewport.height
boundingBox.height > viewport.height)
) { ) {
const newViewport = { const newViewport = {
width: Math.max(viewport.width, Math.ceil(boundingBox.width)), width: Math.max(viewport.width, Math.ceil(boundingBox.width)),
@ -1061,14 +1055,21 @@ export class ElementHandle<
} }
/** /**
* Runs `element.querySelector` within the page. If no element matches the selector, * Runs `element.querySelector` within the page.
* the return value resolves to `null`. *
* @param selector The selector to query with.
* @returns `null` if no element matches the selector.
* @throws `Error` if the selector has no associated query handler.
*/ */
async $<T extends Element = Element>( async $<T extends Element = Element>(
selector: string selector: string
): Promise<ElementHandle<T> | null> { ): Promise<ElementHandle<T> | null> {
const { updatedSelector, queryHandler } = const { updatedSelector, queryHandler } =
getQueryHandlerAndSelector(selector); getQueryHandlerAndSelector(selector);
assert(
queryHandler.queryOne,
'Cannot handle queries for a single element with the given selector'
);
return queryHandler.queryOne(this, updatedSelector); return queryHandler.queryOne(this, updatedSelector);
} }
@ -1076,11 +1077,22 @@ export class ElementHandle<
* Runs `element.querySelectorAll` within the page. If no elements match the selector, * Runs `element.querySelectorAll` within the page. If no elements match the selector,
* the return value resolves to `[]`. * the return value resolves to `[]`.
*/ */
/**
* Runs `element.querySelectorAll` within the page.
*
* @param selector The selector to query with.
* @returns `[]` if no element matches the selector.
* @throws `Error` if the selector has no associated query handler.
*/
async $$<T extends Element = Element>( async $$<T extends Element = Element>(
selector: string selector: string
): Promise<Array<ElementHandle<T>>> { ): Promise<Array<ElementHandle<T>>> {
const { updatedSelector, queryHandler } = const { updatedSelector, queryHandler } =
getQueryHandlerAndSelector(selector); getQueryHandlerAndSelector(selector);
assert(
queryHandler.queryAll,
'Cannot handle queries for a multiple element with the given selector'
);
return queryHandler.queryAll(this, updatedSelector); return queryHandler.queryAll(this, updatedSelector);
} }
@ -1156,21 +1168,21 @@ export class ElementHandle<
*/ */
async $$eval<ReturnType>( async $$eval<ReturnType>(
selector: string, selector: string,
pageFunction: ( pageFunction: EvaluateFn<
elements: Element[], Element[],
...args: unknown[] unknown,
) => ReturnType | Promise<ReturnType>, ReturnType | Promise<ReturnType>
>,
...args: SerializableOrJSHandle[] ...args: SerializableOrJSHandle[]
): Promise<WrapElementHandle<ReturnType>> { ): Promise<WrapElementHandle<ReturnType>> {
const { updatedSelector, queryHandler } = const { updatedSelector, queryHandler } =
getQueryHandlerAndSelector(selector); getQueryHandlerAndSelector(selector);
assert(queryHandler.queryAllArray);
const arrayHandle = await queryHandler.queryAllArray(this, updatedSelector); const arrayHandle = await queryHandler.queryAllArray(this, updatedSelector);
const result = await arrayHandle.evaluate< const result = await arrayHandle.evaluate<EvaluateFn<Element[]>>(
( pageFunction,
elements: Element[], ...args
...args: unknown[] );
) => ReturnType | Promise<ReturnType>
>(pageFunction, ...args);
await arrayHandle.dispose(); await arrayHandle.dispose();
/* This `as` exists for the same reason as the `as` in $eval above. /* This `as` exists for the same reason as the `as` in $eval above.
* See the comment there for a full explanation. * See the comment there for a full explanation.
@ -1220,7 +1232,7 @@ export class ElementHandle<
return await this.evaluate(async (element: Element, threshold: number) => { return await this.evaluate(async (element: Element, threshold: number) => {
const visibleRatio = await new Promise<number>((resolve) => { const visibleRatio = await new Promise<number>((resolve) => {
const observer = new IntersectionObserver((entries) => { const observer = new IntersectionObserver((entries) => {
resolve(entries[0].intersectionRatio); resolve(entries[0]!.intersectionRatio);
observer.disconnect(); observer.disconnect();
}); });
observer.observe(element); observer.observe(element);
@ -1290,14 +1302,14 @@ export interface Point {
y: number; y: number;
} }
function computeQuadArea(quad: Array<{ x: number; y: number }>): number { function computeQuadArea(quad: Point[]): number {
/* Compute sum of all directed areas of adjacent triangles /* Compute sum of all directed areas of adjacent triangles
https://en.wikipedia.org/wiki/Polygon#Simple_polygons https://en.wikipedia.org/wiki/Polygon#Simple_polygons
*/ */
let area = 0; let area = 0;
for (let i = 0; i < quad.length; ++i) { for (let i = 0; i < quad.length; ++i) {
const p1 = quad[i]; const p1 = quad[i]!;
const p2 = quad[(i + 1) % quad.length]; const p2 = quad[(i + 1) % quad.length]!;
area += (p1.x * p2.y - p2.x * p1.y) / 2; area += (p1.x * p2.y - p2.x * p1.y) / 2;
} }
return Math.abs(area); return Math.abs(area);

View File

@ -24,7 +24,7 @@ export interface PDFMargin {
right?: string | number; right?: string | number;
} }
type LowerCasePaperFormat = export type LowerCasePaperFormat =
| 'letter' | 'letter'
| 'legal' | 'legal'
| 'tabloid' | 'tabloid'

View File

@ -14,55 +14,58 @@
* limitations under the License. * limitations under the License.
*/ */
import { Protocol } from 'devtools-protocol';
import type { Readable } from 'stream'; import type { Readable } from 'stream';
import { isNode } from '../environment.js';
import { EventEmitter, Handler } from './EventEmitter.js'; import { Accessibility } from './Accessibility.js';
import { assert, assertNever } from './assert.js';
import { Browser, BrowserContext } from './Browser.js';
import { import {
Connection,
CDPSession, CDPSession,
CDPSessionEmittedEvents, CDPSessionEmittedEvents,
Connection,
} from './Connection.js'; } from './Connection.js';
import { ConsoleMessage, ConsoleMessageType } from './ConsoleMessage.js';
import { Coverage } from './Coverage.js';
import { Dialog } from './Dialog.js'; import { Dialog } from './Dialog.js';
import { EmulationManager } from './EmulationManager.js'; import { EmulationManager } from './EmulationManager.js';
import {
EvaluateFn,
EvaluateFnReturnType,
EvaluateHandleFn,
SerializableOrJSHandle,
UnwrapPromiseLike,
WrapElementHandle,
} from './EvalTypes.js';
import { EventEmitter, Handler } from './EventEmitter.js';
import { FileChooser } from './FileChooser.js';
import { import {
Frame, Frame,
FrameManager, FrameManager,
FrameManagerEmittedEvents, FrameManagerEmittedEvents,
} from './FrameManager.js'; } from './FrameManager.js';
import { Keyboard, Mouse, Touchscreen, MouseButton } from './Input.js'; import { debugError, helper } from './helper.js';
import { Tracing } from './Tracing.js'; import { HTTPRequest } from './HTTPRequest.js';
import { assert, assertNever } from './assert.js'; import { HTTPResponse } from './HTTPResponse.js';
import { helper, debugError } from './helper.js'; import { Keyboard, Mouse, MouseButton, Touchscreen } from './Input.js';
import { Coverage } from './Coverage.js'; import { createJSHandle, ElementHandle, JSHandle } from './JSHandle.js';
import { WebWorker } from './WebWorker.js'; import { PuppeteerLifeCycleEvent } from './LifecycleWatcher.js';
import { Browser, BrowserContext } from './Browser.js';
import { Target } from './Target.js';
import { createJSHandle, JSHandle, ElementHandle } from './JSHandle.js';
import { Viewport } from './PuppeteerViewport.js';
import { import {
Credentials, Credentials,
NetworkConditions, NetworkConditions,
NetworkManagerEmittedEvents, NetworkManagerEmittedEvents,
} from './NetworkManager.js'; } from './NetworkManager.js';
import { HTTPRequest } from './HTTPRequest.js';
import { HTTPResponse } from './HTTPResponse.js';
import { Accessibility } from './Accessibility.js';
import { TimeoutSettings } from './TimeoutSettings.js';
import { FileChooser } from './FileChooser.js';
import { ConsoleMessage, ConsoleMessageType } from './ConsoleMessage.js';
import { PuppeteerLifeCycleEvent } from './LifecycleWatcher.js';
import { Protocol } from 'devtools-protocol';
import { import {
SerializableOrJSHandle, LowerCasePaperFormat,
EvaluateHandleFn, paperFormats,
WrapElementHandle, PDFOptions,
EvaluateFn, } from './PDFOptions.js';
EvaluateFnReturnType, import { Viewport } from './PuppeteerViewport.js';
UnwrapPromiseLike, import { Target } from './Target.js';
} from './EvalTypes.js';
import { PDFOptions, paperFormats } from './PDFOptions.js';
import { isNode } from '../environment.js';
import { TaskQueue } from './TaskQueue.js'; import { TaskQueue } from './TaskQueue.js';
import { TimeoutSettings } from './TimeoutSettings.js';
import { Tracing } from './Tracing.js';
import { WebWorker } from './WebWorker.js';
/** /**
* @public * @public
@ -492,35 +495,35 @@ export class Page extends EventEmitter {
client.on( client.on(
'Target.attachedToTarget', 'Target.attachedToTarget',
(event: Protocol.Target.AttachedToTargetEvent) => { (event: Protocol.Target.AttachedToTargetEvent) => {
if ( switch (event.targetInfo.type) {
event.targetInfo.type !== 'worker' && case 'worker':
event.targetInfo.type !== 'iframe' const connection = Connection.fromSession(client);
) { assert(connection);
// If we don't detach from service workers, they will never die. const session = connection.session(event.sessionId);
// We still want to attach to workers for emitting events. assert(session);
// We still want to attach to iframes so sessions may interact with them. const worker = new WebWorker(
// We detach from all other types out of an abundance of caution. session,
// See https://source.chromium.org/chromium/chromium/src/+/main:content/browser/devtools/devtools_agent_host_impl.cc?ss=chromium&q=f:devtools%20-f:out%20%22::kTypePage%5B%5D%22 event.targetInfo.url,
// for the complete list of available types. this._addConsoleMessage.bind(this),
client this._handleException.bind(this)
.send('Target.detachFromTarget', { );
sessionId: event.sessionId, this._workers.set(event.sessionId, worker);
}) this.emit(PageEmittedEvents.WorkerCreated, worker);
.catch(debugError); break;
return; case 'iframe':
} break;
if (event.targetInfo.type === 'worker') { default:
const session = Connection.fromSession(client).session( // If we don't detach from service workers, they will never die.
event.sessionId // We still want to attach to workers for emitting events.
); // We still want to attach to iframes so sessions may interact with them.
const worker = new WebWorker( // We detach from all other types out of an abundance of caution.
session, // See https://source.chromium.org/chromium/chromium/src/+/main:content/browser/devtools/devtools_agent_host_impl.cc?ss=chromium&q=f:devtools%20-f:out%20%22::kTypePage%5B%5D%22
event.targetInfo.url, // for the complete list of available types.
this._addConsoleMessage.bind(this), client
this._handleException.bind(this) .send('Target.detachFromTarget', {
); sessionId: event.sessionId,
this._workers.set(event.sessionId, worker); })
this.emit(PageEmittedEvents.WorkerCreated, worker); .catch(debugError);
} }
} }
); );
@ -598,6 +601,7 @@ export class Page extends EventEmitter {
): Promise<void> { ): Promise<void> {
if (!this._fileChooserInterceptors.size) return; if (!this._fileChooserInterceptors.size) return;
const frame = this._frameManager.frame(event.frameId); const frame = this._frameManager.frame(event.frameId);
assert(frame);
const context = await frame.executionContext(); const context = await frame.executionContext();
const element = await context._adoptBackendNodeId(event.backendNodeId); const element = await context._adoptBackendNodeId(event.backendNodeId);
const interceptors = Array.from(this._fileChooserInterceptors); const interceptors = Array.from(this._fileChooserInterceptors);
@ -626,7 +630,7 @@ export class Page extends EventEmitter {
// Note: this method exists to define event typings and handle // Note: this method exists to define event typings and handle
// proper wireup of cooperative request interception. Actual event listening and // proper wireup of cooperative request interception. Actual event listening and
// dispatching is delegated to EventEmitter. // dispatching is delegated to EventEmitter.
public on<K extends keyof PageEventObject>( public override on<K extends keyof PageEventObject>(
eventName: K, eventName: K,
handler: (event: PageEventObject[K]) => void handler: (event: PageEventObject[K]) => void
): EventEmitter { ): EventEmitter {
@ -646,7 +650,7 @@ export class Page extends EventEmitter {
return super.on(eventName, handler); return super.on(eventName, handler);
} }
public once<K extends keyof PageEventObject>( public override once<K extends keyof PageEventObject>(
eventName: K, eventName: K,
handler: (event: PageEventObject[K]) => void handler: (event: PageEventObject[K]) => void
): EventEmitter { ): EventEmitter {
@ -655,7 +659,7 @@ export class Page extends EventEmitter {
return super.once(eventName, handler); return super.once(eventName, handler);
} }
off<K extends keyof PageEventObject>( override off<K extends keyof PageEventObject>(
eventName: K, eventName: K,
handler: (event: PageEventObject[K]) => void handler: (event: PageEventObject[K]) => void
): EventEmitter { ): EventEmitter {
@ -697,7 +701,7 @@ export class Page extends EventEmitter {
}); });
const { timeout = this._timeoutSettings.timeout() } = options; const { timeout = this._timeoutSettings.timeout() } = options;
let callback: (value: FileChooser | PromiseLike<FileChooser>) => void; let callback!: (value: FileChooser | PromiseLike<FileChooser>) => void;
const promise = new Promise<FileChooser>((x) => (callback = x)); const promise = new Promise<FileChooser>((x) => (callback = x));
this._fileChooserInterceptors.add(callback); this._fileChooserInterceptors.add(callback);
return helper return helper
@ -1253,7 +1257,9 @@ export class Page extends EventEmitter {
const filterUnsupportedAttributes = ( const filterUnsupportedAttributes = (
cookie: Protocol.Network.Cookie cookie: Protocol.Network.Cookie
): Protocol.Network.Cookie => { ): Protocol.Network.Cookie => {
for (const attr of unsupportedCookieAttributes) delete cookie[attr]; for (const attr of unsupportedCookieAttributes) {
delete (cookie as unknown as Record<string, unknown>)[attr];
}
return cookie; return cookie;
}; };
return originalCookies.map(filterUnsupportedAttributes); return originalCookies.map(filterUnsupportedAttributes);
@ -1505,9 +1511,14 @@ export class Page extends EventEmitter {
private _buildMetricsObject( private _buildMetricsObject(
metrics?: Protocol.Performance.Metric[] metrics?: Protocol.Performance.Metric[]
): Metrics { ): Metrics {
const result = {}; const result: Record<
Protocol.Performance.Metric['name'],
Protocol.Performance.Metric['value']
> = {};
for (const metric of metrics || []) { for (const metric of metrics || []) {
if (supportedMetrics.has(metric.name)) result[metric.name] = metric.value; if (supportedMetrics.has(metric.name)) {
result[metric.name] = metric.value;
}
} }
return result; return result;
} }
@ -1563,7 +1574,9 @@ export class Page extends EventEmitter {
if (type !== 'exposedFun' || !this._pageBindings.has(name)) return; if (type !== 'exposedFun' || !this._pageBindings.has(name)) return;
let expression = null; let expression = null;
try { try {
const result = await this._pageBindings.get(name)(...args); const pageBinding = this._pageBindings.get(name);
assert(pageBinding);
const result = await pageBinding(...args);
expression = helper.pageBindingDeliverResultString(name, seq, result); expression = helper.pageBindingDeliverResultString(name, seq, result);
} catch (error) { } catch (error) {
if (error instanceof Error) if (error instanceof Error)
@ -1589,7 +1602,7 @@ export class Page extends EventEmitter {
} }
private _addConsoleMessage( private _addConsoleMessage(
type: ConsoleMessageType, eventType: ConsoleMessageType,
args: JSHandle[], args: JSHandle[],
stackTrace?: Protocol.Runtime.StackTrace stackTrace?: Protocol.Runtime.StackTrace
): void { ): void {
@ -1614,7 +1627,7 @@ export class Page extends EventEmitter {
} }
} }
const message = new ConsoleMessage( const message = new ConsoleMessage(
type, eventType,
textTokens.join(' '), textTokens.join(' '),
args, args,
stackTraceLocations stackTraceLocations
@ -1764,7 +1777,7 @@ export class Page extends EventEmitter {
async goto( async goto(
url: string, url: string,
options: WaitForOptions & { referer?: string } = {} options: WaitForOptions & { referer?: string } = {}
): Promise<HTTPResponse> { ): Promise<HTTPResponse | null> {
return await this._frameManager.mainFrame().goto(url, options); return await this._frameManager.mainFrame().goto(url, options);
} }
@ -1949,17 +1962,17 @@ export class Page extends EventEmitter {
const networkManager = this._frameManager.networkManager(); const networkManager = this._frameManager.networkManager();
let idleResolveCallback; let idleResolveCallback: () => void;
const idlePromise = new Promise((resolve) => { const idlePromise = new Promise<void>((resolve) => {
idleResolveCallback = resolve; idleResolveCallback = resolve;
}); });
let abortRejectCallback; let abortRejectCallback: (error: Error) => void;
const abortPromise = new Promise<Error>((_, reject) => { const abortPromise = new Promise<Error>((_, reject) => {
abortRejectCallback = reject; abortRejectCallback = reject;
}); });
let idleTimer; let idleTimer: NodeJS.Timeout;
const onIdle = () => idleResolveCallback(); const onIdle = () => idleResolveCallback();
const cleanup = () => { const cleanup = () => {
@ -1980,7 +1993,7 @@ export class Page extends EventEmitter {
return false; return false;
}; };
const listenToEvent = (event) => const listenToEvent = (event: symbol) =>
helper.waitForEvent( helper.waitForEvent(
networkManager, networkManager,
event, event,
@ -2033,15 +2046,21 @@ export class Page extends EventEmitter {
): Promise<Frame> { ): Promise<Frame> {
const { timeout = this._timeoutSettings.timeout() } = options; const { timeout = this._timeoutSettings.timeout() } = options;
async function predicate(frame: Frame) { let predicate: (frame: Frame) => Promise<boolean>;
if (helper.isString(urlOrPredicate)) if (helper.isString(urlOrPredicate)) {
return urlOrPredicate === frame.url(); predicate = (frame: Frame) =>
if (typeof urlOrPredicate === 'function') Promise.resolve(urlOrPredicate === frame.url());
return !!(await urlOrPredicate(frame)); } else {
return false; predicate = (frame: Frame) => {
const value = urlOrPredicate(frame);
if (typeof value === 'boolean') {
return Promise.resolve(value);
}
return value;
};
} }
const eventRace = Promise.race([ const eventRace: Promise<Frame> = Promise.race([
helper.waitForEvent( helper.waitForEvent(
this._frameManager, this._frameManager,
FrameManagerEmittedEvents.FrameAttached, FrameManagerEmittedEvents.FrameAttached,
@ -2056,19 +2075,15 @@ export class Page extends EventEmitter {
timeout, timeout,
this._sessionClosePromise() this._sessionClosePromise()
), ),
...this.frames().map(async (frame) => {
if (await predicate(frame)) {
return frame;
}
return await eventRace;
}),
]); ]);
return Promise.race([ return eventRace;
eventRace,
(async () => {
for (const frame of this.frames()) {
if (await predicate(frame)) {
return frame;
}
}
await eventRace;
})(),
]);
} }
/** /**
@ -2317,10 +2332,9 @@ export class Page extends EventEmitter {
* ``` * ```
*/ */
async emulateMediaFeatures(features?: MediaFeature[]): Promise<void> { async emulateMediaFeatures(features?: MediaFeature[]): Promise<void> {
if (features === null) if (!features) await this._client.send('Emulation.setEmulatedMedia', {});
await this._client.send('Emulation.setEmulatedMedia', { features: null });
if (Array.isArray(features)) { if (Array.isArray(features)) {
features.every((mediaFeature) => { for (const mediaFeature of features) {
const name = mediaFeature.name; const name = mediaFeature.name;
assert( assert(
/^(?:prefers-(?:color-scheme|reduced-motion)|color-gamut)$/.test( /^(?:prefers-(?:color-scheme|reduced-motion)|color-gamut)$/.test(
@ -2328,8 +2342,7 @@ export class Page extends EventEmitter {
), ),
'Unsupported media feature: ' + name 'Unsupported media feature: ' + name
); );
return true; }
});
await this._client.send('Emulation.setEmulatedMedia', { await this._client.send('Emulation.setEmulatedMedia', {
features: features, features: features,
}); });
@ -2348,7 +2361,7 @@ export class Page extends EventEmitter {
timezoneId: timezoneId || '', timezoneId: timezoneId || '',
}); });
} catch (error) { } catch (error) {
if (error.message.includes('Invalid timezone')) if (error instanceof Error && error.message.includes('Invalid timezone'))
throw new Error(`Invalid timezone ID: ${timezoneId}`); throw new Error(`Invalid timezone ID: ${timezoneId}`);
throw error; throw error;
} }
@ -2652,7 +2665,7 @@ export class Page extends EventEmitter {
* the value of `encoding`) with captured screenshot. * the value of `encoding`) with captured screenshot.
*/ */
async screenshot(options: ScreenshotOptions = {}): Promise<Buffer | string> { async screenshot(options: ScreenshotOptions = {}): Promise<Buffer | string> {
let screenshotType = null; let screenshotType = Protocol.Page.CaptureScreenshotRequestFormat.Png;
// options.type takes precedence over inferring the type from options.path // options.type takes precedence over inferring the type from options.path
// because it may be a 0-length file with no extension created beforehand // because it may be a 0-length file with no extension created beforehand
// (i.e. as a temp file). // (i.e. as a temp file).
@ -2661,27 +2674,35 @@ export class Page extends EventEmitter {
if (type !== 'png' && type !== 'jpeg' && type !== 'webp') { if (type !== 'png' && type !== 'jpeg' && type !== 'webp') {
assertNever(type, 'Unknown options.type value: ' + type); assertNever(type, 'Unknown options.type value: ' + type);
} }
screenshotType = options.type; screenshotType =
options.type as Protocol.Page.CaptureScreenshotRequestFormat;
} else if (options.path) { } else if (options.path) {
const filePath = options.path; const filePath = options.path;
const extension = filePath const extension = filePath
.slice(filePath.lastIndexOf('.') + 1) .slice(filePath.lastIndexOf('.') + 1)
.toLowerCase(); .toLowerCase();
if (extension === 'png') screenshotType = 'png'; switch (extension) {
else if (extension === 'jpg' || extension === 'jpeg') case 'png':
screenshotType = 'jpeg'; screenshotType = Protocol.Page.CaptureScreenshotRequestFormat.Png;
else if (extension === 'webp') screenshotType = 'webp'; break;
assert( case 'jpeg':
screenshotType, case 'jpg':
`Unsupported screenshot type for extension \`.${extension}\`` screenshotType = Protocol.Page.CaptureScreenshotRequestFormat.Jpeg;
); break;
case 'webp':
screenshotType = Protocol.Page.CaptureScreenshotRequestFormat.Webp;
break;
default:
throw new Error(
`Unsupported screenshot type for extension \`.${extension}\``
);
}
} }
if (!screenshotType) screenshotType = 'png';
if (options.quality) { if (options.quality) {
assert( assert(
screenshotType === 'jpeg' || screenshotType === 'webp', screenshotType === Protocol.Page.CaptureScreenshotRequestFormat.Jpeg ||
screenshotType === Protocol.Page.CaptureScreenshotRequestFormat.Webp,
'options.quality is unsupported for the ' + 'options.quality is unsupported for the ' +
screenshotType + screenshotType +
' screenshots' ' screenshots'
@ -2742,7 +2763,7 @@ export class Page extends EventEmitter {
private async _screenshotTask( private async _screenshotTask(
format: Protocol.Page.CaptureScreenshotRequestFormat, format: Protocol.Page.CaptureScreenshotRequestFormat,
options?: ScreenshotOptions options: ScreenshotOptions = {}
): Promise<Buffer | string> { ): Promise<Buffer | string> {
await this._client.send('Target.activateTarget', { await this._client.send('Target.activateTarget', {
targetId: this._target._targetId, targetId: this._target._targetId,
@ -2861,7 +2882,8 @@ export class Page extends EventEmitter {
let paperWidth = 8.5; let paperWidth = 8.5;
let paperHeight = 11; let paperHeight = 11;
if (options.format) { if (options.format) {
const format = paperFormats[options.format.toLowerCase()]; const format =
paperFormats[options.format.toLowerCase() as LowerCasePaperFormat];
assert(format, 'Unknown paper format: ' + options.format); assert(format, 'Unknown paper format: ' + options.format);
paperWidth = format.width; paperWidth = format.width;
paperHeight = format.height; paperHeight = format.height;
@ -2908,6 +2930,7 @@ export class Page extends EventEmitter {
await this._resetDefaultBackgroundColor(); await this._resetDefaultBackgroundColor();
} }
assert(result.stream, '`stream` is missing from `Page.printToPDF');
return helper.getReadableFromProtocolStream(this._client, result.stream); return helper.getReadableFromProtocolStream(this._client, result.stream);
} }
@ -2918,7 +2941,9 @@ export class Page extends EventEmitter {
async pdf(options: PDFOptions = {}): Promise<Buffer> { async pdf(options: PDFOptions = {}): Promise<Buffer> {
const { path = undefined } = options; const { path = undefined } = options;
const readable = await this.createPDFStream(options); const readable = await this.createPDFStream(options);
return await helper.getReadableAsBuffer(readable, path); const buffer = await helper.getReadableAsBuffer(readable, path);
assert(buffer, 'Could not create buffer');
return buffer;
} }
/** /**
@ -3134,7 +3159,7 @@ export class Page extends EventEmitter {
polling?: string | number; polling?: string | number;
} = {}, } = {},
...args: SerializableOrJSHandle[] ...args: SerializableOrJSHandle[]
): Promise<JSHandle> { ): Promise<JSHandle | null> {
return this.mainFrame().waitFor( return this.mainFrame().waitFor(
selectorOrFunctionOrTimeout, selectorOrFunctionOrTimeout,
options, options,
@ -3396,7 +3421,7 @@ function convertPrintParameterToInches(
const text = /** @type {string} */ parameter; const text = /** @type {string} */ parameter;
let unit = text.substring(text.length - 2).toLowerCase(); let unit = text.substring(text.length - 2).toLowerCase();
let valueText = ''; let valueText = '';
if (unitToPixels.hasOwnProperty(unit)) { if (unit in unitToPixels) {
valueText = text.substring(0, text.length - 2); valueText = text.substring(0, text.length - 2);
} else { } else {
// In case of unknown unit try to parse the whole parameter as number of pixels. // In case of unknown unit try to parse the whole parameter as number of pixels.
@ -3406,7 +3431,7 @@ function convertPrintParameterToInches(
} }
const value = Number(valueText); const value = Number(valueText);
assert(!isNaN(value), 'Failed to parse parameter value: ' + text); assert(!isNaN(value), 'Failed to parse parameter value: ' + text);
pixels = value * unitToPixels[unit]; pixels = value * unitToPixels[unit as keyof typeof unitToPixels];
} else { } else {
throw new Error( throw new Error(
'page.pdf() Cannot handle parameter type: ' + typeof parameter 'page.pdf() Cannot handle parameter type: ' + typeof parameter

View File

@ -38,7 +38,7 @@ export interface InternalQueryHandler {
queryAllArray?: ( queryAllArray?: (
element: ElementHandle, element: ElementHandle,
selector: string selector: string
) => Promise<JSHandle>; ) => Promise<JSHandle<Element[]>>;
} }
/** /**
@ -64,8 +64,9 @@ function makeQueryHandler(handler: CustomQueryHandler): InternalQueryHandler {
const internalHandler: InternalQueryHandler = {}; const internalHandler: InternalQueryHandler = {};
if (handler.queryOne) { if (handler.queryOne) {
const queryOne = handler.queryOne;
internalHandler.queryOne = async (element, selector) => { internalHandler.queryOne = async (element, selector) => {
const jsHandle = await element.evaluateHandle(handler.queryOne, selector); const jsHandle = await element.evaluateHandle(queryOne, selector);
const elementHandle = jsHandle.asElement(); const elementHandle = jsHandle.asElement();
if (elementHandle) return elementHandle; if (elementHandle) return elementHandle;
await jsHandle.dispose(); await jsHandle.dispose();
@ -75,12 +76,13 @@ function makeQueryHandler(handler: CustomQueryHandler): InternalQueryHandler {
domWorld: DOMWorld, domWorld: DOMWorld,
selector: string, selector: string,
options: WaitForSelectorOptions options: WaitForSelectorOptions
) => domWorld.waitForSelectorInPage(handler.queryOne, selector, options); ) => domWorld.waitForSelectorInPage(queryOne, selector, options);
} }
if (handler.queryAll) { if (handler.queryAll) {
const queryAll = handler.queryAll;
internalHandler.queryAll = async (element, selector) => { internalHandler.queryAll = async (element, selector) => {
const jsHandle = await element.evaluateHandle(handler.queryAll, selector); const jsHandle = await element.evaluateHandle(queryAll, selector);
const properties = await jsHandle.getProperties(); const properties = await jsHandle.getProperties();
await jsHandle.dispose(); await jsHandle.dispose();
const result = []; const result = [];
@ -91,10 +93,7 @@ function makeQueryHandler(handler: CustomQueryHandler): InternalQueryHandler {
return result; return result;
}; };
internalHandler.queryAllArray = async (element, selector) => { internalHandler.queryAllArray = async (element, selector) => {
const resultHandle = await element.evaluateHandle( const resultHandle = await element.evaluateHandle(queryAll, selector);
handler.queryAll,
selector
);
const arrayHandle = await resultHandle.evaluateHandle( const arrayHandle = await resultHandle.evaluateHandle(
(res: Element[] | NodeListOf<Element>) => Array.from(res) (res: Element[] | NodeListOf<Element>) => Array.from(res)
); );
@ -106,9 +105,9 @@ function makeQueryHandler(handler: CustomQueryHandler): InternalQueryHandler {
} }
const _defaultHandler = makeQueryHandler({ const _defaultHandler = makeQueryHandler({
queryOne: (element: Element, selector: string) => queryOne: (element: Element | Document, selector: string) =>
element.querySelector(selector), element.querySelector(selector),
queryAll: (element: Element, selector: string) => queryAll: (element: Element | Document, selector: string) =>
element.querySelectorAll(selector), element.querySelectorAll(selector),
}); });

View File

@ -42,7 +42,7 @@ export class Target {
/** /**
* @internal * @internal
*/ */
_initializedCallback: (x: boolean) => void; _initializedCallback!: (x: boolean) => void;
/** /**
* @internal * @internal
*/ */
@ -50,7 +50,7 @@ export class Target {
/** /**
* @internal * @internal
*/ */
_closedCallback: () => void; _closedCallback!: () => void;
/** /**
* @internal * @internal
*/ */
@ -81,13 +81,9 @@ export class Target {
this._targetId = targetInfo.targetId; this._targetId = targetInfo.targetId;
this._sessionFactory = sessionFactory; this._sessionFactory = sessionFactory;
this._ignoreHTTPSErrors = ignoreHTTPSErrors; this._ignoreHTTPSErrors = ignoreHTTPSErrors;
this._defaultViewport = defaultViewport; this._defaultViewport = defaultViewport ?? undefined;
this._screenshotTaskQueue = screenshotTaskQueue; this._screenshotTaskQueue = screenshotTaskQueue;
this._isPageTargetCallback = isPageTargetCallback; this._isPageTargetCallback = isPageTargetCallback;
/** @type {?Promise<!Puppeteer.Page>} */
this._pagePromise = null;
/** @type {?Promise<!WebWorker>} */
this._workerPromise = null;
this._initializedPromise = new Promise<boolean>( this._initializedPromise = new Promise<boolean>(
(fulfill) => (this._initializedCallback = fulfill) (fulfill) => (this._initializedCallback = fulfill)
).then(async (success) => { ).then(async (success) => {
@ -120,14 +116,14 @@ export class Target {
/** /**
* If the target is not of type `"page"` or `"background_page"`, returns `null`. * If the target is not of type `"page"` or `"background_page"`, returns `null`.
*/ */
async page(): Promise<Page | null> { async page(): Promise<Page | undefined> {
if (this._isPageTargetCallback(this._targetInfo) && !this._pagePromise) { if (this._isPageTargetCallback(this._targetInfo) && !this._pagePromise) {
this._pagePromise = this._sessionFactory().then((client) => this._pagePromise = this._sessionFactory().then((client) =>
Page.create( Page.create(
client, client,
this, this,
this._ignoreHTTPSErrors, this._ignoreHTTPSErrors,
this._defaultViewport, this._defaultViewport ?? null,
this._screenshotTaskQueue this._screenshotTaskQueue
) )
); );
@ -208,9 +204,9 @@ export class Target {
/** /**
* Get the target that opened this target. Top-level targets return `null`. * Get the target that opened this target. Top-level targets return `null`.
*/ */
opener(): Target | null { opener(): Target | undefined {
const { openerId } = this._targetInfo; const { openerId } = this._targetInfo;
if (!openerId) return null; if (!openerId) return;
return this.browser()._targets.get(openerId); return this.browser()._targets.get(openerId);
} }

View File

@ -44,7 +44,7 @@ export interface TracingOptions {
export class Tracing { export class Tracing {
_client: CDPSession; _client: CDPSession;
_recording = false; _recording = false;
_path = ''; _path?: string;
/** /**
* @internal * @internal
@ -79,7 +79,7 @@ export class Tracing {
'disabled-by-default-v8.cpu_profiler', 'disabled-by-default-v8.cpu_profiler',
]; ];
const { const {
path = null, path,
screenshots = false, screenshots = false,
categories = defaultCategories, categories = defaultCategories,
} = options; } = options;
@ -106,11 +106,11 @@ export class Tracing {
* Stops a trace started with the `start` method. * Stops a trace started with the `start` method.
* @returns Promise which resolves to buffer with trace data. * @returns Promise which resolves to buffer with trace data.
*/ */
async stop(): Promise<Buffer> { async stop(): Promise<Buffer | undefined> {
let fulfill: (value: Buffer) => void; let resolve: (value: Buffer | undefined) => void;
let reject: (err: Error) => void; let reject: (err: Error) => void;
const contentPromise = new Promise<Buffer>((x, y) => { const contentPromise = new Promise<Buffer | undefined>((x, y) => {
fulfill = x; resolve = x;
reject = y; reject = y;
}); });
this._client.once('Tracing.tracingComplete', async (event) => { this._client.once('Tracing.tracingComplete', async (event) => {
@ -120,9 +120,13 @@ export class Tracing {
event.stream event.stream
); );
const buffer = await helper.getReadableAsBuffer(readable, this._path); const buffer = await helper.getReadableAsBuffer(readable, this._path);
fulfill(buffer); resolve(buffer ?? undefined);
} catch (error) { } catch (error) {
reject(error); if (error instanceof Error) {
reject(error);
} else {
reject(new Error(`Unknown error: ${error}`));
}
} }
}); });
await this._client.send('Tracing.end'); await this._client.send('Tracing.end');

View File

@ -20,12 +20,13 @@ import { JSHandle } from './JSHandle.js';
import { CDPSession } from './Connection.js'; import { CDPSession } from './Connection.js';
import { Protocol } from 'devtools-protocol'; import { Protocol } from 'devtools-protocol';
import { EvaluateHandleFn, SerializableOrJSHandle } from './EvalTypes.js'; import { EvaluateHandleFn, SerializableOrJSHandle } from './EvalTypes.js';
import { ConsoleMessageType } from './ConsoleMessage.js';
/** /**
* @internal * @internal
*/ */
export type ConsoleAPICalledCallback = ( export type ConsoleAPICalledCallback = (
eventType: string, eventType: ConsoleMessageType,
handles: JSHandle[], handles: JSHandle[],
trace: Protocol.Runtime.StackTrace trace: Protocol.Runtime.StackTrace
) => void; ) => void;
@ -63,7 +64,7 @@ export class WebWorker extends EventEmitter {
_client: CDPSession; _client: CDPSession;
_url: string; _url: string;
_executionContextPromise: Promise<ExecutionContext>; _executionContextPromise: Promise<ExecutionContext>;
_executionContextCallback: (value: ExecutionContext) => void; _executionContextCallback!: (value: ExecutionContext) => void;
/** /**
* *
@ -87,11 +88,7 @@ export class WebWorker extends EventEmitter {
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
jsHandleFactory = (remoteObject) => jsHandleFactory = (remoteObject) =>
new JSHandle(executionContext, client, remoteObject); new JSHandle(executionContext, client, remoteObject);
const executionContext = new ExecutionContext( const executionContext = new ExecutionContext(client, event.context);
client,
event.context,
null
);
this._executionContextCallback(executionContext); this._executionContextCallback(executionContext);
}); });

View File

@ -19,10 +19,16 @@
* @param value * @param value
* @param message - the error message to throw if the value is not truthy. * @param message - the error message to throw if the value is not truthy.
*/ */
export const assert = (value: unknown, message?: string): void => { export const assert: (value: unknown, message?: string) => asserts value = (
value,
message
) => {
if (!value) throw new Error(message); if (!value) throw new Error(message);
}; };
export const assertNever = (value: never, message?: string): void => { export const assertNever: (
value: unknown,
message?: string
) => asserts value is never = (value, message) => {
if (value) throw new Error(message); if (value) throw new Error(message);
}; };

View File

@ -228,13 +228,13 @@ function pageBindingDeliverErrorString(
name: string, name: string,
seq: number, seq: number,
message: string, message: string,
stack: string stack?: string
): string { ): string {
function deliverError( function deliverError(
name: string, name: string,
seq: number, seq: number,
message: string, message: string,
stack: string stack?: string
): void { ): void {
const error = new Error(message); const error = new Error(message);
error.stack = stack; error.stack = stack;
@ -304,7 +304,7 @@ async function waitWithTimeout<T>(
const timeoutError = new TimeoutError( const timeoutError = new TimeoutError(
`waiting for ${taskName} failed: timeout ${timeout}ms exceeded` `waiting for ${taskName} failed: timeout ${timeout}ms exceeded`
); );
const timeoutPromise = new Promise<T>((resolve, x) => (reject = x)); const timeoutPromise = new Promise<T>((_res, rej) => (reject = rej));
let timeoutTimer = null; let timeoutTimer = null;
if (timeout) timeoutTimer = setTimeout(() => reject(timeoutError), timeout); if (timeout) timeoutTimer = setTimeout(() => reject(timeoutError), timeout);
try { try {
@ -362,7 +362,7 @@ async function getReadableFromProtocolStream(
return new Readable({ return new Readable({
async read(size: number) { async read(size: number) {
if (eof) { if (eof) {
return null; return;
} }
const response = await client.send('IO.read', { handle, size }); const response = await client.send('IO.read', { handle, size });

View File

@ -27,9 +27,9 @@ export const initializePuppeteerNode = (packageName: string): PuppeteerNode => {
// puppeteer-core ignores environment variables // puppeteer-core ignores environment variables
const productName = isPuppeteerCore const productName = isPuppeteerCore
? undefined ? undefined
: process.env.PUPPETEER_PRODUCT || : process.env['PUPPETEER_PRODUCT'] ||
process.env.npm_config_puppeteer_product || process.env['npm_config_puppeteer_product'] ||
process.env.npm_package_config_puppeteer_product; process.env['npm_package_config_puppeteer_product'];
if (!isPuppeteerCore && productName === 'firefox') if (!isPuppeteerCore && productName === 'firefox')
preferredRevision = PUPPETEER_REVISIONS.firefox; preferredRevision = PUPPETEER_REVISIONS.firefox;

View File

@ -42,7 +42,7 @@ const { PUPPETEER_EXPERIMENTAL_CHROMIUM_MAC_ARM } = process.env;
const debugFetcher = debug('puppeteer:fetcher'); const debugFetcher = debug('puppeteer:fetcher');
const downloadURLs = { const downloadURLs: Record<Product, Partial<Record<Platform, string>>> = {
chrome: { chrome: {
linux: '%s/chromium-browser-snapshots/Linux_x64/%d/%s.zip', linux: '%s/chromium-browser-snapshots/Linux_x64/%d/%s.zip',
mac: '%s/chromium-browser-snapshots/Mac/%d/%s.zip', mac: '%s/chromium-browser-snapshots/Mac/%d/%s.zip',
@ -56,7 +56,7 @@ const downloadURLs = {
win32: '%s/firefox-%s.en-US.%s.zip', win32: '%s/firefox-%s.en-US.%s.zip',
win64: '%s/firefox-%s.en-US.%s.zip', win64: '%s/firefox-%s.en-US.%s.zip',
}, },
} as const; };
const browserConfig = { const browserConfig = {
chrome: { chrome: {
@ -80,15 +80,23 @@ function archiveName(
platform: Platform, platform: Platform,
revision: string revision: string
): string { ): string {
if (product === 'chrome') { switch (product) {
if (platform === 'linux') return 'chrome-linux'; case 'chrome':
if (platform === 'mac' || platform === 'mac_arm') return 'chrome-mac'; switch (platform) {
if (platform === 'win32' || platform === 'win64') { case 'linux':
// Windows archive name changed at r591479. return 'chrome-linux';
return parseInt(revision, 10) > 591479 ? 'chrome-win' : 'chrome-win32'; case 'mac_arm':
} case 'mac':
} else if (product === 'firefox') { return 'chrome-mac';
return platform; case 'win32':
case 'win64':
// Windows archive name changed at r591479.
return parseInt(revision, 10) > 591479
? 'chrome-win'
: 'chrome-win32';
}
case 'firefox':
return platform;
} }
} }
@ -114,9 +122,9 @@ function downloadURL(
* @internal * @internal
*/ */
function handleArm64(): void { function handleArm64(): void {
fs.stat('/usr/bin/chromium-browser', function (err, stats) { fs.stat('/usr/bin/chromium-browser', function (_err, stats) {
if (stats === undefined) { if (stats === undefined) {
fs.stat('/usr/bin/chromium', function (err, stats) { fs.stat('/usr/bin/chromium', function (_err, stats) {
if (stats === undefined) { if (stats === undefined) {
console.error( console.error(
'The chromium binary is not available for arm64.' + 'The chromium binary is not available for arm64.' +
@ -206,38 +214,42 @@ export class BrowserFetcher {
options.path || options.path ||
path.join(projectRoot, browserConfig[this._product].destination); path.join(projectRoot, browserConfig[this._product].destination);
this._downloadHost = options.host || browserConfig[this._product].host; this._downloadHost = options.host || browserConfig[this._product].host;
this.setPlatform(options.platform, this._product);
if (options.platform) {
this._platform = options.platform;
} else {
const platform = os.platform();
switch (platform) {
case 'darwin':
switch (this._product) {
case 'chrome':
this._platform =
os.arch() === 'arm64' && PUPPETEER_EXPERIMENTAL_CHROMIUM_MAC_ARM
? 'mac_arm'
: 'mac';
break;
case 'firefox':
this._platform = 'mac';
break;
}
break;
case 'linux':
this._platform = 'linux';
break;
case 'win32':
this._platform = os.arch() === 'x64' ? 'win64' : 'win32';
return;
default:
assert(false, 'Unsupported platform: ' + platform);
}
}
assert( assert(
downloadURLs[this._product][this._platform], downloadURLs[this._product][this._platform],
'Unsupported platform: ' + this._platform 'Unsupported platform: ' + this._platform
); );
} }
private setPlatform(
platformFromOptions?: Platform,
productFromOptions?: Product
): void {
if (platformFromOptions) {
this._platform = platformFromOptions;
return;
}
const platform = os.platform();
if (platform === 'darwin') {
if (productFromOptions === 'chrome') {
this._platform =
os.arch() === 'arm64' && PUPPETEER_EXPERIMENTAL_CHROMIUM_MAC_ARM
? 'mac_arm'
: 'mac';
} else if (productFromOptions === 'firefox') {
this._platform = 'mac';
}
} else if (platform === 'linux') this._platform = 'linux';
else if (platform === 'win32')
this._platform = os.arch() === 'x64' ? 'win64' : 'win32';
else assert(this._platform, 'Unsupported platform: ' + platform);
}
/** /**
* @returns Returns the current `Platform`, which is one of `mac`, `linux`, * @returns Returns the current `Platform`, which is one of `mac`, `linux`,
* `win32` or `win64`. * `win32` or `win64`.
@ -305,7 +317,7 @@ export class BrowserFetcher {
async download( async download(
revision: string, revision: string,
progressCallback: (x: number, y: number) => void = (): void => {} progressCallback: (x: number, y: number) => void = (): void => {}
): Promise<BrowserFetcherRevisionInfo> { ): Promise<BrowserFetcherRevisionInfo | undefined> {
const url = downloadURL( const url = downloadURL(
this._product, this._product,
this._platform, this._platform,
@ -313,6 +325,7 @@ export class BrowserFetcher {
revision revision
); );
const fileName = url.split('/').pop(); const fileName = url.split('/').pop();
assert(fileName, `A malformed download URL was found: ${url}.`);
const archivePath = path.join(this._downloadsFolder, fileName); const archivePath = path.join(this._downloadsFolder, fileName);
const outputPath = this._getFolderPath(revision); const outputPath = this._getFolderPath(revision);
if (await existsAsync(outputPath)) return this.revisionInfo(revision); if (await existsAsync(outputPath)) return this.revisionInfo(revision);
@ -346,7 +359,12 @@ export class BrowserFetcher {
const fileNames = await readdirAsync(this._downloadsFolder); const fileNames = await readdirAsync(this._downloadsFolder);
return fileNames return fileNames
.map((fileName) => parseFolderPath(this._product, fileName)) .map((fileName) => parseFolderPath(this._product, fileName))
.filter((entry) => entry && entry.platform === this._platform) .filter(
(
entry
): entry is { product: string; platform: string; revision: string } =>
(entry && entry.platform === this._platform) ?? false
)
.map((entry) => entry.revision); .map((entry) => entry.revision);
} }
@ -447,12 +465,12 @@ export class BrowserFetcher {
function parseFolderPath( function parseFolderPath(
product: Product, product: Product,
folderPath: string folderPath: string
): { product: string; platform: string; revision: string } | null { ): { product: string; platform: string; revision: string } | undefined {
const name = path.basename(folderPath); const name = path.basename(folderPath);
const splits = name.split('-'); const splits = name.split('-');
if (splits.length !== 2) return null; if (splits.length !== 2) return;
const [platform, revision] = splits; const [platform, revision] = splits;
if (!downloadURLs[product][platform]) return null; if (!revision || !platform || !(platform in downloadURLs[product])) return;
return { product, platform, revision }; return { product, platform, revision };
} }
@ -462,18 +480,19 @@ function parseFolderPath(
function downloadFile( function downloadFile(
url: string, url: string,
destinationPath: string, destinationPath: string,
progressCallback: (x: number, y: number) => void progressCallback?: (x: number, y: number) => void
): Promise<void> { ): Promise<void> {
debugFetcher(`Downloading binary from ${url}`); debugFetcher(`Downloading binary from ${url}`);
let fulfill, reject; let fulfill: (value: void | PromiseLike<void>) => void;
let downloadedBytes = 0; let reject: (err: Error) => void;
let totalBytes = 0;
const promise = new Promise<void>((x, y) => { const promise = new Promise<void>((x, y) => {
fulfill = x; fulfill = x;
reject = y; reject = y;
}); });
let downloadedBytes = 0;
let totalBytes = 0;
const request = httpRequest(url, 'GET', (response) => { const request = httpRequest(url, 'GET', (response) => {
if (response.statusCode !== 200) { if (response.statusCode !== 200) {
const error = new Error( const error = new Error(
@ -488,10 +507,7 @@ function downloadFile(
file.on('finish', () => fulfill()); file.on('finish', () => fulfill());
file.on('error', (error) => reject(error)); file.on('error', (error) => reject(error));
response.pipe(file); response.pipe(file);
totalBytes = parseInt( totalBytes = parseInt(response.headers['content-length']!, 10);
/** @type {string} */ response.headers['content-length'],
10
);
if (progressCallback) response.on('data', onData); if (progressCallback) response.on('data', onData);
}); });
request.on('error', (error) => reject(error)); request.on('error', (error) => reject(error));
@ -499,7 +515,7 @@ function downloadFile(
function onData(chunk: string): void { function onData(chunk: string): void {
downloadedBytes += chunk.length; downloadedBytes += chunk.length;
progressCallback(downloadedBytes, totalBytes); progressCallback!(downloadedBytes, totalBytes);
} }
} }
@ -533,16 +549,16 @@ function extractTar(tarPath: string, folderPath: string): Promise<unknown> {
* @internal * @internal
*/ */
function installDMG(dmgPath: string, folderPath: string): Promise<void> { function installDMG(dmgPath: string, folderPath: string): Promise<void> {
let mountPath; let mountPath: string | undefined;
function mountAndCopy(fulfill: () => void, reject: (Error) => void): void { return new Promise<void>((fulfill, reject): void => {
const mountCommand = `hdiutil attach -nobrowse -noautoopen "${dmgPath}"`; const mountCommand = `hdiutil attach -nobrowse -noautoopen "${dmgPath}"`;
childProcess.exec(mountCommand, (err, stdout) => { childProcess.exec(mountCommand, (err, stdout) => {
if (err) return reject(err); if (err) return reject(err);
const volumes = stdout.match(/\/Volumes\/(.*)/m); const volumes = stdout.match(/\/Volumes\/(.*)/m);
if (!volumes) if (!volumes)
return reject(new Error(`Could not find volume path in ${stdout}`)); return reject(new Error(`Could not find volume path in ${stdout}`));
mountPath = volumes[0]; mountPath = volumes[0]!;
readdirAsync(mountPath) readdirAsync(mountPath)
.then((fileNames) => { .then((fileNames) => {
const appName = fileNames.find( const appName = fileNames.find(
@ -550,7 +566,7 @@ function installDMG(dmgPath: string, folderPath: string): Promise<void> {
); );
if (!appName) if (!appName)
return reject(new Error(`Cannot find app in ${mountPath}`)); return reject(new Error(`Cannot find app in ${mountPath}`));
const copyPath = path.join(mountPath, appName); const copyPath = path.join(mountPath!, appName);
debugFetcher(`Copying ${copyPath} to ${folderPath}`); debugFetcher(`Copying ${copyPath} to ${folderPath}`);
childProcess.exec(`cp -R "${copyPath}" "${folderPath}"`, (err) => { childProcess.exec(`cp -R "${copyPath}" "${folderPath}"`, (err) => {
if (err) reject(err); if (err) reject(err);
@ -559,22 +575,18 @@ function installDMG(dmgPath: string, folderPath: string): Promise<void> {
}) })
.catch(reject); .catch(reject);
}); });
} })
function unmount(): void {
if (!mountPath) return;
const unmountCommand = `hdiutil detach "${mountPath}" -quiet`;
debugFetcher(`Unmounting ${mountPath}`);
childProcess.exec(unmountCommand, (err) => {
if (err) console.error(`Error unmounting dmg: ${err}`);
});
}
return new Promise<void>(mountAndCopy)
.catch((error) => { .catch((error) => {
console.error(error); console.error(error);
}) })
.finally(unmount); .finally((): void => {
if (!mountPath) return;
const unmountCommand = `hdiutil detach "${mountPath}" -quiet`;
debugFetcher(`Unmounting ${mountPath}`);
childProcess.exec(unmountCommand, (err) => {
if (err) console.error(`Error unmounting dmg: ${err}`);
});
});
} }
function httpRequest( function httpRequest(
@ -625,7 +637,12 @@ function httpRequest(
} }
const requestCallback = (res: http.IncomingMessage): void => { const requestCallback = (res: http.IncomingMessage): void => {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) if (
res.statusCode &&
res.statusCode >= 300 &&
res.statusCode < 400 &&
res.headers.location
)
httpRequest(res.headers.location, method, response); httpRequest(res.headers.location, method, response);
else response(res); else response(res);
}; };

View File

@ -24,7 +24,11 @@ import removeFolder from 'rimraf';
import { promisify } from 'util'; import { promisify } from 'util';
import { assert } from '../common/assert.js'; import { assert } from '../common/assert.js';
import { helper, debugError } from '../common/helper.js'; import {
helper,
debugError,
PuppeteerEventListener,
} from '../common/helper.js';
import { LaunchOptions } from './LaunchOptions.js'; import { LaunchOptions } from './LaunchOptions.js';
import { Connection } from '../common/Connection.js'; import { Connection } from '../common/Connection.js';
import { NodeWebSocketTransport as WebSocketTransport } from '../node/NodeWebSocketTransport.js'; import { NodeWebSocketTransport as WebSocketTransport } from '../node/NodeWebSocketTransport.js';
@ -50,12 +54,12 @@ export class BrowserRunner {
private _userDataDir: string; private _userDataDir: string;
private _isTempUserDataDir?: boolean; private _isTempUserDataDir?: boolean;
proc = null; proc?: childProcess.ChildProcess;
connection = null; connection?: Connection;
private _closed = true; private _closed = true;
private _listeners = []; private _listeners: PuppeteerEventListener[] = [];
private _processClosing: Promise<void>; private _processClosing!: Promise<void>;
constructor( constructor(
product: Product, product: Product,
@ -100,12 +104,12 @@ export class BrowserRunner {
} }
); );
if (dumpio) { if (dumpio) {
this.proc.stderr.pipe(process.stderr); this.proc.stderr?.pipe(process.stderr);
this.proc.stdout.pipe(process.stdout); this.proc.stdout?.pipe(process.stdout);
} }
this._closed = false; this._closed = false;
this._processClosing = new Promise((fulfill, reject) => { this._processClosing = new Promise((fulfill, reject) => {
this.proc.once('exit', async () => { this.proc!.once('exit', async () => {
this._closed = true; this._closed = true;
// Cleanup as processes exit. // Cleanup as processes exit.
if (this._isTempUserDataDir) { if (this._isTempUserDataDir) {
@ -184,6 +188,7 @@ export class BrowserRunner {
// is invalid), then the process does not get a pid assigned. A call to // is invalid), then the process does not get a pid assigned. A call to
// `proc.kill` would error, as the `pid` to-be-killed can not be found. // `proc.kill` would error, as the `pid` to-be-killed can not be found.
if (this.proc && this.proc.pid && pidExists(this.proc.pid)) { if (this.proc && this.proc.pid && pidExists(this.proc.pid)) {
const proc = this.proc;
try { try {
if (process.platform === 'win32') { if (process.platform === 'win32') {
childProcess.exec(`taskkill /pid ${this.proc.pid} /T /F`, (error) => { childProcess.exec(`taskkill /pid ${this.proc.pid} /T /F`, (error) => {
@ -191,7 +196,7 @@ export class BrowserRunner {
// taskkill can fail to kill the process e.g. due to missing permissions. // taskkill can fail to kill the process e.g. due to missing permissions.
// Let's kill the process via Node API. This delays killing of all child // Let's kill the process via Node API. This delays killing of all child
// proccesses of `this.proc` until the main Node.js process dies. // proccesses of `this.proc` until the main Node.js process dies.
this.proc.kill(); proc.kill();
} }
}); });
} else { } else {
@ -202,7 +207,9 @@ export class BrowserRunner {
} }
} catch (error) { } catch (error) {
throw new Error( throw new Error(
`${PROCESS_ERROR_EXPLANATION}\nError cause: ${error.stack}` `${PROCESS_ERROR_EXPLANATION}\nError cause: ${
error instanceof Error ? error.stack : error
}`
); );
} }
} }
@ -225,6 +232,8 @@ export class BrowserRunner {
slowMo: number; slowMo: number;
preferredRevision: string; preferredRevision: string;
}): Promise<Connection> { }): Promise<Connection> {
assert(this.proc, 'BrowserRunner not started.');
const { usePipe, timeout, slowMo, preferredRevision } = options; const { usePipe, timeout, slowMo, preferredRevision } = options;
if (!usePipe) { if (!usePipe) {
const browserWSEndpoint = await waitForWSEndpoint( const browserWSEndpoint = await waitForWSEndpoint(
@ -253,9 +262,11 @@ function waitForWSEndpoint(
timeout: number, timeout: number,
preferredRevision: string preferredRevision: string
): Promise<string> { ): Promise<string> {
assert(browserProcess.stderr, '`browserProcess` does not have stderr.');
const rl = readline.createInterface(browserProcess.stderr);
let stderr = '';
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const rl = readline.createInterface({ input: browserProcess.stderr });
let stderr = '';
const listeners = [ const listeners = [
helper.addEventListener(rl, 'line', onLine), helper.addEventListener(rl, 'line', onLine),
helper.addEventListener(rl, 'close', () => onClose()), helper.addEventListener(rl, 'close', () => onClose()),
@ -299,7 +310,8 @@ function waitForWSEndpoint(
const match = line.match(/^DevTools listening on (ws:\/\/.*)$/); const match = line.match(/^DevTools listening on (ws:\/\/.*)$/);
if (!match) return; if (!match) return;
cleanup(); cleanup();
resolve(match[1]); // The RegExp matches, so this will obviously exist.
resolve(match[1]!);
} }
function cleanup(): void { function cleanup(): void {
@ -313,10 +325,12 @@ function pidExists(pid: number): boolean {
try { try {
return process.kill(pid, 0); return process.kill(pid, 0);
} catch (error) { } catch (error) {
if (error && error.code && error.code === 'ESRCH') { if (error instanceof Error) {
return false; const err = error as NodeJS.ErrnoException;
} else { if (err.code && err.code === 'ESRCH') {
throw error; return false;
}
} }
throw error;
} }
} }

View File

@ -35,7 +35,7 @@ import {
import { Product } from '../common/Product.js'; import { Product } from '../common/Product.js';
const tmpDir = () => process.env.PUPPETEER_TMP_DIR || os.tmpdir(); const tmpDir = () => process.env['PUPPETEER_TMP_DIR'] || os.tmpdir();
/** /**
* Describes a launcher - a class that is able to create and launch a browser instance. * Describes a launcher - a class that is able to create and launch a browser instance.
@ -112,31 +112,33 @@ class ChromeLauncher implements ProductLauncher {
} }
} }
let userDataDir;
let isTempUserDataDir = true; let isTempUserDataDir = true;
// Check for the user data dir argument, which will always be set even // Check for the user data dir argument, which will always be set even
// with a custom directory specified via the userDataDir option. // with a custom directory specified via the userDataDir option.
const userDataDirIndex = chromeArguments.findIndex((arg) => { let userDataDirIndex = chromeArguments.findIndex((arg) => {
return arg.startsWith('--user-data-dir'); return arg.startsWith('--user-data-dir');
}); });
if (userDataDirIndex < 0) {
if (userDataDirIndex !== -1) { chromeArguments.push(
userDataDir = chromeArguments[userDataDirIndex].split('=')[1]; `--user-data-dir=${await mkdtempAsync(
isTempUserDataDir = false; path.join(tmpDir(), 'puppeteer_dev_chrome_profile-')
} else { )}`
userDataDir = await mkdtempAsync(
path.join(tmpDir(), 'puppeteer_dev_chrome_profile-')
); );
chromeArguments.push(`--user-data-dir=${userDataDir}`); userDataDirIndex = chromeArguments.length - 1;
} }
const userDataDir = chromeArguments[userDataDirIndex]!.split('=', 2)[1];
assert(typeof userDataDir === 'string', '`--user-data-dir` is malformed');
isTempUserDataDir = false;
let chromeExecutable = executablePath; let chromeExecutable = executablePath;
if (channel) { if (channel) {
// executablePath is detected by channel, so it should not be specified by user. // executablePath is detected by channel, so it should not be specified by user.
assert( assert(
!executablePath, typeof executablePath === 'string',
'`executablePath` must not be specified when `channel` is given.' '`executablePath` must not be specified when `channel` is given.'
); );
@ -238,7 +240,7 @@ class ChromeLauncher implements ProductLauncher {
devtools = false, devtools = false,
headless = !devtools, headless = !devtools,
args = [], args = [],
userDataDir = null, userDataDir,
} = options; } = options;
if (userDataDir) if (userDataDir)
chromeArguments.push(`--user-data-dir=${path.resolve(userDataDir)}`); chromeArguments.push(`--user-data-dir=${path.resolve(userDataDir)}`);
@ -331,7 +333,7 @@ class FirefoxLauncher implements ProductLauncher {
firefoxArguments.push(`--remote-debugging-port=${debuggingPort || 0}`); firefoxArguments.push(`--remote-debugging-port=${debuggingPort || 0}`);
} }
let userDataDir = null; let userDataDir: string | undefined;
let isTempUserDataDir = true; let isTempUserDataDir = true;
// Check for the profile argument, which will always be set even // Check for the profile argument, which will always be set even
@ -342,7 +344,7 @@ class FirefoxLauncher implements ProductLauncher {
if (profileArgIndex !== -1) { if (profileArgIndex !== -1) {
userDataDir = firefoxArguments[profileArgIndex + 1]; userDataDir = firefoxArguments[profileArgIndex + 1];
if (!fs.existsSync(userDataDir)) { if (!userDataDir || !fs.existsSync(userDataDir)) {
throw new Error(`Firefox profile not found at '${userDataDir}'`); throw new Error(`Firefox profile not found at '${userDataDir}'`);
} }
@ -726,16 +728,16 @@ function executablePathForChannel(channel: ChromeReleaseChannel): string {
case 'win32': case 'win32':
switch (channel) { switch (channel) {
case 'chrome': case 'chrome':
chromePath = `${process.env.PROGRAMFILES}\\Google\\Chrome\\Application\\chrome.exe`; chromePath = `${process.env['PROGRAMFILES']}\\Google\\Chrome\\Application\\chrome.exe`;
break; break;
case 'chrome-beta': case 'chrome-beta':
chromePath = `${process.env.PROGRAMFILES}\\Google\\Chrome Beta\\Application\\chrome.exe`; chromePath = `${process.env['PROGRAMFILES']}\\Google\\Chrome Beta\\Application\\chrome.exe`;
break; break;
case 'chrome-canary': case 'chrome-canary':
chromePath = `${process.env.PROGRAMFILES}\\Google\\Chrome SxS\\Application\\chrome.exe`; chromePath = `${process.env['PROGRAMFILES']}\\Google\\Chrome SxS\\Application\\chrome.exe`;
break; break;
case 'chrome-dev': case 'chrome-dev':
chromePath = `${process.env.PROGRAMFILES}\\Google\\Chrome Dev\\Application\\chrome.exe`; chromePath = `${process.env['PROGRAMFILES']}\\Google\\Chrome Dev\\Application\\chrome.exe`;
break; break;
} }
break; break;
@ -802,9 +804,9 @@ function resolveExecutablePath(launcher: ChromeLauncher | FirefoxLauncher): {
// puppeteer-core doesn't take into account PUPPETEER_* env variables. // puppeteer-core doesn't take into account PUPPETEER_* env variables.
if (!_isPuppeteerCore) { if (!_isPuppeteerCore) {
const executablePath = const executablePath =
process.env.PUPPETEER_EXECUTABLE_PATH || process.env['PUPPETEER_EXECUTABLE_PATH'] ||
process.env.npm_config_puppeteer_executable_path || process.env['npm_config_puppeteer_executable_path'] ||
process.env.npm_package_config_puppeteer_executable_path; process.env['npm_package_config_puppeteer_executable_path'];
if (executablePath) { if (executablePath) {
const missingText = !fs.existsSync(executablePath) const missingText = !fs.existsSync(executablePath)
? 'Tried to use PUPPETEER_EXECUTABLE_PATH env variable to launch browser but did not find any executable at: ' + ? 'Tried to use PUPPETEER_EXECUTABLE_PATH env variable to launch browser but did not find any executable at: ' +
@ -822,9 +824,9 @@ function resolveExecutablePath(launcher: ChromeLauncher | FirefoxLauncher): {
return { executablePath: ubuntuChromiumPath, missingText: undefined }; return { executablePath: ubuntuChromiumPath, missingText: undefined };
} }
downloadPath = downloadPath =
process.env.PUPPETEER_DOWNLOAD_PATH || process.env['PUPPETEER_DOWNLOAD_PATH'] ||
process.env.npm_config_puppeteer_download_path || process.env['npm_config_puppeteer_download_path'] ||
process.env.npm_package_config_puppeteer_download_path; process.env['npm_package_config_puppeteer_download_path'];
} }
if (!_projectRoot) { if (!_projectRoot) {
throw new Error( throw new Error(
@ -871,9 +873,9 @@ export default function Launcher(
// puppeteer-core doesn't take into account PUPPETEER_* env variables. // puppeteer-core doesn't take into account PUPPETEER_* env variables.
if (!product && !isPuppeteerCore) if (!product && !isPuppeteerCore)
product = product =
process.env.PUPPETEER_PRODUCT || process.env['PUPPETEER_PRODUCT'] ||
process.env.npm_config_puppeteer_product || process.env['npm_config_puppeteer_product'] ||
process.env.npm_package_config_puppeteer_product; process.env['npm_package_config_puppeteer_product'];
switch (product) { switch (product) {
case 'firefox': case 'firefox':
return new FirefoxLauncher( return new FirefoxLauncher(

View File

@ -37,7 +37,7 @@ export class NodeWebSocketTransport implements ConnectionTransport {
} }
private _ws: NodeWebSocket; private _ws: NodeWebSocket;
onmessage?: (message: string) => void; onmessage?: (message: NodeWebSocket.Data) => void;
onclose?: () => void; onclose?: () => void;
constructor(ws: NodeWebSocket) { constructor(ws: NodeWebSocket) {
@ -50,8 +50,6 @@ export class NodeWebSocketTransport implements ConnectionTransport {
}); });
// Silently ignore all errors - we don't know what to do with them. // Silently ignore all errors - we don't know what to do with them.
this._ws.addEventListener('error', () => {}); this._ws.addEventListener('error', () => {});
this.onmessage = null;
this.onclose = null;
} }
send(message: string): void { send(message: string): void {

View File

@ -19,54 +19,63 @@ import {
PuppeteerEventListener, PuppeteerEventListener,
} from '../common/helper.js'; } from '../common/helper.js';
import { ConnectionTransport } from '../common/ConnectionTransport.js'; import { ConnectionTransport } from '../common/ConnectionTransport.js';
import { assert } from '../common/assert.js';
export class PipeTransport implements ConnectionTransport { export class PipeTransport implements ConnectionTransport {
_pipeWrite: NodeJS.WritableStream; _pipeWrite: NodeJS.WritableStream;
_pendingMessage: string;
_eventListeners: PuppeteerEventListener[]; _eventListeners: PuppeteerEventListener[];
_isClosed = false;
_pendingMessage = '';
onclose?: () => void; onclose?: () => void;
onmessage?: () => void; onmessage?: (value: string) => void;
constructor( constructor(
pipeWrite: NodeJS.WritableStream, pipeWrite: NodeJS.WritableStream,
pipeRead: NodeJS.ReadableStream pipeRead: NodeJS.ReadableStream
) { ) {
this._pipeWrite = pipeWrite; this._pipeWrite = pipeWrite;
this._pendingMessage = '';
this._eventListeners = [ this._eventListeners = [
helper.addEventListener(pipeRead, 'data', (buffer) => helper.addEventListener(pipeRead, 'data', (buffer) =>
this._dispatch(buffer) this._dispatch(buffer)
), ),
helper.addEventListener(pipeRead, 'close', () => { helper.addEventListener(pipeRead, 'close', () => {
if (this.onclose) this.onclose.call(null); if (this.onclose) {
this.onclose.call(null);
}
}), }),
helper.addEventListener(pipeRead, 'error', debugError), helper.addEventListener(pipeRead, 'error', debugError),
helper.addEventListener(pipeWrite, 'error', debugError), helper.addEventListener(pipeWrite, 'error', debugError),
]; ];
this.onmessage = null;
this.onclose = null;
} }
send(message: string): void { send(message: string): void {
assert(!this._isClosed, '`PipeTransport` is closed.');
this._pipeWrite.write(message); this._pipeWrite.write(message);
this._pipeWrite.write('\0'); this._pipeWrite.write('\0');
} }
_dispatch(buffer: Buffer): void { _dispatch(buffer: Buffer): void {
assert(!this._isClosed, '`PipeTransport` is closed.');
let end = buffer.indexOf('\0'); let end = buffer.indexOf('\0');
if (end === -1) { if (end === -1) {
this._pendingMessage += buffer.toString(); this._pendingMessage += buffer.toString();
return; return;
} }
const message = this._pendingMessage + buffer.toString(undefined, 0, end); const message = this._pendingMessage + buffer.toString(undefined, 0, end);
if (this.onmessage) this.onmessage.call(null, message); if (this.onmessage) {
this.onmessage.call(null, message);
}
let start = end + 1; let start = end + 1;
end = buffer.indexOf('\0', start); end = buffer.indexOf('\0', start);
while (end !== -1) { while (end !== -1) {
if (this.onmessage) if (this.onmessage) {
this.onmessage.call(null, buffer.toString(undefined, start, end)); this.onmessage.call(null, buffer.toString(undefined, start, end));
}
start = end + 1; start = end + 1;
end = buffer.indexOf('\0', start); end = buffer.indexOf('\0', start);
} }
@ -74,7 +83,7 @@ export class PipeTransport implements ConnectionTransport {
} }
close(): void { close(): void {
this._pipeWrite = null; this._isClosed = true;
helper.removeEventListeners(this._eventListeners); helper.removeEventListeners(this._eventListeners);
} }
} }

View File

@ -100,7 +100,7 @@ export class PuppeteerNode extends Puppeteer {
* @param options - Set of configurable options to set on the browser. * @param options - Set of configurable options to set on the browser.
* @returns Promise which resolves to browser instance. * @returns Promise which resolves to browser instance.
*/ */
connect(options: ConnectOptions): Promise<Browser> { override connect(options: ConnectOptions): Promise<Browser> {
if (options.product) this._productName = options.product; if (options.product) this._productName = options.product;
return super.connect(options); return super.connect(options);
} }

View File

@ -39,19 +39,19 @@ function getProduct(input: string): 'chrome' | 'firefox' {
export async function downloadBrowser(): Promise<void> { export async function downloadBrowser(): Promise<void> {
const downloadHost = const downloadHost =
process.env.PUPPETEER_DOWNLOAD_HOST || process.env['PUPPETEER_DOWNLOAD_HOST'] ||
process.env.npm_config_puppeteer_download_host || process.env['npm_config_puppeteer_download_host'] ||
process.env.npm_package_config_puppeteer_download_host; process.env['npm_package_config_puppeteer_download_host'];
const product = getProduct( const product = getProduct(
process.env.PUPPETEER_PRODUCT || process.env['PUPPETEER_PRODUCT'] ||
process.env.npm_config_puppeteer_product || process.env['npm_config_puppeteer_product'] ||
process.env.npm_package_config_puppeteer_product || process.env['npm_package_config_puppeteer_product'] ||
'chrome' 'chrome'
); );
const downloadPath = const downloadPath =
process.env.PUPPETEER_DOWNLOAD_PATH || process.env['PUPPETEER_DOWNLOAD_PATH'] ||
process.env.npm_config_puppeteer_download_path || process.env['npm_config_puppeteer_download_path'] ||
process.env.npm_package_config_puppeteer_download_path; process.env['npm_package_config_puppeteer_download_path'];
const browserFetcher = (puppeteer as PuppeteerNode).createBrowserFetcher({ const browserFetcher = (puppeteer as PuppeteerNode).createBrowserFetcher({
product, product,
host: downloadHost, host: downloadHost,
@ -63,8 +63,8 @@ export async function downloadBrowser(): Promise<void> {
async function getRevision(): Promise<string> { async function getRevision(): Promise<string> {
if (product === 'chrome') { if (product === 'chrome') {
return ( return (
process.env.PUPPETEER_CHROMIUM_REVISION || process.env['PUPPETEER_CHROMIUM_REVISION'] ||
process.env.npm_config_puppeteer_chromium_revision || process.env['npm_config_puppeteer_chromium_revision'] ||
PUPPETEER_REVISIONS.chromium PUPPETEER_REVISIONS.chromium
); );
} else if (product === 'firefox') { } else if (product === 'firefox') {
@ -92,14 +92,14 @@ export async function downloadBrowser(): Promise<void> {
// Override current environment proxy settings with npm configuration, if any. // Override current environment proxy settings with npm configuration, if any.
const NPM_HTTPS_PROXY = const NPM_HTTPS_PROXY =
process.env.npm_config_https_proxy || process.env.npm_config_proxy; process.env['npm_config_https_proxy'] || process.env['npm_config_proxy'];
const NPM_HTTP_PROXY = const NPM_HTTP_PROXY =
process.env.npm_config_http_proxy || process.env.npm_config_proxy; process.env['npm_config_http_proxy'] || process.env['npm_config_proxy'];
const NPM_NO_PROXY = process.env.npm_config_no_proxy; const NPM_NO_PROXY = process.env['npm_config_no_proxy'];
if (NPM_HTTPS_PROXY) process.env.HTTPS_PROXY = NPM_HTTPS_PROXY; if (NPM_HTTPS_PROXY) process.env['HTTPS_PROXY'] = NPM_HTTPS_PROXY;
if (NPM_HTTP_PROXY) process.env.HTTP_PROXY = NPM_HTTP_PROXY; if (NPM_HTTP_PROXY) process.env['HTTP_PROXY'] = NPM_HTTP_PROXY;
if (NPM_NO_PROXY) process.env.NO_PROXY = NPM_NO_PROXY; if (NPM_NO_PROXY) process.env['NO_PROXY'] = NPM_NO_PROXY;
function onSuccess(localRevisions: string[]): void { function onSuccess(localRevisions: string[]): void {
logPolitely( logPolitely(
@ -203,7 +203,7 @@ export async function downloadBrowser(): Promise<void> {
} }
export function logPolitely(toBeLogged: unknown): void { export function logPolitely(toBeLogged: unknown): void {
const logLevel = process.env.npm_config_loglevel || ''; const logLevel = process.env['npm_config_loglevel'] || '';
const logLevelDisplay = ['silent', 'error', 'warn'].indexOf(logLevel) > -1; const logLevelDisplay = ['silent', 'error', 'warn'].indexOf(logLevel) > -1;
// eslint-disable-next-line no-console // eslint-disable-next-line no-console

View File

@ -211,7 +211,7 @@ describe('Page.click', function () {
.click('button.does-not-exist') .click('button.does-not-exist')
.catch((error_) => (error = error_)); .catch((error_) => (error = error_));
expect(error.message).toBe( expect(error.message).toBe(
'No node found for selector: button.does-not-exist' 'No element found for selector: button.does-not-exist'
); );
}); });
// @see https://github.com/puppeteer/puppeteer/issues/161 // @see https://github.com/puppeteer/puppeteer/issues/161

View File

@ -40,7 +40,7 @@ describe('Evaluation specs', function () {
(bigint ? it : xit)('should transfer BigInt', async () => { (bigint ? it : xit)('should transfer BigInt', async () => {
const { page } = getTestState(); const { page } = getTestState();
const result = await page.evaluate((a: BigInt) => a, BigInt(42)); const result = await page.evaluate((a: bigint) => a, BigInt(42));
expect(result).toBe(BigInt(42)); expect(result).toBe(BigInt(42));
}); });
it('should transfer NaN', async () => { it('should transfer NaN', async () => {

View File

@ -60,30 +60,34 @@ export const getTestState = (): PuppeteerTestState =>
state as PuppeteerTestState; state as PuppeteerTestState;
const product = const product =
process.env.PRODUCT || process.env.PUPPETEER_PRODUCT || 'Chromium'; process.env['PRODUCT'] || process.env['PUPPETEER_PRODUCT'] || 'Chromium';
const alternativeInstall = process.env.PUPPETEER_ALT_INSTALL || false; const alternativeInstall = process.env['PUPPETEER_ALT_INSTALL'] || false;
const headless = (process.env.HEADLESS || 'true').trim().toLowerCase(); const headless = (process.env['HEADLESS'] || 'true').trim().toLowerCase();
const isHeadless = headless === 'true' || headless === 'chrome'; const isHeadless = headless === 'true' || headless === 'chrome';
const isFirefox = product === 'firefox'; const isFirefox = product === 'firefox';
const isChrome = product === 'Chromium'; const isChrome = product === 'Chromium';
let extraLaunchOptions = {}; let extraLaunchOptions = {};
try { try {
extraLaunchOptions = JSON.parse(process.env.EXTRA_LAUNCH_OPTIONS || '{}'); extraLaunchOptions = JSON.parse(process.env['EXTRA_LAUNCH_OPTIONS'] || '{}');
} catch (error) { } catch (error) {
console.warn( if (error instanceof Error) {
`Error parsing EXTRA_LAUNCH_OPTIONS: ${error.message}. Skipping.` console.warn(
); `Error parsing EXTRA_LAUNCH_OPTIONS: ${error.message}. Skipping.`
);
} else {
throw error;
}
} }
const defaultBrowserOptions = Object.assign( const defaultBrowserOptions = Object.assign(
{ {
handleSIGINT: true, handleSIGINT: true,
executablePath: process.env.BINARY, executablePath: process.env['BINARY'],
headless: headless === 'chrome' ? ('chrome' as const) : isHeadless, headless: headless === 'chrome' ? ('chrome' as const) : isHeadless,
dumpio: !!process.env.DUMPIO, dumpio: !!process.env['DUMPIO'],
}, },
extraLaunchOptions extraLaunchOptions
); );
@ -178,7 +182,8 @@ export const itOnlyRegularInstall = (
description: string, description: string,
body: Mocha.Func body: Mocha.Func
): Mocha.Test => { ): Mocha.Test => {
if (alternativeInstall || process.env.BINARY) return xit(description, body); if (alternativeInstall || process.env['BINARY'])
return xit(description, body);
else return it(description, body); else return it(description, body);
}; };
@ -216,7 +221,7 @@ export const describeFailsFirefox = (
export const describeChromeOnly = ( export const describeChromeOnly = (
description: string, description: string,
body: (this: Mocha.Suite) => void body: (this: Mocha.Suite) => void
): Mocha.Suite => { ): Mocha.Suite | void => {
if (isChrome) return describe(description, body); if (isChrome) return describe(description, body);
}; };
@ -225,7 +230,7 @@ let coverageHooks = {
afterAll: (): void => {}, afterAll: (): void => {},
}; };
if (process.env.COVERAGE) { if (process.env['COVERAGE']) {
coverageHooks = trackCoverage(); coverageHooks = trackCoverage();
} }
@ -249,21 +254,21 @@ export const setupTestBrowserHooks = (): void => {
}); });
after(async () => { after(async () => {
await state.browser.close(); await state.browser!.close();
state.browser = null; state.browser = undefined;
}); });
}; };
export const setupTestPageAndContextHooks = (): void => { export const setupTestPageAndContextHooks = (): void => {
beforeEach(async () => { beforeEach(async () => {
state.context = await state.browser.createIncognitoBrowserContext(); state.context = await state.browser!.createIncognitoBrowserContext();
state.page = await state.context.newPage(); state.page = await state.context.newPage();
}); });
afterEach(async () => { afterEach(async () => {
await state.context.close(); await state.context!.close();
state.context = null; state.context = undefined;
state.page = null; state.page = undefined;
}); });
}; };

View File

@ -271,7 +271,7 @@ describe('Target', function () {
server.PREFIX + '/popup/popup.html' server.PREFIX + '/popup/popup.html'
); );
expect(createdTarget.opener()).toBe(page.target()); expect(createdTarget.opener()).toBe(page.target());
expect(page.target().opener()).toBe(null); expect(page.target().opener()).toBeUndefined();
}); });
describe('Browser.waitForTarget', () => { describe('Browser.waitForTarget', () => {

View File

@ -119,7 +119,7 @@ describeChromeOnly('Tracing', function () {
expect(trace).toBeTruthy(); expect(trace).toBeTruthy();
}); });
it('should return null in case of Buffer error', async () => { it('should return undefined in case of Buffer error', async () => {
const { server } = getTestState(); const { server } = getTestState();
await page.tracing.start({ screenshots: true }); await page.tracing.start({ screenshots: true });
@ -129,7 +129,7 @@ describeChromeOnly('Tracing', function () {
throw 'error'; throw 'error';
}; };
const trace = await page.tracing.stop(); const trace = await page.tracing.stop();
expect(trace).toEqual(null); expect(trace).toEqual(undefined);
Buffer.concat = oldBufferConcat; Buffer.concat = oldBufferConcat;
}); });

View File

@ -1,7 +1,6 @@
{ {
"extends": "../tsconfig.json", "extends": "./tsconfig.test.json",
"compilerOptions": { "compilerOptions": {
"noEmit": true "noEmit": true
}, }
"include": ["*.ts", "*.js"]
} }

View File

@ -1,6 +1,14 @@
{ {
"extends": "../tsconfig.base.json",
"compilerOptions": { "compilerOptions": {
"module": "CommonJS" "esModuleInterop": true,
"allowJs": true,
"checkJs": true,
"target": "esnext",
"module": "CommonJS",
"moduleResolution": "node",
"declaration": true,
"declarationMap": true,
"resolveJsonModule": true,
"sourceMap": true
} }
} }

View File

@ -1,13 +1,29 @@
{ {
"compilerOptions": { "compilerOptions": {
"esModuleInterop": true,
"allowJs": true, "allowJs": true,
"alwaysStrict": true,
"checkJs": true, "checkJs": true,
"target": "ES2019",
"moduleResolution": "node",
"declaration": true, "declaration": true,
"declarationMap": true, "declarationMap": true,
"esModuleInterop": true,
"moduleResolution": "node",
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noPropertyAccessFromIndexSignature": true,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"sourceMap": true "sourceMap": true,
"strict": true,
"strictBindCallApply": true,
"strictFunctionTypes": true,
"strictNullChecks": true,
"strictPropertyInitialization": true,
"target": "ES2019",
"useUnknownInCatchVariables": true
} }
} }

View File

@ -28,9 +28,13 @@ const fulfillSymbol = Symbol('fullfil callback');
const rejectSymbol = Symbol('reject callback'); const rejectSymbol = Symbol('reject callback');
class TestServer { class TestServer {
/** @type number */
PORT = undefined; PORT = undefined;
/** @type string */
PREFIX = undefined; PREFIX = undefined;
/** @type string */
CROSS_PROCESS_PREFIX = undefined; CROSS_PROCESS_PREFIX = undefined;
/** @type string */
EMPTY_PAGE = undefined; EMPTY_PAGE = undefined;
/** /**

View File

@ -1,5 +1,4 @@
{ {
"extends": "../tsconfig.base.json",
"exclude": [ "exclude": [
"mitt/dist" "mitt/dist"
], ],
@ -7,6 +6,7 @@
"composite": true, "composite": true,
"outDir": "../lib/cjs/vendor", "outDir": "../lib/cjs/vendor",
"module": "CommonJS", "module": "CommonJS",
"moduleResolution": "node",
"strict": false "strict": false
} }
} }

View File

@ -1,5 +1,4 @@
{ {
"extends": "../tsconfig.base.json",
"exclude": [ "exclude": [
"mitt/dist" "mitt/dist"
], ],
@ -7,6 +6,7 @@
"composite": true, "composite": true,
"outDir": "../lib/esm/vendor", "outDir": "../lib/esm/vendor",
"module": "esnext", "module": "esnext",
"moduleResolution": "node",
"strict": false "strict": false
} }
} }