chore: use curly (#8519)

This commit is contained in:
jrandolf 2022-06-14 13:55:35 +02:00 committed by GitHub
parent 0678343b53
commit e6442dd767
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
68 changed files with 1507 additions and 590 deletions

View File

@ -21,6 +21,8 @@ module.exports = {
extends: ['plugin:prettier/recommended'], extends: ['plugin:prettier/recommended'],
rules: { rules: {
// Brackets keep code readable.
curly: [2, 'all'],
// Error if files are not formatted with Prettier correctly. // Error if files are not formatted with Prettier correctly.
'prettier/prettier': 2, 'prettier/prettier': 2,
// syntax preferences // syntax preferences
@ -130,6 +132,8 @@ module.exports = {
], ],
plugins: ['eslint-plugin-tsdoc'], plugins: ['eslint-plugin-tsdoc'],
rules: { rules: {
// Brackets keep code readable.
curly: [2, 'all'],
// Error if comments do not adhere to `tsdoc`. // Error if comments do not adhere to `tsdoc`.
'tsdoc/syntax': 2, 'tsdoc/syntax': 2,
'no-unused-vars': 0, 'no-unused-vars': 0,

View File

@ -23,8 +23,11 @@ const puppeteer = require('puppeteer');
const page = await browser.newPage(); const page = await browser.newPage();
await page.setRequestInterception(true); await page.setRequestInterception(true);
page.on('request', (request) => { page.on('request', (request) => {
if (request.resourceType() === 'image') request.abort(); if (request.resourceType() === 'image') {
else request.continue(); request.abort();
} else {
request.continue();
}
}); });
await page.goto('https://news.google.com/news/'); await page.goto('https://news.google.com/news/');
await page.screenshot({ path: 'news.png', fullPage: true }); await page.screenshot({ path: 'news.png', fullPage: true });

View File

@ -196,13 +196,19 @@ export class Accessibility {
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] ?? null;
} }
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] ?? null; return this.serializeTree(needle, interestingNodes)[0] ?? null;
} }
@ -211,13 +217,18 @@ export class Accessibility {
interestingNodes?: Set<AXNode> interestingNodes?: Set<AXNode>
): SerializedAXNode[] { ): SerializedAXNode[] {
const children: SerializedAXNode[] = []; const children: SerializedAXNode[] = [];
for (const child of node.children) for (const child of node.children) {
children.push(...this.serializeTree(child, interestingNodes)); children.push(...this.serializeTree(child, interestingNodes));
}
if (interestingNodes && !interestingNodes.has(node)) return children; if (interestingNodes && !interestingNodes.has(node)) {
return children;
}
const serializedNode = node.serialize(); const serializedNode = node.serialize();
if (children.length) serializedNode.children = children; if (children.length) {
serializedNode.children = children;
}
return [serializedNode]; return [serializedNode];
} }
@ -226,13 +237,18 @@ export class Accessibility {
node: AXNode, node: AXNode,
insideControl: boolean insideControl: boolean
): void { ): void {
if (node.isInteresting(insideControl)) collection.add(node); if (node.isInteresting(insideControl)) {
if (node.isLeafNode()) return; collection.add(node);
}
if (node.isLeafNode()) {
return;
}
insideControl = insideControl || node.isControl(); insideControl = insideControl || node.isControl();
for (const child of node.children) for (const child of node.children) {
this.collectInterestingNodes(collection, child, insideControl); this.collectInterestingNodes(collection, child, insideControl);
} }
} }
}
class AXNode { class AXNode {
public payload: Protocol.Accessibility.AXNode; public payload: Protocol.Accessibility.AXNode;
@ -258,14 +274,22 @@ class AXNode {
this.#richlyEditable = property.value.value === 'richtext'; this.#richlyEditable = property.value.value === 'richtext';
this.#editable = true; this.#editable = true;
} }
if (property.name === 'focusable') this.#focusable = property.value.value; if (property.name === 'focusable') {
if (property.name === 'hidden') this.#hidden = property.value.value; this.#focusable = property.value.value;
}
if (property.name === 'hidden') {
this.#hidden = property.value.value;
}
} }
} }
#isPlainTextField(): boolean { #isPlainTextField(): boolean {
if (this.#richlyEditable) return false; if (this.#richlyEditable) {
if (this.#editable) return true; return false;
}
if (this.#editable) {
return true;
}
return this.#role === 'textbox' || this.#role === 'searchbox'; return this.#role === 'textbox' || this.#role === 'searchbox';
} }
@ -288,22 +312,30 @@ class AXNode {
} }
public find(predicate: (x: AXNode) => boolean): AXNode | null { public find(predicate: (x: AXNode) => boolean): AXNode | null {
if (predicate(this)) return this; if (predicate(this)) {
return this;
}
for (const child of this.children) { for (const child of this.children) {
const result = child.find(predicate); const result = child.find(predicate);
if (result) return result; if (result) {
return result;
}
} }
return null; return null;
} }
public isLeafNode(): boolean { public isLeafNode(): boolean {
if (!this.children.length) return true; if (!this.children.length) {
return true;
}
// These types of objects may have children that we use as internal // These types of objects may have children that we use as internal
// implementation details, but we want to expose them as leaves to platform // implementation details, but we want to expose them as leaves to platform
// accessibility APIs because screen readers might be confused if they find // accessibility APIs because screen readers might be confused if they find
// any children. // any children.
if (this.#isPlainTextField() || this.#isTextOnlyObject()) return true; if (this.#isPlainTextField() || this.#isTextOnlyObject()) {
return true;
}
// Roles whose children are only presentational according to the ARIA and // Roles whose children are only presentational according to the ARIA and
// HTML5 Specs should be hidden from screen readers. // HTML5 Specs should be hidden from screen readers.
@ -324,9 +356,15 @@ class AXNode {
} }
// Here and below: Android heuristics // Here and below: Android heuristics
if (this.#hasFocusableChild()) return false; if (this.#hasFocusableChild()) {
if (this.#focusable && this.#name) return true; return false;
if (this.#role === 'heading' && this.#name) return true; }
if (this.#focusable && this.#name) {
return true;
}
if (this.#role === 'heading' && this.#name) {
return true;
}
return false; return false;
} }
@ -361,27 +399,41 @@ class AXNode {
public isInteresting(insideControl: boolean): boolean { public isInteresting(insideControl: boolean): boolean {
const role = this.#role; const role = this.#role;
if (role === 'Ignored' || this.#hidden || this.#ignored) return false; if (role === 'Ignored' || this.#hidden || this.#ignored) {
return false;
}
if (this.#focusable || this.#richlyEditable) return true; if (this.#focusable || this.#richlyEditable) {
return true;
}
// If it's not focusable but has a control role, then it's interesting. // If it's not focusable but has a control role, then it's interesting.
if (this.isControl()) return true; if (this.isControl()) {
return true;
}
// A non focusable child of a control is not interesting // A non focusable child of a control is not interesting
if (insideControl) return false; if (insideControl) {
return false;
}
return this.isLeafNode() && !!this.#name; return this.isLeafNode() && !!this.#name;
} }
public serialize(): SerializedAXNode { public serialize(): SerializedAXNode {
const properties = new Map<string, number | string | boolean>(); const properties = new Map<string, number | string | boolean>();
for (const property of this.payload.properties || []) for (const property of this.payload.properties || []) {
properties.set(property.name.toLowerCase(), property.value.value); properties.set(property.name.toLowerCase(), property.value.value);
if (this.payload.name) properties.set('name', this.payload.name.value); }
if (this.payload.value) properties.set('value', this.payload.value.value); if (this.payload.name) {
if (this.payload.description) properties.set('name', this.payload.name.value);
}
if (this.payload.value) {
properties.set('value', this.payload.value.value);
}
if (this.payload.description) {
properties.set('description', this.payload.description.value); properties.set('description', this.payload.description.value);
}
const node: SerializedAXNode = { const node: SerializedAXNode = {
role: this.#role, role: this.#role,
@ -407,7 +459,9 @@ class AXNode {
properties.get(key) as string; properties.get(key) as string;
for (const userStringProperty of userStringProperties) { for (const userStringProperty of userStringProperties) {
if (!properties.has(userStringProperty)) continue; if (!properties.has(userStringProperty)) {
continue;
}
node[userStringProperty] = getUserStringPropertyValue(userStringProperty); node[userStringProperty] = getUserStringPropertyValue(userStringProperty);
} }
@ -440,17 +494,22 @@ class AXNode {
// RootWebArea's treat focus differently than other nodes. They report whether // RootWebArea's treat focus differently than other nodes. They report whether
// their frame has focus, not whether focus is specifically on the root // their frame has focus, not whether focus is specifically on the root
// node. // node.
if (booleanProperty === 'focused' && this.#role === 'RootWebArea') if (booleanProperty === 'focused' && this.#role === 'RootWebArea') {
continue; continue;
}
const value = getBooleanPropertyValue(booleanProperty); const value = getBooleanPropertyValue(booleanProperty);
if (!value) continue; if (!value) {
continue;
}
node[booleanProperty] = getBooleanPropertyValue(booleanProperty); node[booleanProperty] = getBooleanPropertyValue(booleanProperty);
} }
type TristateProperty = 'checked' | 'pressed'; type TristateProperty = 'checked' | 'pressed';
const tristateProperties: TristateProperty[] = ['checked', 'pressed']; const tristateProperties: TristateProperty[] = ['checked', 'pressed'];
for (const tristateProperty of tristateProperties) { for (const tristateProperty of tristateProperties) {
if (!properties.has(tristateProperty)) continue; if (!properties.has(tristateProperty)) {
continue;
}
const value = properties.get(tristateProperty); const value = properties.get(tristateProperty);
node[tristateProperty] = node[tristateProperty] =
value === 'mixed' ? 'mixed' : value === 'true' ? true : false; value === 'mixed' ? 'mixed' : value === 'true' ? true : false;
@ -465,7 +524,9 @@ class AXNode {
const getNumericalPropertyValue = (key: NumbericalProperty): number => const getNumericalPropertyValue = (key: NumbericalProperty): number =>
properties.get(key) as number; properties.get(key) as number;
for (const numericalProperty of numericalProperties) { for (const numericalProperty of numericalProperties) {
if (!properties.has(numericalProperty)) continue; if (!properties.has(numericalProperty)) {
continue;
}
node[numericalProperty] = getNumericalPropertyValue(numericalProperty); node[numericalProperty] = getNumericalPropertyValue(numericalProperty);
} }
@ -484,7 +545,9 @@ class AXNode {
properties.get(key) as string; properties.get(key) as string;
for (const tokenProperty of tokenProperties) { for (const tokenProperty of tokenProperties) {
const value = getTokenPropertyValue(tokenProperty); const value = getTokenPropertyValue(tokenProperty);
if (!value || value === 'false') continue; if (!value || value === 'false') {
continue;
}
node[tokenProperty] = getTokenPropertyValue(tokenProperty); node[tokenProperty] = getTokenPropertyValue(tokenProperty);
} }
return node; return node;
@ -492,12 +555,14 @@ class AXNode {
public static createTree(payloads: Protocol.Accessibility.AXNode[]): AXNode { public static createTree(payloads: Protocol.Accessibility.AXNode[]): AXNode {
const nodeById = new Map<string, AXNode>(); const nodeById = new Map<string, AXNode>();
for (const payload of payloads) for (const payload of payloads) {
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

@ -76,8 +76,9 @@ function parseAriaSelector(selector: string): ARIAQueryOption {
return ''; return '';
} }
); );
if (defaultName && !queryOptions.name) if (defaultName && !queryOptions.name) {
queryOptions.name = normalizeValue(defaultName); queryOptions.name = normalizeValue(defaultName);
}
return queryOptions; return queryOptions;
} }

View File

@ -285,11 +285,12 @@ export class Browser extends EventEmitter {
this.#defaultContext = new BrowserContext(this.#connection, this); this.#defaultContext = new BrowserContext(this.#connection, this);
this.#contexts = new Map(); this.#contexts = new Map();
for (const contextId of contextIds) for (const contextId of contextIds) {
this.#contexts.set( this.#contexts.set(
contextId, contextId,
new BrowserContext(this.#connection, this, contextId) new BrowserContext(this.#connection, this, contextId)
); );
}
this.#targets = new Map(); this.#targets = new Map();
this.#connection.on(ConnectionEmittedEvents.Disconnected, () => this.#connection.on(ConnectionEmittedEvents.Disconnected, () =>
@ -441,7 +442,9 @@ export class Browser extends EventEmitter {
} }
async #targetDestroyed(event: { targetId: string }): Promise<void> { async #targetDestroyed(event: { targetId: string }): Promise<void> {
if (this.#ignoredTargets.has(event.targetId)) return; if (this.#ignoredTargets.has(event.targetId)) {
return;
}
const target = this.#targets.get(event.targetId); const target = this.#targets.get(event.targetId);
if (!target) { if (!target) {
throw new Error( throw new Error(
@ -460,7 +463,9 @@ export class Browser extends EventEmitter {
} }
#targetInfoChanged(event: Protocol.Target.TargetInfoChangedEvent): void { #targetInfoChanged(event: Protocol.Target.TargetInfoChangedEvent): void {
if (this.#ignoredTargets.has(event.targetInfo.targetId)) return; if (this.#ignoredTargets.has(event.targetInfo.targetId)) {
return;
}
const target = this.#targets.get(event.targetInfo.targetId); const target = this.#targets.get(event.targetInfo.targetId);
if (!target) { if (!target) {
throw new Error( throw new Error(
@ -580,7 +585,9 @@ export class Browser extends EventEmitter {
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;
}
this.targets().forEach(check); this.targets().forEach(check);
return await waitWithTimeout(targetPromise, 'target', timeout); return await waitWithTimeout(targetPromise, 'target', timeout);
} finally { } finally {
@ -827,8 +834,9 @@ export class BrowserContext extends EventEmitter {
const protocolPermissions = permissions.map((permission) => { const protocolPermissions = permissions.map((permission) => {
const protocolPermission = const protocolPermission =
WEB_PERMISSION_TO_PROTOCOL_PERMISSION.get(permission); WEB_PERMISSION_TO_PROTOCOL_PERMISSION.get(permission);
if (!protocolPermission) if (!protocolPermission) {
throw new Error('Unknown permission: ' + permission); throw new Error('Unknown permission: ' + permission);
}
return protocolPermission; return protocolPermission;
}); });
await this.#connection.send('Browser.grantPermissions', { await this.#connection.send('Browser.grantPermissions', {

View File

@ -34,10 +34,14 @@ export class BrowserWebSocketTransport implements ConnectionTransport {
constructor(ws: WebSocket) { constructor(ws: WebSocket) {
this.#ws = ws; this.#ws = ws;
this.#ws.addEventListener('message', (event) => { this.#ws.addEventListener('message', (event) => {
if (this.onmessage) this.onmessage.call(null, event.data); if (this.onmessage) {
this.onmessage.call(null, event.data);
}
}); });
this.#ws.addEventListener('close', () => { this.#ws.addEventListener('close', () => {
if (this.onclose) this.onclose.call(null); if (this.onclose) {
this.onclose.call(null);
}
}); });
// 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', () => {});

View File

@ -129,7 +129,9 @@ export class Connection extends EventEmitter {
} }
async #onMessage(message: string): Promise<void> { async #onMessage(message: string): Promise<void> {
if (this.#delay) await new Promise((f) => setTimeout(f, this.#delay)); if (this.#delay) {
await new Promise((f) => setTimeout(f, this.#delay));
}
debugProtocolReceive(message); debugProtocolReceive(message);
const object = JSON.parse(message); const object = JSON.parse(message);
if (object.method === 'Target.attachedToTarget') { if (object.method === 'Target.attachedToTarget') {
@ -159,17 +161,21 @@ export class Connection extends EventEmitter {
} }
if (object.sessionId) { if (object.sessionId) {
const session = this.#sessions.get(object.sessionId); const session = this.#sessions.get(object.sessionId);
if (session) session._onMessage(object); if (session) {
session._onMessage(object);
}
} else if (object.id) { } else if (object.id) {
const callback = this.#callbacks.get(object.id); const callback = this.#callbacks.get(object.id);
// Callbacks could be all rejected if someone has called `.dispose()`. // Callbacks could be all rejected if someone has called `.dispose()`.
if (callback) { if (callback) {
this.#callbacks.delete(object.id); this.#callbacks.delete(object.id);
if (object.error) if (object.error) {
callback.reject( callback.reject(
createProtocolError(callback.error, callback.method, object) createProtocolError(callback.error, callback.method, object)
); );
else callback.resolve(object.result); } else {
callback.resolve(object.result);
}
} }
} else { } else {
this.emit(object.method, object.params); this.emit(object.method, object.params);
@ -177,19 +183,24 @@ export class Connection extends EventEmitter {
} }
#onClose(): void { #onClose(): void {
if (this.#closed) return; if (this.#closed) {
return;
}
this.#closed = true; this.#closed = true;
this.#transport.onmessage = undefined; this.#transport.onmessage = undefined;
this.#transport.onclose = undefined; this.#transport.onclose = undefined;
for (const callback of this.#callbacks.values()) for (const callback of this.#callbacks.values()) {
callback.reject( callback.reject(
rewriteError( rewriteError(
callback.error, callback.error,
`Protocol error (${callback.method}): Target closed.` `Protocol error (${callback.method}): Target closed.`
) )
); );
}
this.#callbacks.clear(); this.#callbacks.clear();
for (const session of this.#sessions.values()) session._onClosed(); for (const session of this.#sessions.values()) {
session._onClosed();
}
this.#sessions.clear(); this.#sessions.clear();
this.emit(ConnectionEmittedEvents.Disconnected); this.emit(ConnectionEmittedEvents.Disconnected);
} }
@ -287,7 +298,7 @@ export class CDPSession extends EventEmitter {
method: T, method: T,
...paramArgs: ProtocolMapping.Commands[T]['paramsType'] ...paramArgs: ProtocolMapping.Commands[T]['paramsType']
): Promise<ProtocolMapping.Commands[T]['returnType']> { ): Promise<ProtocolMapping.Commands[T]['returnType']> {
if (!this.#connection) if (!this.#connection) {
return Promise.reject( return Promise.reject(
new Error( new Error(
`Protocol error (${method}): Session closed. Most likely the ${ `Protocol error (${method}): Session closed. Most likely the ${
@ -295,6 +306,7 @@ export class CDPSession extends EventEmitter {
} has been closed.` } has been closed.`
) )
); );
}
// See the comment in Connection#send explaining why we do this. // See the comment in Connection#send explaining why we do this.
const params = paramArgs.length ? paramArgs[0] : undefined; const params = paramArgs.length ? paramArgs[0] : undefined;
@ -322,11 +334,13 @@ export class CDPSession extends EventEmitter {
const callback = object.id ? this.#callbacks.get(object.id) : undefined; const callback = object.id ? this.#callbacks.get(object.id) : undefined;
if (object.id && callback) { if (object.id && callback) {
this.#callbacks.delete(object.id); this.#callbacks.delete(object.id);
if (object.error) if (object.error) {
callback.reject( callback.reject(
createProtocolError(callback.error, callback.method, object) createProtocolError(callback.error, callback.method, object)
); );
else callback.resolve(object.result); } else {
callback.resolve(object.result);
}
} else { } else {
assert(!object.id); assert(!object.id);
this.emit(object.method, object.params); this.emit(object.method, object.params);
@ -338,12 +352,13 @@ export class CDPSession extends EventEmitter {
* won't emit any events and can't be used to send messages. * won't emit any events and can't be used to send messages.
*/ */
async detach(): Promise<void> { async detach(): Promise<void> {
if (!this.#connection) if (!this.#connection) {
throw new Error( throw new Error(
`Session already detached. Most likely the ${ `Session already detached. Most likely the ${
this.#targetType this.#targetType
} has been closed.` } has been closed.`
); );
}
await this.#connection.send('Target.detachFromTarget', { await this.#connection.send('Target.detachFromTarget', {
sessionId: this.#sessionId, sessionId: this.#sessionId,
}); });
@ -353,13 +368,14 @@ export class CDPSession extends EventEmitter {
* @internal * @internal
*/ */
_onClosed(): void { _onClosed(): void {
for (const callback of this.#callbacks.values()) for (const callback of this.#callbacks.values()) {
callback.reject( callback.reject(
rewriteError( rewriteError(
callback.error, callback.error,
`Protocol error (${callback.method}): Target closed.` `Protocol error (${callback.method}): Target closed.`
) )
); );
}
this.#callbacks.clear(); this.#callbacks.clear();
this.#connection = undefined; this.#connection = undefined;
this.emit(CDPSessionEmittedEvents.Disconnected); this.emit(CDPSessionEmittedEvents.Disconnected);
@ -379,7 +395,9 @@ function createProtocolError(
object: { error: { message: string; data: any; code: number } } object: { error: { message: string; data: any; code: number } }
): Error { ): Error {
let message = `Protocol error (${method}): ${object.error.message}`; let message = `Protocol error (${method}): ${object.error.message}`;
if ('data' in object.error) message += ` ${object.error.data}`; if ('data' in object.error) {
message += ` ${object.error.data}`;
}
return rewriteError(error, message, object.error.message); return rewriteError(error, message, object.error.message);
} }

View File

@ -244,7 +244,9 @@ export class JSCoverage {
} }
#onExecutionContextsCleared(): void { #onExecutionContextsCleared(): void {
if (!this.#resetOnNavigation) return; if (!this.#resetOnNavigation) {
return;
}
this.#scriptURLs.clear(); this.#scriptURLs.clear();
this.#scriptSources.clear(); this.#scriptSources.clear();
} }
@ -253,9 +255,13 @@ export class JSCoverage {
event: Protocol.Debugger.ScriptParsedEvent event: Protocol.Debugger.ScriptParsedEvent
): Promise<void> { ): Promise<void> {
// Ignore puppeteer-injected scripts // Ignore puppeteer-injected scripts
if (event.url === EVALUATION_SCRIPT_URL) return; if (event.url === EVALUATION_SCRIPT_URL) {
return;
}
// Ignore other anonymous scripts unless the reportAnonymousScripts option is true. // Ignore other anonymous scripts unless the reportAnonymousScripts option is true.
if (!event.url && !this.#reportAnonymousScripts) return; if (!event.url && !this.#reportAnonymousScripts) {
return;
}
try { try {
const response = await this.#client.send('Debugger.getScriptSource', { const response = await this.#client.send('Debugger.getScriptSource', {
scriptId: event.scriptId, scriptId: event.scriptId,
@ -286,12 +292,17 @@ export class JSCoverage {
for (const entry of profileResponse.result) { for (const entry of profileResponse.result) {
let url = this.#scriptURLs.get(entry.scriptId); let url = this.#scriptURLs.get(entry.scriptId);
if (!url && this.#reportAnonymousScripts) if (!url && this.#reportAnonymousScripts) {
url = 'debugger://VM' + entry.scriptId; url = 'debugger://VM' + entry.scriptId;
}
const text = this.#scriptSources.get(entry.scriptId); const text = this.#scriptSources.get(entry.scriptId);
if (text === undefined || url === undefined) continue; if (text === undefined || url === undefined) {
continue;
}
const flattenRanges = []; const flattenRanges = [];
for (const func of entry.functions) flattenRanges.push(...func.ranges); for (const func of entry.functions) {
flattenRanges.push(...func.ranges);
}
const ranges = convertToDisjointRanges(flattenRanges); const ranges = convertToDisjointRanges(flattenRanges);
if (!this.#includeRawScriptCoverage) { if (!this.#includeRawScriptCoverage) {
coverage.push({ url, ranges, text }); coverage.push({ url, ranges, text });
@ -345,7 +356,9 @@ export class CSSCoverage {
} }
#onExecutionContextsCleared(): void { #onExecutionContextsCleared(): void {
if (!this.#resetOnNavigation) return; if (!this.#resetOnNavigation) {
return;
}
this.#stylesheetURLs.clear(); this.#stylesheetURLs.clear();
this.#stylesheetSources.clear(); this.#stylesheetSources.clear();
} }
@ -353,7 +366,9 @@ export class CSSCoverage {
async #onStyleSheet(event: Protocol.CSS.StyleSheetAddedEvent): Promise<void> { async #onStyleSheet(event: Protocol.CSS.StyleSheetAddedEvent): Promise<void> {
const header = event.header; const header = event.header;
// Ignore anonymous scripts // Ignore anonymous scripts
if (!header.sourceURL) return; if (!header.sourceURL) {
return;
}
try { try {
const response = await this.#client.send('CSS.getStyleSheetText', { const response = await this.#client.send('CSS.getStyleSheetText', {
styleSheetId: header.styleSheetId, styleSheetId: header.styleSheetId,
@ -420,13 +435,19 @@ function convertToDisjointRanges(
// Sort points to form a valid parenthesis sequence. // Sort points to form a valid parenthesis sequence.
points.sort((a, b) => { points.sort((a, b) => {
// Sort with increasing offsets. // Sort with increasing offsets.
if (a.offset !== b.offset) return a.offset - b.offset; if (a.offset !== b.offset) {
return a.offset - b.offset;
}
// All "end" points should go before "start" points. // All "end" points should go before "start" points.
if (a.type !== b.type) return b.type - a.type; if (a.type !== b.type) {
return b.type - a.type;
}
const aLength = a.range.endOffset - a.range.startOffset; const aLength = a.range.endOffset - a.range.startOffset;
const bLength = b.range.endOffset - b.range.startOffset; const bLength = b.range.endOffset - b.range.startOffset;
// For two "start" points, the one with longer range goes first. // For two "start" points, the one with longer range goes first.
if (a.type === 0) return bLength - aLength; if (a.type === 0) {
return bLength - aLength;
}
// For two "end" points, the one with shorter range goes first. // For two "end" points, the one with shorter range goes first.
return aLength - bLength; return aLength - bLength;
}); });
@ -445,13 +466,18 @@ function convertToDisjointRanges(
hitCountStack[hitCountStack.length - 1]! > 0 hitCountStack[hitCountStack.length - 1]! > 0
) { ) {
const lastResult = results[results.length - 1]; 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 });
}
} }
lastOffset = point.offset; lastOffset = point.offset;
if (point.type === 0) hitCountStack.push(point.range.count); if (point.type === 0) {
else hitCountStack.pop(); hitCountStack.push(point.range.count);
} else {
hitCountStack.pop();
}
} }
// Filter out empty ranges. // Filter out empty ranges.
return results.filter((range) => range.end - range.start > 1); return results.filter((range) => range.end - range.start > 1);

View File

@ -143,7 +143,9 @@ export class DOMWorld {
this.#ctxBindings.clear(); this.#ctxBindings.clear();
this.#contextResolveCallback?.call(null, context); this.#contextResolveCallback?.call(null, context);
this.#contextResolveCallback = null; this.#contextResolveCallback = null;
for (const waitTask of this._waitTasks) waitTask.rerun(); for (const waitTask of this._waitTasks) {
waitTask.rerun();
}
} else { } else {
this.#documentPromise = null; this.#documentPromise = null;
this.#contextPromise = new Promise((fulfill) => { this.#contextPromise = new Promise((fulfill) => {
@ -165,19 +167,22 @@ export class DOMWorld {
_detach(): void { _detach(): void {
this.#detached = true; this.#detached = true;
this.#client.off('Runtime.bindingCalled', this.#onBindingCalled); this.#client.off('Runtime.bindingCalled', this.#onBindingCalled);
for (const waitTask of this._waitTasks) for (const waitTask of this._waitTasks) {
waitTask.terminate( waitTask.terminate(
new Error('waitForFunction failed: frame got detached.') new Error('waitForFunction failed: frame got detached.')
); );
} }
}
executionContext(): Promise<ExecutionContext> { executionContext(): Promise<ExecutionContext> {
if (this.#detached) if (this.#detached) {
throw new Error( throw new Error(
`Execution context is not available in detached frame "${this.#frame.url()}" (are you trying to evaluate?)` `Execution context is not available in detached frame "${this.#frame.url()}" (are you trying to evaluate?)`
); );
if (this.#contextPromise === null) }
if (this.#contextPromise === null) {
throw new Error(`Execution content promise is missing`); throw new Error(`Execution content promise is missing`);
}
return this.#contextPromise; return this.#contextPromise;
} }
@ -212,7 +217,9 @@ export class DOMWorld {
* @internal * @internal
*/ */
async _document(): Promise<ElementHandle> { async _document(): Promise<ElementHandle> {
if (this.#documentPromise) return this.#documentPromise; if (this.#documentPromise) {
return this.#documentPromise;
}
this.#documentPromise = this.executionContext().then(async (context) => { this.#documentPromise = this.executionContext().then(async (context) => {
const document = await context.evaluateHandle('document'); const document = await context.evaluateHandle('document');
const element = document.asElement(); const element = document.asElement();
@ -270,10 +277,12 @@ export class DOMWorld {
async content(): Promise<string> { async content(): Promise<string> {
return await this.evaluate(() => { return await this.evaluate(() => {
let retVal = ''; let retVal = '';
if (document.doctype) if (document.doctype) {
retVal = new XMLSerializer().serializeToString(document.doctype); retVal = new XMLSerializer().serializeToString(document.doctype);
if (document.documentElement) }
if (document.documentElement) {
retVal += document.documentElement.outerHTML; retVal += document.documentElement.outerHTML;
}
return retVal; return retVal;
}); });
} }
@ -307,7 +316,9 @@ export class DOMWorld {
watcher.lifecyclePromise(), watcher.lifecyclePromise(),
]); ]);
watcher.dispose(); watcher.dispose();
if (error) throw error; if (error) {
throw error;
}
} }
/** /**
@ -406,8 +417,12 @@ export class DOMWorld {
): Promise<HTMLElement> { ): Promise<HTMLElement> {
const script = document.createElement('script'); const script = document.createElement('script');
script.src = url; script.src = url;
if (id) script.id = id; if (id) {
if (type) script.type = type; script.id = id;
}
if (type) {
script.type = type;
}
const promise = new Promise((res, rej) => { const promise = new Promise((res, rej) => {
script.onload = res; script.onload = res;
script.onerror = rej; script.onerror = rej;
@ -425,11 +440,15 @@ export class DOMWorld {
const script = document.createElement('script'); const script = document.createElement('script');
script.type = type; script.type = type;
script.text = content; script.text = content;
if (id) script.id = id; if (id) {
script.id = id;
}
let error = null; let error = null;
script.onerror = (e) => (error = e); script.onerror = (e) => (error = e);
document.head.appendChild(script); document.head.appendChild(script);
if (error) throw error; if (error) {
throw error;
}
return script; return script;
} }
} }
@ -656,7 +675,9 @@ export class DOMWorld {
event: Protocol.Runtime.BindingCalledEvent event: Protocol.Runtime.BindingCalledEvent
): Promise<void> => { ): Promise<void> => {
let payload: { type: string; name: string; seq: number; args: unknown[] }; let payload: { type: string; name: string; seq: number; args: unknown[] };
if (!this._hasContext()) return; if (!this._hasContext()) {
return;
}
const context = await this.executionContext(); const context = await this.executionContext();
try { try {
payload = JSON.parse(event.payload); payload = JSON.parse(event.payload);
@ -671,9 +692,12 @@ export class DOMWorld {
!this.#ctxBindings.has( !this.#ctxBindings.has(
DOMWorld.#bindingIdentifier(name, context._contextId) DOMWorld.#bindingIdentifier(name, context._contextId)
) )
) ) {
return; return;
if (context._contextId !== event.executionContextId) return; }
if (context._contextId !== event.executionContextId) {
return;
}
try { try {
const fn = this._boundFunctions.get(name); const fn = this._boundFunctions.get(name);
if (!fn) { if (!fn) {
@ -687,7 +711,9 @@ export class DOMWorld {
// In both caes, the promises above are rejected with a protocol error. // In both caes, the promises above are rejected with a protocol error.
// We can safely ignores these, as the WaitTask is re-installed in // We can safely ignores these, as the WaitTask is re-installed in
// the next execution context if needed. // the next execution context if needed.
if ((error as Error).message.includes('Protocol error')) return; if ((error as Error).message.includes('Protocol error')) {
return;
}
debugError(error); debugError(error);
} }
function deliverResult(name: string, seq: number, result: unknown): void { function deliverResult(name: string, seq: number, result: unknown): void {
@ -859,20 +885,24 @@ export class WaitTask {
promise: Promise<JSHandle>; promise: Promise<JSHandle>;
constructor(options: WaitTaskOptions) { constructor(options: WaitTaskOptions) {
if (isString(options.polling)) if (isString(options.polling)) {
assert( assert(
options.polling === 'raf' || options.polling === 'mutation', options.polling === 'raf' || options.polling === 'mutation',
'Unknown polling option: ' + options.polling 'Unknown polling option: ' + options.polling
); );
else if (isNumber(options.polling)) } else if (isNumber(options.polling)) {
assert( assert(
options.polling > 0, options.polling > 0,
'Cannot poll with non-positive interval: ' + options.polling 'Cannot poll with non-positive interval: ' + options.polling
); );
else throw new Error('Unknown polling options: ' + options.polling); } else {
throw new Error('Unknown polling options: ' + options.polling);
}
function getPredicateBody(predicateBody: Function | string) { function getPredicateBody(predicateBody: Function | string) {
if (isString(predicateBody)) return `return (${predicateBody});`; if (isString(predicateBody)) {
return `return (${predicateBody});`;
}
return `return (${predicateBody})(...args);`; return `return (${predicateBody})(...args);`;
} }
@ -922,11 +952,15 @@ export class WaitTask {
let success: JSHandle | null = null; let success: JSHandle | null = null;
let error: Error | null = null; let error: Error | null = null;
const context = await this.#domWorld.executionContext(); const context = await this.#domWorld.executionContext();
if (this.#terminated || runCount !== this.#runCount) return; if (this.#terminated || runCount !== this.#runCount) {
return;
}
if (this.#binding) { if (this.#binding) {
await this.#domWorld._addBindingToContext(context, this.#binding.name); await this.#domWorld._addBindingToContext(context, this.#binding.name);
} }
if (this.#terminated || runCount !== this.#runCount) return; if (this.#terminated || runCount !== this.#runCount) {
return;
}
try { try {
success = await context.evaluateHandle( success = await context.evaluateHandle(
waitForPredicatePageFunction, waitForPredicatePageFunction,
@ -942,7 +976,9 @@ export class WaitTask {
} }
if (this.#terminated || runCount !== this.#runCount) { if (this.#terminated || runCount !== this.#runCount) {
if (success) await success.dispose(); if (success) {
await success.dispose();
}
return; return;
} }
@ -953,8 +989,9 @@ export class WaitTask {
!error && !error &&
(await this.#domWorld.evaluate((s) => !s, success).catch(() => true)) (await this.#domWorld.evaluate((s) => !s, success).catch(() => true))
) { ) {
if (!success) if (!success) {
throw new Error('Assertion: result handle is not available'); throw new Error('Assertion: result handle is not available');
}
await success.dispose(); await success.dispose();
return; return;
} }
@ -978,17 +1015,21 @@ export class WaitTask {
// When the page is navigated, the promise is rejected. // When the page is navigated, the promise is rejected.
// We will try again in the new execution context. // We will try again in the new execution context.
if (error.message.includes('Execution context was destroyed')) return; if (error.message.includes('Execution context was destroyed')) {
return;
}
// We could have tried to evaluate in a context which was already // We could have tried to evaluate in a context which was already
// destroyed. // destroyed.
if (error.message.includes('Cannot find context with specified id')) if (error.message.includes('Cannot find context with specified id')) {
return; return;
}
this.#reject(error); this.#reject(error);
} else { } else {
if (!success) if (!success) {
throw new Error('Assertion: result handle is not available'); throw new Error('Assertion: result handle is not available');
}
this.#resolve(success); this.#resolve(success);
} }
this.#cleanup(); this.#cleanup();
@ -1011,7 +1052,9 @@ async function waitForPredicatePageFunction(
root = root || document; root = root || document;
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);
}
switch (polling) { switch (polling) {
case 'raf': case 'raf':
return await pollRaf(); return await pollRaf();
@ -1025,7 +1068,9 @@ async function waitForPredicatePageFunction(
const success = predicateAcceptsContextElement const success = predicateAcceptsContextElement
? await predicate(root, ...args) ? await predicate(root, ...args)
: await predicate(...args); : await predicate(...args);
if (success) return Promise.resolve(success); if (success) {
return Promise.resolve(success);
}
let fulfill = (_?: unknown) => {}; let fulfill = (_?: unknown) => {};
const result = new Promise((x) => (fulfill = x)); const result = new Promise((x) => (fulfill = x));
@ -1067,8 +1112,11 @@ async function waitForPredicatePageFunction(
const success = predicateAcceptsContextElement const success = predicateAcceptsContextElement
? await predicate(root, ...args) ? await predicate(root, ...args)
: await predicate(...args); : await predicate(...args);
if (success) fulfill(success); if (success) {
else requestAnimationFrame(onRaf); fulfill(success);
} else {
requestAnimationFrame(onRaf);
}
} }
} }
@ -1086,8 +1134,11 @@ async function waitForPredicatePageFunction(
const success = predicateAcceptsContextElement const success = predicateAcceptsContextElement
? await predicate(root, ...args) ? await predicate(root, ...args)
: await predicate(...args); : await predicate(...args);
if (success) fulfill(success); if (success) {
else setTimeout(onTimeout, pollInterval); fulfill(success);
} else {
setTimeout(onTimeout, pollInterval);
}
} }
} }
} }

View File

@ -66,7 +66,9 @@ export const debug = (prefix: string): ((...args: unknown[]) => void) => {
return (...logArgs: unknown[]): void => { return (...logArgs: unknown[]): void => {
const debugLevel = globalThis.__PUPPETEER_DEBUG; const debugLevel = globalThis.__PUPPETEER_DEBUG;
if (!debugLevel) return; if (!debugLevel) {
return;
}
const everythingShouldBeLogged = debugLevel === '*'; const everythingShouldBeLogged = debugLevel === '*';
@ -81,7 +83,9 @@ export const debug = (prefix: string): ((...args: unknown[]) => void) => {
? prefix.startsWith(debugLevel) ? prefix.startsWith(debugLevel)
: prefix === debugLevel); : prefix === debugLevel);
if (!prefixMatchesDebugLevel) return; if (!prefixMatchesDebugLevel) {
return;
}
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(`${prefix}:`, ...logArgs); console.log(`${prefix}:`, ...logArgs);

View File

@ -218,20 +218,22 @@ export class ExecutionContext {
}) })
.catch(rewriteError); .catch(rewriteError);
if (exceptionDetails) if (exceptionDetails) {
throw new Error( throw new Error(
'Evaluation failed: ' + getExceptionMessage(exceptionDetails) 'Evaluation failed: ' + getExceptionMessage(exceptionDetails)
); );
}
return returnByValue return returnByValue
? valueFromRemoteObject(remoteObject) ? valueFromRemoteObject(remoteObject)
: _createJSHandle(this, remoteObject); : _createJSHandle(this, remoteObject);
} }
if (typeof pageFunction !== 'function') if (typeof pageFunction !== 'function') {
throw new Error( throw new Error(
`Expected to get |string| or |function| as the first argument, but got "${pageFunction}" instead.` `Expected to get |string| or |function| as the first argument, but got "${pageFunction}" instead.`
); );
}
let functionText = pageFunction.toString(); let functionText = pageFunction.toString();
try { try {
@ -239,10 +241,12 @@ export class ExecutionContext {
} catch (error) { } catch (error) {
// This means we might have a function shorthand. Try another // This means we might have a function shorthand. Try another
// time prefixing 'function '. // time prefixing 'function '.
if (functionText.startsWith('async ')) if (functionText.startsWith('async ')) {
functionText = functionText =
'async function ' + functionText.substring('async '.length); 'async function ' + functionText.substring('async '.length);
else functionText = 'function ' + functionText; } else {
functionText = 'function ' + functionText;
}
try { try {
new Function('(' + functionText + ')'); new Function('(' + functionText + ')');
} catch (error) { } catch (error) {
@ -271,10 +275,11 @@ export class ExecutionContext {
} }
const { exceptionDetails, result: remoteObject } = const { exceptionDetails, result: remoteObject } =
await callFunctionOnPromise.catch(rewriteError); await callFunctionOnPromise.catch(rewriteError);
if (exceptionDetails) if (exceptionDetails) {
throw new Error( throw new Error(
'Evaluation failed: ' + getExceptionMessage(exceptionDetails) 'Evaluation failed: ' + getExceptionMessage(exceptionDetails)
); );
}
return returnByValue return returnByValue
? valueFromRemoteObject(remoteObject) ? valueFromRemoteObject(remoteObject)
: _createJSHandle(this, remoteObject); : _createJSHandle(this, remoteObject);
@ -283,45 +288,61 @@ export class ExecutionContext {
this: ExecutionContext, this: ExecutionContext,
arg: unknown arg: unknown
): Protocol.Runtime.CallArgument { ): Protocol.Runtime.CallArgument {
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` };
if (Object.is(arg, -0)) return { unserializableValue: '-0' }; }
if (Object.is(arg, Infinity)) return { unserializableValue: 'Infinity' }; if (Object.is(arg, -0)) {
if (Object.is(arg, -Infinity)) return { unserializableValue: '-0' };
}
if (Object.is(arg, Infinity)) {
return { unserializableValue: 'Infinity' };
}
if (Object.is(arg, -Infinity)) {
return { unserializableValue: '-Infinity' }; return { unserializableValue: '-Infinity' };
if (Object.is(arg, NaN)) return { unserializableValue: 'NaN' }; }
if (Object.is(arg, NaN)) {
return { unserializableValue: 'NaN' };
}
const objectHandle = arg && arg instanceof JSHandle ? arg : null; const objectHandle = arg && arg instanceof JSHandle ? arg : null;
if (objectHandle) { if (objectHandle) {
if (objectHandle._context !== this) if (objectHandle._context !== this) {
throw new Error( throw new Error(
'JSHandles can be evaluated only in the context they were created!' 'JSHandles can be evaluated only in the context they were created!'
); );
if (objectHandle._disposed) throw new Error('JSHandle is disposed!'); }
if (objectHandle._remoteObject.unserializableValue) if (objectHandle._disposed) {
throw new Error('JSHandle is disposed!');
}
if (objectHandle._remoteObject.unserializableValue) {
return { return {
unserializableValue: objectHandle._remoteObject.unserializableValue, unserializableValue: objectHandle._remoteObject.unserializableValue,
}; };
if (!objectHandle._remoteObject.objectId) }
if (!objectHandle._remoteObject.objectId) {
return { value: objectHandle._remoteObject.value }; return { value: objectHandle._remoteObject.value };
}
return { objectId: objectHandle._remoteObject.objectId }; return { objectId: objectHandle._remoteObject.objectId };
} }
return { value: arg }; return { value: arg };
} }
function rewriteError(error: Error): Protocol.Runtime.EvaluateResponse { function rewriteError(error: Error): Protocol.Runtime.EvaluateResponse {
if (error.message.includes('Object reference chain is too long')) if (error.message.includes('Object reference chain is too long')) {
return { result: { type: 'undefined' } }; return { result: { type: 'undefined' } };
if (error.message.includes("Object couldn't be returned by value")) }
if (error.message.includes("Object couldn't be returned by value")) {
return { result: { type: 'undefined' } }; return { result: { type: 'undefined' } };
}
if ( if (
error.message.endsWith('Cannot find context with specified id') || error.message.endsWith('Cannot find context with specified id') ||
error.message.endsWith('Inspected target navigated or closed') error.message.endsWith('Inspected target navigated or closed')
) ) {
throw new Error( throw new Error(
'Execution context was destroyed, most likely because of a navigation.' 'Execution context was destroyed, most likely because of a navigation.'
); );
}
throw error; throw error;
} }
} }

View File

@ -221,7 +221,9 @@ export class FrameManager extends EventEmitter {
]); ]);
} }
watcher.dispose(); watcher.dispose();
if (error) throw error; if (error) {
throw error;
}
return await watcher.navigationResponse(); return await watcher.navigationResponse();
async function navigate( async function navigate(
@ -267,7 +269,9 @@ export class FrameManager extends EventEmitter {
watcher.newDocumentNavigationPromise(), watcher.newDocumentNavigationPromise(),
]); ]);
watcher.dispose(); watcher.dispose();
if (error) throw error; if (error) {
throw error;
}
return await watcher.navigationResponse(); return await watcher.navigationResponse();
} }
@ -281,13 +285,17 @@ export class FrameManager extends EventEmitter {
assert(connection); assert(connection);
const session = connection.session(event.sessionId); const session = connection.session(event.sessionId);
assert(session); 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);
} }
async #onDetachedFromTarget(event: Protocol.Target.DetachedFromTargetEvent) { async #onDetachedFromTarget(event: Protocol.Target.DetachedFromTargetEvent) {
if (!event.targetId) return; 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
@ -298,20 +306,26 @@ export class FrameManager extends EventEmitter {
#onLifecycleEvent(event: Protocol.Page.LifecycleEventEvent): void { #onLifecycleEvent(event: Protocol.Page.LifecycleEventEvent): void {
const frame = this.#frames.get(event.frameId); const frame = this.#frames.get(event.frameId);
if (!frame) return; if (!frame) {
return;
}
frame._onLifecycleEvent(event.loaderId, event.name); frame._onLifecycleEvent(event.loaderId, event.name);
this.emit(FrameManagerEmittedEvents.LifecycleEvent, frame); this.emit(FrameManagerEmittedEvents.LifecycleEvent, frame);
} }
#onFrameStartedLoading(frameId: string): void { #onFrameStartedLoading(frameId: string): void {
const frame = this.#frames.get(frameId); const frame = this.#frames.get(frameId);
if (!frame) return; if (!frame) {
return;
}
frame._onLoadingStarted(); frame._onLoadingStarted();
} }
#onFrameStoppedLoading(frameId: string): void { #onFrameStoppedLoading(frameId: string): void {
const frame = this.#frames.get(frameId); const frame = this.#frames.get(frameId);
if (!frame) return; if (!frame) {
return;
}
frame._onLoadingStopped(); frame._onLoadingStopped();
this.emit(FrameManagerEmittedEvents.LifecycleEvent, frame); this.emit(FrameManagerEmittedEvents.LifecycleEvent, frame);
} }
@ -328,7 +342,9 @@ export class FrameManager extends EventEmitter {
); );
} }
this.#onFrameNavigated(frameTree.frame); this.#onFrameNavigated(frameTree.frame);
if (!frameTree.childFrames) return; if (!frameTree.childFrames) {
return;
}
for (const child of frameTree.childFrames) { for (const child of frameTree.childFrames) {
this.#handleFrameTree(session, child); this.#handleFrameTree(session, child);
@ -387,9 +403,10 @@ export class FrameManager extends EventEmitter {
// Detach all child frames first. // Detach all child frames first.
if (frame) { if (frame) {
for (const child of frame.childFrames()) for (const child of frame.childFrames()) {
this.#removeFramesRecursively(child); this.#removeFramesRecursively(child);
} }
}
// Update or create main frame. // Update or create main frame.
if (isMainFrame) { if (isMainFrame) {
@ -414,7 +431,9 @@ export class FrameManager extends EventEmitter {
async _ensureIsolatedWorld(session: CDPSession, name: string): Promise<void> { async _ensureIsolatedWorld(session: CDPSession, name: string): Promise<void> {
const key = `${session.id()}:${name}`; const key = `${session.id()}:${name}`;
if (this.#isolatedWorlds.has(key)) return; if (this.#isolatedWorlds.has(key)) {
return;
}
this.#isolatedWorlds.add(key); this.#isolatedWorlds.add(key);
await session.send('Page.addScriptToEvaluateOnNewDocument', { await session.send('Page.addScriptToEvaluateOnNewDocument', {
@ -439,7 +458,9 @@ export class FrameManager extends EventEmitter {
#onFrameNavigatedWithinDocument(frameId: string, url: string): void { #onFrameNavigatedWithinDocument(frameId: string, url: string): void {
const frame = this.#frames.get(frameId); const frame = this.#frames.get(frameId);
if (!frame) return; if (!frame) {
return;
}
frame._navigatedWithinDocument(url); frame._navigatedWithinDocument(url);
this.emit(FrameManagerEmittedEvents.FrameNavigatedWithinDocument, frame); this.emit(FrameManagerEmittedEvents.FrameNavigatedWithinDocument, frame);
this.emit(FrameManagerEmittedEvents.FrameNavigated, frame); this.emit(FrameManagerEmittedEvents.FrameNavigated, frame);
@ -454,7 +475,9 @@ export class FrameManager extends EventEmitter {
// Only remove the frame if the reason for the detached event is // Only remove the frame if the reason for the detached event is
// an actual removement of the frame. // an actual removement of the frame.
// For frames that become OOP iframes, the reason would be 'swap'. // For frames that become OOP iframes, the reason would be 'swap'.
if (frame) this.#removeFramesRecursively(frame); if (frame) {
this.#removeFramesRecursively(frame);
}
} else if (reason === 'swap') { } else if (reason === 'swap') {
this.emit(FrameManagerEmittedEvents.FrameSwapped, frame); this.emit(FrameManagerEmittedEvents.FrameSwapped, frame);
} }
@ -471,7 +494,9 @@ export class FrameManager extends EventEmitter {
let world: DOMWorld | 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;
}
if (contextPayload.auxData && !!contextPayload.auxData['isDefault']) { if (contextPayload.auxData && !!contextPayload.auxData['isDefault']) {
world = frame._mainWorld; world = frame._mainWorld;
@ -490,7 +515,9 @@ export class FrameManager extends EventEmitter {
contextPayload, contextPayload,
world world
); );
if (world) world._setContext(context); if (world) {
world._setContext(context);
}
const key = `${session.id()}:${contextPayload.id}`; const key = `${session.id()}:${contextPayload.id}`;
this.#contextIdToContext.set(key, context); this.#contextIdToContext.set(key, context);
} }
@ -501,17 +528,25 @@ export class FrameManager extends EventEmitter {
): void { ): void {
const key = `${session.id()}:${executionContextId}`; const key = `${session.id()}:${executionContextId}`;
const context = this.#contextIdToContext.get(key); const context = this.#contextIdToContext.get(key);
if (!context) return; if (!context) {
return;
}
this.#contextIdToContext.delete(key); this.#contextIdToContext.delete(key);
if (context._world) context._world._setContext(null); if (context._world) {
context._world._setContext(null);
}
} }
#onExecutionContextsCleared(session: CDPSession): void { #onExecutionContextsCleared(session: CDPSession): void {
for (const [key, context] of this.#contextIdToContext.entries()) { for (const [key, context] of this.#contextIdToContext.entries()) {
// Make sure to only clear execution contexts that belong // Make sure to only clear execution contexts that belong
// to the current session. // to the current session.
if (context._client !== session) continue; if (context._client !== session) {
if (context._world) context._world._setContext(null); continue;
}
if (context._world) {
context._world._setContext(null);
}
this.#contextIdToContext.delete(key); this.#contextIdToContext.delete(key);
} }
} }
@ -527,8 +562,9 @@ export class FrameManager extends EventEmitter {
} }
#removeFramesRecursively(frame: Frame): void { #removeFramesRecursively(frame: Frame): void {
for (const child of frame.childFrames()) for (const child of frame.childFrames()) {
this.#removeFramesRecursively(child); this.#removeFramesRecursively(child);
}
frame._detach(); frame._detach();
this.#frames.delete(frame._id); this.#frames.delete(frame._id);
this.emit(FrameManagerEmittedEvents.FrameDetached, frame); this.emit(FrameManagerEmittedEvents.FrameDetached, frame);
@ -716,7 +752,9 @@ export class Frame {
this._loaderId = ''; this._loaderId = '';
this._childFrames = new Set(); this._childFrames = new Set();
if (this.#parentFrame) this.#parentFrame._childFrames.add(this); if (this.#parentFrame) {
this.#parentFrame._childFrames.add(this);
}
this._updateClient(client); this._updateClient(client);
} }
@ -1237,19 +1275,23 @@ export class Frame {
if (isString(selectorOrFunctionOrTimeout)) { if (isString(selectorOrFunctionOrTimeout)) {
const string = selectorOrFunctionOrTimeout; const string = selectorOrFunctionOrTimeout;
if (xPathPattern.test(string)) return this.waitForXPath(string, options); if (xPathPattern.test(string)) {
return this.waitForXPath(string, options);
}
return this.waitForSelector(string, options); return this.waitForSelector(string, options);
} }
if (isNumber(selectorOrFunctionOrTimeout)) if (isNumber(selectorOrFunctionOrTimeout)) {
return new Promise((fulfill) => return new Promise((fulfill) =>
setTimeout(fulfill, selectorOrFunctionOrTimeout) setTimeout(fulfill, selectorOrFunctionOrTimeout)
); );
if (typeof selectorOrFunctionOrTimeout === 'function') }
if (typeof selectorOrFunctionOrTimeout === 'function') {
return this.waitForFunction( return this.waitForFunction(
selectorOrFunctionOrTimeout, selectorOrFunctionOrTimeout,
options, options,
...args ...args
); );
}
return Promise.reject( return Promise.reject(
new Error( new Error(
'Unsupported target type: ' + typeof selectorOrFunctionOrTimeout 'Unsupported target type: ' + typeof selectorOrFunctionOrTimeout
@ -1324,7 +1366,9 @@ export class Frame {
selector, selector,
options options
); );
if (!handle) return null; if (!handle) {
return null;
}
const mainExecutionContext = await this._mainWorld.executionContext(); const mainExecutionContext = await this._mainWorld.executionContext();
const result = await mainExecutionContext._adoptElementHandle(handle); const result = await mainExecutionContext._adoptElementHandle(handle);
await handle.dispose(); await handle.dispose();
@ -1351,7 +1395,9 @@ export class Frame {
options: WaitForSelectorOptions = {} options: WaitForSelectorOptions = {}
): Promise<ElementHandle | null> { ): Promise<ElementHandle | null> {
const handle = await this._secondaryWorld.waitForXPath(xpath, options); const handle = await this._secondaryWorld.waitForXPath(xpath, options);
if (!handle) return null; if (!handle) {
return null;
}
const mainExecutionContext = await this._mainWorld.executionContext(); const mainExecutionContext = await this._mainWorld.executionContext();
const result = await mainExecutionContext._adoptElementHandle(handle); const result = await mainExecutionContext._adoptElementHandle(handle);
await handle.dispose(); await handle.dispose();
@ -1456,7 +1502,9 @@ export class Frame {
this.#detached = true; this.#detached = true;
this._mainWorld._detach(); this._mainWorld._detach();
this._secondaryWorld._detach(); this._secondaryWorld._detach();
if (this.#parentFrame) this.#parentFrame._childFrames.delete(this); if (this.#parentFrame) {
this.#parentFrame._childFrames.delete(this);
}
this.#parentFrame = null; this.#parentFrame = null;
} }
} }

View File

@ -184,9 +184,10 @@ export class HTTPRequest {
this.#interceptHandlers = []; this.#interceptHandlers = [];
this.#initiator = event.initiator; this.#initiator = event.initiator;
for (const [key, value] of Object.entries(event.request.headers)) for (const [key, value] of Object.entries(event.request.headers)) {
this.#headers[key.toLowerCase()] = value; this.#headers[key.toLowerCase()] = value;
} }
}
/** /**
* @returns the URL of the request * @returns the URL of the request
@ -234,10 +235,12 @@ export class HTTPRequest {
* `disabled`, `none`, or `already-handled`. * `disabled`, `none`, or `already-handled`.
*/ */
interceptResolutionState(): InterceptResolutionState { interceptResolutionState(): InterceptResolutionState {
if (!this.#allowInterception) if (!this.#allowInterception) {
return { action: InterceptResolutionAction.Disabled }; return { action: InterceptResolutionAction.Disabled };
if (this.#interceptionHandled) }
if (this.#interceptionHandled) {
return { action: InterceptResolutionAction.AlreadyHandled }; return { action: InterceptResolutionAction.AlreadyHandled };
}
return { ...this.#interceptResolutionState }; return { ...this.#interceptResolutionState };
} }
@ -396,7 +399,9 @@ export class HTTPRequest {
* failure text if the request fails. * failure text if the request fails.
*/ */
failure(): { errorText: string } | null { failure(): { errorText: string } | null {
if (!this._failureText) return null; if (!this._failureText) {
return null;
}
return { return {
errorText: this._failureText, errorText: this._failureText,
}; };
@ -435,7 +440,9 @@ export class HTTPRequest {
priority?: number priority?: number
): Promise<void> { ): Promise<void> {
// Request interception is not supported for data: urls. // Request interception is not supported for data: urls.
if (this.#url.startsWith('data:')) return; if (this.#url.startsWith('data:')) {
return;
}
assert(this.#allowInterception, 'Request Interception is not enabled!'); assert(this.#allowInterception, 'Request Interception is not enabled!');
assert(!this.#interceptionHandled, 'Request is already handled!'); assert(!this.#interceptionHandled, 'Request is already handled!');
if (priority === undefined) { if (priority === undefined) {
@ -473,10 +480,11 @@ export class HTTPRequest {
? Buffer.from(postData).toString('base64') ? Buffer.from(postData).toString('base64')
: undefined; : undefined;
if (this._interceptionId === undefined) if (this._interceptionId === undefined) {
throw new Error( throw new Error(
'HTTPRequest is missing _interceptionId needed for Fetch.continueRequest' 'HTTPRequest is missing _interceptionId needed for Fetch.continueRequest'
); );
}
await this.#client await this.#client
.send('Fetch.continueRequest', { .send('Fetch.continueRequest', {
requestId: this._interceptionId, requestId: this._interceptionId,
@ -527,7 +535,9 @@ export class HTTPRequest {
priority?: number priority?: number
): Promise<void> { ): Promise<void> {
// Mocking responses for dataURL requests is not currently supported. // Mocking responses for dataURL requests is not currently supported.
if (this.#url.startsWith('data:')) return; if (this.#url.startsWith('data:')) {
return;
}
assert(this.#allowInterception, 'Request Interception is not enabled!'); assert(this.#allowInterception, 'Request Interception is not enabled!');
assert(!this.#interceptionHandled, 'Request is already handled!'); assert(!this.#interceptionHandled, 'Request is already handled!');
if (priority === undefined) { if (priority === undefined) {
@ -570,18 +580,21 @@ export class HTTPRequest {
: String(value); : String(value);
} }
} }
if (response.contentType) if (response.contentType) {
responseHeaders['content-type'] = response.contentType; responseHeaders['content-type'] = response.contentType;
if (responseBody && !('content-length' in responseHeaders)) }
if (responseBody && !('content-length' in responseHeaders)) {
responseHeaders['content-length'] = String( responseHeaders['content-length'] = String(
Buffer.byteLength(responseBody) Buffer.byteLength(responseBody)
); );
}
const status = response.status || 200; const status = response.status || 200;
if (this._interceptionId === undefined) if (this._interceptionId === undefined) {
throw new Error( throw new Error(
'HTTPRequest is missing _interceptionId needed for Fetch.fulfillRequest' 'HTTPRequest is missing _interceptionId needed for Fetch.fulfillRequest'
); );
}
await this.#client await this.#client
.send('Fetch.fulfillRequest', { .send('Fetch.fulfillRequest', {
requestId: this._interceptionId, requestId: this._interceptionId,
@ -614,7 +627,9 @@ export class HTTPRequest {
priority?: number priority?: number
): Promise<void> { ): Promise<void> {
// Request interception is not supported for data: urls. // Request interception is not supported for data: urls.
if (this.#url.startsWith('data:')) return; if (this.#url.startsWith('data:')) {
return;
}
const errorReason = errorReasons[errorCode]; const errorReason = errorReasons[errorCode];
assert(errorReason, 'Unknown error code: ' + errorCode); assert(errorReason, 'Unknown error code: ' + errorCode);
assert(this.#allowInterception, 'Request Interception is not enabled!'); assert(this.#allowInterception, 'Request Interception is not enabled!');
@ -639,10 +654,11 @@ export class HTTPRequest {
errorReason: Protocol.Network.ErrorReason | null errorReason: Protocol.Network.ErrorReason | null
): Promise<void> { ): Promise<void> {
this.#interceptionHandled = true; this.#interceptionHandled = true;
if (this._interceptionId === undefined) if (this._interceptionId === undefined) {
throw new Error( throw new Error(
'HTTPRequest is missing _interceptionId needed for Fetch.failRequest' 'HTTPRequest is missing _interceptionId needed for Fetch.failRequest'
); );
}
await this.#client await this.#client
.send('Fetch.failRequest', { .send('Fetch.failRequest', {
requestId: this._interceptionId, requestId: this._interceptionId,

View File

@ -101,13 +101,21 @@ export class HTTPResponse {
#parseStatusTextFromExtrInfo( #parseStatusTextFromExtrInfo(
extraInfo: Protocol.Network.ResponseReceivedExtraInfoEvent | null extraInfo: Protocol.Network.ResponseReceivedExtraInfoEvent | null
): string | undefined { ): string | undefined {
if (!extraInfo || !extraInfo.headersText) return; if (!extraInfo || !extraInfo.headersText) {
return;
}
const firstLine = extraInfo.headersText.split('\r', 1)[0]; const firstLine = extraInfo.headersText.split('\r', 1)[0];
if (!firstLine) return; if (!firstLine) {
return;
}
const match = firstLine.match(/[^ ]* [^ ]* (.*)/); const match = firstLine.match(/[^ ]* [^ ]* (.*)/);
if (!match) return; if (!match) {
return;
}
const statusText = match[1]; const statusText = match[1];
if (!statusText) return; if (!statusText) {
return;
}
return statusText; return statusText;
} }
@ -188,7 +196,9 @@ export class HTTPResponse {
buffer(): Promise<Buffer> { buffer(): Promise<Buffer> {
if (!this.#contentPromise) { if (!this.#contentPromise) {
this.#contentPromise = this.#bodyLoadedPromise.then(async (error) => { this.#contentPromise = this.#bodyLoadedPromise.then(async (error) => {
if (error) throw error; if (error) {
throw error;
}
try { try {
const response = await this.#client.send('Network.getResponseBody', { const response = await this.#client.send('Network.getResponseBody', {
requestId: this.#request._requestId, requestId: this.#request._requestId,

View File

@ -134,10 +134,18 @@ export class Keyboard {
} }
#modifierBit(key: string): number { #modifierBit(key: string): number {
if (key === 'Alt') return 1; if (key === 'Alt') {
if (key === 'Control') return 2; return 1;
if (key === 'Meta') return 4; }
if (key === 'Shift') return 8; if (key === 'Control') {
return 2;
}
if (key === 'Meta') {
return 4;
}
if (key === 'Shift') {
return 8;
}
return 0; return 0;
} }
@ -154,24 +162,43 @@ export class Keyboard {
const definition = _keyDefinitions[keyString]; const definition = _keyDefinitions[keyString];
assert(definition, `Unknown key: "${keyString}"`); assert(definition, `Unknown key: "${keyString}"`);
if (definition.key) description.key = definition.key; if (definition.key) {
if (shift && definition.shiftKey) description.key = definition.shiftKey; description.key = definition.key;
}
if (shift && definition.shiftKey) {
description.key = definition.shiftKey;
}
if (definition.keyCode) description.keyCode = definition.keyCode; if (definition.keyCode) {
if (shift && definition.shiftKeyCode) description.keyCode = definition.keyCode;
}
if (shift && definition.shiftKeyCode) {
description.keyCode = definition.shiftKeyCode; description.keyCode = definition.shiftKeyCode;
}
if (definition.code) description.code = definition.code; if (definition.code) {
description.code = definition.code;
}
if (definition.location) description.location = definition.location; if (definition.location) {
description.location = definition.location;
}
if (description.key.length === 1) description.text = description.key; if (description.key.length === 1) {
description.text = description.key;
}
if (definition.text) description.text = definition.text; if (definition.text) {
if (shift && definition.shiftText) description.text = definition.shiftText; description.text = definition.text;
}
if (shift && definition.shiftText) {
description.text = definition.shiftText;
}
// if any modifiers besides shift are pressed, no text should be sent // if any modifiers besides shift are pressed, no text should be sent
if (this._modifiers & ~8) description.text = ''; if (this._modifiers & ~8) {
description.text = '';
}
return description; return description;
} }
@ -249,7 +276,9 @@ export class Keyboard {
if (this.charIsKey(char)) { if (this.charIsKey(char)) {
await this.press(char, { delay }); await this.press(char, { delay });
} else { } else {
if (delay) await new Promise((f) => setTimeout(f, delay)); if (delay) {
await new Promise((f) => setTimeout(f, delay));
}
await this.sendCharacter(char); await this.sendCharacter(char);
} }
} }
@ -281,7 +310,9 @@ export class Keyboard {
): Promise<void> { ): Promise<void> {
const { delay = null } = options; const { delay = null } = options;
await this.down(key, options); await this.down(key, options);
if (delay) await new Promise((f) => setTimeout(f, options.delay)); if (delay) {
await new Promise((f) => setTimeout(f, options.delay));
}
await this.up(key); await this.up(key);
} }
} }

View File

@ -250,7 +250,9 @@ export class JSHandle<HandleObjectType = unknown> {
}); });
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 || !property.value) 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;
@ -294,7 +296,9 @@ export class JSHandle<HandleObjectType = unknown> {
* successfully disposed of. * successfully disposed of.
*/ */
async dispose(): Promise<void> { async dispose(): Promise<void> {
if (this.#disposed) return; if (this.#disposed) {
return;
}
this.#disposed = true; this.#disposed = true;
await releaseObject(this.#client, this.#remoteObject); await releaseObject(this.#client, this.#remoteObject);
} }
@ -418,7 +422,9 @@ export class ElementHandle<
root: adoptedRoot, root: adoptedRoot,
}); });
await adoptedRoot.dispose(); await adoptedRoot.dispose();
if (!handle) return null; if (!handle) {
return null;
}
const mainExecutionContext = await frame._mainWorld.executionContext(); const mainExecutionContext = await frame._mainWorld.executionContext();
const result = await mainExecutionContext._adoptElementHandle(handle); const result = await mainExecutionContext._adoptElementHandle(handle);
await handle.dispose(); await handle.dispose();
@ -497,7 +503,9 @@ export class ElementHandle<
root: adoptedRoot, root: adoptedRoot,
}); });
await adoptedRoot.dispose(); await adoptedRoot.dispose();
if (!handle) return null; if (!handle) {
return null;
}
const mainExecutionContext = await frame._mainWorld.executionContext(); const mainExecutionContext = await frame._mainWorld.executionContext();
const result = await mainExecutionContext._adoptElementHandle(handle); const result = await mainExecutionContext._adoptElementHandle(handle);
await handle.dispose(); await handle.dispose();
@ -516,7 +524,9 @@ export class ElementHandle<
const nodeInfo = await this._client.send('DOM.describeNode', { const nodeInfo = await this._client.send('DOM.describeNode', {
objectId: this._remoteObject.objectId, objectId: this._remoteObject.objectId,
}); });
if (typeof nodeInfo.node.frameId !== 'string') return null; if (typeof nodeInfo.node.frameId !== 'string') {
return null;
}
return this.#frameManager.frame(nodeInfo.node.frameId); return this.#frameManager.frame(nodeInfo.node.frameId);
} }
@ -526,9 +536,12 @@ export class ElementHandle<
element: Element, element: Element,
pageJavascriptEnabled: boolean pageJavascriptEnabled: boolean
): Promise<string | false> => { ): Promise<string | false> => {
if (!element.isConnected) return 'Node is detached from document'; if (!element.isConnected) {
if (element.nodeType !== Node.ELEMENT_NODE) return 'Node is detached from document';
}
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({
@ -563,7 +576,9 @@ export class ElementHandle<
this.#page.isJavaScriptEnabled() this.#page.isJavaScriptEnabled()
); );
if (error) throw new Error(error); if (error) {
throw new Error(error);
}
} }
async #getOOPIFOffsets( async #getOOPIFOffsets(
@ -610,8 +625,9 @@ export class ElementHandle<
.catch(debugError), .catch(debugError),
this.#page._client().send('Page.getLayoutMetrics'), this.#page._client().send('Page.getLayoutMetrics'),
]); ]);
if (!result || !result.quads.length) if (!result || !result.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');
}
// Filter out quads that have too small area to click into. // Filter out quads that have too small area to click into.
// Fallback to `layoutViewport` in case of using Firefox. // Fallback to `layoutViewport` in case of using Firefox.
const { clientWidth, clientHeight } = const { clientWidth, clientHeight } =
@ -624,8 +640,9 @@ export class ElementHandle<
this.#intersectQuadWithViewport(quad, clientWidth, clientHeight) this.#intersectQuadWithViewport(quad, clientWidth, clientHeight)
) )
.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.
@ -971,7 +988,9 @@ export class ElementHandle<
async boundingBox(): Promise<BoundingBox | null> { async boundingBox(): Promise<BoundingBox | null> {
const result = await this.#getBoxModel(); const result = await this.#getBoxModel();
if (!result) return null; if (!result) {
return null;
}
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;
@ -994,7 +1013,9 @@ export class ElementHandle<
async boxModel(): Promise<BoxModel | null> { async boxModel(): Promise<BoxModel | null> {
const result = await this.#getBoxModel(); const result = await this.#getBoxModel();
if (!result) return null; if (!result) {
return null;
}
const { offsetX, offsetY } = await this.#getOOPIFOffsets(this.#frame); const { offsetX, offsetY } = await this.#getOOPIFOffsets(this.#frame);
@ -1078,7 +1099,9 @@ export class ElementHandle<
) )
); );
if (needsViewportReset) await this.#page.setViewport(viewport); if (needsViewportReset) {
await this.#page.setViewport(viewport);
}
return imageData; return imageData;
} }
@ -1149,10 +1172,11 @@ export class ElementHandle<
...args: SerializableOrJSHandle[] ...args: SerializableOrJSHandle[]
): Promise<WrapElementHandle<ReturnType>> { ): Promise<WrapElementHandle<ReturnType>> {
const elementHandle = await this.$(selector); const elementHandle = await this.$(selector);
if (!elementHandle) if (!elementHandle) {
throw new Error( throw new Error(
`Error: failed to find element matching selector "${selector}"` `Error: failed to find element matching selector "${selector}"`
); );
}
const result = await elementHandle.evaluate< const result = await elementHandle.evaluate<
( (
element: Element, element: Element,
@ -1236,7 +1260,9 @@ export class ElementHandle<
); );
const array = []; const array = [];
let item; let item;
while ((item = iterator.iterateNext())) array.push(item); while ((item = iterator.iterateNext())) {
array.push(item);
}
return array; return array;
}, },
expression expression
@ -1246,7 +1272,9 @@ export class ElementHandle<
const result = []; const result = [];
for (const property of properties.values()) { for (const property of properties.values()) {
const elementHandle = property.asElement(); const elementHandle = property.asElement();
if (elementHandle) result.push(elementHandle); if (elementHandle) {
result.push(elementHandle);
}
} }
return result; return result;
} }

View File

@ -106,8 +106,11 @@ export class LifecycleWatcher {
waitUntil: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[], waitUntil: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[],
timeout: number timeout: number
) { ) {
if (Array.isArray(waitUntil)) waitUntil = waitUntil.slice(); if (Array.isArray(waitUntil)) {
else if (typeof waitUntil === 'string') waitUntil = [waitUntil]; waitUntil = waitUntil.slice();
} else if (typeof waitUntil === 'string') {
waitUntil = [waitUntil];
}
this.#expectedLifecycle = waitUntil.map((value) => { this.#expectedLifecycle = waitUntil.map((value) => {
const protocolEvent = puppeteerToProtocolLifecycle.get(value); const protocolEvent = puppeteerToProtocolLifecycle.get(value);
assert(protocolEvent, 'Unknown value for options.waitUntil: ' + value); assert(protocolEvent, 'Unknown value for options.waitUntil: ' + value);
@ -163,8 +166,9 @@ export class LifecycleWatcher {
} }
#onRequest(request: HTTPRequest): void { #onRequest(request: HTTPRequest): void {
if (request.frame() !== this.#frame || !request.isNavigationRequest()) if (request.frame() !== this.#frame || !request.isNavigationRequest()) {
return; return;
}
this.#navigationRequest = request; this.#navigationRequest = request;
} }
@ -205,7 +209,9 @@ export class LifecycleWatcher {
} }
async #createTimeoutPromise(): Promise<TimeoutError | undefined> { async #createTimeoutPromise(): Promise<TimeoutError | undefined> {
if (!this.#timeout) return new Promise(noop); if (!this.#timeout) {
return new Promise(noop);
}
const errorMessage = const errorMessage =
'Navigation timeout of ' + this.#timeout + ' ms exceeded'; 'Navigation timeout of ' + this.#timeout + ' ms exceeded';
await new Promise( await new Promise(
@ -215,38 +221,50 @@ export class LifecycleWatcher {
} }
#navigatedWithinDocument(frame: Frame): void { #navigatedWithinDocument(frame: Frame): void {
if (frame !== this.#frame) return; if (frame !== this.#frame) {
return;
}
this.#hasSameDocumentNavigation = true; this.#hasSameDocumentNavigation = true;
this.#checkLifecycleComplete(); this.#checkLifecycleComplete();
} }
#navigated(frame: Frame): void { #navigated(frame: Frame): void {
if (frame !== this.#frame) return; if (frame !== this.#frame) {
return;
}
this.#newDocumentNavigation = true; this.#newDocumentNavigation = true;
this.#checkLifecycleComplete(); this.#checkLifecycleComplete();
} }
#frameSwapped(frame: Frame): void { #frameSwapped(frame: Frame): void {
if (frame !== this.#frame) return; if (frame !== this.#frame) {
return;
}
this.#swapped = true; this.#swapped = true;
this.#checkLifecycleComplete(); this.#checkLifecycleComplete();
} }
#checkLifecycleComplete(): void { #checkLifecycleComplete(): void {
// We expect navigation to commit. // We expect navigation to commit.
if (!checkLifecycle(this.#frame, this.#expectedLifecycle)) return; if (!checkLifecycle(this.#frame, this.#expectedLifecycle)) {
return;
}
this.#lifecycleCallback(); this.#lifecycleCallback();
if (this.#hasSameDocumentNavigation) if (this.#hasSameDocumentNavigation) {
this.#sameDocumentNavigationCompleteCallback(); this.#sameDocumentNavigationCompleteCallback();
if (this.#swapped || this.#newDocumentNavigation) }
if (this.#swapped || this.#newDocumentNavigation) {
this.#newDocumentNavigationCompleteCallback(); this.#newDocumentNavigationCompleteCallback();
}
function checkLifecycle( function checkLifecycle(
frame: Frame, frame: Frame,
expectedLifecycle: ProtocolLifeCycleEvent[] expectedLifecycle: ProtocolLifeCycleEvent[]
): boolean { ): boolean {
for (const event of expectedLifecycle) { for (const event of expectedLifecycle) {
if (!frame._lifecycleEvents.has(event)) return false; if (!frame._lifecycleEvents.has(event)) {
return false;
}
} }
for (const child of frame.childFrames()) { for (const child of frame.childFrames()) {
if ( if (

View File

@ -133,11 +133,12 @@ export class NetworkManager extends EventEmitter {
async initialize(): Promise<void> { async initialize(): Promise<void> {
await this.#client.send('Network.enable'); await this.#client.send('Network.enable');
if (this.#ignoreHTTPSErrors) if (this.#ignoreHTTPSErrors) {
await this.#client.send('Security.setIgnoreCertificateErrors', { await this.#client.send('Security.setIgnoreCertificateErrors', {
ignore: true, ignore: true,
}); });
} }
}
async authenticate(credentials?: Credentials): Promise<void> { async authenticate(credentials?: Credentials): Promise<void> {
this.#credentials = credentials; this.#credentials = credentials;
@ -221,7 +222,9 @@ export class NetworkManager extends EventEmitter {
async #updateProtocolRequestInterception(): Promise<void> { async #updateProtocolRequestInterception(): Promise<void> {
const enabled = this.#userRequestInterceptionEnabled || !!this.#credentials; const enabled = this.#userRequestInterceptionEnabled || !!this.#credentials;
if (enabled === this.#protocolRequestInterceptionEnabled) return; if (enabled === this.#protocolRequestInterceptionEnabled) {
return;
}
this.#protocolRequestInterceptionEnabled = enabled; this.#protocolRequestInterceptionEnabled = enabled;
if (enabled) { if (enabled) {
await Promise.all([ await Promise.all([
@ -420,7 +423,9 @@ export class NetworkManager extends EventEmitter {
event: Protocol.Network.RequestServedFromCacheEvent event: Protocol.Network.RequestServedFromCacheEvent
): void { ): void {
const request = this.#networkEventManager.getRequest(event.requestId); const request = this.#networkEventManager.getRequest(event.requestId);
if (request) request._fromMemoryCache = true; if (request) {
request._fromMemoryCache = true;
}
this.emit(NetworkManagerEmittedEvents.RequestServedFromCache, request); this.emit(NetworkManagerEmittedEvents.RequestServedFromCache, request);
} }
@ -453,7 +458,9 @@ export class NetworkManager extends EventEmitter {
responseReceived.requestId responseReceived.requestId
); );
// FileUpload sends a response without a matching request. // FileUpload sends a response without a matching request.
if (!request) return; if (!request) {
return;
}
const extraInfos = this.#networkEventManager.responseExtraInfo( const extraInfos = this.#networkEventManager.responseExtraInfo(
responseReceived.requestId responseReceived.requestId
@ -561,11 +568,15 @@ export class NetworkManager extends EventEmitter {
const request = this.#networkEventManager.getRequest(event.requestId); const request = this.#networkEventManager.getRequest(event.requestId);
// For certain requestIds we never receive requestWillBeSent event. // For certain requestIds we never receive requestWillBeSent event.
// @see https://crbug.com/750469 // @see https://crbug.com/750469
if (!request) return; if (!request) {
return;
}
// Under certain conditions we never get the Network.responseReceived // Under certain conditions we never get the Network.responseReceived
// event from protocol. @see https://crbug.com/883475 // event from protocol. @see https://crbug.com/883475
if (request.response()) request.response()?._resolveBody(null); if (request.response()) {
request.response()?._resolveBody(null);
}
this.#forgetRequest(request, true); this.#forgetRequest(request, true);
this.emit(NetworkManagerEmittedEvents.RequestFinished, request); this.emit(NetworkManagerEmittedEvents.RequestFinished, request);
} }
@ -587,10 +598,14 @@ export class NetworkManager extends EventEmitter {
const request = this.#networkEventManager.getRequest(event.requestId); const request = this.#networkEventManager.getRequest(event.requestId);
// For certain requestIds we never receive requestWillBeSent event. // For certain requestIds we never receive requestWillBeSent event.
// @see https://crbug.com/750469 // @see https://crbug.com/750469
if (!request) return; if (!request) {
return;
}
request._failureText = event.errorText; request._failureText = event.errorText;
const response = request.response(); const response = request.response();
if (response) response._resolveBody(null); if (response) {
response._resolveBody(null);
}
this.#forgetRequest(request, true); this.#forgetRequest(request, true);
this.emit(NetworkManagerEmittedEvents.RequestFailed, request); this.emit(NetworkManagerEmittedEvents.RequestFailed, request);
} }

View File

@ -451,7 +451,9 @@ export class Page extends EventEmitter {
screenshotTaskQueue screenshotTaskQueue
); );
await page.#initialize(); await page.#initialize();
if (defaultViewport) await page.setViewport(defaultViewport); if (defaultViewport) {
await page.setViewport(defaultViewport);
}
return page; return page;
} }
@ -545,7 +547,9 @@ export class Page extends EventEmitter {
); );
client.on('Target.detachedFromTarget', (event) => { client.on('Target.detachedFromTarget', (event) => {
const worker = this.#workers.get(event.sessionId); const worker = this.#workers.get(event.sessionId);
if (!worker) return; if (!worker) {
return;
}
this.#workers.delete(event.sessionId); this.#workers.delete(event.sessionId);
this.emit(PageEmittedEvents.WorkerDestroyed, worker); this.emit(PageEmittedEvents.WorkerDestroyed, worker);
}); });
@ -615,7 +619,9 @@ export class Page extends EventEmitter {
async #onFileChooser( async #onFileChooser(
event: Protocol.Page.FileChooserOpenedEvent event: Protocol.Page.FileChooserOpenedEvent
): 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); assert(frame);
const context = await frame.executionContext(); const context = await frame.executionContext();
@ -623,7 +629,9 @@ export class Page extends EventEmitter {
const interceptors = Array.from(this.#fileChooserInterceptors); const interceptors = Array.from(this.#fileChooserInterceptors);
this.#fileChooserInterceptors.clear(); this.#fileChooserInterceptors.clear();
const fileChooser = new FileChooser(element, event); const fileChooser = new FileChooser(element, event);
for (const interceptor of interceptors) interceptor.call(null, fileChooser); for (const interceptor of interceptors) {
interceptor.call(null, fileChooser);
}
} }
/** /**
@ -711,10 +719,11 @@ export class Page extends EventEmitter {
async waitForFileChooser( async waitForFileChooser(
options: WaitTimeoutOptions = {} options: WaitTimeoutOptions = {}
): Promise<FileChooser> { ): Promise<FileChooser> {
if (!this.#fileChooserInterceptors.size) if (!this.#fileChooserInterceptors.size) {
await this.#client.send('Page.setInterceptFileChooserDialog', { await this.#client.send('Page.setInterceptFileChooserDialog', {
enabled: true, enabled: true,
}); });
}
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;
@ -742,18 +751,21 @@ export class Page extends EventEmitter {
*/ */
async setGeolocation(options: GeolocationOptions): Promise<void> { async setGeolocation(options: GeolocationOptions): Promise<void> {
const { longitude, latitude, accuracy = 0 } = options; const { longitude, latitude, accuracy = 0 } = options;
if (longitude < -180 || longitude > 180) if (longitude < -180 || longitude > 180) {
throw new Error( throw new Error(
`Invalid longitude "${longitude}": precondition -180 <= LONGITUDE <= 180 failed.` `Invalid longitude "${longitude}": precondition -180 <= LONGITUDE <= 180 failed.`
); );
if (latitude < -90 || latitude > 90) }
if (latitude < -90 || latitude > 90) {
throw new Error( throw new Error(
`Invalid latitude "${latitude}": precondition -90 <= LATITUDE <= 90 failed.` `Invalid latitude "${latitude}": precondition -90 <= LATITUDE <= 90 failed.`
); );
if (accuracy < 0) }
if (accuracy < 0) {
throw new Error( throw new Error(
`Invalid accuracy "${accuracy}": precondition 0 <= ACCURACY failed.` `Invalid accuracy "${accuracy}": precondition 0 <= ACCURACY failed.`
); );
}
await this.#client.send('Emulation.setGeolocationOverride', { await this.#client.send('Emulation.setGeolocationOverride', {
longitude, longitude,
latitude, latitude,
@ -795,13 +807,16 @@ export class Page extends EventEmitter {
#onLogEntryAdded(event: Protocol.Log.EntryAddedEvent): void { #onLogEntryAdded(event: Protocol.Log.EntryAddedEvent): void {
const { level, text, args, source, url, lineNumber } = event.entry; const { level, text, args, source, url, lineNumber } = event.entry;
if (args) args.map((arg) => releaseObject(this.#client, arg)); if (args) {
if (source !== 'worker') args.map((arg) => releaseObject(this.#client, arg));
}
if (source !== 'worker') {
this.emit( this.emit(
PageEmittedEvents.Console, PageEmittedEvents.Console,
new ConsoleMessage(level, text, [], [{ url, lineNumber }]) new ConsoleMessage(level, text, [], [{ url, lineNumber }])
); );
} }
}
/** /**
* @returns The page's main frame. * @returns The page's main frame.
@ -1284,7 +1299,9 @@ export class Page extends EventEmitter {
const pageURL = this.url(); const pageURL = this.url();
for (const cookie of cookies) { for (const cookie of cookies) {
const item = Object.assign({}, cookie); const item = Object.assign({}, cookie);
if (!cookie.url && pageURL.startsWith('http')) item.url = pageURL; if (!cookie.url && pageURL.startsWith('http')) {
item.url = pageURL;
}
await this.#client.send('Network.deleteCookies', item); await this.#client.send('Network.deleteCookies', item);
} }
} }
@ -1300,7 +1317,9 @@ export class Page extends EventEmitter {
const startsWithHTTP = pageURL.startsWith('http'); const startsWithHTTP = pageURL.startsWith('http');
const items = cookies.map((cookie) => { const items = cookies.map((cookie) => {
const item = Object.assign({}, cookie); const item = Object.assign({}, cookie);
if (!item.url && startsWithHTTP) item.url = pageURL; if (!item.url && startsWithHTTP) {
item.url = pageURL;
}
assert( assert(
item.url !== 'about:blank', item.url !== 'about:blank',
`Blank page can not have cookie "${item.name}"` `Blank page can not have cookie "${item.name}"`
@ -1312,9 +1331,10 @@ export class Page extends EventEmitter {
return item; return item;
}); });
await this.deleteCookie(...items); await this.deleteCookie(...items);
if (items.length) if (items.length) {
await this.#client.send('Network.setCookies', { cookies: items }); await this.#client.send('Network.setCookies', { cookies: items });
} }
}
/** /**
* Adds a `<script>` tag into the page with the desired URL or content. * Adds a `<script>` tag into the page with the desired URL or content.
@ -1410,10 +1430,11 @@ export class Page extends EventEmitter {
name: string, name: string,
puppeteerFunction: Function | { default: Function } puppeteerFunction: Function | { default: Function }
): Promise<void> { ): Promise<void> {
if (this.#pageBindings.has(name)) if (this.#pageBindings.has(name)) {
throw new Error( throw new Error(
`Failed to add page binding with name ${name}: window['${name}'] already exists!` `Failed to add page binding with name ${name}: window['${name}'] already exists!`
); );
}
let exposedFunction: Function; let exposedFunction: Function;
if (typeof puppeteerFunction === 'function') { if (typeof puppeteerFunction === 'function') {
@ -1580,7 +1601,9 @@ export class Page extends EventEmitter {
return; return;
} }
const { type, name, seq, args } = payload; const { type, name, seq, args } = payload;
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 pageBinding = this.#pageBindings.get(name); const pageBinding = this.#pageBindings.get(name);
@ -1588,14 +1611,16 @@ export class Page extends EventEmitter {
const result = await pageBinding(...args); const result = await pageBinding(...args);
expression = pageBindingDeliverResultString(name, seq, result); expression = pageBindingDeliverResultString(name, seq, result);
} catch (error) { } catch (error) {
if (isErrorLike(error)) if (isErrorLike(error)) {
expression = pageBindingDeliverErrorString( expression = pageBindingDeliverErrorString(
name, name,
seq, seq,
error.message, error.message,
error.stack error.stack
); );
else expression = pageBindingDeliverErrorValueString(name, seq, error); } else {
expression = pageBindingDeliverErrorValueString(name, seq, error);
}
} }
this.#client this.#client
.send('Runtime.evaluate', { .send('Runtime.evaluate', {
@ -1617,8 +1642,11 @@ export class Page extends EventEmitter {
const textTokens = []; const textTokens = [];
for (const arg of args) { for (const arg of args) {
const remoteObject = arg._remoteObject; const remoteObject = arg._remoteObject;
if (remoteObject.objectId) textTokens.push(arg.toString()); if (remoteObject.objectId) {
else textTokens.push(valueFromRemoteObject(remoteObject)); textTokens.push(arg.toString());
} else {
textTokens.push(valueFromRemoteObject(remoteObject));
}
} }
const stackTraceLocations = []; const stackTraceLocations = [];
if (stackTrace) { if (stackTrace) {
@ -1853,12 +1881,13 @@ export class Page extends EventEmitter {
} }
#sessionClosePromise(): Promise<Error> { #sessionClosePromise(): Promise<Error> {
if (!this.#disconnectPromise) if (!this.#disconnectPromise) {
this.#disconnectPromise = new Promise((fulfill) => this.#disconnectPromise = new Promise((fulfill) =>
this.#client.once(CDPSessionEmittedEvents.Disconnected, () => this.#client.once(CDPSessionEmittedEvents.Disconnected, () =>
fulfill(new Error('Target closed')) fulfill(new Error('Target closed'))
) )
); );
}
return this.#disconnectPromise; return this.#disconnectPromise;
} }
@ -1896,9 +1925,12 @@ export class Page extends EventEmitter {
this.#frameManager.networkManager(), this.#frameManager.networkManager(),
NetworkManagerEmittedEvents.Request, NetworkManagerEmittedEvents.Request,
(request) => { (request) => {
if (isString(urlOrPredicate)) return urlOrPredicate === request.url(); if (isString(urlOrPredicate)) {
if (typeof urlOrPredicate === 'function') return urlOrPredicate === request.url();
}
if (typeof urlOrPredicate === 'function') {
return !!urlOrPredicate(request); return !!urlOrPredicate(request);
}
return false; return false;
}, },
timeout, timeout,
@ -1942,9 +1974,12 @@ export class Page extends EventEmitter {
this.#frameManager.networkManager(), this.#frameManager.networkManager(),
NetworkManagerEmittedEvents.Response, NetworkManagerEmittedEvents.Response,
async (response) => { async (response) => {
if (isString(urlOrPredicate)) return urlOrPredicate === response.url(); if (isString(urlOrPredicate)) {
if (typeof urlOrPredicate === 'function') return urlOrPredicate === response.url();
}
if (typeof urlOrPredicate === 'function') {
return !!(await urlOrPredicate(response)); return !!(await urlOrPredicate(response));
}
return false; return false;
}, },
timeout, timeout,
@ -1984,8 +2019,9 @@ export class Page extends EventEmitter {
const evaluate = () => { const evaluate = () => {
idleTimer && clearTimeout(idleTimer); idleTimer && clearTimeout(idleTimer);
if (networkManager.numRequestsInProgress() === 0) if (networkManager.numRequestsInProgress() === 0) {
idleTimer = setTimeout(onIdle, idleTime); idleTimer = setTimeout(onIdle, idleTime);
}
}; };
evaluate(); evaluate();
@ -2152,7 +2188,9 @@ export class Page extends EventEmitter {
): Promise<HTTPResponse | null> { ): Promise<HTTPResponse | null> {
const history = await this.#client.send('Page.getNavigationHistory'); const history = await this.#client.send('Page.getNavigationHistory');
const entry = history.entries[history.currentIndex + delta]; const entry = history.entries[history.currentIndex + delta];
if (!entry) return null; if (!entry) {
return null;
}
const result = await Promise.all([ const result = await Promise.all([
this.waitForNavigation(options), this.waitForNavigation(options),
this.#client.send('Page.navigateToHistoryEntry', { entryId: entry.id }), this.#client.send('Page.navigateToHistoryEntry', { entryId: entry.id }),
@ -2208,7 +2246,9 @@ export class Page extends EventEmitter {
* It will take full effect on the next navigation. * It will take full effect on the next navigation.
*/ */
async setJavaScriptEnabled(enabled: boolean): Promise<void> { async setJavaScriptEnabled(enabled: boolean): Promise<void> {
if (this.#javascriptEnabled === enabled) return; if (this.#javascriptEnabled === enabled) {
return;
}
this.#javascriptEnabled = enabled; this.#javascriptEnabled = enabled;
await this.#client.send('Emulation.setScriptExecutionDisabled', { await this.#client.send('Emulation.setScriptExecutionDisabled', {
value: !enabled, value: !enabled,
@ -2328,7 +2368,9 @@ export class Page extends EventEmitter {
* ``` * ```
*/ */
async emulateMediaFeatures(features?: MediaFeature[]): Promise<void> { async emulateMediaFeatures(features?: MediaFeature[]): Promise<void> {
if (!features) await this.#client.send('Emulation.setEmulatedMedia', {}); if (!features) {
await this.#client.send('Emulation.setEmulatedMedia', {});
}
if (Array.isArray(features)) { if (Array.isArray(features)) {
for (const mediaFeature of features) { for (const mediaFeature of features) {
const name = mediaFeature.name; const name = mediaFeature.name;
@ -2491,7 +2533,9 @@ export class Page extends EventEmitter {
async setViewport(viewport: Viewport): Promise<void> { async setViewport(viewport: Viewport): Promise<void> {
const needsReload = await this.#emulationManager.emulateViewport(viewport); const needsReload = await this.#emulationManager.emulateViewport(viewport);
this.#viewport = viewport; this.#viewport = viewport;
if (needsReload) await this.reload(); if (needsReload) {
await this.reload();
}
} }
/** /**
@ -2813,8 +2857,9 @@ export class Page extends EventEmitter {
await this.#resetDefaultBackgroundColor(); await this.#resetDefaultBackgroundColor();
} }
if (options.fullPage && this.#viewport) if (options.fullPage && this.#viewport) {
await this.setViewport(this.#viewport); await this.setViewport(this.#viewport);
}
const buffer = const buffer =
options.encoding === 'base64' options.encoding === 'base64'
@ -3414,7 +3459,9 @@ const unitToPixels = {
function convertPrintParameterToInches( function convertPrintParameterToInches(
parameter?: string | number parameter?: string | number
): number | undefined { ): number | undefined {
if (typeof parameter === 'undefined') return undefined; if (typeof parameter === 'undefined') {
return undefined;
}
let pixels; let pixels;
if (isNumber(parameter)) { if (isNumber(parameter)) {
// Treat numbers as pixel values to be aligned with phantom's paperSize. // Treat numbers as pixel values to be aligned with phantom's paperSize.

View File

@ -68,7 +68,9 @@ function makeQueryHandler(handler: CustomQueryHandler): InternalQueryHandler {
internalHandler.queryOne = async (element, selector) => { internalHandler.queryOne = async (element, selector) => {
const jsHandle = await element.evaluateHandle(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();
return null; return null;
}; };
@ -88,7 +90,9 @@ function makeQueryHandler(handler: CustomQueryHandler): InternalQueryHandler {
const result = []; const result = [];
for (const property of properties.values()) { for (const property of properties.values()) {
const elementHandle = property.asElement(); const elementHandle = property.asElement();
if (elementHandle) result.push(elementHandle); if (elementHandle) {
result.push(elementHandle);
}
} }
return result; return result;
}; };
@ -174,12 +178,14 @@ export function _registerCustomQueryHandler(
name: string, name: string,
handler: CustomQueryHandler handler: CustomQueryHandler
): void { ): void {
if (queryHandlers.get(name)) if (queryHandlers.get(name)) {
throw new Error(`A custom query handler named "${name}" already exists`); throw new Error(`A custom query handler named "${name}" already exists`);
}
const isValidName = /^[a-zA-Z]+$/.test(name); const isValidName = /^[a-zA-Z]+$/.test(name);
if (!isValidName) if (!isValidName) {
throw new Error(`Custom query handler names may only contain [a-zA-Z]`); throw new Error(`Custom query handler names may only contain [a-zA-Z]`);
}
const internalHandler = makeQueryHandler(handler); const internalHandler = makeQueryHandler(handler);
@ -217,17 +223,19 @@ export function _getQueryHandlerAndSelector(selector: string): {
queryHandler: InternalQueryHandler; queryHandler: InternalQueryHandler;
} { } {
const hasCustomQueryHandler = /^[a-zA-Z]+\//.test(selector); const hasCustomQueryHandler = /^[a-zA-Z]+\//.test(selector);
if (!hasCustomQueryHandler) if (!hasCustomQueryHandler) {
return { updatedSelector: selector, queryHandler: _defaultHandler }; return { updatedSelector: selector, queryHandler: _defaultHandler };
}
const index = selector.indexOf('/'); const index = selector.indexOf('/');
const name = selector.slice(0, index); const name = selector.slice(0, index);
const updatedSelector = selector.slice(index + 1); const updatedSelector = selector.slice(index + 1);
const queryHandler = queryHandlers.get(name); const queryHandler = queryHandlers.get(name);
if (!queryHandler) if (!queryHandler) {
throw new Error( throw new Error(
`Query set to use "${name}", but no query handler of that name was found` `Query set to use "${name}", but no query handler of that name was found`
); );
}
return { return {
updatedSelector, updatedSelector,

View File

@ -87,12 +87,17 @@ export class Target {
this._initializedPromise = new Promise<boolean>( this._initializedPromise = new Promise<boolean>(
(fulfill) => (this._initializedCallback = fulfill) (fulfill) => (this._initializedCallback = fulfill)
).then(async (success) => { ).then(async (success) => {
if (!success) return false; if (!success) {
return false;
}
const opener = this.opener(); const opener = this.opener();
if (!opener || !opener.#pagePromise || this.type() !== 'page') if (!opener || !opener.#pagePromise || this.type() !== 'page') {
return true; return true;
}
const openerPage = await opener.#pagePromise; const openerPage = await opener.#pagePromise;
if (!openerPage.listenerCount(PageEmittedEvents.Popup)) return true; if (!openerPage.listenerCount(PageEmittedEvents.Popup)) {
return true;
}
const popupPage = await this.page(); const popupPage = await this.page();
openerPage.emit(PageEmittedEvents.Popup, popupPage); openerPage.emit(PageEmittedEvents.Popup, popupPage);
return true; return true;
@ -103,7 +108,9 @@ export class Target {
this._isInitialized = this._isInitialized =
!this._isPageTargetCallback(this.#targetInfo) || !this._isPageTargetCallback(this.#targetInfo) ||
this.#targetInfo.url !== ''; this.#targetInfo.url !== '';
if (this._isInitialized) this._initializedCallback(true); if (this._isInitialized) {
this._initializedCallback(true);
}
} }
/** /**
@ -145,8 +152,9 @@ export class Target {
if ( if (
this.#targetInfo.type !== 'service_worker' && this.#targetInfo.type !== 'service_worker' &&
this.#targetInfo.type !== 'shared_worker' this.#targetInfo.type !== 'shared_worker'
) ) {
return null; return null;
}
if (!this.#workerPromise) { if (!this.#workerPromise) {
// TODO(einbinder): Make workers send their console logs. // TODO(einbinder): Make workers send their console logs.
this.#workerPromise = this.#sessionFactory().then( this.#workerPromise = this.#sessionFactory().then(
@ -189,8 +197,9 @@ export class Target {
type === 'shared_worker' || type === 'shared_worker' ||
type === 'browser' || type === 'browser' ||
type === 'webview' type === 'webview'
) ) {
return type; return type;
}
return 'other'; return 'other';
} }
@ -213,7 +222,9 @@ export class Target {
*/ */
opener(): Target | undefined { opener(): Target | undefined {
const { openerId } = this.#targetInfo; const { openerId } = this.#targetInfo;
if (!openerId) return; if (!openerId) {
return;
}
return this.browser()._targets.get(openerId); return this.browser()._targets.get(openerId);
} }

View File

@ -37,14 +37,19 @@ export class TimeoutSettings {
} }
navigationTimeout(): number { navigationTimeout(): number {
if (this.#defaultNavigationTimeout !== null) if (this.#defaultNavigationTimeout !== null) {
return this.#defaultNavigationTimeout; return this.#defaultNavigationTimeout;
if (this.#defaultTimeout !== null) return this.#defaultTimeout; }
if (this.#defaultTimeout !== null) {
return this.#defaultTimeout;
}
return DEFAULT_TIMEOUT; return DEFAULT_TIMEOUT;
} }
timeout(): number { timeout(): number {
if (this.#defaultTimeout !== null) return this.#defaultTimeout; if (this.#defaultTimeout !== null) {
return this.#defaultTimeout;
}
return DEFAULT_TIMEOUT; return DEFAULT_TIMEOUT;
} }
} }

View File

@ -89,7 +89,9 @@ export class Tracing {
categories = defaultCategories, categories = defaultCategories,
} = options; } = options;
if (screenshots) categories.push('disabled-by-default-devtools.screenshot'); if (screenshots) {
categories.push('disabled-by-default-devtools.screenshot');
}
const excludedCategories = categories const excludedCategories = categories
.filter((cat) => cat.startsWith('-')) .filter((cat) => cat.startsWith('-'))

View File

@ -23,12 +23,16 @@ export const assert: (value: unknown, message?: string) => asserts value = (
value, value,
message message
) => { ) => {
if (!value) throw new Error(message); if (!value) {
throw new Error(message);
}
}; };
export const assertNever: ( export const assertNever: (
value: unknown, value: unknown,
message?: string message?: string
) => asserts value is never = (value, message) => { ) => asserts value is never = (value, message) => {
if (value) throw new Error(message); if (value) {
throw new Error(message);
}
}; };

View File

@ -28,10 +28,11 @@ export const debugError = debug('puppeteer:error');
export function getExceptionMessage( export function getExceptionMessage(
exceptionDetails: Protocol.Runtime.ExceptionDetails exceptionDetails: Protocol.Runtime.ExceptionDetails
): string { ): string {
if (exceptionDetails.exception) if (exceptionDetails.exception) {
return ( return (
exceptionDetails.exception.description || exceptionDetails.exception.value exceptionDetails.exception.description || exceptionDetails.exception.value
); );
}
let message = exceptionDetails.text; let message = exceptionDetails.text;
if (exceptionDetails.stackTrace) { if (exceptionDetails.stackTrace) {
for (const callframe of exceptionDetails.stackTrace.callFrames) { for (const callframe of exceptionDetails.stackTrace.callFrames) {
@ -53,8 +54,9 @@ export function valueFromRemoteObject(
): any { ): any {
assert(!remoteObject.objectId, 'Cannot extract value when objectId is given'); assert(!remoteObject.objectId, 'Cannot extract value when objectId is given');
if (remoteObject.unserializableValue) { if (remoteObject.unserializableValue) {
if (remoteObject.type === 'bigint' && typeof BigInt !== 'undefined') if (remoteObject.type === 'bigint' && typeof BigInt !== 'undefined') {
return BigInt(remoteObject.unserializableValue.replace('n', '')); return BigInt(remoteObject.unserializableValue.replace('n', ''));
}
switch (remoteObject.unserializableValue) { switch (remoteObject.unserializableValue) {
case '-0': case '-0':
return -0; return -0;
@ -78,7 +80,9 @@ export async function releaseObject(
client: CDPSession, client: CDPSession,
remoteObject: Protocol.Runtime.RemoteObject remoteObject: Protocol.Runtime.RemoteObject
): Promise<void> { ): Promise<void> {
if (!remoteObject.objectId) return; if (!remoteObject.objectId) {
return;
}
await client await client
.send('Runtime.releaseObject', { objectId: remoteObject.objectId }) .send('Runtime.releaseObject', { objectId: remoteObject.objectId })
.catch((error) => { .catch((error) => {
@ -110,8 +114,9 @@ export function removeEventListeners(
handler: (...args: any[]) => void; handler: (...args: any[]) => void;
}> }>
): void { ): void {
for (const listener of listeners) for (const listener of listeners) {
listener.emitter.removeListener(listener.eventName, listener.handler); listener.emitter.removeListener(listener.eventName, listener.handler);
}
listeners.length = 0; listeners.length = 0;
} }
@ -138,7 +143,9 @@ export async function waitForEvent<T>(
rejectCallback = reject; rejectCallback = reject;
}); });
const listener = addEventListener(emitter, eventName, async (event) => { const listener = addEventListener(emitter, eventName, async (event) => {
if (!(await predicate(event))) return; if (!(await predicate(event))) {
return;
}
resolveCallback(event); resolveCallback(event);
}); });
if (timeout) { if (timeout) {
@ -179,7 +186,9 @@ export function evaluationString(
} }
function serializeArgument(arg: unknown): string { function serializeArgument(arg: unknown): string {
if (Object.is(arg, undefined)) return 'undefined'; if (Object.is(arg, undefined)) {
return 'undefined';
}
return JSON.stringify(arg); return JSON.stringify(arg);
} }
@ -266,8 +275,12 @@ export function makePredicateString(
waitForVisible: boolean, waitForVisible: boolean,
waitForHidden: boolean waitForHidden: boolean
): Node | null | boolean { ): Node | null | boolean {
if (!node) return waitForHidden; if (!node) {
if (!waitForVisible && !waitForHidden) return node; return waitForHidden;
}
if (!waitForVisible && !waitForHidden) {
return node;
}
const element = const element =
node.nodeType === Node.TEXT_NODE node.nodeType === Node.TEXT_NODE
? (node.parentElement as Element) ? (node.parentElement as Element)
@ -307,11 +320,15 @@ export async function waitWithTimeout<T>(
); );
const timeoutPromise = new Promise<T>((_res, rej) => (reject = rej)); 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 {
return await Promise.race([promise, timeoutPromise]); return await Promise.race([promise, timeoutPromise]);
} finally { } finally {
if (timeoutTimer) clearTimeout(timeoutTimer); if (timeoutTimer) {
clearTimeout(timeoutTimer);
}
} }
} }

View File

@ -31,8 +31,9 @@ export const initializePuppeteer = (packageName: string): PuppeteerNode => {
process.env['npm_package_config_puppeteer_product']) as Product) process.env['npm_package_config_puppeteer_product']) as Product)
: undefined; : undefined;
if (!isPuppeteerCore && productName === 'firefox') if (!isPuppeteerCore && productName === 'firefox') {
preferredRevision = PUPPETEER_REVISIONS.firefox; preferredRevision = PUPPETEER_REVISIONS.firefox;
}
return new PuppeteerNode({ return new PuppeteerNode({
projectRoot: puppeteerRootDirectory, projectRoot: puppeteerRootDirectory,

View File

@ -326,9 +326,12 @@ export class BrowserFetcher {
assert(fileName, `A malformed download URL was found: ${url}.`); 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)) {
if (!(await existsAsync(this.#downloadsFolder))) return this.revisionInfo(revision);
}
if (!(await existsAsync(this.#downloadsFolder))) {
await mkdirAsync(this.#downloadsFolder); await mkdirAsync(this.#downloadsFolder);
}
// Use system Chromium builds on Linux ARM devices // Use system Chromium builds on Linux ARM devices
if (os.platform() !== 'darwin' && os.arch() === 'arm64') { if (os.platform() !== 'darwin' && os.arch() === 'arm64') {
@ -339,10 +342,14 @@ export class BrowserFetcher {
await _downloadFile(url, archivePath, progressCallback); await _downloadFile(url, archivePath, progressCallback);
await install(archivePath, outputPath); await install(archivePath, outputPath);
} finally { } finally {
if (await existsAsync(archivePath)) await unlinkAsync(archivePath); if (await existsAsync(archivePath)) {
await unlinkAsync(archivePath);
}
} }
const revisionInfo = this.revisionInfo(revision); const revisionInfo = this.revisionInfo(revision);
if (revisionInfo) await chmodAsync(revisionInfo.executablePath, 0o755); if (revisionInfo) {
await chmodAsync(revisionInfo.executablePath, 0o755);
}
return revisionInfo; return revisionInfo;
} }
@ -353,7 +360,9 @@ export class BrowserFetcher {
* available locally on disk. * available locally on disk.
*/ */
async localRevisions(): Promise<string[]> { async localRevisions(): Promise<string[]> {
if (!(await existsAsync(this.#downloadsFolder))) return []; if (!(await existsAsync(this.#downloadsFolder))) {
return [];
}
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))
@ -390,7 +399,7 @@ export class BrowserFetcher {
const folderPath = this.#getFolderPath(revision); const folderPath = this.#getFolderPath(revision);
let executablePath = ''; let executablePath = '';
if (this.#product === 'chrome') { if (this.#product === 'chrome') {
if (this.#platform === 'mac' || this.#platform === 'mac_arm') if (this.#platform === 'mac' || this.#platform === 'mac_arm') {
executablePath = path.join( executablePath = path.join(
folderPath, folderPath,
archiveName(this.#product, this.#platform, revision), archiveName(this.#product, this.#platform, revision),
@ -399,21 +408,23 @@ export class BrowserFetcher {
'MacOS', 'MacOS',
'Chromium' 'Chromium'
); );
else if (this.#platform === 'linux') } else if (this.#platform === 'linux') {
executablePath = path.join( executablePath = path.join(
folderPath, folderPath,
archiveName(this.#product, this.#platform, revision), archiveName(this.#product, this.#platform, revision),
'chrome' 'chrome'
); );
else if (this.#platform === 'win32' || this.#platform === 'win64') } else if (this.#platform === 'win32' || this.#platform === 'win64') {
executablePath = path.join( executablePath = path.join(
folderPath, folderPath,
archiveName(this.#product, this.#platform, revision), archiveName(this.#product, this.#platform, revision),
'chrome.exe' 'chrome.exe'
); );
else throw new Error('Unsupported platform: ' + this.#platform); } else {
throw new Error('Unsupported platform: ' + this.#platform);
}
} else if (this.#product === 'firefox') { } else if (this.#product === 'firefox') {
if (this.#platform === 'mac' || this.#platform === 'mac_arm') if (this.#platform === 'mac' || this.#platform === 'mac_arm') {
executablePath = path.join( executablePath = path.join(
folderPath, folderPath,
'Firefox Nightly.app', 'Firefox Nightly.app',
@ -421,12 +432,16 @@ export class BrowserFetcher {
'MacOS', 'MacOS',
'firefox' 'firefox'
); );
else if (this.#platform === 'linux') } else if (this.#platform === 'linux') {
executablePath = path.join(folderPath, 'firefox', 'firefox'); executablePath = path.join(folderPath, 'firefox', 'firefox');
else if (this.#platform === 'win32' || this.#platform === 'win64') } else if (this.#platform === 'win32' || this.#platform === 'win64') {
executablePath = path.join(folderPath, 'firefox', 'firefox.exe'); executablePath = path.join(folderPath, 'firefox', 'firefox.exe');
else throw new Error('Unsupported platform: ' + this.#platform); } else {
} else throw new Error('Unsupported product: ' + this.#product); throw new Error('Unsupported platform: ' + this.#platform);
}
} else {
throw new Error('Unsupported product: ' + this.#product);
}
const url = _downloadURL( const url = _downloadURL(
this.#product, this.#product,
this.#platform, this.#platform,
@ -463,9 +478,13 @@ function parseFolderPath(
): { product: string; platform: string; revision: string } | undefined { ): { 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; if (splits.length !== 2) {
return;
}
const [platform, revision] = splits; const [platform, revision] = splits;
if (!revision || !platform || !(platform in downloadURLs[product])) return; if (!revision || !platform || !(platform in downloadURLs[product])) {
return;
}
return { product, platform, revision }; return { product, platform, revision };
} }
@ -503,7 +522,9 @@ function _downloadFile(
file.on('error', (error) => reject(error)); file.on('error', (error) => reject(error));
response.pipe(file); response.pipe(file);
totalBytes = parseInt(response.headers['content-length']!, 10); totalBytes = parseInt(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));
return promise; return promise;
@ -516,15 +537,17 @@ function _downloadFile(
function install(archivePath: string, folderPath: string): Promise<unknown> { function install(archivePath: string, folderPath: string): Promise<unknown> {
debugFetcher(`Installing ${archivePath} to ${folderPath}`); debugFetcher(`Installing ${archivePath} to ${folderPath}`);
if (archivePath.endsWith('.zip')) if (archivePath.endsWith('.zip')) {
return extractZip(archivePath, { dir: folderPath }); return extractZip(archivePath, { dir: folderPath });
else if (archivePath.endsWith('.tar.bz2')) } else if (archivePath.endsWith('.tar.bz2')) {
return _extractTar(archivePath, folderPath); return _extractTar(archivePath, folderPath);
else if (archivePath.endsWith('.dmg')) } else if (archivePath.endsWith('.dmg')) {
return mkdirAsync(folderPath).then(() => return mkdirAsync(folderPath).then(() =>
_installDMG(archivePath, folderPath) _installDMG(archivePath, folderPath)
); );
else throw new Error(`Unsupported archive format: ${archivePath}`); } else {
throw new Error(`Unsupported archive format: ${archivePath}`);
}
} }
/** /**
@ -549,23 +572,30 @@ function _installDMG(dmgPath: string, folderPath: string): Promise<void> {
return new Promise<void>((fulfill, reject): 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(
(item) => typeof item === 'string' && item.endsWith('.app') (item) => typeof item === 'string' && item.endsWith('.app')
); );
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) {
else fulfill(); reject(err);
} else {
fulfill();
}
}); });
}) })
.catch(reject); .catch(reject);
@ -575,11 +605,15 @@ function _installDMG(dmgPath: string, folderPath: string): Promise<void> {
console.error(error); console.error(error);
}) })
.finally((): void => { .finally((): void => {
if (!mountPath) return; if (!mountPath) {
return;
}
const unmountCommand = `hdiutil detach "${mountPath}" -quiet`; const unmountCommand = `hdiutil detach "${mountPath}" -quiet`;
debugFetcher(`Unmounting ${mountPath}`); debugFetcher(`Unmounting ${mountPath}`);
childProcess.exec(unmountCommand, (err) => { childProcess.exec(unmountCommand, (err) => {
if (err) console.error(`Error unmounting dmg: ${err}`); if (err) {
console.error(`Error unmounting dmg: ${err}`);
}
}); });
}); });
} }
@ -637,9 +671,11 @@ function httpRequest(
res.statusCode >= 300 && res.statusCode >= 300 &&
res.statusCode < 400 && res.statusCode < 400 &&
res.headers.location res.headers.location
) ) {
httpRequest(res.headers.location, method, response); httpRequest(res.headers.location, method, response);
else response(res); } else {
response(res);
}
}; };
const request = const request =
options.protocol === 'https:' options.protocol === 'https:'

View File

@ -80,11 +80,17 @@ export class BrowserRunner {
options; options;
let stdio: Array<'ignore' | 'pipe'>; let stdio: Array<'ignore' | 'pipe'>;
if (pipe) { if (pipe) {
if (dumpio) stdio = ['ignore', 'pipe', 'pipe', 'pipe', 'pipe']; if (dumpio) {
else stdio = ['ignore', 'ignore', 'ignore', 'pipe', 'pipe']; stdio = ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'];
} else { } else {
if (dumpio) stdio = ['pipe', 'pipe', 'pipe']; stdio = ['ignore', 'ignore', 'ignore', 'pipe', 'pipe'];
else stdio = ['pipe', 'ignore', 'pipe']; }
} else {
if (dumpio) {
stdio = ['pipe', 'pipe', 'pipe'];
} else {
stdio = ['pipe', 'ignore', 'pipe'];
}
} }
assert(!this.proc, 'This process has previously been started.'); assert(!this.proc, 'This process has previously been started.');
debugLauncher( debugLauncher(
@ -147,25 +153,30 @@ export class BrowserRunner {
}); });
}); });
this.#listeners = [addEventListener(process, 'exit', this.kill.bind(this))]; this.#listeners = [addEventListener(process, 'exit', this.kill.bind(this))];
if (handleSIGINT) if (handleSIGINT) {
this.#listeners.push( this.#listeners.push(
addEventListener(process, 'SIGINT', () => { addEventListener(process, 'SIGINT', () => {
this.kill(); this.kill();
process.exit(130); process.exit(130);
}) })
); );
if (handleSIGTERM) }
if (handleSIGTERM) {
this.#listeners.push( this.#listeners.push(
addEventListener(process, 'SIGTERM', this.close.bind(this)) addEventListener(process, 'SIGTERM', this.close.bind(this))
); );
if (handleSIGHUP) }
if (handleSIGHUP) {
this.#listeners.push( this.#listeners.push(
addEventListener(process, 'SIGHUP', this.close.bind(this)) addEventListener(process, 'SIGHUP', this.close.bind(this))
); );
} }
}
close(): Promise<void> { close(): Promise<void> {
if (this.#closed) return Promise.resolve(); if (this.#closed) {
return Promise.resolve();
}
if (this.#isTempUserDataDir) { if (this.#isTempUserDataDir) {
this.kill(); this.kill();
} else if (this.connection) { } else if (this.connection) {
@ -309,14 +320,18 @@ function waitForWSEndpoint(
function onLine(line: string): void { function onLine(line: string): void {
stderr += line + '\n'; stderr += line + '\n';
const match = line.match(/^DevTools listening on (ws:\/\/.*)$/); const match = line.match(/^DevTools listening on (ws:\/\/.*)$/);
if (!match) return; if (!match) {
return;
}
cleanup(); cleanup();
// The RegExp matches, so this will obviously exist. // The RegExp matches, so this will obviously exist.
resolve(match[1]!); resolve(match[1]!);
} }
function cleanup(): void { function cleanup(): void {
if (timeoutId) clearTimeout(timeoutId); if (timeoutId) {
clearTimeout(timeoutId);
}
removeEventListeners(listeners); removeEventListeners(listeners);
} }
}); });

View File

@ -96,14 +96,17 @@ class ChromeLauncher implements ProductLauncher {
} = options; } = options;
const chromeArguments = []; const chromeArguments = [];
if (!ignoreDefaultArgs) chromeArguments.push(...this.defaultArgs(options)); if (!ignoreDefaultArgs) {
else if (Array.isArray(ignoreDefaultArgs)) chromeArguments.push(...this.defaultArgs(options));
} else if (Array.isArray(ignoreDefaultArgs)) {
chromeArguments.push( chromeArguments.push(
...this.defaultArgs(options).filter( ...this.defaultArgs(options).filter(
(arg) => !ignoreDefaultArgs.includes(arg) (arg) => !ignoreDefaultArgs.includes(arg)
) )
); );
else chromeArguments.push(...args); } else {
chromeArguments.push(...args);
}
if ( if (
!chromeArguments.some((argument) => !chromeArguments.some((argument) =>
@ -248,9 +251,12 @@ class ChromeLauncher implements ProductLauncher {
args = [], args = [],
userDataDir, userDataDir,
} = options; } = options;
if (userDataDir) if (userDataDir) {
chromeArguments.push(`--user-data-dir=${path.resolve(userDataDir)}`); chromeArguments.push(`--user-data-dir=${path.resolve(userDataDir)}`);
if (devtools) chromeArguments.push('--auto-open-devtools-for-tabs'); }
if (devtools) {
chromeArguments.push('--auto-open-devtools-for-tabs');
}
if (headless) { if (headless) {
chromeArguments.push( chromeArguments.push(
headless === 'chrome' ? '--headless=chrome' : '--headless', headless === 'chrome' ? '--headless=chrome' : '--headless',
@ -258,8 +264,9 @@ class ChromeLauncher implements ProductLauncher {
'--mute-audio' '--mute-audio'
); );
} }
if (args.every((arg) => arg.startsWith('-'))) if (args.every((arg) => arg.startsWith('-'))) {
chromeArguments.push('about:blank'); chromeArguments.push('about:blank');
}
chromeArguments.push(...args); chromeArguments.push(...args);
return chromeArguments; return chromeArguments;
} }
@ -326,14 +333,17 @@ class FirefoxLauncher implements ProductLauncher {
} = options; } = options;
const firefoxArguments = []; const firefoxArguments = [];
if (!ignoreDefaultArgs) firefoxArguments.push(...this.defaultArgs(options)); if (!ignoreDefaultArgs) {
else if (Array.isArray(ignoreDefaultArgs)) firefoxArguments.push(...this.defaultArgs(options));
} else if (Array.isArray(ignoreDefaultArgs)) {
firefoxArguments.push( firefoxArguments.push(
...this.defaultArgs(options).filter( ...this.defaultArgs(options).filter(
(arg) => !ignoreDefaultArgs.includes(arg) (arg) => !ignoreDefaultArgs.includes(arg)
) )
); );
else firefoxArguments.push(...args); } else {
firefoxArguments.push(...args);
}
if ( if (
!firefoxArguments.some((argument) => !firefoxArguments.some((argument) =>
@ -379,7 +389,9 @@ class FirefoxLauncher implements ProductLauncher {
let firefoxExecutable = executablePath; let firefoxExecutable = executablePath;
if (!executablePath) { if (!executablePath) {
const { missingText, executablePath } = resolveExecutablePath(this); const { missingText, executablePath } = resolveExecutablePath(this);
if (missingText) throw new Error(missingText); if (missingText) {
throw new Error(missingText);
}
firefoxExecutable = executablePath; firefoxExecutable = executablePath;
} }
@ -452,7 +464,9 @@ class FirefoxLauncher implements ProductLauncher {
product: this.product, product: this.product,
}); });
const localRevisions = await browserFetcher.localRevisions(); const localRevisions = await browserFetcher.localRevisions();
if (localRevisions[0]) this._preferredRevision = localRevisions[0]; if (localRevisions[0]) {
this._preferredRevision = localRevisions[0];
}
} }
} }
@ -470,18 +484,24 @@ class FirefoxLauncher implements ProductLauncher {
const firefoxArguments = ['--no-remote']; const firefoxArguments = ['--no-remote'];
if (os.platform() === 'darwin') firefoxArguments.push('--foreground'); if (os.platform() === 'darwin') {
else if (os.platform().startsWith('win')) { firefoxArguments.push('--foreground');
} else if (os.platform().startsWith('win')) {
firefoxArguments.push('--wait-for-browser'); firefoxArguments.push('--wait-for-browser');
} }
if (userDataDir) { if (userDataDir) {
firefoxArguments.push('--profile'); firefoxArguments.push('--profile');
firefoxArguments.push(userDataDir); firefoxArguments.push(userDataDir);
} }
if (headless) firefoxArguments.push('--headless'); if (headless) {
if (devtools) firefoxArguments.push('--devtools'); firefoxArguments.push('--headless');
if (args.every((arg) => arg.startsWith('-'))) }
if (devtools) {
firefoxArguments.push('--devtools');
}
if (args.every((arg) => arg.startsWith('-'))) {
firefoxArguments.push('about:blank'); firefoxArguments.push('about:blank');
}
firefoxArguments.push(...args); firefoxArguments.push(...args);
return firefoxArguments; return firefoxArguments;
} }
@ -887,11 +907,12 @@ export default function Launcher(
product?: string product?: string
): ProductLauncher { ): ProductLauncher {
// 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

@ -57,10 +57,14 @@ export class NodeWebSocketTransport implements ConnectionTransport {
constructor(ws: NodeWebSocket) { constructor(ws: NodeWebSocket) {
this.#ws = ws; this.#ws = ws;
this.#ws.addEventListener('message', (event) => { this.#ws.addEventListener('message', (event) => {
if (this.onmessage) this.onmessage.call(null, event.data); if (this.onmessage) {
this.onmessage.call(null, event.data);
}
}); });
this.#ws.addEventListener('close', () => { this.#ws.addEventListener('close', () => {
if (this.onclose) this.onclose.call(null); if (this.onclose) {
this.onclose.call(null);
}
}); });
// 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', () => {});

View File

@ -116,7 +116,9 @@ export class PuppeteerNode extends Puppeteer {
* @returns Promise which resolves to browser instance. * @returns Promise which resolves to browser instance.
*/ */
override 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);
} }
@ -131,7 +133,9 @@ export class PuppeteerNode extends Puppeteer {
* @internal * @internal
*/ */
set _productName(name: Product | undefined) { set _productName(name: Product | undefined) {
if (this.#productName !== name) this._changedProduct = true; if (this.#productName !== name) {
this._changedProduct = true;
}
this.#productName = name; this.#productName = name;
} }
@ -161,7 +165,9 @@ export class PuppeteerNode extends Puppeteer {
* @returns Promise which resolves to browser instance. * @returns Promise which resolves to browser instance.
*/ */
launch(options: PuppeteerLaunchOptions = {}): Promise<Browser> { launch(options: PuppeteerLaunchOptions = {}): Promise<Browser> {
if (options.product) this._productName = options.product; if (options.product) {
this._productName = options.product;
}
return this._launcher.launch(options); return this._launcher.launch(options);
} }

View File

@ -97,9 +97,15 @@ export async function downloadBrowser(): Promise<void> {
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) {
if (NPM_HTTP_PROXY) process.env['HTTP_PROXY'] = NPM_HTTP_PROXY; process.env['HTTPS_PROXY'] = NPM_HTTPS_PROXY;
if (NPM_NO_PROXY) process.env['NO_PROXY'] = NPM_NO_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 { function onSuccess(localRevisions: string[]): void {
logPolitely( logPolitely(
@ -182,8 +188,9 @@ export async function downloadBrowser(): Promise<void> {
); );
https https
.get(firefoxVersionsUrl, requestOptions, (r) => { .get(firefoxVersionsUrl, requestOptions, (r) => {
if (r.statusCode && r.statusCode >= 400) if (r.statusCode && r.statusCode >= 400) {
return reject(new Error(`Got status code ${r.statusCode}`)); return reject(new Error(`Got status code ${r.statusCode}`));
}
r.on('data', (chunk) => { r.on('data', (chunk) => {
data += chunk; data += chunk;
}); });
@ -207,5 +214,7 @@ export function logPolitely(toBeLogged: unknown): void {
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
if (!logLevelDisplay) console.log(toBeLogged); if (!logLevelDisplay) {
console.log(toBeLogged);
}
} }

View File

@ -493,10 +493,14 @@ describeFailsFirefox('Accessibility', function () {
}); });
}); });
function findFocusedNode(node) { function findFocusedNode(node) {
if (node.focused) return node; if (node.focused) {
return node;
}
for (const child of node.children || []) { for (const child of node.children || []) {
const focusedChild = findFocusedNode(child); const focusedChild = findFocusedNode(child);
if (focusedChild) return focusedChild; if (focusedChild) {
return focusedChild;
}
} }
return null; return null;
} }

View File

@ -69,6 +69,6 @@
); );
function updateButtons(buttons) { function updateButtons(buttons) {
for (let i = 0; i < 5; i++) for (let i = 0; i < 5; i++)
box.classList.toggle('button-' + i, buttons & (1 << i)); {box.classList.toggle('button-' + i, buttons & (1 << i));}
} }
})(); })();

View File

@ -38,8 +38,11 @@ describe('Browser specs', function () {
const userAgent = await browser.userAgent(); const userAgent = await browser.userAgent();
expect(userAgent.length).toBeGreaterThan(0); expect(userAgent.length).toBeGreaterThan(0);
if (isChrome) expect(userAgent).toContain('WebKit'); if (isChrome) {
else expect(userAgent).toContain('Gecko'); expect(userAgent).toContain('WebKit');
} else {
expect(userAgent).toContain('Gecko');
}
}); });
}); });

View File

@ -113,7 +113,9 @@ describe('BrowserContext', function () {
resolved = true; resolved = true;
if (error instanceof puppeteer.errors.TimeoutError) { if (error instanceof puppeteer.errors.TimeoutError) {
console.error(error); console.error(error);
} else throw error; } else {
throw error;
}
}); });
const page = await context.newPage(); const page = await context.newPage();
expect(resolved).toBe(false); expect(resolved).toBe(false);
@ -124,7 +126,9 @@ describe('BrowserContext', function () {
} catch (error) { } catch (error) {
if (error instanceof puppeteer.errors.TimeoutError) { if (error instanceof puppeteer.errors.TimeoutError) {
console.error(error); console.error(error);
} else throw error; } else {
throw error;
}
} }
await context.close(); await context.close();
}); });

View File

@ -85,8 +85,9 @@ function traceAPICoverage(apiCoverage, className, modulePath) {
typeof methodName !== 'string' || typeof methodName !== 'string' ||
methodName.startsWith('_') || methodName.startsWith('_') ||
typeof method !== 'function' typeof method !== 'function'
) ) {
continue; continue;
}
apiCoverage.set(`${className}.${methodName}`, false); apiCoverage.set(`${className}.${methodName}`, false);
Reflect.set(classType.prototype, methodName, function (...args) { Reflect.set(classType.prototype, methodName, function (...args) {
apiCoverage.set(`${className}.${methodName}`, true); apiCoverage.set(`${className}.${methodName}`, true);
@ -102,13 +103,15 @@ function traceAPICoverage(apiCoverage, className, modulePath) {
const eventsName = `${className}EmittedEvents`; const eventsName = `${className}EmittedEvents`;
if (loadedModule[eventsName]) { if (loadedModule[eventsName]) {
for (const event of Object.values(loadedModule[eventsName])) { for (const event of Object.values(loadedModule[eventsName])) {
if (typeof event !== 'symbol') if (typeof event !== 'symbol') {
apiCoverage.set(`${className}.emit(${JSON.stringify(event)})`, false); apiCoverage.set(`${className}.emit(${JSON.stringify(event)})`, false);
} }
}
const method = Reflect.get(classType.prototype, 'emit'); const method = Reflect.get(classType.prototype, 'emit');
Reflect.set(classType.prototype, 'emit', function (event, ...args) { Reflect.set(classType.prototype, 'emit', function (event, ...args) {
if (typeof event !== 'symbol' && this.listenerCount(event)) if (typeof event !== 'symbol' && this.listenerCount(event)) {
apiCoverage.set(`${className}.emit(${JSON.stringify(event)})`, true); apiCoverage.set(`${className}.emit(${JSON.stringify(event)})`, true);
}
return method.call(this, event, ...args); return method.call(this, event, ...args);
}); });
} }

View File

@ -49,9 +49,11 @@ describe('ElementHandle specs', function () {
const nestedFrame = page.frames()[1].childFrames()[1]; const nestedFrame = page.frames()[1].childFrames()[1];
const elementHandle = await nestedFrame.$('div'); const elementHandle = await nestedFrame.$('div');
const box = await elementHandle.boundingBox(); const box = await elementHandle.boundingBox();
if (isChrome) if (isChrome) {
expect(box).toEqual({ x: 28, y: 182, width: 264, height: 18 }); expect(box).toEqual({ x: 28, y: 182, width: 264, height: 18 });
else expect(box).toEqual({ x: 28, y: 182, width: 254, height: 18 }); } else {
expect(box).toEqual({ x: 28, y: 182, width: 254, height: 18 });
}
}); });
it('should return null for invisible elements', async () => { it('should return null for invisible elements', async () => {
const { page } = getTestState(); const { page } = getTestState();

View File

@ -75,8 +75,9 @@ describe('Fixtures', function () {
let output = ''; let output = '';
res.stdout.on('data', (data) => { res.stdout.on('data', (data) => {
output += data; output += data;
if (output.indexOf('\n')) if (output.indexOf('\n')) {
wsEndPointCallback(output.substring(0, output.indexOf('\n'))); wsEndPointCallback(output.substring(0, output.indexOf('\n')));
}
}); });
const browser = await puppeteer.connect({ const browser = await puppeteer.connect({
browserWSEndpoint: await wsEndPointPromise, browserWSEndpoint: await wsEndPointPromise,
@ -85,9 +86,11 @@ describe('Fixtures', function () {
new Promise((resolve) => browser.once('disconnected', resolve)), new Promise((resolve) => browser.once('disconnected', resolve)),
new Promise((resolve) => res.on('close', resolve)), new Promise((resolve) => res.on('close', resolve)),
]; ];
if (process.platform === 'win32') if (process.platform === 'win32') {
execSync(`taskkill /pid ${res.pid} /T /F`); execSync(`taskkill /pid ${res.pid} /T /F`);
else process.kill(res.pid); } else {
process.kill(res.pid);
}
await Promise.all(promises); await Promise.all(promises);
}); });
}); });

View File

@ -37,8 +37,9 @@ const GoldenComparators = {
* @returns {?{diff: (!Object:undefined), errorMessage: (string|undefined)}} * @returns {?{diff: (!Object:undefined), errorMessage: (string|undefined)}}
*/ */
function compareImages(actualBuffer, expectedBuffer, mimeType) { function compareImages(actualBuffer, expectedBuffer, mimeType) {
if (!actualBuffer || !(actualBuffer instanceof Buffer)) if (!actualBuffer || !(actualBuffer instanceof Buffer)) {
return { errorMessage: 'Actual result should be Buffer.' }; return { errorMessage: 'Actual result should be Buffer.' };
}
const actual = const actual =
mimeType === 'image/png' mimeType === 'image/png'
@ -71,10 +72,13 @@ function compareImages(actualBuffer, expectedBuffer, mimeType) {
* @returns {?{diff: (!Object:undefined), errorMessage: (string|undefined)}} * @returns {?{diff: (!Object:undefined), errorMessage: (string|undefined)}}
*/ */
function compareText(actual, expectedBuffer) { function compareText(actual, expectedBuffer) {
if (typeof actual !== 'string') if (typeof actual !== 'string') {
return { errorMessage: 'Actual result should be string' }; return { errorMessage: 'Actual result should be string' };
}
const expected = expectedBuffer.toString('utf-8'); const expected = expectedBuffer.toString('utf-8');
if (expected === actual) return null; if (expected === actual) {
return null;
}
const diff = new Diff(); const diff = new Diff();
const result = diff.main(expected, actual); const result = diff.main(expected, actual);
diff.cleanupSemantic(result); diff.cleanupSemantic(result);
@ -120,7 +124,9 @@ function compare(goldenPath, outputPath, actual, goldenName) {
}; };
} }
const result = comparator(actual, expected, mimeType); const result = comparator(actual, expected, mimeType);
if (!result) return { pass: true }; if (!result) {
return { pass: true };
}
ensureOutputDir(); ensureOutputDir();
if (goldenPath === outputPath) { if (goldenPath === outputPath) {
fs.writeFileSync(addSuffix(actualPath, '-actual'), actual); fs.writeFileSync(addSuffix(actualPath, '-actual'), actual);
@ -135,14 +141,18 @@ function compare(goldenPath, outputPath, actual, goldenName) {
} }
let message = goldenName + ' mismatch!'; let message = goldenName + ' mismatch!';
if (result.errorMessage) message += ' ' + result.errorMessage; if (result.errorMessage) {
message += ' ' + result.errorMessage;
}
return { return {
pass: false, pass: false,
message: message + ' ' + messageSuffix, message: message + ' ' + messageSuffix,
}; };
function ensureOutputDir() { function ensureOutputDir() {
if (!fs.existsSync(outputPath)) fs.mkdirSync(outputPath); if (!fs.existsSync(outputPath)) {
fs.mkdirSync(outputPath);
}
} }
} }

View File

@ -338,15 +338,17 @@ describeChromeOnly('headful tests', function () {
}) })
); );
const promises = []; const promises = [];
for (let i = 0; i < N; ++i) for (let i = 0; i < N; ++i) {
promises.push( promises.push(
pages[i].screenshot({ pages[i].screenshot({
clip: { x: 50 * i, y: 0, width: 50, height: 50 }, clip: { x: 50 * i, y: 0, width: 50, height: 50 },
}) })
); );
}
const screenshots = await Promise.all(promises); const screenshots = await Promise.all(promises);
for (let i = 0; i < N; ++i) for (let i = 0; i < N; ++i) {
expect(screenshots[i]).toBeGolden(`grid-cell-${i}.png`); expect(screenshots[i]).toBeGolden(`grid-cell-${i}.png`);
}
await Promise.all(pages.map((page) => page.close())); await Promise.all(pages.map((page) => page.close()));
await browser.close(); await browser.close();

View File

@ -152,9 +152,11 @@ describe('JSHandle', function () {
const windowHandle = await page.evaluateHandle('window'); const windowHandle = await page.evaluateHandle('window');
let error = null; let error = null;
await windowHandle.jsonValue().catch((error_) => (error = error_)); await windowHandle.jsonValue().catch((error_) => (error = error_));
if (isChrome) if (isChrome) {
expect(error.message).toContain('Object reference chain is too long'); expect(error.message).toContain('Object reference chain is too long');
else expect(error.message).toContain('Object is not serializable'); } else {
expect(error.message).toContain('Object is not serializable');
}
}); });
}); });

View File

@ -64,14 +64,17 @@ describe('Keyboard', function () {
expect( expect(
await page.evaluate(() => document.querySelector('textarea').value) await page.evaluate(() => document.querySelector('textarea').value)
).toBe('Hello World!'); ).toBe('Hello World!');
for (let i = 0; i < 'World!'.length; i++) page.keyboard.press('ArrowLeft'); for (let i = 0; i < 'World!'.length; i++) {
page.keyboard.press('ArrowLeft');
}
await page.keyboard.type('inserted '); await page.keyboard.type('inserted ');
expect( expect(
await page.evaluate(() => document.querySelector('textarea').value) await page.evaluate(() => document.querySelector('textarea').value)
).toBe('Hello inserted World!'); ).toBe('Hello inserted World!');
page.keyboard.down('Shift'); page.keyboard.down('Shift');
for (let i = 0; i < 'inserted '.length; i++) for (let i = 0; i < 'inserted '.length; i++) {
page.keyboard.press('ArrowLeft'); page.keyboard.press('ArrowLeft');
}
page.keyboard.up('Shift'); page.keyboard.up('Shift');
await page.keyboard.press('Backspace'); await page.keyboard.press('Backspace');
expect( expect(
@ -152,7 +155,7 @@ describe('Keyboard', function () {
); );
await keyboard.down('!'); await keyboard.down('!');
// Shift+! will generate a keypress // Shift+! will generate a keypress
if (modifierKey === 'Shift') if (modifierKey === 'Shift') {
expect(await page.evaluate(() => globalThis.getResult())).toBe( expect(await page.evaluate(() => globalThis.getResult())).toBe(
'Keydown: ! Digit1 49 [' + 'Keydown: ! Digit1 49 [' +
modifierKey + modifierKey +
@ -160,10 +163,11 @@ describe('Keyboard', function () {
modifierKey + modifierKey +
']' ']'
); );
else } else {
expect(await page.evaluate(() => globalThis.getResult())).toBe( expect(await page.evaluate(() => globalThis.getResult())).toBe(
'Keydown: ! Digit1 49 [' + modifierKey + ']' 'Keydown: ! Digit1 49 [' + modifierKey + ']'
); );
}
await keyboard.up('!'); await keyboard.up('!');
expect(await page.evaluate(() => globalThis.getResult())).toBe( expect(await page.evaluate(() => globalThis.getResult())).toBe(
@ -260,8 +264,12 @@ describe('Keyboard', function () {
(event) => { (event) => {
event.stopPropagation(); event.stopPropagation();
event.stopImmediatePropagation(); event.stopImmediatePropagation();
if (event.key === 'l') event.preventDefault(); if (event.key === 'l') {
if (event.key === 'o') event.preventDefault(); event.preventDefault();
}
if (event.key === 'o') {
event.preventDefault();
}
}, },
false false
); );
@ -396,13 +404,22 @@ describe('Keyboard', function () {
string, string,
boolean boolean
]; ];
if (isFirefox && os.platform() !== 'darwin') expect(key).toBe('OS'); if (isFirefox && os.platform() !== 'darwin') {
else expect(key).toBe('Meta'); expect(key).toBe('OS');
} else {
expect(key).toBe('Meta');
}
if (isFirefox) expect(code).toBe('OSLeft'); if (isFirefox) {
else expect(code).toBe('MetaLeft'); expect(code).toBe('OSLeft');
} else {
expect(code).toBe('MetaLeft');
}
if (isFirefox && os.platform() !== 'darwin') expect(metaKey).toBe(false); if (isFirefox && os.platform() !== 'darwin') {
else expect(metaKey).toBe(true); expect(metaKey).toBe(false);
} else {
expect(metaKey).toBe(true);
}
}); });
}); });

View File

@ -41,7 +41,9 @@ const TMP_FOLDER = path.join(os.tmpdir(), 'pptr_tmp_folder-');
const FIREFOX_TIMEOUT = 30 * 1000; const FIREFOX_TIMEOUT = 30 * 1000;
describe('Launcher specs', function () { describe('Launcher specs', function () {
if (getTestState().isFirefox) this.timeout(FIREFOX_TIMEOUT); if (getTestState().isFirefox) {
this.timeout(FIREFOX_TIMEOUT);
}
describe('Puppeteer', function () { describe('Puppeteer', function () {
describe('BrowserFetcher', function () { describe('BrowserFetcher', function () {
@ -394,8 +396,11 @@ describe('Launcher specs', function () {
}); });
it('should report the correct product', async () => { it('should report the correct product', async () => {
const { isChrome, isFirefox, puppeteer } = getTestState(); const { isChrome, isFirefox, puppeteer } = getTestState();
if (isChrome) expect(puppeteer.product).toBe('chrome'); if (isChrome) {
else if (isFirefox) expect(puppeteer.product).toBe('firefox'); expect(puppeteer.product).toBe('chrome');
} else if (isFirefox) {
expect(puppeteer.product).toBe('firefox');
}
}); });
it('should work with no default arguments', async () => { it('should work with no default arguments', async () => {
const { defaultBrowserOptions, puppeteer } = getTestState(); const { defaultBrowserOptions, puppeteer } = getTestState();
@ -468,7 +473,9 @@ describe('Launcher specs', function () {
const pages = await browser.pages(); const pages = await browser.pages();
expect(pages.length).toBe(1); expect(pages.length).toBe(1);
const page = pages[0]; const page = pages[0];
if (page.url() !== server.EMPTY_PAGE) await page.waitForNavigation(); if (page.url() !== server.EMPTY_PAGE) {
await page.waitForNavigation();
}
expect(page.url()).toBe(server.EMPTY_PAGE); expect(page.url()).toBe(server.EMPTY_PAGE);
await browser.close(); await browser.close();
} }

View File

@ -99,16 +99,19 @@ const defaultBrowserOptions = Object.assign(
} else { } else {
// TODO(jackfranklin): declare updateRevision in some form for the Firefox // TODO(jackfranklin): declare updateRevision in some form for the Firefox
// launcher. // launcher.
if (product === 'firefox') {
// @ts-expect-error _updateRevision is defined on the FF launcher // @ts-expect-error _updateRevision is defined on the FF launcher
// but not the Chrome one. The types need tidying so that TS can infer that // but not the Chrome one. The types need tidying so that TS can infer that
// properly and not error here. // properly and not error here.
if (product === 'firefox') await puppeteer._launcher._updateRevision(); await puppeteer._launcher._updateRevision();
}
const executablePath = puppeteer.executablePath(); const executablePath = puppeteer.executablePath();
if (!fs.existsSync(executablePath)) if (!fs.existsSync(executablePath)) {
throw new Error( throw new Error(
`Browser is not downloaded at ${executablePath}. Run 'npm install' and try to re-run tests` `Browser is not downloaded at ${executablePath}. Run 'npm install' and try to re-run tests`
); );
} }
}
})(); })();
declare module 'expect/build/types' { declare module 'expect/build/types' {
@ -121,7 +124,9 @@ const setupGoldenAssertions = (): void => {
const suffix = product.toLowerCase(); const suffix = product.toLowerCase();
const GOLDEN_DIR = path.join(__dirname, 'golden-' + suffix); const GOLDEN_DIR = path.join(__dirname, 'golden-' + suffix);
const OUTPUT_DIR = path.join(__dirname, 'output-' + suffix); const OUTPUT_DIR = path.join(__dirname, 'output-' + suffix);
if (fs.existsSync(OUTPUT_DIR)) rimraf.sync(OUTPUT_DIR); if (fs.existsSync(OUTPUT_DIR)) {
rimraf.sync(OUTPUT_DIR);
}
utils.extendExpectWithToBeGolden(GOLDEN_DIR, OUTPUT_DIR); utils.extendExpectWithToBeGolden(GOLDEN_DIR, OUTPUT_DIR);
}; };
@ -149,41 +154,55 @@ export const itFailsFirefox = (
description: string, description: string,
body: Mocha.Func body: Mocha.Func
): Mocha.Test => { ): Mocha.Test => {
if (isFirefox) return xit(description, body); if (isFirefox) {
else return it(description, body); return xit(description, body);
} else {
return it(description, body);
}
}; };
export const itChromeOnly = ( export const itChromeOnly = (
description: string, description: string,
body: Mocha.Func body: Mocha.Func
): Mocha.Test => { ): Mocha.Test => {
if (isChrome) return it(description, body); if (isChrome) {
else return xit(description, body); return it(description, body);
} else {
return xit(description, body);
}
}; };
export const itHeadlessOnly = ( export const itHeadlessOnly = (
description: string, description: string,
body: Mocha.Func body: Mocha.Func
): Mocha.Test => { ): Mocha.Test => {
if (isChrome && isHeadless === true) return it(description, body); if (isChrome && isHeadless === true) {
else return xit(description, body); return it(description, body);
} else {
return xit(description, body);
}
}; };
export const itFirefoxOnly = ( export const itFirefoxOnly = (
description: string, description: string,
body: Mocha.Func body: Mocha.Func
): Mocha.Test => { ): Mocha.Test => {
if (isFirefox) return it(description, body); if (isFirefox) {
else return xit(description, body); return it(description, body);
} else {
return xit(description, body);
}
}; };
export const itOnlyRegularInstall = ( export const itOnlyRegularInstall = (
description: string, description: string,
body: Mocha.Func body: Mocha.Func
): Mocha.Test => { ): Mocha.Test => {
if (alternativeInstall || process.env['BINARY']) if (alternativeInstall || process.env['BINARY']) {
return xit(description, body); return xit(description, body);
else return it(description, body); } else {
return it(description, body);
}
}; };
export const itFailsWindowsUntilDate = ( export const itFailsWindowsUntilDate = (
@ -213,15 +232,20 @@ export const describeFailsFirefox = (
description: string, description: string,
body: (this: Mocha.Suite) => void body: (this: Mocha.Suite) => void
): void | Mocha.Suite => { ): void | Mocha.Suite => {
if (isFirefox) return xdescribe(description, body); if (isFirefox) {
else return describe(description, body); return xdescribe(description, body);
} else {
return describe(description, body);
}
}; };
export const describeChromeOnly = ( export const describeChromeOnly = (
description: string, description: string,
body: (this: Mocha.Suite) => void body: (this: Mocha.Suite) => void
): Mocha.Suite | void => { ): Mocha.Suite | void => {
if (isChrome) return describe(description, body); if (isChrome) {
return describe(description, body);
}
}; };
console.log( console.log(

View File

@ -161,21 +161,27 @@ describe('Mouse', function () {
['Meta', 'metaKey'], ['Meta', 'metaKey'],
]); ]);
// In Firefox, the Meta modifier only exists on Mac // In Firefox, the Meta modifier only exists on Mac
if (isFirefox && os.platform() !== 'darwin') delete modifiers['Meta']; if (isFirefox && os.platform() !== 'darwin') {
delete modifiers['Meta'];
}
for (const [modifier, key] of modifiers) { for (const [modifier, key] of modifiers) {
await page.keyboard.down(modifier); await page.keyboard.down(modifier);
await page.click('#button-3'); await page.click('#button-3');
if ( if (
!(await page.evaluate((mod: string) => globalThis.lastEvent[mod], key)) !(await page.evaluate((mod: string) => globalThis.lastEvent[mod], key))
) ) {
throw new Error(key + ' should be true'); throw new Error(key + ' should be true');
}
await page.keyboard.up(modifier); await page.keyboard.up(modifier);
} }
await page.click('#button-3'); await page.click('#button-3');
for (const [modifier, key] of modifiers) { for (const [modifier, key] of modifiers) {
if (await page.evaluate((mod: string) => globalThis.lastEvent[mod], key)) if (
await page.evaluate((mod: string) => globalThis.lastEvent[mod], key)
) {
throw new Error(modifiers[modifier] + ' should be false'); throw new Error(modifiers[modifier] + ' should be false');
} }
}
}); });
itFailsFirefox('should send mouse wheel events', async () => { itFailsFirefox('should send mouse wheel events', async () => {
const { page, server } = getTestState(); const { page, server } = getTestState();

View File

@ -88,8 +88,11 @@ describe('navigation', function () {
let error = null; let error = null;
await page.goto(server.EMPTY_PAGE).catch((error_) => (error = error_)); await page.goto(server.EMPTY_PAGE).catch((error_) => (error = error_));
expect(error).not.toBe(null); expect(error).not.toBe(null);
if (isChrome) expect(error.message).toContain('net::ERR_ABORTED'); if (isChrome) {
else expect(error.message).toContain('NS_BINDING_ABORTED'); expect(error.message).toContain('net::ERR_ABORTED');
} else {
expect(error.message).toContain('NS_BINDING_ABORTED');
}
}); });
it('should navigate to empty page with domcontentloaded', async () => { it('should navigate to empty page with domcontentloaded', async () => {
const { page, server } = getTestState(); const { page, server } = getTestState();
@ -140,9 +143,11 @@ describe('navigation', function () {
let error = null; let error = null;
await page.goto('asdfasdf').catch((error_) => (error = error_)); await page.goto('asdfasdf').catch((error_) => (error = error_));
if (isChrome) if (isChrome) {
expect(error.message).toContain('Cannot navigate to invalid URL'); expect(error.message).toContain('Cannot navigate to invalid URL');
else expect(error.message).toContain('Invalid url'); } else {
expect(error.message).toContain('Invalid url');
}
}); });
/* If you are running this on pre-Catalina versions of macOS this will fail locally. /* If you are running this on pre-Catalina versions of macOS this will fail locally.
@ -169,8 +174,11 @@ describe('navigation', function () {
await page await page
.goto(httpsServer.EMPTY_PAGE) .goto(httpsServer.EMPTY_PAGE)
.catch((error_) => (error = error_)); .catch((error_) => (error = error_));
if (isChrome) expect(error.message).toContain(EXPECTED_SSL_CERT_MESSAGE); if (isChrome) {
else expect(error.message).toContain('SSL_ERROR_UNKNOWN'); expect(error.message).toContain(EXPECTED_SSL_CERT_MESSAGE);
} else {
expect(error.message).toContain('SSL_ERROR_UNKNOWN');
}
expect(requests.length).toBe(2); expect(requests.length).toBe(2);
expect(requests[0]).toBe('request'); expect(requests[0]).toBe('request');
@ -185,8 +193,11 @@ describe('navigation', function () {
await page await page
.goto(httpsServer.PREFIX + '/redirect/1.html') .goto(httpsServer.PREFIX + '/redirect/1.html')
.catch((error_) => (error = error_)); .catch((error_) => (error = error_));
if (isChrome) expect(error.message).toContain(EXPECTED_SSL_CERT_MESSAGE); if (isChrome) {
else expect(error.message).toContain('SSL_ERROR_UNKNOWN'); expect(error.message).toContain(EXPECTED_SSL_CERT_MESSAGE);
} else {
expect(error.message).toContain('SSL_ERROR_UNKNOWN');
}
}); });
it('should throw if networkidle is passed as an option', async () => { it('should throw if networkidle is passed as an option', async () => {
const { page, server } = getTestState(); const { page, server } = getTestState();
@ -207,9 +218,11 @@ describe('navigation', function () {
await page await page
.goto('http://localhost:44123/non-existing-url') .goto('http://localhost:44123/non-existing-url')
.catch((error_) => (error = error_)); .catch((error_) => (error = error_));
if (isChrome) if (isChrome) {
expect(error.message).toContain('net::ERR_CONNECTION_REFUSED'); expect(error.message).toContain('net::ERR_CONNECTION_REFUSED');
else expect(error.message).toContain('NS_ERROR_CONNECTION_REFUSED'); } else {
expect(error.message).toContain('NS_ERROR_CONNECTION_REFUSED');
}
}); });
it('should fail when exceeding maximum navigation timeout', async () => { it('should fail when exceeding maximum navigation timeout', async () => {
const { page, server, puppeteer } = getTestState(); const { page, server, puppeteer } = getTestState();
@ -385,7 +398,9 @@ describe('navigation', function () {
let warning = null; let warning = null;
const warningHandler = (w) => (warning = w); const warningHandler = (w) => (warning = w);
process.on('warning', warningHandler); process.on('warning', warningHandler);
for (let i = 0; i < 20; ++i) await page.goto(server.EMPTY_PAGE); for (let i = 0; i < 20; ++i) {
await page.goto(server.EMPTY_PAGE);
}
process.removeListener('warning', warningHandler); process.removeListener('warning', warningHandler);
expect(warning).toBe(null); expect(warning).toBe(null);
}); });
@ -395,10 +410,11 @@ describe('navigation', function () {
let warning = null; let warning = null;
const warningHandler = (w) => (warning = w); const warningHandler = (w) => (warning = w);
process.on('warning', warningHandler); process.on('warning', warningHandler);
for (let i = 0; i < 20; ++i) for (let i = 0; i < 20; ++i) {
await page.goto('asdf').catch(() => { await page.goto('asdf').catch(() => {
/* swallow navigation error */ /* swallow navigation error */
}); });
}
process.removeListener('warning', warningHandler); process.removeListener('warning', warningHandler);
expect(warning).toBe(null); expect(warning).toBe(null);
}); });
@ -615,7 +631,9 @@ describe('navigation', function () {
const frame = await utils.waitEvent(page, 'frameattached'); const frame = await utils.waitEvent(page, 'frameattached');
await new Promise<void>((fulfill) => { await new Promise<void>((fulfill) => {
page.on('framenavigated', (f) => { page.on('framenavigated', (f) => {
if (f === frame) fulfill(); if (f === frame) {
fulfill();
}
}); });
}); });
await Promise.all([ await Promise.all([

View File

@ -255,7 +255,9 @@ describe('network', function () {
server.setRoute('/post', (req, res) => res.end()); server.setRoute('/post', (req, res) => res.end());
let request = null; let request = null;
page.on('request', (r) => { page.on('request', (r) => {
if (!utils.isFavicon(r)) request = r; if (!utils.isFavicon(r)) {
request = r;
}
}); });
await page.evaluate(() => await page.evaluate(() =>
fetch('./post', { fetch('./post', {
@ -504,8 +506,11 @@ describe('network', function () {
await page.setRequestInterception(true); await page.setRequestInterception(true);
page.on('request', (request) => { page.on('request', (request) => {
if (request.url().endsWith('css')) request.abort(); if (request.url().endsWith('css')) {
else request.continue(); request.abort();
} else {
request.continue();
}
}); });
const failedRequests = []; const failedRequests = [];
page.on('requestfailed', (request) => failedRequests.push(request)); page.on('requestfailed', (request) => failedRequests.push(request));
@ -514,10 +519,11 @@ describe('network', function () {
expect(failedRequests[0].url()).toContain('one-style.css'); expect(failedRequests[0].url()).toContain('one-style.css');
expect(failedRequests[0].response()).toBe(null); expect(failedRequests[0].response()).toBe(null);
expect(failedRequests[0].resourceType()).toBe('stylesheet'); expect(failedRequests[0].resourceType()).toBe('stylesheet');
if (isChrome) if (isChrome) {
expect(failedRequests[0].failure().errorText).toBe('net::ERR_FAILED'); expect(failedRequests[0].failure().errorText).toBe('net::ERR_FAILED');
else } else {
expect(failedRequests[0].failure().errorText).toBe('NS_ERROR_FAILURE'); expect(failedRequests[0].failure().errorText).toBe('NS_ERROR_FAILURE');
}
expect(failedRequests[0].frame()).toBeTruthy(); expect(failedRequests[0].frame()).toBeTruthy();
}); });
it('Page.Events.RequestFinished', async () => { it('Page.Events.RequestFinished', async () => {

View File

@ -67,8 +67,11 @@ describe('Page', function () {
const dialog = await waitEvent(newPage, 'dialog'); const dialog = await waitEvent(newPage, 'dialog');
expect(dialog.type()).toBe('beforeunload'); expect(dialog.type()).toBe('beforeunload');
expect(dialog.defaultValue()).toBe(''); expect(dialog.defaultValue()).toBe('');
if (isChrome) expect(dialog.message()).toBe(''); if (isChrome) {
else expect(dialog.message()).toBeTruthy(); expect(dialog.message()).toBe('');
} else {
expect(dialog.message()).toBeTruthy();
}
await dialog.accept(); await dialog.accept();
await pageClosingPromise; await pageClosingPromise;
}); });
@ -313,7 +316,9 @@ describe('Page', function () {
const { page, server, context, isHeadless } = getTestState(); const { page, server, context, isHeadless } = getTestState();
// TODO: re-enable this test in headful once crbug.com/1324480 rolls out. // TODO: re-enable this test in headful once crbug.com/1324480 rolls out.
if (!isHeadless) return; if (!isHeadless) {
return;
}
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
await page.evaluate(() => { await page.evaluate(() => {
@ -585,8 +590,11 @@ describe('Page', function () {
), ),
]); ]);
expect(message.text()).toContain('Access-Control-Allow-Origin'); expect(message.text()).toContain('Access-Control-Allow-Origin');
if (isChrome) expect(message.type()).toEqual('error'); if (isChrome) {
else expect(message.type()).toEqual('warn'); expect(message.type()).toEqual('error');
} else {
expect(message.type()).toEqual('warn');
}
}); });
it('should have location when fetch fails', async () => { it('should have location when fetch fails', async () => {
const { page, server } = getTestState(); const { page, server } = getTestState();
@ -1260,7 +1268,9 @@ describe('Page', function () {
it('should work fast enough', async () => { it('should work fast enough', async () => {
const { page } = getTestState(); const { page } = getTestState();
for (let i = 0; i < 20; ++i) await page.setContent('<div>yo</div>'); for (let i = 0; i < 20; ++i) {
await page.setContent('<div>yo</div>');
}
}); });
it('should work with tricky content', async () => { it('should work with tricky content', async () => {
const { page } = getTestState(); const { page } = getTestState();
@ -1703,7 +1713,9 @@ describe('Page', function () {
// Printing to pdf is currently only supported in headless // Printing to pdf is currently only supported in headless
const { isHeadless, page } = getTestState(); const { isHeadless, page } = getTestState();
if (!isHeadless) return; if (!isHeadless) {
return;
}
const outputFile = __dirname + '/assets/output.pdf'; const outputFile = __dirname + '/assets/output.pdf';
await page.pdf({ path: outputFile }); await page.pdf({ path: outputFile });
@ -1715,7 +1727,9 @@ describe('Page', function () {
// Printing to pdf is currently only supported in headless // Printing to pdf is currently only supported in headless
const { isHeadless, page } = getTestState(); const { isHeadless, page } = getTestState();
if (!isHeadless) return; if (!isHeadless) {
return;
}
const stream = await page.createPDFStream(); const stream = await page.createPDFStream();
let size = 0; let size = 0;
@ -1727,7 +1741,9 @@ describe('Page', function () {
it('should respect timeout', async () => { it('should respect timeout', async () => {
const { isHeadless, page, server, puppeteer } = getTestState(); const { isHeadless, page, server, puppeteer } = getTestState();
if (!isHeadless) return; if (!isHeadless) {
return;
}
await page.goto(server.PREFIX + '/pdf.html'); await page.goto(server.PREFIX + '/pdf.html');

View File

@ -40,33 +40,42 @@ describe('request interception', function () {
const actionResults: ActionResult[] = []; const actionResults: ActionResult[] = [];
await page.setRequestInterception(true); await page.setRequestInterception(true);
page.on('request', (request) => { page.on('request', (request) => {
if (request.url().endsWith('.css')) if (request.url().endsWith('.css')) {
request.continue( request.continue(
{ headers: { ...request.headers(), xaction: 'continue' } }, { headers: { ...request.headers(), xaction: 'continue' } },
expectedAction === 'continue' ? 1 : 0 expectedAction === 'continue' ? 1 : 0
); );
else request.continue({}, 0); } else {
request.continue({}, 0);
}
}); });
page.on('request', (request) => { page.on('request', (request) => {
if (request.url().endsWith('.css')) if (request.url().endsWith('.css')) {
request.respond( request.respond(
{ headers: { xaction: 'respond' } }, { headers: { xaction: 'respond' } },
expectedAction === 'respond' ? 1 : 0 expectedAction === 'respond' ? 1 : 0
); );
else request.continue({}, 0); } else {
request.continue({}, 0);
}
}); });
page.on('request', (request) => { page.on('request', (request) => {
if (request.url().endsWith('.css')) if (request.url().endsWith('.css')) {
request.abort('aborted', expectedAction === 'abort' ? 1 : 0); request.abort('aborted', expectedAction === 'abort' ? 1 : 0);
else request.continue({}, 0); } else {
request.continue({}, 0);
}
}); });
page.on('response', (response) => { page.on('response', (response) => {
const { xaction } = response.headers(); const { xaction } = response.headers();
if (response.url().endsWith('.css') && !!xaction) if (response.url().endsWith('.css') && !!xaction) {
actionResults.push(xaction as ActionResult); actionResults.push(xaction as ActionResult);
}
}); });
page.on('requestfailed', (request) => { page.on('requestfailed', (request) => {
if (request.url().endsWith('.css')) actionResults.push('abort'); if (request.url().endsWith('.css')) {
actionResults.push('abort');
}
}); });
const response = await (async () => { const response = await (async () => {
@ -172,7 +181,9 @@ describe('request interception', function () {
await page.setRequestInterception(true); await page.setRequestInterception(true);
const requests = []; const requests = [];
page.on('request', (request) => { page.on('request', (request) => {
if (!utils.isFavicon(request)) requests.push(request); if (!utils.isFavicon(request)) {
requests.push(request);
}
request.continue({}, 0); request.continue({}, 0);
}); });
await page.goto(server.PREFIX + '/one-style.html'); await page.goto(server.PREFIX + '/one-style.html');
@ -248,8 +259,11 @@ describe('request interception', function () {
await page.setRequestInterception(true); await page.setRequestInterception(true);
page.on('request', (request) => { page.on('request', (request) => {
if (request.url().endsWith('.css')) request.abort('failed', 0); if (request.url().endsWith('.css')) {
else request.continue({}, 0); request.abort('failed', 0);
} else {
request.continue({}, 0);
}
}); });
let failedRequests = 0; let failedRequests = 0;
page.on('requestfailed', () => ++failedRequests); page.on('requestfailed', () => ++failedRequests);
@ -310,8 +324,11 @@ describe('request interception', function () {
let error = null; let error = null;
await page.goto(server.EMPTY_PAGE).catch((error_) => (error = error_)); await page.goto(server.EMPTY_PAGE).catch((error_) => (error = error_));
expect(error).toBeTruthy(); expect(error).toBeTruthy();
if (isChrome) expect(error.message).toContain('net::ERR_FAILED'); if (isChrome) {
else expect(error.message).toContain('NS_ERROR_FAILURE'); expect(error.message).toContain('net::ERR_FAILED');
} else {
expect(error.message).toContain('NS_ERROR_FAILURE');
}
}); });
it('should work with redirects', async () => { it('should work with redirects', async () => {
const { page, server } = getTestState(); const { page, server } = getTestState();
@ -360,7 +377,9 @@ describe('request interception', function () {
const requests = []; const requests = [];
page.on('request', (request) => { page.on('request', (request) => {
request.continue({}, 0); request.continue({}, 0);
if (!utils.isFavicon(request)) requests.push(request); if (!utils.isFavicon(request)) {
requests.push(request);
}
}); });
server.setRedirect('/one-style.css', '/two-style.css'); server.setRedirect('/one-style.css', '/two-style.css');
server.setRedirect('/two-style.css', '/three-style.css'); server.setRedirect('/two-style.css', '/three-style.css');
@ -388,9 +407,11 @@ describe('request interception', function () {
server.setRedirect('/non-existing.json', '/non-existing-2.json'); server.setRedirect('/non-existing.json', '/non-existing-2.json');
server.setRedirect('/non-existing-2.json', '/simple.html'); server.setRedirect('/non-existing-2.json', '/simple.html');
page.on('request', (request) => { page.on('request', (request) => {
if (request.url().includes('non-existing-2')) if (request.url().includes('non-existing-2')) {
request.abort('failed', 0); request.abort('failed', 0);
else request.continue({}, 0); } else {
request.continue({}, 0);
}
}); });
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
const result = await page.evaluate(async () => { const result = await page.evaluate(async () => {
@ -400,8 +421,11 @@ describe('request interception', function () {
return error.message; return error.message;
} }
}); });
if (isChrome) expect(result).toContain('Failed to fetch'); if (isChrome) {
else expect(result).toContain('NetworkError'); expect(result).toContain('Failed to fetch');
} else {
expect(result).toContain('NetworkError');
}
}); });
it('should work with equal requests', async () => { it('should work with equal requests', async () => {
const { page, server } = getTestState(); const { page, server } = getTestState();
@ -868,6 +892,8 @@ describe('request interception', function () {
function pathToFileURL(path: string): string { function pathToFileURL(path: string): string {
let pathName = path.replace(/\\/g, '/'); let pathName = path.replace(/\\/g, '/');
// Windows drive letter must be prefixed with a slash. // Windows drive letter must be prefixed with a slash.
if (!pathName.startsWith('/')) pathName = '/' + pathName; if (!pathName.startsWith('/')) {
pathName = '/' + pathName;
}
return 'file://' + pathName; return 'file://' + pathName;
} }

View File

@ -111,7 +111,9 @@ describe('request interception', function () {
await page.setRequestInterception(true); await page.setRequestInterception(true);
const requests = []; const requests = [];
page.on('request', (request) => { page.on('request', (request) => {
if (!utils.isFavicon(request)) requests.push(request); if (!utils.isFavicon(request)) {
requests.push(request);
}
request.continue(); request.continue();
}); });
await page.goto(server.PREFIX + '/one-style.html'); await page.goto(server.PREFIX + '/one-style.html');
@ -187,8 +189,11 @@ describe('request interception', function () {
await page.setRequestInterception(true); await page.setRequestInterception(true);
page.on('request', (request) => { page.on('request', (request) => {
if (request.url().endsWith('.css')) request.abort(); if (request.url().endsWith('.css')) {
else request.continue(); request.abort();
} else {
request.continue();
}
}); });
let failedRequests = 0; let failedRequests = 0;
page.on('requestfailed', () => ++failedRequests); page.on('requestfailed', () => ++failedRequests);
@ -234,8 +239,11 @@ describe('request interception', function () {
let error = null; let error = null;
await page.goto(server.EMPTY_PAGE).catch((error_) => (error = error_)); await page.goto(server.EMPTY_PAGE).catch((error_) => (error = error_));
expect(error).toBeTruthy(); expect(error).toBeTruthy();
if (isChrome) expect(error.message).toContain('net::ERR_FAILED'); if (isChrome) {
else expect(error.message).toContain('NS_ERROR_FAILURE'); expect(error.message).toContain('net::ERR_FAILED');
} else {
expect(error.message).toContain('NS_ERROR_FAILURE');
}
}); });
it('should work with redirects', async () => { it('should work with redirects', async () => {
const { page, server } = getTestState(); const { page, server } = getTestState();
@ -284,7 +292,9 @@ describe('request interception', function () {
const requests = []; const requests = [];
page.on('request', (request) => { page.on('request', (request) => {
request.continue(); request.continue();
if (!utils.isFavicon(request)) requests.push(request); if (!utils.isFavicon(request)) {
requests.push(request);
}
}); });
server.setRedirect('/one-style.css', '/two-style.css'); server.setRedirect('/one-style.css', '/two-style.css');
server.setRedirect('/two-style.css', '/three-style.css'); server.setRedirect('/two-style.css', '/three-style.css');
@ -312,8 +322,11 @@ describe('request interception', function () {
server.setRedirect('/non-existing.json', '/non-existing-2.json'); server.setRedirect('/non-existing.json', '/non-existing-2.json');
server.setRedirect('/non-existing-2.json', '/simple.html'); server.setRedirect('/non-existing-2.json', '/simple.html');
page.on('request', (request) => { page.on('request', (request) => {
if (request.url().includes('non-existing-2')) request.abort(); if (request.url().includes('non-existing-2')) {
else request.continue(); request.abort();
} else {
request.continue();
}
}); });
await page.goto(server.EMPTY_PAGE); await page.goto(server.EMPTY_PAGE);
const result = await page.evaluate(async () => { const result = await page.evaluate(async () => {
@ -323,8 +336,11 @@ describe('request interception', function () {
return error.message; return error.message;
} }
}); });
if (isChrome) expect(result).toContain('Failed to fetch'); if (isChrome) {
else expect(result).toContain('NetworkError'); expect(result).toContain('Failed to fetch');
} else {
expect(result).toContain('NetworkError');
}
}); });
it('should work with equal requests', async () => { it('should work with equal requests', async () => {
const { page, server } = getTestState(); const { page, server } = getTestState();
@ -809,6 +825,8 @@ describe('request interception', function () {
function pathToFileURL(path: string): string { function pathToFileURL(path: string): string {
let pathName = path.replace(/\\/g, '/'); let pathName = path.replace(/\\/g, '/');
// Windows drive letter must be prefixed with a slash. // Windows drive letter must be prefixed with a slash.
if (!pathName.startsWith('/')) pathName = '/' + pathName; if (!pathName.startsWith('/')) {
pathName = '/' + pathName;
}
return 'file://' + pathName; return 'file://' + pathName;
} }

View File

@ -112,15 +112,17 @@ describe('Screenshots', function () {
}) })
); );
const promises = []; const promises = [];
for (let i = 0; i < N; ++i) for (let i = 0; i < N; ++i) {
promises.push( promises.push(
pages[i].screenshot({ pages[i].screenshot({
clip: { x: 50 * i, y: 0, width: 50, height: 50 }, clip: { x: 50 * i, y: 0, width: 50, height: 50 },
}) })
); );
}
const screenshots = await Promise.all(promises); const screenshots = await Promise.all(promises);
for (let i = 0; i < N; ++i) for (let i = 0; i < N; ++i) {
expect(screenshots[i]).toBeGolden(`grid-cell-${i}.png`); expect(screenshots[i]).toBeGolden(`grid-cell-${i}.png`);
}
await Promise.all(pages.map((page) => page.close())); await Promise.all(pages.map((page) => page.close()));
}); });
itFailsFirefox('should allow transparency', async () => { itFailsFirefox('should allow transparency', async () => {

View File

@ -288,7 +288,9 @@ describe('Target', function () {
resolved = true; resolved = true;
if (error instanceof puppeteer.errors.TimeoutError) { if (error instanceof puppeteer.errors.TimeoutError) {
console.error(error); console.error(error);
} else throw error; } else {
throw error;
}
}); });
const page = await browser.newPage(); const page = await browser.newPage();
expect(resolved).toBe(false); expect(resolved).toBe(false);
@ -299,7 +301,9 @@ describe('Target', function () {
} catch (error) { } catch (error) {
if (error instanceof puppeteer.errors.TimeoutError) { if (error instanceof puppeteer.errors.TimeoutError) {
console.error(error); console.error(error);
} else throw error; } else {
throw error;
}
} }
await page.close(); await page.close();
}); });

View File

@ -111,10 +111,13 @@ const utils = (module.exports = {
dumpFrames: function (frame, indentation) { dumpFrames: function (frame, indentation) {
indentation = indentation || ''; indentation = indentation || '';
let description = frame.url().replace(/:\d{4}\//, ':<PORT>/'); let description = frame.url().replace(/:\d{4}\//, ':<PORT>/');
if (frame.name()) description += ' (' + frame.name() + ')'; if (frame.name()) {
description += ' (' + frame.name() + ')';
}
const result = [indentation + description]; const result = [indentation + description];
for (const child of frame.childFrames()) for (const child of frame.childFrames()) {
result.push(...utils.dumpFrames(child, ' ' + indentation)); result.push(...utils.dumpFrames(child, ' ' + indentation));
}
return result; return result;
}, },
@ -126,7 +129,9 @@ const utils = (module.exports = {
waitEvent: function (emitter, eventName, predicate = () => true) { waitEvent: function (emitter, eventName, predicate = () => true) {
return new Promise((fulfill) => { return new Promise((fulfill) => {
emitter.on(eventName, function listener(event) { emitter.on(eventName, function listener(event) {
if (!predicate(event)) return; if (!predicate(event)) {
return;
}
emitter.removeListener(eventName, listener); emitter.removeListener(eventName, listener);
fulfill(event); fulfill(event);
}); });

View File

@ -146,7 +146,9 @@ describe('waittask specs', function () {
await page.evaluateOnNewDocument(() => (globalThis.__RELOADED = true)); await page.evaluateOnNewDocument(() => (globalThis.__RELOADED = true));
await page.waitForFunction(() => { await page.waitForFunction(() => {
if (!globalThis.__RELOADED) window.location.reload(); if (!globalThis.__RELOADED) {
window.location.reload();
}
return true; return true;
}); });
}); });

View File

@ -49,13 +49,17 @@ async function compileTypeScript() {
async function compileTypeScriptIfRequired() { async function compileTypeScriptIfRequired() {
const libPath = path.join(__dirname, 'lib'); const libPath = path.join(__dirname, 'lib');
const libExists = await fileExists(libPath); const libExists = await fileExists(libPath);
if (libExists) return; if (libExists) {
return;
}
console.log('Puppeteer:', 'Compiling TypeScript...'); console.log('Puppeteer:', 'Compiling TypeScript...');
await compileTypeScript(); await compileTypeScript();
} }
// It's being run as node typescript-if-required.js, not require('..') // It's being run as node typescript-if-required.js, not require('..')
if (require.main === module) compileTypeScriptIfRequired(); if (require.main === module) {
compileTypeScriptIfRequired();
}
module.exports = compileTypeScriptIfRequired; module.exports = compileTypeScriptIfRequired;

View File

@ -27,7 +27,9 @@ class ESTreeWalker {
* @param {?ESTree.Node} parent * @param {?ESTree.Node} parent
*/ */
_innerWalk(node, parent) { _innerWalk(node, parent) {
if (!node) return; if (!node) {
return;
}
node.parent = parent; node.parent = parent;
if (this._beforeVisit.call(null, node) === ESTreeWalker.SkipSubtree) { if (this._beforeVisit.call(null, node) === ESTreeWalker.SkipSubtree) {
@ -36,7 +38,9 @@ class ESTreeWalker {
} }
const walkOrder = ESTreeWalker._walkOrder[node.type]; const walkOrder = ESTreeWalker._walkOrder[node.type];
if (!walkOrder) return; if (!walkOrder) {
return;
}
if (node.type === 'TemplateLiteral') { if (node.type === 'TemplateLiteral') {
const templateLiteral = /** @type {!ESTree.TemplateLiteralNode} */ (node); const templateLiteral = /** @type {!ESTree.TemplateLiteralNode} */ (node);
@ -52,8 +56,11 @@ class ESTreeWalker {
} else { } else {
for (let i = 0; i < walkOrder.length; ++i) { for (let i = 0; i < walkOrder.length; ++i) {
const entity = node[walkOrder[i]]; const entity = node[walkOrder[i]];
if (Array.isArray(entity)) this._walkArray(entity, node); if (Array.isArray(entity)) {
else this._innerWalk(entity, node); this._walkArray(entity, node);
} else {
this._innerWalk(entity, node);
}
} }
} }
@ -65,10 +72,11 @@ class ESTreeWalker {
* @param {?ESTree.Node} parentNode * @param {?ESTree.Node} parentNode
*/ */
_walkArray(nodeArray, parentNode) { _walkArray(nodeArray, parentNode) {
for (let i = 0; i < nodeArray.length; ++i) for (let i = 0; i < nodeArray.length; ++i) {
this._innerWalk(nodeArray[i], parentNode); this._innerWalk(nodeArray[i], parentNode);
} }
} }
}
/** @typedef {!Object} ESTreeWalker.SkipSubtree */ /** @typedef {!Object} ESTreeWalker.SkipSubtree */
ESTreeWalker.SkipSubtree = {}; ESTreeWalker.SkipSubtree = {};

View File

@ -111,14 +111,18 @@ if (argv.script && !fs.existsSync(scriptPath)) {
while (true) { while (true) {
const middle = Math.round((good + bad) / 2); const middle = Math.round((good + bad) / 2);
const revision = await findDownloadableRevision(middle, good, bad); const revision = await findDownloadableRevision(middle, good, bad);
if (!revision || revision === good || revision === bad) break; if (!revision || revision === good || revision === bad) {
break;
}
let info = browserFetcher.revisionInfo(revision); let info = browserFetcher.revisionInfo(revision);
const shouldRemove = noCache && !info.local; const shouldRemove = noCache && !info.local;
info = await downloadRevision(revision); info = await downloadRevision(revision);
const exitCode = await (pattern const exitCode = await (pattern
? runUnitTest(pattern, info) ? runUnitTest(pattern, info)
: runScript(scriptPath, info)); : runScript(scriptPath, info));
if (shouldRemove) await browserFetcher.remove(revision); if (shouldRemove) {
await browserFetcher.remove(revision);
}
let outcome; let outcome;
if (exitCode) { if (exitCode) {
bad = revision; bad = revision;
@ -224,7 +228,9 @@ async function findDownloadableRevision(rev, from, to) {
const min = Math.min(from, to); const min = Math.min(from, to);
const max = Math.max(from, to); const max = Math.max(from, to);
log(`Looking around ${rev} from [${min}, ${max}]`); log(`Looking around ${rev} from [${min}, ${max}]`);
if (await browserFetcher.canDownload(rev)) return rev; if (await browserFetcher.canDownload(rev)) {
return rev;
}
let down = rev; let down = rev;
let up = rev; let up = rev;
while (min <= down || up <= max) { while (min <= down || up <= max) {
@ -232,8 +238,12 @@ async function findDownloadableRevision(rev, from, to) {
down > min ? probe(--down) : Promise.resolve(false), down > min ? probe(--down) : Promise.resolve(false),
up < max ? probe(++up) : Promise.resolve(false), up < max ? probe(++up) : Promise.resolve(false),
]); ]);
if (downOk) return down; if (downOk) {
if (upOk) return up; return down;
}
if (upOk) {
return up;
}
} }
return null; return null;
@ -283,6 +293,8 @@ function getChromiumRevision(gitRevision = null) {
}); });
const m = result.match(/chromium: '(\d+)'/); const m = result.match(/chromium: '(\d+)'/);
if (!m) return null; if (!m) {
return null;
}
return parseInt(m[1], 10); return parseInt(m[1], 10);
} }

View File

@ -43,8 +43,9 @@ class Table {
drawRow(values) { drawRow(values) {
assert(values.length === this.widths.length); assert(values.length === this.widths.length);
let row = ''; let row = '';
for (let i = 0; i < values.length; ++i) for (let i = 0; i < values.length; ++i) {
row += padCenter(values[i], this.widths[i]); row += padCenter(values[i], this.widths[i]);
}
console.log(row); console.log(row);
} }
} }
@ -262,9 +263,13 @@ async function checkRangeAvailability({
} }
table.drawRow(values); table.drawRow(values);
} else { } else {
if (allAvailable) console.log(revision); if (allAvailable) {
console.log(revision);
}
}
if (allAvailable && stopWhenAllAvailable) {
break;
} }
if (allAvailable && stopWhenAllAvailable) break;
} }
} }
@ -328,7 +333,9 @@ function filterOutColors(text) {
*/ */
function padCenter(text, length) { function padCenter(text, length) {
const printableCharacters = filterOutColors(text); const printableCharacters = filterOutColors(text);
if (printableCharacters.length >= length) return text; if (printableCharacters.length >= length) {
return text;
}
const left = Math.floor((length - printableCharacters.length) / 2); const left = Math.floor((length - printableCharacters.length) / 2);
const right = Math.ceil((length - printableCharacters.length) / 2); const right = Math.ceil((length - printableCharacters.length) / 2);
return spaceString(left) + text + spaceString(right); return spaceString(left) + text + spaceString(right);

View File

@ -63,7 +63,9 @@ class Source {
* @returns {boolean} * @returns {boolean}
*/ */
setText(text) { setText(text) {
if (text === this._text) return false; if (text === this._text) {
return false;
}
this._hasUpdatedText = true; this._hasUpdatedText = true;
this._text = text; this._text = text;
return true; return true;

View File

@ -22,7 +22,9 @@ class Documentation {
this.classesArray = classesArray; this.classesArray = classesArray;
/** @type {!Map<string, !Documentation.Class>} */ /** @type {!Map<string, !Documentation.Class>} */
this.classes = new Map(); this.classes = new Map();
for (const cls of classesArray) this.classes.set(cls.name, cls); for (const cls of classesArray) {
this.classes.set(cls.name, cls);
}
} }
} }
@ -93,7 +95,9 @@ Documentation.Member = class {
this.required = required; this.required = required;
/** @type {!Map<string, !Documentation.Member>} */ /** @type {!Map<string, !Documentation.Member>} */
this.args = new Map(); this.args = new Map();
for (const arg of argsArray) this.args.set(arg.name, arg); for (const arg of argsArray) {
this.args.set(arg.name, arg);
}
} }
/** /**

View File

@ -12,7 +12,7 @@ function checkSources(sources) {
const eventsSource = sources.find((source) => source.name() === 'Events.js'); const eventsSource = sources.find((source) => source.name() === 'Events.js');
if (eventsSource) { if (eventsSource) {
const { Events } = require(eventsSource.filePath()); const { Events } = require(eventsSource.filePath());
for (const [className, events] of Object.entries(Events)) for (const [className, events] of Object.entries(Events)) {
classEvents.set( classEvents.set(
className, className,
Array.from(Object.values(events)) Array.from(Object.values(events))
@ -20,6 +20,7 @@ function checkSources(sources) {
.map((e) => Documentation.Member.createEvent(e)) .map((e) => Documentation.Member.createEvent(e))
); );
} }
}
const excludeClasses = new Set([]); const excludeClasses = new Set([]);
const program = ts.createProgram({ const program = ts.createProgram({
@ -78,7 +79,9 @@ function checkSources(sources) {
for (const member of wp.membersArray) { for (const member of wp.membersArray) {
// Member was overridden. // Member was overridden.
const memberId = member.kind + ':' + member.name; const memberId = member.kind + ':' + member.name;
if (membersMap.has(memberId)) continue; if (membersMap.has(memberId)) {
continue;
}
membersMap.set(memberId, member); membersMap.set(memberId, member);
} }
} }
@ -98,13 +101,17 @@ function checkSources(sources) {
if (className === '__class') { if (className === '__class') {
let parent = node; let parent = node;
while (parent.parent) parent = parent.parent; while (parent.parent) {
parent = parent.parent;
}
className = path.basename(parent.fileName, '.js'); className = path.basename(parent.fileName, '.js');
} }
if (className && !excludeClasses.has(className)) { if (className && !excludeClasses.has(className)) {
classes.push(serializeClass(className, symbol, node)); classes.push(serializeClass(className, symbol, node));
const parentClassName = parentClass(node); const parentClassName = parentClass(node);
if (parentClassName) inheritance.set(className, parentClassName); if (parentClassName) {
inheritance.set(className, parentClassName);
}
excludeClasses.add(className); excludeClasses.add(className);
} }
} }
@ -139,7 +146,9 @@ function checkSources(sources) {
* isn't going to be here for much longer so we'll just silence this * isn't going to be here for much longer so we'll just silence this
* warning than try to add support which would warrant a huge rewrite. * warning than try to add support which would warrant a huge rewrite.
*/ */
if (name !== 'paramArgs') throw error; if (name !== 'paramArgs') {
throw error;
}
} }
} }
return Documentation.Member.createProperty( return Documentation.Member.createProperty(
@ -152,13 +161,27 @@ function checkSources(sources) {
* @param {!ts.ObjectType} type * @param {!ts.ObjectType} type
*/ */
function isRegularObject(type) { function isRegularObject(type) {
if (type.isIntersection()) return true; if (type.isIntersection()) {
if (!type.objectFlags) return false; return true;
if (!('aliasSymbol' in type)) return false; }
if (type.getConstructSignatures().length) return false; if (!type.objectFlags) {
if (type.getCallSignatures().length) return false; return false;
if (type.isLiteral()) return false; }
if (type.isUnion()) return false; if (!('aliasSymbol' in type)) {
return false;
}
if (type.getConstructSignatures().length) {
return false;
}
if (type.getCallSignatures().length) {
return false;
}
if (type.isLiteral()) {
return false;
}
if (type.isUnion()) {
return false;
}
return true; return true;
} }
@ -173,16 +196,18 @@ function checkSources(sources) {
typeName === 'any' || typeName === 'any' ||
typeName === '{ [x: string]: string; }' || typeName === '{ [x: string]: string; }' ||
typeName === '{}' typeName === '{}'
) ) {
typeName = 'Object'; typeName = 'Object';
}
const nextCircular = [typeName].concat(circular); const nextCircular = [typeName].concat(circular);
if (isRegularObject(type)) { if (isRegularObject(type)) {
let properties = undefined; let properties = undefined;
if (!circular.includes(typeName)) if (!circular.includes(typeName)) {
properties = type properties = type
.getProperties() .getProperties()
.map((property) => serializeSymbol(property, nextCircular)); .map((property) => serializeSymbol(property, nextCircular));
}
return new Documentation.Type('Object', properties); return new Documentation.Type('Object', properties);
} }
if (type.isUnion() && typeName.includes('|')) { if (type.isUnion() && typeName.includes('|')) {
@ -199,14 +224,17 @@ function checkSources(sources) {
const innerTypeNames = []; const innerTypeNames = [];
for (const typeArgument of type.typeArguments) { for (const typeArgument of type.typeArguments) {
const innerType = serializeType(typeArgument, nextCircular); const innerType = serializeType(typeArgument, nextCircular);
if (innerType.properties) properties.push(...innerType.properties); if (innerType.properties) {
properties.push(...innerType.properties);
}
innerTypeNames.push(innerType.name); innerTypeNames.push(innerType.name);
} }
if ( if (
innerTypeNames.length === 0 || innerTypeNames.length === 0 ||
(innerTypeNames.length === 1 && innerTypeNames[0] === 'void') (innerTypeNames.length === 1 && innerTypeNames[0] === 'void')
) ) {
return new Documentation.Type(type.symbol.name); return new Documentation.Type(type.symbol.name);
}
return new Documentation.Type( return new Documentation.Type(
`${type.symbol.name}<${innerTypeNames.join(', ')}>`, `${type.symbol.name}<${innerTypeNames.join(', ')}>`,
properties properties
@ -241,15 +269,20 @@ function checkSources(sources) {
* but in TypeScript we use the private keyword * but in TypeScript we use the private keyword
* hence we check for either here. * hence we check for either here.
*/ */
if (name.startsWith('_') || symbolHasPrivateModifier(member)) continue; if (name.startsWith('_') || symbolHasPrivateModifier(member)) {
continue;
}
const memberType = checker.getTypeOfSymbolAtLocation( const memberType = checker.getTypeOfSymbolAtLocation(
member, member,
member.valueDeclaration member.valueDeclaration
); );
const signature = memberType.getCallSignatures()[0]; const signature = memberType.getCallSignatures()[0];
if (signature) members.push(serializeSignature(name, signature)); if (signature) {
else members.push(serializeProperty(name, memberType)); members.push(serializeSignature(name, signature));
} else {
members.push(serializeProperty(name, memberType));
}
} }
return new Documentation.Class(className, members); return new Documentation.Class(className, members);

View File

@ -92,9 +92,15 @@ class MDOutline {
const start = str.indexOf('<') + 1; const start = str.indexOf('<') + 1;
let count = 1; let count = 1;
for (let i = start; i < str.length; i++) { for (let i = start; i < str.length; i++) {
if (str[i] === '<') count++; if (str[i] === '<') {
if (str[i] === '>') count--; count++;
if (!count) return str.substring(start, i); }
if (str[i] === '>') {
count--;
}
if (!count) {
return str.substring(start, i);
}
} }
return 'unknown'; return 'unknown';
} }
@ -138,7 +144,7 @@ class MDOutline {
* @param {Node} content * @param {Node} content
*/ */
function parseComment(content) { function parseComment(content) {
for (const code of content.querySelectorAll('pre > code')) for (const code of content.querySelectorAll('pre > code')) {
code.replaceWith( code.replaceWith(
'```' + '```' +
code.className.substring('language-'.length) + code.className.substring('language-'.length) +
@ -146,10 +152,13 @@ class MDOutline {
code.textContent + code.textContent +
'```' '```'
); );
for (const code of content.querySelectorAll('code')) }
for (const code of content.querySelectorAll('code')) {
code.replaceWith('`' + code.textContent + '`'); code.replaceWith('`' + code.textContent + '`');
for (const strong of content.querySelectorAll('strong')) }
for (const strong of content.querySelectorAll('strong')) {
strong.replaceWith('**' + parseComment(strong) + '**'); strong.replaceWith('**' + parseComment(strong) + '**');
}
return content.textContent.trim(); return content.textContent.trim();
} }
@ -207,12 +216,13 @@ class MDOutline {
0, 0,
Math.min(angleIndex, spaceIndex) Math.min(angleIndex, spaceIndex)
); );
if (actualText !== expectedText) if (actualText !== expectedText) {
errors.push( errors.push(
`${name} has mistyped 'return' type declaration: expected exactly '${expectedText}', found '${actualText}'.` `${name} has mistyped 'return' type declaration: expected exactly '${expectedText}', found '${actualText}'.`
); );
} }
} }
}
const comment = parseComment( const comment = parseComment(
extractSiblingsIntoFragment(ul ? ul.nextSibling : content) extractSiblingsIntoFragment(ul ? ul.nextSibling : content)
); );
@ -257,7 +267,9 @@ class MDOutline {
let currentClassExtends = null; let currentClassExtends = null;
for (const cls of classes) { for (const cls of classes) {
const match = cls.name.match(classHeading); const match = cls.name.match(classHeading);
if (!match) continue; if (!match) {
continue;
}
currentClassName = match[1]; currentClassName = match[1];
currentClassComment = cls.comment; currentClassComment = cls.comment;
currentClassExtends = cls.extendsName; currentClassExtends = cls.extendsName;
@ -290,7 +302,7 @@ class MDOutline {
return; return;
} }
parameters = parameters.trim().replace(/[\[\]]/g, ''); parameters = parameters.trim().replace(/[\[\]]/g, '');
if (parameters !== member.args.map((arg) => arg.name).join(', ')) if (parameters !== member.args.map((arg) => arg.name).join(', ')) {
this.errors.push( this.errors.push(
`Heading arguments for "${ `Heading arguments for "${
member.name member.name
@ -298,6 +310,7 @@ class MDOutline {
.map((a) => a.name) .map((a) => a.name)
.join(', ')}"` .join(', ')}"`
); );
}
const args = member.args.map(createPropertyFromJSON); const args = member.args.map(createPropertyFromJSON);
let returnType = null; let returnType = null;
let returnComment = ''; let returnComment = '';
@ -369,7 +382,9 @@ class MDOutline {
} }
function flushClassIfNeeded() { function flushClassIfNeeded() {
if (currentClassName === null) return; if (currentClassName === null) {
return;
}
this.classes.push( this.classes.push(
new Documentation.Class( new Documentation.Class(
currentClassName, currentClassName,

View File

@ -78,46 +78,62 @@ function checkSorting(doc) {
; ;
eventIndex < members.length && members[eventIndex].kind === 'event'; eventIndex < members.length && members[eventIndex].kind === 'event';
++eventIndex ++eventIndex
); ) {}
for ( for (
; ;
eventIndex < members.length && members[eventIndex].kind !== 'event'; eventIndex < members.length && members[eventIndex].kind !== 'event';
++eventIndex ++eventIndex
); ) {}
if (eventIndex < members.length) if (eventIndex < members.length) {
errors.push( errors.push(
`Events should go first. Event '${members[eventIndex].name}' in class ${cls.name} breaks order` `Events should go first. Event '${members[eventIndex].name}' in class ${cls.name} breaks order`
); );
}
// Constructor should be right after events and before all other members. // Constructor should be right after events and before all other members.
const constructorIndex = members.findIndex( const constructorIndex = members.findIndex(
(member) => member.kind === 'method' && member.name === 'constructor' (member) => member.kind === 'method' && member.name === 'constructor'
); );
if (constructorIndex > 0 && members[constructorIndex - 1].kind !== 'event') if (
constructorIndex > 0 &&
members[constructorIndex - 1].kind !== 'event'
) {
errors.push(`Constructor of ${cls.name} should go before other methods`); errors.push(`Constructor of ${cls.name} should go before other methods`);
}
// Events should be sorted alphabetically. // Events should be sorted alphabetically.
for (let i = 0; i < members.length - 1; ++i) { for (let i = 0; i < members.length - 1; ++i) {
const member1 = cls.membersArray[i]; const member1 = cls.membersArray[i];
const member2 = cls.membersArray[i + 1]; const member2 = cls.membersArray[i + 1];
if (member1.kind !== 'event' || member2.kind !== 'event') continue; if (member1.kind !== 'event' || member2.kind !== 'event') {
if (member1.name > member2.name) continue;
}
if (member1.name > member2.name) {
errors.push( errors.push(
`Event '${member1.name}' in class ${cls.name} breaks alphabetic ordering of events` `Event '${member1.name}' in class ${cls.name} breaks alphabetic ordering of events`
); );
} }
}
// All other members should be sorted alphabetically. // All other members should be sorted alphabetically.
for (let i = 0; i < members.length - 1; ++i) { for (let i = 0; i < members.length - 1; ++i) {
const member1 = cls.membersArray[i]; const member1 = cls.membersArray[i];
const member2 = cls.membersArray[i + 1]; const member2 = cls.membersArray[i + 1];
if (member1.kind === 'event' || member2.kind === 'event') continue; if (member1.kind === 'event' || member2.kind === 'event') {
if (member1.kind === 'method' && member1.name === 'constructor') continue; continue;
}
if (member1.kind === 'method' && member1.name === 'constructor') {
continue;
}
if (member1.name > member2.name) { if (member1.name > member2.name) {
let memberName1 = `${cls.name}.${member1.name}`; let memberName1 = `${cls.name}.${member1.name}`;
if (member1.kind === 'method') memberName1 += '()'; if (member1.kind === 'method') {
memberName1 += '()';
}
let memberName2 = `${cls.name}.${member2.name}`; let memberName2 = `${cls.name}.${member2.name}`;
if (member2.kind === 'method') memberName2 += '()'; if (member2.kind === 'method') {
memberName2 += '()';
}
errors.push( errors.push(
`Bad alphabetic ordering of ${cls.name} members: ${memberName1} should go after ${memberName2}` `Bad alphabetic ordering of ${cls.name} members: ${memberName1} should go after ${memberName2}`
); );
@ -136,7 +152,9 @@ function filterJSDocumentation(jsDocumentation) {
// Filter private classes and methods. // Filter private classes and methods.
const classes = []; const classes = [];
for (const cls of jsDocumentation.classesArray) { for (const cls of jsDocumentation.classesArray) {
if (includedClasses && !includedClasses.has(cls.name)) continue; if (includedClasses && !includedClasses.has(cls.name)) {
continue;
}
const members = cls.membersArray.filter( const members = cls.membersArray.filter(
(member) => !EXCLUDE_PROPERTIES.has(`${cls.name}.${member.name}`) (member) => !EXCLUDE_PROPERTIES.has(`${cls.name}.${member.name}`)
); );
@ -154,22 +172,25 @@ function checkDuplicates(doc) {
const classes = new Set(); const classes = new Set();
// Report duplicates. // Report duplicates.
for (const cls of doc.classesArray) { for (const cls of doc.classesArray) {
if (classes.has(cls.name)) if (classes.has(cls.name)) {
errors.push(`Duplicate declaration of class ${cls.name}`); errors.push(`Duplicate declaration of class ${cls.name}`);
}
classes.add(cls.name); classes.add(cls.name);
const members = new Set(); const members = new Set();
for (const member of cls.membersArray) { for (const member of cls.membersArray) {
if (members.has(member.kind + ' ' + member.name)) if (members.has(member.kind + ' ' + member.name)) {
errors.push( errors.push(
`Duplicate declaration of ${member.kind} ${cls.name}.${member.name}()` `Duplicate declaration of ${member.kind} ${cls.name}.${member.name}()`
); );
}
members.add(member.kind + ' ' + member.name); members.add(member.kind + ' ' + member.name);
const args = new Set(); const args = new Set();
for (const arg of member.argsArray) { for (const arg of member.argsArray) {
if (args.has(arg.name)) if (args.has(arg.name)) {
errors.push( errors.push(
`Duplicate declaration of argument ${cls.name}.${member.name} "${arg.name}"` `Duplicate declaration of argument ${cls.name}.${member.name} "${arg.name}"`
); );
}
args.add(arg.name); args.add(arg.name);
} }
} }
@ -220,8 +241,9 @@ function compareDocumentations(actual, expected) {
'launch', 'launch',
]); ]);
for (const className of classesDiff.extra) for (const className of classesDiff.extra) {
errors.push(`Non-existing class found: ${className}`); errors.push(`Non-existing class found: ${className}`);
}
for (const className of classesDiff.missing) { for (const className of classesDiff.missing) {
if (className === 'PuppeteerNode') { if (className === 'PuppeteerNode') {
@ -249,8 +271,9 @@ function compareDocumentations(actual, expected) {
for (const methodName of methodDiff.missing) { for (const methodName of methodDiff.missing) {
const missingMethodsForClass = expectedNotFoundMethods.get(className); const missingMethodsForClass = expectedNotFoundMethods.get(className);
if (missingMethodsForClass && missingMethodsForClass.has(methodName)) if (missingMethodsForClass && missingMethodsForClass.has(methodName)) {
continue; continue;
}
errors.push(`Method not found: ${className}.${methodName}()`); errors.push(`Method not found: ${className}.${methodName}()`);
} }
@ -258,14 +281,15 @@ function compareDocumentations(actual, expected) {
const actualMethod = actualClass.methods.get(methodName); const actualMethod = actualClass.methods.get(methodName);
const expectedMethod = expectedClass.methods.get(methodName); const expectedMethod = expectedClass.methods.get(methodName);
if (!actualMethod.type !== !expectedMethod.type) { if (!actualMethod.type !== !expectedMethod.type) {
if (actualMethod.type) if (actualMethod.type) {
errors.push( errors.push(
`Method ${className}.${methodName} has unneeded description of return type` `Method ${className}.${methodName} has unneeded description of return type`
); );
else } else {
errors.push( errors.push(
`Method ${className}.${methodName} is missing return type description` `Method ${className}.${methodName} is missing return type description`
); );
}
} else if (actualMethod.hasReturn) { } else if (actualMethod.hasReturn) {
checkType( checkType(
`Method ${className}.${methodName} has the wrong return type: `, `Method ${className}.${methodName} has the wrong return type: `,
@ -287,21 +311,24 @@ function compareDocumentations(actual, expected) {
const text = [ const text = [
`Method ${className}.${methodName}() fails to describe its parameters:`, `Method ${className}.${methodName}() fails to describe its parameters:`,
]; ];
for (const arg of argsDiff.missing) for (const arg of argsDiff.missing) {
text.push(`- Argument not found: ${arg}`); text.push(`- Argument not found: ${arg}`);
for (const arg of argsDiff.extra) }
for (const arg of argsDiff.extra) {
text.push(`- Non-existing argument found: ${arg}`); text.push(`- Non-existing argument found: ${arg}`);
}
errors.push(text.join('\n')); errors.push(text.join('\n'));
} }
} }
for (const arg of argsDiff.equal) for (const arg of argsDiff.equal) {
checkProperty( checkProperty(
`Method ${className}.${methodName}()`, `Method ${className}.${methodName}()`,
actualMethod.args.get(arg), actualMethod.args.get(arg),
expectedMethod.args.get(arg) expectedMethod.args.get(arg)
); );
} }
}
const actualProperties = Array.from(actualClass.properties.keys()).sort(); const actualProperties = Array.from(actualClass.properties.keys()).sort();
const expectedProperties = Array.from( const expectedProperties = Array.from(
expectedClass.properties.keys() expectedClass.properties.keys()
@ -313,19 +340,22 @@ function compareDocumentations(actual, expected) {
} }
errors.push(`Non-existing property found: ${className}.${propertyName}`); errors.push(`Non-existing property found: ${className}.${propertyName}`);
} }
for (const propertyName of propertyDiff.missing) for (const propertyName of propertyDiff.missing) {
errors.push(`Property not found: ${className}.${propertyName}`); errors.push(`Property not found: ${className}.${propertyName}`);
}
const actualEvents = Array.from(actualClass.events.keys()).sort(); const actualEvents = Array.from(actualClass.events.keys()).sort();
const expectedEvents = Array.from(expectedClass.events.keys()).sort(); const expectedEvents = Array.from(expectedClass.events.keys()).sort();
const eventsDiff = diff(actualEvents, expectedEvents); const eventsDiff = diff(actualEvents, expectedEvents);
for (const eventName of eventsDiff.extra) for (const eventName of eventsDiff.extra) {
errors.push( errors.push(
`Non-existing event found in class ${className}: '${eventName}'` `Non-existing event found in class ${className}: '${eventName}'`
); );
for (const eventName of eventsDiff.missing) }
for (const eventName of eventsDiff.missing) {
errors.push(`Event not found in class ${className}: '${eventName}'`); errors.push(`Event not found in class ${className}: '${eventName}'`);
} }
}
/** /**
* @param {string} source * @param {string} source
@ -1052,7 +1082,9 @@ function compareDocumentations(actual, expected) {
]); ]);
const expectedForSource = expectedNamingMismatches.get(source); const expectedForSource = expectedNamingMismatches.get(source);
if (!expectedForSource) return false; if (!expectedForSource) {
return false;
}
const namingMismatchIsExpected = const namingMismatchIsExpected =
expectedForSource.actualName === actualName && expectedForSource.actualName === actualName &&
@ -1068,8 +1100,12 @@ function compareDocumentations(actual, expected) {
*/ */
function checkType(source, actual, expected) { function checkType(source, actual, expected) {
// TODO(@JoelEinbinder): check functions and Serializable // TODO(@JoelEinbinder): check functions and Serializable
if (actual.name.includes('unction') || actual.name.includes('Serializable')) if (
actual.name.includes('unction') ||
actual.name.includes('Serializable')
) {
return; return;
}
// We don't have nullchecks on for TypeScript // We don't have nullchecks on for TypeScript
const actualName = actual.name.replace(/[\? ]/g, ''); const actualName = actual.name.replace(/[\? ]/g, '');
// TypeScript likes to add some spaces // TypeScript likes to add some spaces
@ -1079,13 +1115,16 @@ function compareDocumentations(actual, expected) {
actualName, actualName,
expectedName expectedName
); );
if (expectedName !== actualName && !namingMismatchIsExpected) if (expectedName !== actualName && !namingMismatchIsExpected) {
errors.push(`${source} ${actualName} != ${expectedName}`); errors.push(`${source} ${actualName} != ${expectedName}`);
}
/* If we got a naming mismatch and it was expected, don't check the properties /* If we got a naming mismatch and it was expected, don't check the properties
* as they will likely be considered "wrong" by DocLint too. * as they will likely be considered "wrong" by DocLint too.
*/ */
if (namingMismatchIsExpected) return; if (namingMismatchIsExpected) {
return;
}
/* Some methods cause errors in the property checks for an unknown reason /* Some methods cause errors in the property checks for an unknown reason
* so we support a list of methods whose parameters are not checked. * so we support a list of methods whose parameters are not checked.
@ -1096,7 +1135,9 @@ function compareDocumentations(actual, expected) {
'Method Puppeteer.connect() options', 'Method Puppeteer.connect() options',
'Method Page.setUserAgent() userAgentMetadata', 'Method Page.setUserAgent() userAgentMetadata',
]); ]);
if (skipPropertyChecksOnMethods.has(source)) return; if (skipPropertyChecksOnMethods.has(source)) {
return;
}
const actualPropertiesMap = new Map( const actualPropertiesMap = new Map(
actual.properties.map((property) => [property.name, property.type]) actual.properties.map((property) => [property.name, property.type])
@ -1108,17 +1149,20 @@ function compareDocumentations(actual, expected) {
Array.from(actualPropertiesMap.keys()).sort(), Array.from(actualPropertiesMap.keys()).sort(),
Array.from(expectedPropertiesMap.keys()).sort() Array.from(expectedPropertiesMap.keys()).sort()
); );
for (const propertyName of propertiesDiff.extra) for (const propertyName of propertiesDiff.extra) {
errors.push(`${source} has unexpected property ${propertyName}`); errors.push(`${source} has unexpected property ${propertyName}`);
for (const propertyName of propertiesDiff.missing) }
for (const propertyName of propertiesDiff.missing) {
errors.push(`${source} is missing property ${propertyName}`); errors.push(`${source} is missing property ${propertyName}`);
for (const propertyName of propertiesDiff.equal) }
for (const propertyName of propertiesDiff.equal) {
checkType( checkType(
source + '.' + propertyName, source + '.' + propertyName,
actualPropertiesMap.get(propertyName), actualPropertiesMap.get(propertyName),
expectedPropertiesMap.get(propertyName) expectedPropertiesMap.get(propertyName)
); );
} }
}
return errors; return errors;
} }
@ -1131,9 +1175,15 @@ function compareDocumentations(actual, expected) {
function diff(actual, expected) { function diff(actual, expected) {
const N = actual.length; const N = actual.length;
const M = expected.length; const M = expected.length;
if (N === 0 && M === 0) return { extra: [], missing: [], equal: [] }; if (N === 0 && M === 0) {
if (N === 0) return { extra: [], missing: expected.slice(), equal: [] }; return { extra: [], missing: [], equal: [] };
if (M === 0) return { extra: actual.slice(), missing: [], equal: [] }; }
if (N === 0) {
return { extra: [], missing: expected.slice(), equal: [] };
}
if (M === 0) {
return { extra: actual.slice(), missing: [], equal: [] };
}
const d = new Array(N); const d = new Array(N);
const bt = new Array(N); const bt = new Array(N);
for (let i = 0; i < N; ++i) { for (let i = 0; i < N; ++i) {
@ -1179,8 +1229,12 @@ function diff(actual, expected) {
break; break;
} }
} }
while (i >= 0) extra.push(actual[i--]); while (i >= 0) {
while (j >= 0) missing.push(expected[j--]); extra.push(actual[i--]);
}
while (j >= 0) {
missing.push(expected[j--]);
}
extra.reverse(); extra.reverse();
missing.reverse(); missing.reverse();
equal.reverse(); equal.reverse();

View File

@ -105,7 +105,9 @@ async function run() {
await browser.close(); await browser.close();
for (const source of mdSources) { for (const source of mdSources) {
if (!source.hasUpdatedText()) continue; if (!source.hasUpdatedText()) {
continue;
}
await source.save(); await source.save();
changedFiles = true; changedFiles = true;
} }

View File

@ -32,9 +32,10 @@ module.exports.ensureReleasedAPILinks = function (
for (const source of sources) { for (const source of sources) {
const text = source.text(); const text = source.text();
const newText = text.replace(apiLinkRegex, lastReleasedAPI); const newText = text.replace(apiLinkRegex, lastReleasedAPI);
if (source.setText(newText)) if (source.setText(newText)) {
messages.push(Message.info(`GEN: updated ${source.projectPath()}`)); messages.push(Message.info(`GEN: updated ${source.projectPath()}`));
} }
}
return messages; return messages;
}; };
@ -121,7 +122,9 @@ function generateTableOfContents(mdText) {
insideCodeBlock = !insideCodeBlock; insideCodeBlock = !insideCodeBlock;
continue; continue;
} }
if (!insideCodeBlock && line.startsWith('#')) titles.push(line); if (!insideCodeBlock && line.startsWith('#')) {
titles.push(line);
}
} }
const tocEntries = []; const tocEntries = [];
for (const title of titles) { for (const title of titles) {
@ -134,7 +137,9 @@ function generateTableOfContents(mdText) {
.replace(/[^-0-9a-zа-яё]/gi, ''); .replace(/[^-0-9a-zа-яё]/gi, '');
let dedupId = id; let dedupId = id;
let counter = 0; let counter = 0;
while (ids.has(dedupId)) dedupId = id + '-' + ++counter; while (ids.has(dedupId)) {
dedupId = id + '-' + ++counter;
}
ids.add(dedupId); ids.add(dedupId);
tocEntries.push({ tocEntries.push({
level: nesting.length, level: nesting.length,
@ -162,7 +167,9 @@ const generateVersionsPerRelease = () => {
const { versionsPerRelease } = require('../../../versions.js'); const { versionsPerRelease } = require('../../../versions.js');
const buffer = ['- Releases per Chromium version:']; const buffer = ['- Releases per Chromium version:'];
for (const [chromiumVersion, puppeteerVersion] of versionsPerRelease) { for (const [chromiumVersion, puppeteerVersion] of versionsPerRelease) {
if (puppeteerVersion === 'NEXT') continue; if (puppeteerVersion === 'NEXT') {
continue;
}
buffer.push( buffer.push(
` * Chromium ${chromiumVersion} - [Puppeteer ${puppeteerVersion}](https://github.com/puppeteer/puppeteer/blob/${puppeteerVersion}/docs/api.md)` ` * Chromium ${chromiumVersion} - [Puppeteer ${puppeteerVersion}](https://github.com/puppeteer/puppeteer/blob/${puppeteerVersion}/docs/api.md)`
); );

View File

@ -91,12 +91,15 @@ async function main(url) {
let devices = []; let devices = [];
for (const payload of devicePayloads) { for (const payload of devicePayloads) {
let names = []; let names = [];
if (payload.title === 'iPhone 6/7/8') if (payload.title === 'iPhone 6/7/8') {
names = ['iPhone 6', 'iPhone 7', 'iPhone 8']; names = ['iPhone 6', 'iPhone 7', 'iPhone 8'];
else if (payload.title === 'iPhone 6/7/8 Plus') } else if (payload.title === 'iPhone 6/7/8 Plus') {
names = ['iPhone 6 Plus', 'iPhone 7 Plus', 'iPhone 8 Plus']; names = ['iPhone 6 Plus', 'iPhone 7 Plus', 'iPhone 8 Plus'];
else if (payload.title === 'iPhone 5/SE') names = ['iPhone 5', 'iPhone SE']; } else if (payload.title === 'iPhone 5/SE') {
else names = [payload.title]; names = ['iPhone 5', 'iPhone SE'];
} else {
names = [payload.title];
}
for (const name of names) { for (const name of names) {
const device = createDevice(chromeVersion, name, payload, false); const device = createDevice(chromeVersion, name, payload, false);
const landscape = createDevice(chromeVersion, name, payload, true); const landscape = createDevice(chromeVersion, name, payload, true);
@ -104,10 +107,11 @@ async function main(url) {
if ( if (
landscape.viewport.width !== device.viewport.width || landscape.viewport.width !== device.viewport.width ||
landscape.viewport.height !== device.viewport.height landscape.viewport.height !== device.viewport.height
) ) {
devices.push(landscape); devices.push(landscape);
} }
} }
}
devices = devices.filter((device) => device.viewport.isMobile); devices = devices.filter((device) => device.viewport.isMobile);
devices.sort((a, b) => a.name.localeCompare(b.name)); devices.sort((a, b) => a.name.localeCompare(b.name));
// Use single-quotes instead of double-quotes to conform with codestyle. // Use single-quotes instead of double-quotes to conform with codestyle.
@ -164,13 +168,15 @@ function loadFromJSONV1(json) {
object === null || object === null ||
!object.hasOwnProperty(key) !object.hasOwnProperty(key)
) { ) {
if (typeof defaultValue !== 'undefined') return defaultValue; if (typeof defaultValue !== 'undefined') {
return defaultValue;
}
throw new Error( throw new Error(
"Emulated device is missing required property '" + key + "'" "Emulated device is missing required property '" + key + "'"
); );
} }
const value = object[key]; const value = object[key];
if (typeof value !== type || value === null) if (typeof value !== type || value === null) {
throw new Error( throw new Error(
"Emulated device property '" + "Emulated device property '" +
key + key +
@ -178,6 +184,7 @@ function loadFromJSONV1(json) {
typeof value + typeof value +
"'" "'"
); );
}
return value; return value;
} }
@ -188,8 +195,9 @@ function loadFromJSONV1(json) {
*/ */
function parseIntValue(object, key) { function parseIntValue(object, key) {
const value = /** @type {number} */ (parseValue(object, key, 'number')); const value = /** @type {number} */ (parseValue(object, key, 'number'));
if (value !== Math.abs(value)) if (value !== Math.abs(value)) {
throw new Error("Emulated device value '" + key + "' must be integer"); throw new Error("Emulated device value '" + key + "' must be integer");
}
return value; return value;
} }
@ -206,16 +214,18 @@ function loadFromJSONV1(json) {
result.width < 0 || result.width < 0 ||
result.width > maxDeviceSize || result.width > maxDeviceSize ||
result.width < minDeviceSize result.width < minDeviceSize
) ) {
throw new Error('Emulated device has wrong width: ' + result.width); throw new Error('Emulated device has wrong width: ' + result.width);
}
result.height = parseIntValue(json, 'height'); result.height = parseIntValue(json, 'height');
if ( if (
result.height < 0 || result.height < 0 ||
result.height > maxDeviceSize || result.height > maxDeviceSize ||
result.height < minDeviceSize result.height < minDeviceSize
) ) {
throw new Error('Emulated device has wrong height: ' + result.height); throw new Error('Emulated device has wrong height: ' + result.height);
}
return /** @type {!{width: number, height: number}} */ (result); return /** @type {!{width: number, height: number}} */ (result);
} }
@ -227,22 +237,25 @@ function loadFromJSONV1(json) {
); );
const capabilities = parseValue(json, 'capabilities', 'object', []); const capabilities = parseValue(json, 'capabilities', 'object', []);
if (!Array.isArray(capabilities)) if (!Array.isArray(capabilities)) {
throw new Error('Emulated device capabilities must be an array'); throw new Error('Emulated device capabilities must be an array');
}
result.capabilities = []; result.capabilities = [];
for (let i = 0; i < capabilities.length; ++i) { for (let i = 0; i < capabilities.length; ++i) {
if (typeof capabilities[i] !== 'string') if (typeof capabilities[i] !== 'string') {
throw new Error('Emulated device capability must be a string'); throw new Error('Emulated device capability must be a string');
}
result.capabilities.push(capabilities[i]); result.capabilities.push(capabilities[i]);
} }
result.deviceScaleFactor = /** @type {number} */ ( result.deviceScaleFactor = /** @type {number} */ (
parseValue(json['screen'], 'device-pixel-ratio', 'number') parseValue(json['screen'], 'device-pixel-ratio', 'number')
); );
if (result.deviceScaleFactor < 0 || result.deviceScaleFactor > 100) if (result.deviceScaleFactor < 0 || result.deviceScaleFactor > 100) {
throw new Error( throw new Error(
'Emulated device has wrong deviceScaleFactor: ' + result.deviceScaleFactor 'Emulated device has wrong deviceScaleFactor: ' + result.deviceScaleFactor
); );
}
result.vertical = parseOrientation( result.vertical = parseOrientation(
parseValue(json['screen'], 'vertical', 'object') parseValue(json['screen'], 'vertical', 'object')

View File

@ -69,9 +69,11 @@ class TestServer {
* @param {!Object=} sslOptions * @param {!Object=} sslOptions
*/ */
constructor(dirPath, port, sslOptions) { constructor(dirPath, port, sslOptions) {
if (sslOptions) if (sslOptions) {
this._server = https.createServer(sslOptions, this._onRequest.bind(this)); this._server = https.createServer(sslOptions, this._onRequest.bind(this));
else this._server = http.createServer(this._onRequest.bind(this)); } else {
this._server = http.createServer(this._onRequest.bind(this));
}
this._server.on('connection', (socket) => this._onSocket(socket)); this._server.on('connection', (socket) => this._onSocket(socket));
this._wsServer = new WebSocketServer({ server: this._server }); this._wsServer = new WebSocketServer({ server: this._server });
this._wsServer.on('connection', this._onWebSocketConnection.bind(this)); this._wsServer.on('connection', this._onWebSocketConnection.bind(this));
@ -101,7 +103,9 @@ class TestServer {
// ECONNRESET is a legit error given // ECONNRESET is a legit error given
// that tab closing simply kills process. // that tab closing simply kills process.
socket.on('error', (error) => { socket.on('error', (error) => {
if (error.code !== 'ECONNRESET') throw error; if (error.code !== 'ECONNRESET') {
throw error;
}
}); });
socket.once('close', () => this._sockets.delete(socket)); socket.once('close', () => this._sockets.delete(socket));
} }
@ -136,7 +140,9 @@ class TestServer {
async stop() { async stop() {
this.reset(); this.reset();
for (const socket of this._sockets) socket.destroy(); for (const socket of this._sockets) {
socket.destroy();
}
this._sockets.clear(); this._sockets.clear();
await new Promise((x) => this._server.close(x)); await new Promise((x) => this._server.close(x));
} }
@ -166,7 +172,9 @@ class TestServer {
*/ */
waitForRequest(path) { waitForRequest(path) {
let promise = this._requestSubscribers.get(path); let promise = this._requestSubscribers.get(path);
if (promise) return promise; if (promise) {
return promise;
}
let fulfill, reject; let fulfill, reject;
promise = new Promise((f, r) => { promise = new Promise((f, r) => {
fulfill = f; fulfill = f;
@ -184,15 +192,19 @@ class TestServer {
this._csp.clear(); this._csp.clear();
this._gzipRoutes.clear(); this._gzipRoutes.clear();
const error = new Error('Static Server has been reset'); const error = new Error('Static Server has been reset');
for (const subscriber of this._requestSubscribers.values()) for (const subscriber of this._requestSubscribers.values()) {
subscriber[rejectSymbol].call(null, error); subscriber[rejectSymbol].call(null, error);
}
this._requestSubscribers.clear(); this._requestSubscribers.clear();
} }
_onRequest(request, response) { _onRequest(request, response) {
request.on('error', (error) => { request.on('error', (error) => {
if (error.code === 'ECONNRESET') response.end(); if (error.code === 'ECONNRESET') {
else throw error; response.end();
} else {
throw error;
}
}); });
request.postBody = new Promise((resolve) => { request.postBody = new Promise((resolve) => {
let body = ''; let body = '';
@ -234,7 +246,9 @@ class TestServer {
* @param {string} pathName * @param {string} pathName
*/ */
serveFile(request, response, pathName) { serveFile(request, response, pathName) {
if (pathName === '/') pathName = '/index.html'; if (pathName === '/') {
pathName = '/index.html';
}
const filePath = path.join(this._dirPath, pathName.substring(1)); const filePath = path.join(this._dirPath, pathName.substring(1));
if ( if (
@ -251,8 +265,9 @@ class TestServer {
} else { } else {
response.setHeader('Cache-Control', 'no-cache, no-store'); response.setHeader('Cache-Control', 'no-cache, no-store');
} }
if (this._csp.has(pathName)) if (this._csp.has(pathName)) {
response.setHeader('Content-Security-Policy', this._csp.get(pathName)); response.setHeader('Content-Security-Policy', this._csp.get(pathName));
}
fs.readFile(filePath, (err, data) => { fs.readFile(filePath, (err, data) => {
if (err) { if (err) {