feat: implement detailed errors for evaluation (#10114)

This commit is contained in:
jrandolf 2023-05-10 08:23:29 +00:00 committed by GitHub
parent 75a50257e0
commit 317fa732f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 568 additions and 70 deletions

View File

@ -128,7 +128,6 @@ sidebar_label: API
| [defaultArgs](./puppeteer.defaultargs.md) | | | [defaultArgs](./puppeteer.defaultargs.md) | |
| [devices](./puppeteer.devices.md) | | | [devices](./puppeteer.devices.md) | |
| [errors](./puppeteer.errors.md) | | | [errors](./puppeteer.errors.md) | |
| [EVALUATION_SCRIPT_URL](./puppeteer.evaluation_script_url.md) | |
| [executablePath](./puppeteer.executablepath.md) | | | [executablePath](./puppeteer.executablepath.md) | |
| [KnownDevices](./puppeteer.knowndevices.md) | A list of devices to be used with [Page.emulate()](./puppeteer.page.emulate.md). | | [KnownDevices](./puppeteer.knowndevices.md) | A list of devices to be used with [Page.emulate()](./puppeteer.page.emulate.md). |
| [launch](./puppeteer.launch.md) | | | [launch](./puppeteer.launch.md) | |

View File

@ -1,11 +0,0 @@
---
sidebar_label: EVALUATION_SCRIPT_URL
---
# EVALUATION_SCRIPT_URL variable
#### Signature:
```typescript
EVALUATION_SCRIPT_URL = 'pptr://__puppeteer_evaluation_script__';
```

View File

@ -19,9 +19,13 @@ import {Protocol} from 'devtools-protocol';
import {assert} from '../util/assert.js'; import {assert} from '../util/assert.js';
import {CDPSession} from './Connection.js'; import {CDPSession} from './Connection.js';
import {EVALUATION_SCRIPT_URL} from './ExecutionContext.js'; import {
import {addEventListener, debugError, PuppeteerEventListener} from './util.js'; addEventListener,
import {removeEventListeners} from './util.js'; debugError,
PuppeteerEventListener,
PuppeteerURL,
removeEventListeners,
} from './util.js';
/** /**
* @internal * @internal
@ -264,7 +268,7 @@ 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) { if (PuppeteerURL.isPuppeteerURL(event.url)) {
return; return;
} }
// Ignore other anonymous scripts unless the reportAnonymousScripts option is true. // Ignore other anonymous scripts unless the reportAnonymousScripts option is true.

View File

@ -41,7 +41,7 @@ import {LazyArg} from './LazyArg.js';
import {CDPPage} from './Page.js'; import {CDPPage} from './Page.js';
import {ElementFor, EvaluateFuncWith, HandleFor, NodeFor} from './types.js'; import {ElementFor, EvaluateFuncWith, HandleFor, NodeFor} from './types.js';
import {KeyInput} from './USKeyboardLayout.js'; import {KeyInput} from './USKeyboardLayout.js';
import {debugError, isString} from './util.js'; import {debugError, isString, withSourcePuppeteerURLIfNone} from './util.js';
const applyOffsetsToQuad = ( const applyOffsetsToQuad = (
quad: Point[], quad: Point[],
@ -138,6 +138,7 @@ export class CDPElementHandle<
pageFunction: Func | string, pageFunction: Func | string,
...args: Params ...args: Params
): Promise<Awaited<ReturnType<Func>>> { ): Promise<Awaited<ReturnType<Func>>> {
pageFunction = withSourcePuppeteerURLIfNone(this.$eval.name, pageFunction);
const elementHandle = await this.$(selector); const elementHandle = await this.$(selector);
if (!elementHandle) { if (!elementHandle) {
throw new Error( throw new Error(
@ -161,6 +162,7 @@ export class CDPElementHandle<
pageFunction: Func | string, pageFunction: Func | string,
...args: Params ...args: Params
): Promise<Awaited<ReturnType<Func>>> { ): Promise<Awaited<ReturnType<Func>>> {
pageFunction = withSourcePuppeteerURLIfNone(this.$$eval.name, pageFunction);
const results = await this.$$(selector); const results = await this.$$(selector);
const elements = await this.evaluateHandle((_, ...elements) => { const elements = await this.evaluateHandle((_, ...elements) => {
return elements; return elements;

View File

@ -32,18 +32,20 @@ import {LazyArg} from './LazyArg.js';
import {scriptInjector} from './ScriptInjector.js'; import {scriptInjector} from './ScriptInjector.js';
import {EvaluateFunc, HandleFor} from './types.js'; import {EvaluateFunc, HandleFor} from './types.js';
import { import {
PuppeteerURL,
createEvaluationError,
createJSHandle, createJSHandle,
getExceptionMessage, getSourcePuppeteerURLIfAvailable,
isString, isString,
valueFromRemoteObject, valueFromRemoteObject,
} from './util.js'; } from './util.js';
/**
* @public
*/
export const EVALUATION_SCRIPT_URL = 'pptr://__puppeteer_evaluation_script__';
const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m; const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m;
const getSourceUrlComment = (url: string) => {
return `//# sourceURL=${url}`;
};
/** /**
* Represents a context for JavaScript execution. * Represents a context for JavaScript execution.
* *
@ -270,14 +272,17 @@ export class ExecutionContext {
pageFunction: Func | string, pageFunction: Func | string,
...args: Params ...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>> | Awaited<ReturnType<Func>>> { ): Promise<HandleFor<Awaited<ReturnType<Func>>> | Awaited<ReturnType<Func>>> {
const suffix = `//# sourceURL=${EVALUATION_SCRIPT_URL}`; const sourceUrlComment = getSourceUrlComment(
getSourcePuppeteerURLIfAvailable(pageFunction)?.toString() ??
PuppeteerURL.INTERNAL_URL
);
if (isString(pageFunction)) { if (isString(pageFunction)) {
const contextId = this._contextId; const contextId = this._contextId;
const expression = pageFunction; const expression = pageFunction;
const expressionWithSourceUrl = SOURCE_URL_REGEX.test(expression) const expressionWithSourceUrl = SOURCE_URL_REGEX.test(expression)
? expression ? expression
: expression + '\n' + suffix; : `${expression}\n${sourceUrlComment}\n`;
const {exceptionDetails, result: remoteObject} = await this._client const {exceptionDetails, result: remoteObject} = await this._client
.send('Runtime.evaluate', { .send('Runtime.evaluate', {
@ -290,9 +295,7 @@ export class ExecutionContext {
.catch(rewriteError); .catch(rewriteError);
if (exceptionDetails) { if (exceptionDetails) {
throw new Error( throw createEvaluationError(exceptionDetails);
'Evaluation failed: ' + getExceptionMessage(exceptionDetails)
);
} }
return returnByValue return returnByValue
@ -300,10 +303,16 @@ export class ExecutionContext {
: createJSHandle(this, remoteObject); : createJSHandle(this, remoteObject);
} }
const functionDeclaration = stringifyFunction(pageFunction);
const functionDeclarationWithSourceUrl = SOURCE_URL_REGEX.test(
functionDeclaration
)
? functionDeclaration
: `${functionDeclaration}\n${sourceUrlComment}\n`;
let callFunctionOnPromise; let callFunctionOnPromise;
try { try {
callFunctionOnPromise = this._client.send('Runtime.callFunctionOn', { callFunctionOnPromise = this._client.send('Runtime.callFunctionOn', {
functionDeclaration: `${stringifyFunction(pageFunction)}\n${suffix}\n`, functionDeclaration: functionDeclarationWithSourceUrl,
executionContextId: this._contextId, executionContextId: this._contextId,
arguments: await Promise.all(args.map(convertArgument.bind(this))), arguments: await Promise.all(args.map(convertArgument.bind(this))),
returnByValue, returnByValue,
@ -322,9 +331,7 @@ 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 createEvaluationError(exceptionDetails);
'Evaluation failed: ' + getExceptionMessage(exceptionDetails)
);
} }
return returnByValue return returnByValue
? valueFromRemoteObject(remoteObject) ? valueFromRemoteObject(remoteObject)

View File

@ -39,7 +39,7 @@ import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js';
import {LazyArg} from './LazyArg.js'; import {LazyArg} from './LazyArg.js';
import {LifecycleWatcher, PuppeteerLifeCycleEvent} from './LifecycleWatcher.js'; import {LifecycleWatcher, PuppeteerLifeCycleEvent} from './LifecycleWatcher.js';
import {EvaluateFunc, EvaluateFuncWith, HandleFor, NodeFor} from './types.js'; import {EvaluateFunc, EvaluateFuncWith, HandleFor, NodeFor} from './types.js';
import {importFSPromises} from './util.js'; import {importFSPromises, withSourcePuppeteerURLIfNone} from './util.js';
/** /**
* @public * @public
@ -459,6 +459,10 @@ export class Frame {
pageFunction: Func | string, pageFunction: Func | string,
...args: Params ...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>> { ): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
pageFunction = withSourcePuppeteerURLIfNone(
this.evaluateHandle.name,
pageFunction
);
return this.worlds[MAIN_WORLD].evaluateHandle(pageFunction, ...args); return this.worlds[MAIN_WORLD].evaluateHandle(pageFunction, ...args);
} }
@ -475,6 +479,10 @@ export class Frame {
pageFunction: Func | string, pageFunction: Func | string,
...args: Params ...args: Params
): Promise<Awaited<ReturnType<Func>>> { ): Promise<Awaited<ReturnType<Func>>> {
pageFunction = withSourcePuppeteerURLIfNone(
this.evaluate.name,
pageFunction
);
return this.worlds[MAIN_WORLD].evaluate(pageFunction, ...args); return this.worlds[MAIN_WORLD].evaluate(pageFunction, ...args);
} }
@ -536,6 +544,7 @@ export class Frame {
pageFunction: Func | string, pageFunction: Func | string,
...args: Params ...args: Params
): Promise<Awaited<ReturnType<Func>>> { ): Promise<Awaited<ReturnType<Func>>> {
pageFunction = withSourcePuppeteerURLIfNone(this.$eval.name, pageFunction);
return this.worlds[MAIN_WORLD].$eval(selector, pageFunction, ...args); return this.worlds[MAIN_WORLD].$eval(selector, pageFunction, ...args);
} }
@ -571,6 +580,7 @@ export class Frame {
pageFunction: Func | string, pageFunction: Func | string,
...args: Params ...args: Params
): Promise<Awaited<ReturnType<Func>>> { ): Promise<Awaited<ReturnType<Func>>> {
pageFunction = withSourcePuppeteerURLIfNone(this.$$eval.name, pageFunction);
return this.worlds[MAIN_WORLD].$$eval(selector, pageFunction, ...args); return this.worlds[MAIN_WORLD].$$eval(selector, pageFunction, ...args);
} }

View File

@ -23,7 +23,7 @@ import {isErrorLike} from '../util/ErrorLike.js';
import {CDPSession, isTargetClosedError} from './Connection.js'; import {CDPSession, isTargetClosedError} from './Connection.js';
import {DeviceRequestPromptManager} from './DeviceRequestPrompt.js'; import {DeviceRequestPromptManager} from './DeviceRequestPrompt.js';
import {EventEmitter} from './EventEmitter.js'; import {EventEmitter} from './EventEmitter.js';
import {EVALUATION_SCRIPT_URL, ExecutionContext} from './ExecutionContext.js'; import {ExecutionContext} from './ExecutionContext.js';
import {Frame} from './Frame.js'; import {Frame} from './Frame.js';
import {FrameTree} from './FrameTree.js'; import {FrameTree} from './FrameTree.js';
import {IsolatedWorld} from './IsolatedWorld.js'; import {IsolatedWorld} from './IsolatedWorld.js';
@ -31,7 +31,7 @@ import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js';
import {NetworkManager} from './NetworkManager.js'; import {NetworkManager} from './NetworkManager.js';
import {Target} from './Target.js'; import {Target} from './Target.js';
import {TimeoutSettings} from './TimeoutSettings.js'; import {TimeoutSettings} from './TimeoutSettings.js';
import {debugError} from './util.js'; import {debugError, PuppeteerURL} from './util.js';
const UTILITY_WORLD_NAME = '__puppeteer_utility_world__'; const UTILITY_WORLD_NAME = '__puppeteer_utility_world__';
@ -349,7 +349,7 @@ export class FrameManager extends EventEmitter {
} }
await session.send('Page.addScriptToEvaluateOnNewDocument', { await session.send('Page.addScriptToEvaluateOnNewDocument', {
source: `//# sourceURL=${EVALUATION_SCRIPT_URL}`, source: `//# sourceURL=${PuppeteerURL.INTERNAL_URL}`,
worldName: name, worldName: name,
}); });

View File

@ -42,6 +42,7 @@ import {
createJSHandle, createJSHandle,
debugError, debugError,
setPageContent, setPageContent,
withSourcePuppeteerURLIfNone,
} from './util.js'; } from './util.js';
import {TaskManager, WaitTask} from './WaitTask.js'; import {TaskManager, WaitTask} from './WaitTask.js';
@ -183,6 +184,10 @@ export class IsolatedWorld {
pageFunction: Func | string, pageFunction: Func | string,
...args: Params ...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>> { ): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
pageFunction = withSourcePuppeteerURLIfNone(
this.evaluateHandle.name,
pageFunction
);
const context = await this.executionContext(); const context = await this.executionContext();
return context.evaluateHandle(pageFunction, ...args); return context.evaluateHandle(pageFunction, ...args);
} }
@ -194,6 +199,10 @@ export class IsolatedWorld {
pageFunction: Func | string, pageFunction: Func | string,
...args: Params ...args: Params
): Promise<Awaited<ReturnType<Func>>> { ): Promise<Awaited<ReturnType<Func>>> {
pageFunction = withSourcePuppeteerURLIfNone(
this.evaluate.name,
pageFunction
);
const context = await this.executionContext(); const context = await this.executionContext();
return context.evaluate(pageFunction, ...args); return context.evaluate(pageFunction, ...args);
} }
@ -240,6 +249,7 @@ export class IsolatedWorld {
pageFunction: Func | string, pageFunction: Func | string,
...args: Params ...args: Params
): Promise<Awaited<ReturnType<Func>>> { ): Promise<Awaited<ReturnType<Func>>> {
pageFunction = withSourcePuppeteerURLIfNone(this.$eval.name, pageFunction);
const document = await this.document(); const document = await this.document();
return document.$eval(selector, pageFunction, ...args); return document.$eval(selector, pageFunction, ...args);
} }
@ -256,6 +266,7 @@ export class IsolatedWorld {
pageFunction: Func | string, pageFunction: Func | string,
...args: Params ...args: Params
): Promise<Awaited<ReturnType<Func>>> { ): Promise<Awaited<ReturnType<Func>>> {
pageFunction = withSourcePuppeteerURLIfNone(this.$$eval.name, pageFunction);
const document = await this.document(); const document = await this.document();
return document.$$eval(selector, pageFunction, ...args); return document.$$eval(selector, pageFunction, ...args);
} }

View File

@ -23,7 +23,12 @@ import {CDPSession} from './Connection.js';
import type {CDPElementHandle} from './ElementHandle.js'; import type {CDPElementHandle} from './ElementHandle.js';
import {ExecutionContext} from './ExecutionContext.js'; import {ExecutionContext} from './ExecutionContext.js';
import {EvaluateFuncWith, HandleFor, HandleOr} from './types.js'; import {EvaluateFuncWith, HandleFor, HandleOr} from './types.js';
import {createJSHandle, releaseObject, valueFromRemoteObject} from './util.js'; import {
createJSHandle,
releaseObject,
valueFromRemoteObject,
withSourcePuppeteerURLIfNone,
} from './util.js';
declare const __JSHandleSymbol: unique symbol; declare const __JSHandleSymbol: unique symbol;
@ -71,6 +76,10 @@ export class CDPJSHandle<T = unknown> extends JSHandle<T> {
pageFunction: Func | string, pageFunction: Func | string,
...args: Params ...args: Params
): Promise<Awaited<ReturnType<Func>>> { ): Promise<Awaited<ReturnType<Func>>> {
pageFunction = withSourcePuppeteerURLIfNone(
this.evaluate.name,
pageFunction
);
return await this.executionContext().evaluate(pageFunction, this, ...args); return await this.executionContext().evaluate(pageFunction, this, ...args);
} }
@ -84,6 +93,10 @@ export class CDPJSHandle<T = unknown> extends JSHandle<T> {
pageFunction: Func | string, pageFunction: Func | string,
...args: Params ...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>> { ): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
pageFunction = withSourcePuppeteerURLIfNone(
this.evaluateHandle.name,
pageFunction
);
return await this.executionContext().evaluateHandle( return await this.executionContext().evaluateHandle(
pageFunction, pageFunction,
this, this,

View File

@ -85,10 +85,10 @@ import {
NodeFor, NodeFor,
} from './types.js'; } from './types.js';
import { import {
createClientError,
createJSHandle, createJSHandle,
debugError, debugError,
evaluationString, evaluationString,
getExceptionMessage,
getReadableAsBuffer, getReadableAsBuffer,
getReadableFromProtocolStream, getReadableFromProtocolStream,
isString, isString,
@ -97,6 +97,7 @@ import {
valueFromRemoteObject, valueFromRemoteObject,
waitForEvent, waitForEvent,
waitWithTimeout, waitWithTimeout,
withSourcePuppeteerURLIfNone,
} from './util.js'; } from './util.js';
import {WebWorker} from './WebWorker.js'; import {WebWorker} from './WebWorker.js';
@ -518,6 +519,10 @@ export class CDPPage extends Page {
pageFunction: Func | string, pageFunction: Func | string,
...args: Params ...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>> { ): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
pageFunction = withSourcePuppeteerURLIfNone(
this.evaluateHandle.name,
pageFunction
);
const context = await this.mainFrame().executionContext(); const context = await this.mainFrame().executionContext();
return context.evaluateHandle(pageFunction, ...args); return context.evaluateHandle(pageFunction, ...args);
} }
@ -549,6 +554,7 @@ export class CDPPage extends Page {
pageFunction: Func | string, pageFunction: Func | string,
...args: Params ...args: Params
): Promise<Awaited<ReturnType<Func>>> { ): Promise<Awaited<ReturnType<Func>>> {
pageFunction = withSourcePuppeteerURLIfNone(this.$eval.name, pageFunction);
return this.mainFrame().$eval(selector, pageFunction, ...args); return this.mainFrame().$eval(selector, pageFunction, ...args);
} }
@ -564,6 +570,7 @@ export class CDPPage extends Page {
pageFunction: Func | string, pageFunction: Func | string,
...args: Params ...args: Params
): Promise<Awaited<ReturnType<Func>>> { ): Promise<Awaited<ReturnType<Func>>> {
pageFunction = withSourcePuppeteerURLIfNone(this.$$eval.name, pageFunction);
return this.mainFrame().$$eval(selector, pageFunction, ...args); return this.mainFrame().$$eval(selector, pageFunction, ...args);
} }
@ -735,10 +742,7 @@ export class CDPPage extends Page {
} }
#handleException(exceptionDetails: Protocol.Runtime.ExceptionDetails): void { #handleException(exceptionDetails: Protocol.Runtime.ExceptionDetails): void {
const message = getExceptionMessage(exceptionDetails); this.emit(PageEmittedEvents.PageError, createClientError(exceptionDetails));
const err = new Error(message);
err.stack = ''; // Don't report clientside error with a node stack attached
this.emit(PageEmittedEvents.PageError, err);
} }
async #onConsoleAPI( async #onConsoleAPI(
@ -1257,6 +1261,10 @@ export class CDPPage extends Page {
pageFunction: Func | string, pageFunction: Func | string,
...args: Params ...args: Params
): Promise<Awaited<ReturnType<Func>>> { ): Promise<Awaited<ReturnType<Func>>> {
pageFunction = withSourcePuppeteerURLIfNone(
this.evaluate.name,
pageFunction
);
return this.#frameManager.mainFrame().evaluate(pageFunction, ...args); return this.#frameManager.mainFrame().evaluate(pageFunction, ...args);
} }

View File

@ -23,7 +23,7 @@ import {EventEmitter} from './EventEmitter.js';
import {ExecutionContext} from './ExecutionContext.js'; import {ExecutionContext} from './ExecutionContext.js';
import {CDPJSHandle} from './JSHandle.js'; import {CDPJSHandle} from './JSHandle.js';
import {EvaluateFunc, HandleFor} from './types.js'; import {EvaluateFunc, HandleFor} from './types.js';
import {debugError} from './util.js'; import {debugError, withSourcePuppeteerURLIfNone} from './util.js';
/** /**
* @internal * @internal
@ -150,6 +150,10 @@ export class WebWorker extends EventEmitter {
pageFunction: Func | string, pageFunction: Func | string,
...args: Params ...args: Params
): Promise<Awaited<ReturnType<Func>>> { ): Promise<Awaited<ReturnType<Func>>> {
pageFunction = withSourcePuppeteerURLIfNone(
this.evaluate.name,
pageFunction
);
const context = await this.#executionContext; const context = await this.#executionContext;
return context.evaluate(pageFunction, ...args); return context.evaluate(pageFunction, ...args);
} }
@ -173,6 +177,10 @@ export class WebWorker extends EventEmitter {
pageFunction: Func | string, pageFunction: Func | string,
...args: Params ...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>> { ): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
pageFunction = withSourcePuppeteerURLIfNone(
this.evaluateHandle.name,
pageFunction
);
const context = await this.#executionContext; const context = await this.#executionContext;
return context.evaluateHandle(pageFunction, ...args); return context.evaluateHandle(pageFunction, ...args);
} }

View File

@ -26,12 +26,25 @@ import {EventEmitter} from '../EventEmitter.js';
import {PuppeteerLifeCycleEvent} from '../LifecycleWatcher.js'; import {PuppeteerLifeCycleEvent} from '../LifecycleWatcher.js';
import {TimeoutSettings} from '../TimeoutSettings.js'; import {TimeoutSettings} from '../TimeoutSettings.js';
import {EvaluateFunc, HandleFor} from '../types.js'; import {EvaluateFunc, HandleFor} from '../types.js';
import {isString, setPageContent, waitWithTimeout} from '../util.js'; import {
getSourcePuppeteerURLIfAvailable,
isString,
PuppeteerURL,
setPageContent,
waitWithTimeout,
} from '../util.js';
import {Connection} from './Connection.js'; import {Connection} from './Connection.js';
import {ElementHandle} from './ElementHandle.js'; import {ElementHandle} from './ElementHandle.js';
import {JSHandle} from './JSHandle.js'; import {JSHandle} from './JSHandle.js';
import {BidiSerializer} from './Serializer.js'; import {BidiSerializer} from './Serializer.js';
import {createEvaluationError} from './utils.js';
const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m;
const getSourceUrlComment = (url: string) => {
return `//# sourceURL=${url}`;
};
/** /**
* @internal * @internal
@ -120,18 +133,31 @@ export class Context extends EventEmitter {
pageFunction: Func | string, pageFunction: Func | string,
...args: Params ...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>> | Awaited<ReturnType<Func>>> { ): Promise<HandleFor<Awaited<ReturnType<Func>>> | Awaited<ReturnType<Func>>> {
const sourceUrlComment = getSourceUrlComment(
getSourcePuppeteerURLIfAvailable(pageFunction)?.toString() ??
PuppeteerURL.INTERNAL_URL
);
let responsePromise; let responsePromise;
const resultOwnership = returnByValue ? 'none' : 'root'; const resultOwnership = returnByValue ? 'none' : 'root';
if (isString(pageFunction)) { if (isString(pageFunction)) {
const expression = SOURCE_URL_REGEX.test(pageFunction)
? pageFunction
: `${pageFunction}\n${sourceUrlComment}\n`;
responsePromise = this.#connection.send('script.evaluate', { responsePromise = this.#connection.send('script.evaluate', {
expression: pageFunction, expression: expression,
target: {context: this._contextId}, target: {context: this._contextId},
resultOwnership, resultOwnership,
awaitPromise: true, awaitPromise: true,
}); });
} else { } else {
let functionDeclaration = stringifyFunction(pageFunction);
functionDeclaration = SOURCE_URL_REGEX.test(functionDeclaration)
? functionDeclaration
: `${functionDeclaration}\n${sourceUrlComment}\n`;
responsePromise = this.#connection.send('script.callFunction', { responsePromise = this.#connection.send('script.callFunction', {
functionDeclaration: stringifyFunction(pageFunction), functionDeclaration,
arguments: await Promise.all( arguments: await Promise.all(
args.map(arg => { args.map(arg => {
return BidiSerializer.serialize(arg, this); return BidiSerializer.serialize(arg, this);
@ -146,7 +172,7 @@ export class Context extends EventEmitter {
const {result} = await responsePromise; const {result} = await responsePromise;
if ('type' in result && result.type === 'exception') { if ('type' in result && result.type === 'exception') {
throw new Error(result.exceptionDetails.text); throw createEvaluationError(result.exceptionDetails);
} }
return returnByValue return returnByValue

View File

@ -19,6 +19,7 @@ import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {ElementHandle} from '../../api/ElementHandle.js'; import {ElementHandle} from '../../api/ElementHandle.js';
import {JSHandle as BaseJSHandle} from '../../api/JSHandle.js'; import {JSHandle as BaseJSHandle} from '../../api/JSHandle.js';
import {EvaluateFuncWith, HandleFor, HandleOr} from '../../common/types.js'; import {EvaluateFuncWith, HandleFor, HandleOr} from '../../common/types.js';
import {withSourcePuppeteerURLIfNone} from '../util.js';
import {Connection} from './Connection.js'; import {Connection} from './Connection.js';
import {Context} from './Context.js'; import {Context} from './Context.js';
@ -55,6 +56,10 @@ export class JSHandle<T = unknown> extends BaseJSHandle<T> {
pageFunction: Func | string, pageFunction: Func | string,
...args: Params ...args: Params
): Promise<Awaited<ReturnType<Func>>> { ): Promise<Awaited<ReturnType<Func>>> {
pageFunction = withSourcePuppeteerURLIfNone(
this.evaluate.name,
pageFunction
);
return await this.context().evaluate(pageFunction, this, ...args); return await this.context().evaluate(pageFunction, this, ...args);
} }
@ -65,6 +70,10 @@ export class JSHandle<T = unknown> extends BaseJSHandle<T> {
pageFunction: Func | string, pageFunction: Func | string,
...args: Params ...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>> { ): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
pageFunction = withSourcePuppeteerURLIfNone(
this.evaluateHandle.name,
pageFunction
);
return await this.context().evaluateHandle(pageFunction, this, ...args); return await this.context().evaluateHandle(pageFunction, this, ...args);
} }

View File

@ -31,7 +31,11 @@ import {Handler} from '../EventEmitter.js';
import {PDFOptions} from '../PDFOptions.js'; import {PDFOptions} from '../PDFOptions.js';
import {Viewport} from '../PuppeteerViewport.js'; import {Viewport} from '../PuppeteerViewport.js';
import {EvaluateFunc, HandleFor} from '../types.js'; import {EvaluateFunc, HandleFor} from '../types.js';
import {debugError, waitWithTimeout} from '../util.js'; import {
debugError,
waitWithTimeout,
withSourcePuppeteerURLIfNone,
} from '../util.js';
import {Context, getBidiHandle} from './Context.js'; import {Context, getBidiHandle} from './Context.js';
import {BidiSerializer} from './Serializer.js'; import {BidiSerializer} from './Serializer.js';
@ -151,6 +155,10 @@ export class Page extends PageBase {
pageFunction: Func | string, pageFunction: Func | string,
...args: Params ...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>> { ): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
pageFunction = withSourcePuppeteerURLIfNone(
this.evaluateHandle.name,
pageFunction
);
return this.#context.evaluateHandle(pageFunction, ...args); return this.#context.evaluateHandle(pageFunction, ...args);
} }
@ -161,6 +169,10 @@ export class Page extends PageBase {
pageFunction: Func | string, pageFunction: Func | string,
...args: Params ...args: Params
): Promise<Awaited<ReturnType<Func>>> { ): Promise<Awaited<ReturnType<Func>>> {
pageFunction = withSourcePuppeteerURLIfNone(
this.evaluate.name,
pageFunction
);
return this.#context.evaluate(pageFunction, ...args); return this.#context.evaluate(pageFunction, ...args);
} }

View File

@ -17,8 +17,10 @@
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
import {debug} from '../Debug.js'; import {debug} from '../Debug.js';
import {PuppeteerURL} from '../util.js';
import {Context} from './Context.js'; import {Context} from './Context.js';
import {BidiSerializer} from './Serializer.js';
/** /**
* @internal * @internal
@ -45,3 +47,50 @@ export async function releaseReference(
debugError(error); debugError(error);
}); });
} }
/**
* @internal
*/
export function createEvaluationError(
details: Bidi.Script.ExceptionDetails
): unknown {
if (details.exception.type !== 'error') {
return BidiSerializer.deserialize(details.exception);
}
const [name = '', ...parts] = details.text.split(': ');
const message = parts.join(': ');
const error = new Error(message);
error.name = name;
// The first line is this function which we ignore.
const stackLines = [];
if (details.stackTrace && stackLines.length < Error.stackTraceLimit) {
for (const frame of details.stackTrace.callFrames.reverse()) {
if (
PuppeteerURL.isPuppeteerURL(frame.url) &&
frame.url !== PuppeteerURL.INTERNAL_URL
) {
const url = PuppeteerURL.parse(frame.url);
stackLines.unshift(
` at ${frame.functionName || url.functionName} (${
url.functionName
} at ${url.siteString}, <anonymous>:${frame.lineNumber}:${
frame.columnNumber
})`
);
} else {
stackLines.push(
` at ${frame.functionName || '<anonymous>'} (${frame.url}:${
frame.lineNumber
}:${frame.columnNumber})`
);
}
if (stackLines.length >= Error.stackTraceLimit) {
break;
}
}
}
error.stack = [details.text, ...stackLines].join('\n');
return error;
}

View File

@ -41,30 +41,213 @@ export const debugError = debug('puppeteer:error');
/** /**
* @internal * @internal
*/ */
export function getExceptionMessage( export function createEvaluationError(
exceptionDetails: Protocol.Runtime.ExceptionDetails details: Protocol.Runtime.ExceptionDetails
): string { ): unknown {
if (exceptionDetails.exception) { let name: string;
return ( let message: string;
exceptionDetails.exception.description || exceptionDetails.exception.value if (!details.exception) {
); name = 'Error';
message = details.text;
} else if (
details.exception.type !== 'object' ||
details.exception.subtype !== 'error'
) {
return valueFromRemoteObject(details.exception);
} else {
const detail = getErrorDetails(details);
name = detail.name;
message = detail.message;
} }
let message = exceptionDetails.text; const messageHeight = message.split('\n').length;
if (exceptionDetails.stackTrace) { const error = new Error(message);
for (const callframe of exceptionDetails.stackTrace.callFrames) { error.name = name;
const location = const stackLines = error.stack!.split('\n');
callframe.url + const messageLines = stackLines.splice(0, messageHeight);
':' +
callframe.lineNumber + // The first line is this function which we ignore.
':' + stackLines.shift();
callframe.columnNumber; if (details.stackTrace && stackLines.length < Error.stackTraceLimit) {
const functionName = callframe.functionName || '<anonymous>'; for (const frame of details.stackTrace.callFrames.reverse()) {
message += `\n at ${functionName} (${location})`; if (
PuppeteerURL.isPuppeteerURL(frame.url) &&
frame.url !== PuppeteerURL.INTERNAL_URL
) {
const url = PuppeteerURL.parse(frame.url);
stackLines.unshift(
` at ${frame.functionName || url.functionName} (${
url.functionName
} at ${url.siteString}, <anonymous>:${frame.lineNumber}:${
frame.columnNumber
})`
);
} else {
stackLines.push(
` at ${frame.functionName || '<anonymous>'} (${frame.url}:${
frame.lineNumber
}:${frame.columnNumber})`
);
}
if (stackLines.length >= Error.stackTraceLimit) {
break;
}
} }
} }
return message;
error.stack = [...messageLines, ...stackLines].join('\n');
return error;
} }
/**
* @internal
*/
export function createClientError(
details: Protocol.Runtime.ExceptionDetails
): unknown {
let name: string;
let message: string;
if (!details.exception) {
name = 'Error';
message = details.text;
} else if (
details.exception.type !== 'object' ||
details.exception.subtype !== 'error'
) {
return valueFromRemoteObject(details.exception);
} else {
const detail = getErrorDetails(details);
name = detail.name;
message = detail.message;
}
const messageHeight = message.split('\n').length;
const error = new Error(message);
error.name = name;
const stackLines = [];
const messageLines = error.stack!.split('\n').splice(0, messageHeight);
if (details.stackTrace && stackLines.length < Error.stackTraceLimit) {
for (const frame of details.stackTrace.callFrames.reverse()) {
stackLines.push(
` at ${frame.functionName || '<anonymous>'} (${frame.url}:${
frame.lineNumber
}:${frame.columnNumber})`
);
if (stackLines.length >= Error.stackTraceLimit) {
break;
}
}
}
error.stack = [...messageLines, ...stackLines].join('\n');
return error;
}
const getErrorDetails = (details: Protocol.Runtime.ExceptionDetails) => {
let name = '';
let message: string;
const lines = details.exception?.description?.split('\n') ?? [];
const size = details.stackTrace?.callFrames.length ?? 0;
lines.splice(-size, size);
if (details.exception?.className) {
name = details.exception.className;
}
message = lines.join('\n');
if (name && message.startsWith(`${name}: `)) {
message = message.slice(name.length + 2);
}
return {message, name};
};
/**
* @internal
*/
const SOURCE_URL = Symbol('Source URL for Puppeteer evaluation scripts');
/**
* @internal
*/
export class PuppeteerURL {
static INTERNAL_URL = 'pptr:internal';
static fromCallSite(
functionName: string,
site: NodeJS.CallSite
): PuppeteerURL {
const url = new PuppeteerURL();
url.#functionName = functionName;
url.#siteString = site.toString();
return url;
}
static parse = (url: string): PuppeteerURL => {
url = url.slice('pptr:'.length);
const [functionName = '', siteString = ''] = url.split(';');
const puppeteerUrl = new PuppeteerURL();
puppeteerUrl.#functionName = functionName;
puppeteerUrl.#siteString = globalThis.atob(siteString);
return puppeteerUrl;
};
static isPuppeteerURL = (url: string): boolean => {
return url.startsWith('pptr:');
};
#functionName!: string;
#siteString!: string;
get functionName(): string {
return this.#functionName;
}
get siteString(): string {
return this.#siteString;
}
toString(): string {
return `pptr:${[this.#functionName, globalThis.btoa(this.#siteString)].join(
';'
)}`;
}
}
/**
* @internal
*/
export const withSourcePuppeteerURLIfNone = <T extends NonNullable<unknown>>(
functionName: string,
object: T
): T => {
if (Object.prototype.hasOwnProperty.call(object, SOURCE_URL)) {
return object;
}
const original = Error.prepareStackTrace;
Error.prepareStackTrace = (_, stack) => {
// First element is the function. Second element is the caller of this
// function. Third element is the caller of the caller of this function
// which is precisely what we want.
return stack[2];
};
const site = new Error().stack as unknown as NodeJS.CallSite;
Error.prepareStackTrace = original;
return Object.assign(object, {
[SOURCE_URL]: PuppeteerURL.fromCallSite(functionName, site),
});
};
/**
* @internal
*/
export const getSourcePuppeteerURLIfAvailable = <
T extends NonNullable<unknown>
>(
object: T
): PuppeteerURL | undefined => {
if (Object.prototype.hasOwnProperty.call(object, SOURCE_URL)) {
return object[SOURCE_URL as keyof T] as PuppeteerURL;
}
return undefined;
};
/** /**
* @internal * @internal
*/ */

View File

@ -1979,6 +1979,30 @@
"parameters": ["cdp", "firefox"], "parameters": ["cdp", "firefox"],
"expectations": ["SKIP"] "expectations": ["SKIP"]
}, },
{
"testIdPattern": "[stacktrace.spec] Stack trace should work",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["cdp", "firefox"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[stacktrace.spec] Stack trace should work with contiguous evaluation",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["cdp", "firefox"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[stacktrace.spec] Stack trace should work with handles",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["cdp", "firefox"],
"expectations": ["FAIL"]
},
{
"testIdPattern": "[stacktrace.spec] Stack trace should work with nested function calls",
"platforms": ["darwin", "linux", "win32"],
"parameters": ["cdp", "firefox"],
"expectations": ["FAIL"]
},
{ {
"testIdPattern": "[target.spec] Target Browser.waitForTarget should wait for a target", "testIdPattern": "[target.spec] Target Browser.waitForTarget should wait for a target",
"platforms": ["darwin", "linux", "win32"], "platforms": ["darwin", "linux", "win32"],

View File

@ -230,8 +230,7 @@ describe('Evaluation specs', function () {
.catch(error_ => { .catch(error_ => {
return (error = error_); return (error = error_);
}); });
expect(error).toBeTruthy(); expect(error).toEqual('qwerty');
expect(error.message).toContain('qwerty');
}); });
it('should support thrown numbers as error messages', async () => { it('should support thrown numbers as error messages', async () => {
const {page} = getTestState(); const {page} = getTestState();
@ -244,8 +243,7 @@ describe('Evaluation specs', function () {
.catch(error_ => { .catch(error_ => {
return (error = error_); return (error = error_);
}); });
expect(error).toBeTruthy(); expect(error).toEqual(100500);
expect(error.message).toContain('100500');
}); });
it('should return complex objects', async () => { it('should return complex objects', async () => {
const {page} = getTestState(); const {page} = getTestState();

146
test/src/stacktrace.spec.ts Normal file
View File

@ -0,0 +1,146 @@
/**
* Copyright 2023 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import assert from 'assert';
import expect from 'expect';
import {
getTestState,
setupTestBrowserHooks,
setupTestPageAndContextHooks,
} from './mocha-utils.js';
const FILENAME = __filename.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&');
describe('Stack trace', function () {
setupTestBrowserHooks();
setupTestPageAndContextHooks();
it('should work', async () => {
const {page} = getTestState();
const error = (await page
.evaluate(() => {
throw new Error('Test');
})
.catch((error: Error) => {
return error;
})) as Error;
expect(error.name).toEqual('Error');
expect(error.message).toEqual('Test');
assert(error.stack);
error.stack = error.stack.replace(new RegExp(FILENAME, 'g'), '<filename>');
expect(error.stack.split('\n at ').slice(0, 2)).toMatchObject({
...[
'Error: Test',
'evaluate (evaluate at Context.<anonymous> (<filename>:31:14), <anonymous>:1:18)',
],
});
});
it('should work with handles', async () => {
const {page} = getTestState();
const error = (await page
.evaluateHandle(() => {
throw new Error('Test');
})
.catch((error: Error) => {
return error;
})) as Error;
expect(error.name).toEqual('Error');
expect(error.message).toEqual('Test');
assert(error.stack);
error.stack = error.stack.replace(new RegExp(FILENAME, 'g'), '<filename>');
expect(error.stack.split('\n at ').slice(0, 2)).toMatchObject({
...[
'Error: Test',
'evaluateHandle (evaluateHandle at Context.<anonymous> (<filename>:51:14), <anonymous>:1:18)',
],
});
});
it('should work with contiguous evaluation', async () => {
const {page} = getTestState();
const thrower = await page.evaluateHandle(() => {
return () => {
throw new Error('Test');
};
});
const error = (await thrower
.evaluate(thrower => {
thrower();
})
.catch((error: Error) => {
return error;
})) as Error;
expect(error.name).toEqual('Error');
expect(error.message).toEqual('Test');
assert(error.stack);
error.stack = error.stack.replace(new RegExp(FILENAME, 'g'), '<filename>');
expect(error.stack.split('\n at ').slice(0, 3)).toMatchObject({
...[
'Error: Test',
'evaluateHandle (evaluateHandle at Context.<anonymous> (<filename>:70:36), <anonymous>:2:22)',
'evaluate (evaluate at Context.<anonymous> (<filename>:76:14), <anonymous>:1:12)',
],
});
});
it('should work with nested function calls', async () => {
const {page} = getTestState();
const error = (await page
.evaluate(() => {
function a() {
throw new Error('Test');
}
function b() {
a();
}
function c() {
b();
}
function d() {
c();
}
d();
})
.catch((error: Error) => {
return error;
})) as Error;
expect(error.name).toEqual('Error');
expect(error.message).toEqual('Test');
assert(error.stack);
error.stack = error.stack.replace(new RegExp(FILENAME, 'g'), '<filename>');
expect(error.stack.split('\n at ').slice(0, 6)).toMatchObject({
...[
'Error: Test',
'a (evaluate at Context.<anonymous> (<filename>:97:14), <anonymous>:2:22)',
'b (evaluate at Context.<anonymous> (<filename>:97:14), <anonymous>:5:16)',
'c (evaluate at Context.<anonymous> (<filename>:97:14), <anonymous>:8:16)',
'd (evaluate at Context.<anonymous> (<filename>:97:14), <anonymous>:11:16)',
'evaluate (evaluate at Context.<anonymous> (<filename>:97:14), <anonymous>:13:12)',
],
});
});
});