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

View File

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

View File

@ -21,7 +21,7 @@ const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies };
const invalidDeps = new Map<string, string>();
for (const [depKey, depValue] of Object.entries(allDeps)) {
if (/[0-9]/.test(depValue[0])) {
if (/[0-9]/.test(depValue[0]!)) {
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(8,29): error TS2551: Property 'devics' does not exist on type",
'bad.ts(12,39): error TS2554: Expected 0 arguments, but got 1.',
"bad.ts(20,5): error TS2345: Argument of type '(divElem: number) => any' is not assignable to parameter of type 'EvaluateFn<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(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'.",
],
],
@ -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(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(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'.",
],
],
@ -73,7 +73,7 @@ const EXPECTED_ERRORS = new Map<string, string[]>([
]);
const PROJECT_FOLDERS = [...EXPECTED_ERRORS.keys()];
if (!process.env.CI) {
if (!process.env['CI']) {
console.log(`IMPORTANT: this script assumes you have compiled Puppeteer
and its types file before running. Make sure you have run:
=> npm run tsc && npm run generate-d-ts
@ -107,7 +107,7 @@ function packPuppeteer() {
const tar = packPuppeteer();
const tarPath = path.join(process.cwd(), tar);
function compileAndCatchErrors(projectLocation) {
function compileAndCatchErrors(projectLocation: string) {
const { status, stdout, stderr } = spawnSync('npm', ['run', 'compile'], {
cwd: projectLocation,
encoding: 'utf-8',
@ -159,7 +159,7 @@ function testProject(folder: string) {
}
);
if (status > 0) {
if (status) {
console.error(
'Installing the tar file unexpectedly failed',
stdout,

View File

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

View File

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

View File

@ -246,7 +246,7 @@ export class Browser extends EventEmitter {
private _connection: Connection;
private _closeCallback: BrowserCloseCallback;
private _targetFilterCallback: TargetFilterCallback;
private _isPageTargetCallback: IsPageTargetCallback;
private _isPageTargetCallback!: IsPageTargetCallback;
private _defaultContext: BrowserContext;
private _contexts: Map<string, BrowserContext>;
private _screenshotTaskQueue: TaskQueue;
@ -572,33 +572,24 @@ export class Browser extends EventEmitter {
): Promise<Target> {
const { timeout = 30000 } = options;
let resolve: (value: Target | PromiseLike<Target>) => void;
let isResolved = false;
const targetPromise = new Promise<Target>((x) => (resolve = x));
this.on(BrowserEmittedEvents.TargetCreated, check);
this.on(BrowserEmittedEvents.TargetChanged, check);
try {
if (!timeout) return await targetPromise;
return await helper.waitWithTimeout<Target>(
Promise.race([
targetPromise,
(async () => {
for (const target of this.targets()) {
if (await predicate(target)) {
return target;
}
}
await targetPromise;
})(),
]),
'target',
timeout
);
this.targets().forEach(check);
return await helper.waitWithTimeout(targetPromise, 'target', timeout);
} finally {
this.removeListener(BrowserEmittedEvents.TargetCreated, check);
this.removeListener(BrowserEmittedEvents.TargetChanged, check);
this.off(BrowserEmittedEvents.TargetCreated, check);
this.off(BrowserEmittedEvents.TargetChanged, check);
}
async function check(target: Target): Promise<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'
);
let connection = null;
let connection!: Connection;
if (transport) {
connection = new Connection('', transport, slowMo);
} else if (browserWSEndpoint) {
@ -117,7 +117,7 @@ export const connectToBrowser = async (
browserContextIds,
ignoreHTTPSErrors,
defaultViewport,
null,
undefined,
() => connection.send('Browser.close').catch(debugError),
targetFilter,
isPageTarget
@ -138,9 +138,11 @@ async function getWSEndpoint(browserURL: string): Promise<string> {
const data = await result.json();
return data.webSocketDebuggerUrl;
} catch (error) {
if (error instanceof Error) {
error.message =
`Failed to fetch browser webSocket URL from ${endpointURL}: ` +
error.message;
}
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.
this._ws.addEventListener('error', () => {});
this.onmessage = null;
this.onclose = null;
}
send(message: string): void {

View File

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

View File

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

View File

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

View File

@ -16,6 +16,11 @@
import { isNode } from '../environment.js';
declare global {
// eslint-disable-next-line no-var
var __PUPPETEER_DEBUG: string;
}
/**
* A debug function that can be used in any environment.
*
@ -60,7 +65,7 @@ export const debug = (prefix: string): ((...args: unknown[]) => void) => {
}
return (...logArgs: unknown[]): void => {
const debugLevel = globalThis.__PUPPETEER_DEBUG as string;
const debugLevel = globalThis.__PUPPETEER_DEBUG;
if (!debugLevel) return;
const everythingShouldBeLogged = debugLevel === '*';

View File

@ -19,7 +19,9 @@ import { JSHandle, ElementHandle } from './JSHandle.js';
/**
* @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
*/
@ -47,7 +49,7 @@ export type Serializable =
| string
| boolean
| null
| BigInt
| bigint
| JSONArray
| JSONObject;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,12 +20,13 @@ import { JSHandle } from './JSHandle.js';
import { CDPSession } from './Connection.js';
import { Protocol } from 'devtools-protocol';
import { EvaluateHandleFn, SerializableOrJSHandle } from './EvalTypes.js';
import { ConsoleMessageType } from './ConsoleMessage.js';
/**
* @internal
*/
export type ConsoleAPICalledCallback = (
eventType: string,
eventType: ConsoleMessageType,
handles: JSHandle[],
trace: Protocol.Runtime.StackTrace
) => void;
@ -63,7 +64,7 @@ export class WebWorker extends EventEmitter {
_client: CDPSession;
_url: string;
_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
jsHandleFactory = (remoteObject) =>
new JSHandle(executionContext, client, remoteObject);
const executionContext = new ExecutionContext(
client,
event.context,
null
);
const executionContext = new ExecutionContext(client, event.context);
this._executionContextCallback(executionContext);
});

View File

@ -19,10 +19,16 @@
* @param value
* @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);
};
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);
};

View File

@ -228,13 +228,13 @@ function pageBindingDeliverErrorString(
name: string,
seq: number,
message: string,
stack: string
stack?: string
): string {
function deliverError(
name: string,
seq: number,
message: string,
stack: string
stack?: string
): void {
const error = new Error(message);
error.stack = stack;
@ -304,7 +304,7 @@ async function waitWithTimeout<T>(
const timeoutError = new TimeoutError(
`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;
if (timeout) timeoutTimer = setTimeout(() => reject(timeoutError), timeout);
try {
@ -362,7 +362,7 @@ async function getReadableFromProtocolStream(
return new Readable({
async read(size: number) {
if (eof) {
return null;
return;
}
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
const productName = isPuppeteerCore
? undefined
: process.env.PUPPETEER_PRODUCT ||
process.env.npm_config_puppeteer_product ||
process.env.npm_package_config_puppeteer_product;
: process.env['PUPPETEER_PRODUCT'] ||
process.env['npm_config_puppeteer_product'] ||
process.env['npm_package_config_puppeteer_product'];
if (!isPuppeteerCore && productName === '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 downloadURLs = {
const downloadURLs: Record<Product, Partial<Record<Platform, string>>> = {
chrome: {
linux: '%s/chromium-browser-snapshots/Linux_x64/%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',
win64: '%s/firefox-%s.en-US.%s.zip',
},
} as const;
};
const browserConfig = {
chrome: {
@ -80,14 +80,22 @@ function archiveName(
platform: Platform,
revision: string
): string {
if (product === 'chrome') {
if (platform === 'linux') return 'chrome-linux';
if (platform === 'mac' || platform === 'mac_arm') return 'chrome-mac';
if (platform === 'win32' || platform === 'win64') {
switch (product) {
case 'chrome':
switch (platform) {
case 'linux':
return 'chrome-linux';
case 'mac_arm':
case 'mac':
return 'chrome-mac';
case 'win32':
case 'win64':
// Windows archive name changed at r591479.
return parseInt(revision, 10) > 591479 ? 'chrome-win' : 'chrome-win32';
return parseInt(revision, 10) > 591479
? 'chrome-win'
: 'chrome-win32';
}
} else if (product === 'firefox') {
case 'firefox':
return platform;
}
}
@ -114,9 +122,9 @@ function downloadURL(
* @internal
*/
function handleArm64(): void {
fs.stat('/usr/bin/chromium-browser', function (err, stats) {
fs.stat('/usr/bin/chromium-browser', function (_err, stats) {
if (stats === undefined) {
fs.stat('/usr/bin/chromium', function (err, stats) {
fs.stat('/usr/bin/chromium', function (_err, stats) {
if (stats === undefined) {
console.error(
'The chromium binary is not available for arm64.' +
@ -206,36 +214,40 @@ export class BrowserFetcher {
options.path ||
path.join(projectRoot, browserConfig[this._product].destination);
this._downloadHost = options.host || browserConfig[this._product].host;
this.setPlatform(options.platform, this._product);
assert(
downloadURLs[this._product][this._platform],
'Unsupported platform: ' + this._platform
);
}
private setPlatform(
platformFromOptions?: Platform,
productFromOptions?: Product
): void {
if (platformFromOptions) {
this._platform = platformFromOptions;
return;
}
if (options.platform) {
this._platform = options.platform;
} else {
const platform = os.platform();
if (platform === 'darwin') {
if (productFromOptions === 'chrome') {
switch (platform) {
case 'darwin':
switch (this._product) {
case 'chrome':
this._platform =
os.arch() === 'arm64' && PUPPETEER_EXPERIMENTAL_CHROMIUM_MAC_ARM
? 'mac_arm'
: 'mac';
} else if (productFromOptions === 'firefox') {
break;
case 'firefox':
this._platform = 'mac';
break;
}
} else if (platform === 'linux') this._platform = 'linux';
else if (platform === 'win32')
break;
case 'linux':
this._platform = 'linux';
break;
case 'win32':
this._platform = os.arch() === 'x64' ? 'win64' : 'win32';
else assert(this._platform, 'Unsupported platform: ' + platform);
return;
default:
assert(false, 'Unsupported platform: ' + platform);
}
}
assert(
downloadURLs[this._product][this._platform],
'Unsupported platform: ' + this._platform
);
}
/**
@ -305,7 +317,7 @@ export class BrowserFetcher {
async download(
revision: string,
progressCallback: (x: number, y: number) => void = (): void => {}
): Promise<BrowserFetcherRevisionInfo> {
): Promise<BrowserFetcherRevisionInfo | undefined> {
const url = downloadURL(
this._product,
this._platform,
@ -313,6 +325,7 @@ export class BrowserFetcher {
revision
);
const fileName = url.split('/').pop();
assert(fileName, `A malformed download URL was found: ${url}.`);
const archivePath = path.join(this._downloadsFolder, fileName);
const outputPath = this._getFolderPath(revision);
if (await existsAsync(outputPath)) return this.revisionInfo(revision);
@ -346,7 +359,12 @@ export class BrowserFetcher {
const fileNames = await readdirAsync(this._downloadsFolder);
return fileNames
.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);
}
@ -447,12 +465,12 @@ export class BrowserFetcher {
function parseFolderPath(
product: Product,
folderPath: string
): { product: string; platform: string; revision: string } | null {
): { product: string; platform: string; revision: string } | undefined {
const name = path.basename(folderPath);
const splits = name.split('-');
if (splits.length !== 2) return null;
if (splits.length !== 2) return;
const [platform, revision] = splits;
if (!downloadURLs[product][platform]) return null;
if (!revision || !platform || !(platform in downloadURLs[product])) return;
return { product, platform, revision };
}
@ -462,18 +480,19 @@ function parseFolderPath(
function downloadFile(
url: string,
destinationPath: string,
progressCallback: (x: number, y: number) => void
progressCallback?: (x: number, y: number) => void
): Promise<void> {
debugFetcher(`Downloading binary from ${url}`);
let fulfill, reject;
let downloadedBytes = 0;
let totalBytes = 0;
let fulfill: (value: void | PromiseLike<void>) => void;
let reject: (err: Error) => void;
const promise = new Promise<void>((x, y) => {
fulfill = x;
reject = y;
});
let downloadedBytes = 0;
let totalBytes = 0;
const request = httpRequest(url, 'GET', (response) => {
if (response.statusCode !== 200) {
const error = new Error(
@ -488,10 +507,7 @@ function downloadFile(
file.on('finish', () => fulfill());
file.on('error', (error) => reject(error));
response.pipe(file);
totalBytes = parseInt(
/** @type {string} */ response.headers['content-length'],
10
);
totalBytes = parseInt(response.headers['content-length']!, 10);
if (progressCallback) response.on('data', onData);
});
request.on('error', (error) => reject(error));
@ -499,7 +515,7 @@ function downloadFile(
function onData(chunk: string): void {
downloadedBytes += chunk.length;
progressCallback(downloadedBytes, totalBytes);
progressCallback!(downloadedBytes, totalBytes);
}
}
@ -533,16 +549,16 @@ function extractTar(tarPath: string, folderPath: string): Promise<unknown> {
* @internal
*/
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}"`;
childProcess.exec(mountCommand, (err, stdout) => {
if (err) return reject(err);
const volumes = stdout.match(/\/Volumes\/(.*)/m);
if (!volumes)
return reject(new Error(`Could not find volume path in ${stdout}`));
mountPath = volumes[0];
mountPath = volumes[0]!;
readdirAsync(mountPath)
.then((fileNames) => {
const appName = fileNames.find(
@ -550,7 +566,7 @@ function installDMG(dmgPath: string, folderPath: string): Promise<void> {
);
if (!appName)
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}`);
childProcess.exec(`cp -R "${copyPath}" "${folderPath}"`, (err) => {
if (err) reject(err);
@ -559,22 +575,18 @@ function installDMG(dmgPath: string, folderPath: string): Promise<void> {
})
.catch(reject);
});
}
function unmount(): void {
})
.catch((error) => {
console.error(error);
})
.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}`);
});
}
return new Promise<void>(mountAndCopy)
.catch((error) => {
console.error(error);
})
.finally(unmount);
});
}
function httpRequest(
@ -625,7 +637,12 @@ function httpRequest(
}
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);
else response(res);
};

View File

@ -24,7 +24,11 @@ import removeFolder from 'rimraf';
import { promisify } from 'util';
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 { Connection } from '../common/Connection.js';
import { NodeWebSocketTransport as WebSocketTransport } from '../node/NodeWebSocketTransport.js';
@ -50,12 +54,12 @@ export class BrowserRunner {
private _userDataDir: string;
private _isTempUserDataDir?: boolean;
proc = null;
connection = null;
proc?: childProcess.ChildProcess;
connection?: Connection;
private _closed = true;
private _listeners = [];
private _processClosing: Promise<void>;
private _listeners: PuppeteerEventListener[] = [];
private _processClosing!: Promise<void>;
constructor(
product: Product,
@ -100,12 +104,12 @@ export class BrowserRunner {
}
);
if (dumpio) {
this.proc.stderr.pipe(process.stderr);
this.proc.stdout.pipe(process.stdout);
this.proc.stderr?.pipe(process.stderr);
this.proc.stdout?.pipe(process.stdout);
}
this._closed = false;
this._processClosing = new Promise((fulfill, reject) => {
this.proc.once('exit', async () => {
this.proc!.once('exit', async () => {
this._closed = true;
// Cleanup as processes exit.
if (this._isTempUserDataDir) {
@ -184,6 +188,7 @@ export class BrowserRunner {
// 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.
if (this.proc && this.proc.pid && pidExists(this.proc.pid)) {
const proc = this.proc;
try {
if (process.platform === 'win32') {
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.
// 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.
this.proc.kill();
proc.kill();
}
});
} else {
@ -202,7 +207,9 @@ export class BrowserRunner {
}
} catch (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;
preferredRevision: string;
}): Promise<Connection> {
assert(this.proc, 'BrowserRunner not started.');
const { usePipe, timeout, slowMo, preferredRevision } = options;
if (!usePipe) {
const browserWSEndpoint = await waitForWSEndpoint(
@ -253,9 +262,11 @@ function waitForWSEndpoint(
timeout: number,
preferredRevision: string
): Promise<string> {
return new Promise((resolve, reject) => {
const rl = readline.createInterface({ input: browserProcess.stderr });
assert(browserProcess.stderr, '`browserProcess` does not have stderr.');
const rl = readline.createInterface(browserProcess.stderr);
let stderr = '';
return new Promise((resolve, reject) => {
const listeners = [
helper.addEventListener(rl, 'line', onLine),
helper.addEventListener(rl, 'close', () => onClose()),
@ -299,7 +310,8 @@ function waitForWSEndpoint(
const match = line.match(/^DevTools listening on (ws:\/\/.*)$/);
if (!match) return;
cleanup();
resolve(match[1]);
// The RegExp matches, so this will obviously exist.
resolve(match[1]!);
}
function cleanup(): void {
@ -313,10 +325,12 @@ function pidExists(pid: number): boolean {
try {
return process.kill(pid, 0);
} catch (error) {
if (error && error.code && error.code === 'ESRCH') {
if (error instanceof Error) {
const err = error as NodeJS.ErrnoException;
if (err.code && err.code === 'ESRCH') {
return false;
} else {
}
}
throw error;
}
}
}

View File

@ -35,7 +35,7 @@ import {
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.
@ -112,31 +112,33 @@ class ChromeLauncher implements ProductLauncher {
}
}
let userDataDir;
let isTempUserDataDir = true;
// Check for the user data dir argument, which will always be set even
// 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');
});
if (userDataDirIndex !== -1) {
userDataDir = chromeArguments[userDataDirIndex].split('=')[1];
isTempUserDataDir = false;
} else {
userDataDir = await mkdtempAsync(
if (userDataDirIndex < 0) {
chromeArguments.push(
`--user-data-dir=${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;
if (channel) {
// executablePath is detected by channel, so it should not be specified by user.
assert(
!executablePath,
typeof executablePath === 'string',
'`executablePath` must not be specified when `channel` is given.'
);
@ -238,7 +240,7 @@ class ChromeLauncher implements ProductLauncher {
devtools = false,
headless = !devtools,
args = [],
userDataDir = null,
userDataDir,
} = options;
if (userDataDir)
chromeArguments.push(`--user-data-dir=${path.resolve(userDataDir)}`);
@ -331,7 +333,7 @@ class FirefoxLauncher implements ProductLauncher {
firefoxArguments.push(`--remote-debugging-port=${debuggingPort || 0}`);
}
let userDataDir = null;
let userDataDir: string | undefined;
let isTempUserDataDir = true;
// Check for the profile argument, which will always be set even
@ -342,7 +344,7 @@ class FirefoxLauncher implements ProductLauncher {
if (profileArgIndex !== -1) {
userDataDir = firefoxArguments[profileArgIndex + 1];
if (!fs.existsSync(userDataDir)) {
if (!userDataDir || !fs.existsSync(userDataDir)) {
throw new Error(`Firefox profile not found at '${userDataDir}'`);
}
@ -726,16 +728,16 @@ function executablePathForChannel(channel: ChromeReleaseChannel): string {
case 'win32':
switch (channel) {
case 'chrome':
chromePath = `${process.env.PROGRAMFILES}\\Google\\Chrome\\Application\\chrome.exe`;
chromePath = `${process.env['PROGRAMFILES']}\\Google\\Chrome\\Application\\chrome.exe`;
break;
case 'chrome-beta':
chromePath = `${process.env.PROGRAMFILES}\\Google\\Chrome Beta\\Application\\chrome.exe`;
chromePath = `${process.env['PROGRAMFILES']}\\Google\\Chrome Beta\\Application\\chrome.exe`;
break;
case 'chrome-canary':
chromePath = `${process.env.PROGRAMFILES}\\Google\\Chrome SxS\\Application\\chrome.exe`;
chromePath = `${process.env['PROGRAMFILES']}\\Google\\Chrome SxS\\Application\\chrome.exe`;
break;
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;
@ -802,9 +804,9 @@ function resolveExecutablePath(launcher: ChromeLauncher | FirefoxLauncher): {
// puppeteer-core doesn't take into account PUPPETEER_* env variables.
if (!_isPuppeteerCore) {
const executablePath =
process.env.PUPPETEER_EXECUTABLE_PATH ||
process.env.npm_config_puppeteer_executable_path ||
process.env.npm_package_config_puppeteer_executable_path;
process.env['PUPPETEER_EXECUTABLE_PATH'] ||
process.env['npm_config_puppeteer_executable_path'] ||
process.env['npm_package_config_puppeteer_executable_path'];
if (executablePath) {
const missingText = !fs.existsSync(executablePath)
? '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 };
}
downloadPath =
process.env.PUPPETEER_DOWNLOAD_PATH ||
process.env.npm_config_puppeteer_download_path ||
process.env.npm_package_config_puppeteer_download_path;
process.env['PUPPETEER_DOWNLOAD_PATH'] ||
process.env['npm_config_puppeteer_download_path'] ||
process.env['npm_package_config_puppeteer_download_path'];
}
if (!_projectRoot) {
throw new Error(
@ -871,9 +873,9 @@ export default function Launcher(
// puppeteer-core doesn't take into account PUPPETEER_* env variables.
if (!product && !isPuppeteerCore)
product =
process.env.PUPPETEER_PRODUCT ||
process.env.npm_config_puppeteer_product ||
process.env.npm_package_config_puppeteer_product;
process.env['PUPPETEER_PRODUCT'] ||
process.env['npm_config_puppeteer_product'] ||
process.env['npm_package_config_puppeteer_product'];
switch (product) {
case 'firefox':
return new FirefoxLauncher(

View File

@ -37,7 +37,7 @@ export class NodeWebSocketTransport implements ConnectionTransport {
}
private _ws: NodeWebSocket;
onmessage?: (message: string) => void;
onmessage?: (message: NodeWebSocket.Data) => void;
onclose?: () => void;
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.
this._ws.addEventListener('error', () => {});
this.onmessage = null;
this.onclose = null;
}
send(message: string): void {

View File

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

View File

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

View File

@ -211,7 +211,7 @@ describe('Page.click', function () {
.click('button.does-not-exist')
.catch((error_) => (error = error_));
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

View File

@ -40,7 +40,7 @@ describe('Evaluation specs', function () {
(bigint ? it : xit)('should transfer BigInt', async () => {
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));
});
it('should transfer NaN', async () => {

View File

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

View File

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

View File

@ -119,7 +119,7 @@ describeChromeOnly('Tracing', function () {
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();
await page.tracing.start({ screenshots: true });
@ -129,7 +129,7 @@ describeChromeOnly('Tracing', function () {
throw 'error';
};
const trace = await page.tracing.stop();
expect(trace).toEqual(null);
expect(trace).toEqual(undefined);
Buffer.concat = oldBufferConcat;
});

View File

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

View File

@ -1,6 +1,14 @@
{
"extends": "../tsconfig.base.json",
"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": {
"esModuleInterop": true,
"allowJs": true,
"alwaysStrict": true,
"checkJs": true,
"target": "ES2019",
"moduleResolution": "node",
"declaration": 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,
"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');
class TestServer {
/** @type number */
PORT = undefined;
/** @type string */
PREFIX = undefined;
/** @type string */
CROSS_PROCESS_PREFIX = undefined;
/** @type string */
EMPTY_PAGE = undefined;
/**

View File

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

View File

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