mirror of
https://github.com/puppeteer/puppeteer
synced 2024-06-14 14:02:48 +00:00
feat: implement detailed errors for evaluation (#10114)
This commit is contained in:
parent
75a50257e0
commit
317fa732f9
@ -128,7 +128,6 @@ sidebar_label: API
|
||||
| [defaultArgs](./puppeteer.defaultargs.md) | |
|
||||
| [devices](./puppeteer.devices.md) | |
|
||||
| [errors](./puppeteer.errors.md) | |
|
||||
| [EVALUATION_SCRIPT_URL](./puppeteer.evaluation_script_url.md) | |
|
||||
| [executablePath](./puppeteer.executablepath.md) | |
|
||||
| [KnownDevices](./puppeteer.knowndevices.md) | A list of devices to be used with [Page.emulate()](./puppeteer.page.emulate.md). |
|
||||
| [launch](./puppeteer.launch.md) | |
|
||||
|
@ -1,11 +0,0 @@
|
||||
---
|
||||
sidebar_label: EVALUATION_SCRIPT_URL
|
||||
---
|
||||
|
||||
# EVALUATION_SCRIPT_URL variable
|
||||
|
||||
#### Signature:
|
||||
|
||||
```typescript
|
||||
EVALUATION_SCRIPT_URL = 'pptr://__puppeteer_evaluation_script__';
|
||||
```
|
@ -19,9 +19,13 @@ import {Protocol} from 'devtools-protocol';
|
||||
import {assert} from '../util/assert.js';
|
||||
|
||||
import {CDPSession} from './Connection.js';
|
||||
import {EVALUATION_SCRIPT_URL} from './ExecutionContext.js';
|
||||
import {addEventListener, debugError, PuppeteerEventListener} from './util.js';
|
||||
import {removeEventListeners} from './util.js';
|
||||
import {
|
||||
addEventListener,
|
||||
debugError,
|
||||
PuppeteerEventListener,
|
||||
PuppeteerURL,
|
||||
removeEventListeners,
|
||||
} from './util.js';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -264,7 +268,7 @@ export class JSCoverage {
|
||||
event: Protocol.Debugger.ScriptParsedEvent
|
||||
): Promise<void> {
|
||||
// Ignore puppeteer-injected scripts
|
||||
if (event.url === EVALUATION_SCRIPT_URL) {
|
||||
if (PuppeteerURL.isPuppeteerURL(event.url)) {
|
||||
return;
|
||||
}
|
||||
// Ignore other anonymous scripts unless the reportAnonymousScripts option is true.
|
||||
|
@ -41,7 +41,7 @@ import {LazyArg} from './LazyArg.js';
|
||||
import {CDPPage} from './Page.js';
|
||||
import {ElementFor, EvaluateFuncWith, HandleFor, NodeFor} from './types.js';
|
||||
import {KeyInput} from './USKeyboardLayout.js';
|
||||
import {debugError, isString} from './util.js';
|
||||
import {debugError, isString, withSourcePuppeteerURLIfNone} from './util.js';
|
||||
|
||||
const applyOffsetsToQuad = (
|
||||
quad: Point[],
|
||||
@ -138,6 +138,7 @@ export class CDPElementHandle<
|
||||
pageFunction: Func | string,
|
||||
...args: Params
|
||||
): Promise<Awaited<ReturnType<Func>>> {
|
||||
pageFunction = withSourcePuppeteerURLIfNone(this.$eval.name, pageFunction);
|
||||
const elementHandle = await this.$(selector);
|
||||
if (!elementHandle) {
|
||||
throw new Error(
|
||||
@ -161,6 +162,7 @@ export class CDPElementHandle<
|
||||
pageFunction: Func | string,
|
||||
...args: Params
|
||||
): Promise<Awaited<ReturnType<Func>>> {
|
||||
pageFunction = withSourcePuppeteerURLIfNone(this.$$eval.name, pageFunction);
|
||||
const results = await this.$$(selector);
|
||||
const elements = await this.evaluateHandle((_, ...elements) => {
|
||||
return elements;
|
||||
|
@ -32,18 +32,20 @@ import {LazyArg} from './LazyArg.js';
|
||||
import {scriptInjector} from './ScriptInjector.js';
|
||||
import {EvaluateFunc, HandleFor} from './types.js';
|
||||
import {
|
||||
PuppeteerURL,
|
||||
createEvaluationError,
|
||||
createJSHandle,
|
||||
getExceptionMessage,
|
||||
getSourcePuppeteerURLIfAvailable,
|
||||
isString,
|
||||
valueFromRemoteObject,
|
||||
} 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 getSourceUrlComment = (url: string) => {
|
||||
return `//# sourceURL=${url}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a context for JavaScript execution.
|
||||
*
|
||||
@ -270,14 +272,17 @@ export class ExecutionContext {
|
||||
pageFunction: Func | string,
|
||||
...args: Params
|
||||
): 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)) {
|
||||
const contextId = this._contextId;
|
||||
const expression = pageFunction;
|
||||
const expressionWithSourceUrl = SOURCE_URL_REGEX.test(expression)
|
||||
? expression
|
||||
: expression + '\n' + suffix;
|
||||
: `${expression}\n${sourceUrlComment}\n`;
|
||||
|
||||
const {exceptionDetails, result: remoteObject} = await this._client
|
||||
.send('Runtime.evaluate', {
|
||||
@ -290,9 +295,7 @@ export class ExecutionContext {
|
||||
.catch(rewriteError);
|
||||
|
||||
if (exceptionDetails) {
|
||||
throw new Error(
|
||||
'Evaluation failed: ' + getExceptionMessage(exceptionDetails)
|
||||
);
|
||||
throw createEvaluationError(exceptionDetails);
|
||||
}
|
||||
|
||||
return returnByValue
|
||||
@ -300,10 +303,16 @@ export class ExecutionContext {
|
||||
: createJSHandle(this, remoteObject);
|
||||
}
|
||||
|
||||
const functionDeclaration = stringifyFunction(pageFunction);
|
||||
const functionDeclarationWithSourceUrl = SOURCE_URL_REGEX.test(
|
||||
functionDeclaration
|
||||
)
|
||||
? functionDeclaration
|
||||
: `${functionDeclaration}\n${sourceUrlComment}\n`;
|
||||
let callFunctionOnPromise;
|
||||
try {
|
||||
callFunctionOnPromise = this._client.send('Runtime.callFunctionOn', {
|
||||
functionDeclaration: `${stringifyFunction(pageFunction)}\n${suffix}\n`,
|
||||
functionDeclaration: functionDeclarationWithSourceUrl,
|
||||
executionContextId: this._contextId,
|
||||
arguments: await Promise.all(args.map(convertArgument.bind(this))),
|
||||
returnByValue,
|
||||
@ -322,9 +331,7 @@ export class ExecutionContext {
|
||||
const {exceptionDetails, result: remoteObject} =
|
||||
await callFunctionOnPromise.catch(rewriteError);
|
||||
if (exceptionDetails) {
|
||||
throw new Error(
|
||||
'Evaluation failed: ' + getExceptionMessage(exceptionDetails)
|
||||
);
|
||||
throw createEvaluationError(exceptionDetails);
|
||||
}
|
||||
return returnByValue
|
||||
? valueFromRemoteObject(remoteObject)
|
||||
|
@ -39,7 +39,7 @@ import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js';
|
||||
import {LazyArg} from './LazyArg.js';
|
||||
import {LifecycleWatcher, PuppeteerLifeCycleEvent} from './LifecycleWatcher.js';
|
||||
import {EvaluateFunc, EvaluateFuncWith, HandleFor, NodeFor} from './types.js';
|
||||
import {importFSPromises} from './util.js';
|
||||
import {importFSPromises, withSourcePuppeteerURLIfNone} from './util.js';
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -459,6 +459,10 @@ export class Frame {
|
||||
pageFunction: Func | string,
|
||||
...args: Params
|
||||
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
|
||||
pageFunction = withSourcePuppeteerURLIfNone(
|
||||
this.evaluateHandle.name,
|
||||
pageFunction
|
||||
);
|
||||
return this.worlds[MAIN_WORLD].evaluateHandle(pageFunction, ...args);
|
||||
}
|
||||
|
||||
@ -475,6 +479,10 @@ export class Frame {
|
||||
pageFunction: Func | string,
|
||||
...args: Params
|
||||
): Promise<Awaited<ReturnType<Func>>> {
|
||||
pageFunction = withSourcePuppeteerURLIfNone(
|
||||
this.evaluate.name,
|
||||
pageFunction
|
||||
);
|
||||
return this.worlds[MAIN_WORLD].evaluate(pageFunction, ...args);
|
||||
}
|
||||
|
||||
@ -536,6 +544,7 @@ export class Frame {
|
||||
pageFunction: Func | string,
|
||||
...args: Params
|
||||
): Promise<Awaited<ReturnType<Func>>> {
|
||||
pageFunction = withSourcePuppeteerURLIfNone(this.$eval.name, pageFunction);
|
||||
return this.worlds[MAIN_WORLD].$eval(selector, pageFunction, ...args);
|
||||
}
|
||||
|
||||
@ -571,6 +580,7 @@ export class Frame {
|
||||
pageFunction: Func | string,
|
||||
...args: Params
|
||||
): Promise<Awaited<ReturnType<Func>>> {
|
||||
pageFunction = withSourcePuppeteerURLIfNone(this.$$eval.name, pageFunction);
|
||||
return this.worlds[MAIN_WORLD].$$eval(selector, pageFunction, ...args);
|
||||
}
|
||||
|
||||
|
@ -23,7 +23,7 @@ import {isErrorLike} from '../util/ErrorLike.js';
|
||||
import {CDPSession, isTargetClosedError} from './Connection.js';
|
||||
import {DeviceRequestPromptManager} from './DeviceRequestPrompt.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 {FrameTree} from './FrameTree.js';
|
||||
import {IsolatedWorld} from './IsolatedWorld.js';
|
||||
@ -31,7 +31,7 @@ import {MAIN_WORLD, PUPPETEER_WORLD} from './IsolatedWorlds.js';
|
||||
import {NetworkManager} from './NetworkManager.js';
|
||||
import {Target} from './Target.js';
|
||||
import {TimeoutSettings} from './TimeoutSettings.js';
|
||||
import {debugError} from './util.js';
|
||||
import {debugError, PuppeteerURL} from './util.js';
|
||||
|
||||
const UTILITY_WORLD_NAME = '__puppeteer_utility_world__';
|
||||
|
||||
@ -349,7 +349,7 @@ export class FrameManager extends EventEmitter {
|
||||
}
|
||||
|
||||
await session.send('Page.addScriptToEvaluateOnNewDocument', {
|
||||
source: `//# sourceURL=${EVALUATION_SCRIPT_URL}`,
|
||||
source: `//# sourceURL=${PuppeteerURL.INTERNAL_URL}`,
|
||||
worldName: name,
|
||||
});
|
||||
|
||||
|
@ -42,6 +42,7 @@ import {
|
||||
createJSHandle,
|
||||
debugError,
|
||||
setPageContent,
|
||||
withSourcePuppeteerURLIfNone,
|
||||
} from './util.js';
|
||||
import {TaskManager, WaitTask} from './WaitTask.js';
|
||||
|
||||
@ -183,6 +184,10 @@ export class IsolatedWorld {
|
||||
pageFunction: Func | string,
|
||||
...args: Params
|
||||
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
|
||||
pageFunction = withSourcePuppeteerURLIfNone(
|
||||
this.evaluateHandle.name,
|
||||
pageFunction
|
||||
);
|
||||
const context = await this.executionContext();
|
||||
return context.evaluateHandle(pageFunction, ...args);
|
||||
}
|
||||
@ -194,6 +199,10 @@ export class IsolatedWorld {
|
||||
pageFunction: Func | string,
|
||||
...args: Params
|
||||
): Promise<Awaited<ReturnType<Func>>> {
|
||||
pageFunction = withSourcePuppeteerURLIfNone(
|
||||
this.evaluate.name,
|
||||
pageFunction
|
||||
);
|
||||
const context = await this.executionContext();
|
||||
return context.evaluate(pageFunction, ...args);
|
||||
}
|
||||
@ -240,6 +249,7 @@ export class IsolatedWorld {
|
||||
pageFunction: Func | string,
|
||||
...args: Params
|
||||
): Promise<Awaited<ReturnType<Func>>> {
|
||||
pageFunction = withSourcePuppeteerURLIfNone(this.$eval.name, pageFunction);
|
||||
const document = await this.document();
|
||||
return document.$eval(selector, pageFunction, ...args);
|
||||
}
|
||||
@ -256,6 +266,7 @@ export class IsolatedWorld {
|
||||
pageFunction: Func | string,
|
||||
...args: Params
|
||||
): Promise<Awaited<ReturnType<Func>>> {
|
||||
pageFunction = withSourcePuppeteerURLIfNone(this.$$eval.name, pageFunction);
|
||||
const document = await this.document();
|
||||
return document.$$eval(selector, pageFunction, ...args);
|
||||
}
|
||||
|
@ -23,7 +23,12 @@ import {CDPSession} from './Connection.js';
|
||||
import type {CDPElementHandle} from './ElementHandle.js';
|
||||
import {ExecutionContext} from './ExecutionContext.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;
|
||||
|
||||
@ -71,6 +76,10 @@ export class CDPJSHandle<T = unknown> extends JSHandle<T> {
|
||||
pageFunction: Func | string,
|
||||
...args: Params
|
||||
): Promise<Awaited<ReturnType<Func>>> {
|
||||
pageFunction = withSourcePuppeteerURLIfNone(
|
||||
this.evaluate.name,
|
||||
pageFunction
|
||||
);
|
||||
return await this.executionContext().evaluate(pageFunction, this, ...args);
|
||||
}
|
||||
|
||||
@ -84,6 +93,10 @@ export class CDPJSHandle<T = unknown> extends JSHandle<T> {
|
||||
pageFunction: Func | string,
|
||||
...args: Params
|
||||
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
|
||||
pageFunction = withSourcePuppeteerURLIfNone(
|
||||
this.evaluateHandle.name,
|
||||
pageFunction
|
||||
);
|
||||
return await this.executionContext().evaluateHandle(
|
||||
pageFunction,
|
||||
this,
|
||||
|
@ -85,10 +85,10 @@ import {
|
||||
NodeFor,
|
||||
} from './types.js';
|
||||
import {
|
||||
createClientError,
|
||||
createJSHandle,
|
||||
debugError,
|
||||
evaluationString,
|
||||
getExceptionMessage,
|
||||
getReadableAsBuffer,
|
||||
getReadableFromProtocolStream,
|
||||
isString,
|
||||
@ -97,6 +97,7 @@ import {
|
||||
valueFromRemoteObject,
|
||||
waitForEvent,
|
||||
waitWithTimeout,
|
||||
withSourcePuppeteerURLIfNone,
|
||||
} from './util.js';
|
||||
import {WebWorker} from './WebWorker.js';
|
||||
|
||||
@ -518,6 +519,10 @@ export class CDPPage extends Page {
|
||||
pageFunction: Func | string,
|
||||
...args: Params
|
||||
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
|
||||
pageFunction = withSourcePuppeteerURLIfNone(
|
||||
this.evaluateHandle.name,
|
||||
pageFunction
|
||||
);
|
||||
const context = await this.mainFrame().executionContext();
|
||||
return context.evaluateHandle(pageFunction, ...args);
|
||||
}
|
||||
@ -549,6 +554,7 @@ export class CDPPage extends Page {
|
||||
pageFunction: Func | string,
|
||||
...args: Params
|
||||
): Promise<Awaited<ReturnType<Func>>> {
|
||||
pageFunction = withSourcePuppeteerURLIfNone(this.$eval.name, pageFunction);
|
||||
return this.mainFrame().$eval(selector, pageFunction, ...args);
|
||||
}
|
||||
|
||||
@ -564,6 +570,7 @@ export class CDPPage extends Page {
|
||||
pageFunction: Func | string,
|
||||
...args: Params
|
||||
): Promise<Awaited<ReturnType<Func>>> {
|
||||
pageFunction = withSourcePuppeteerURLIfNone(this.$$eval.name, pageFunction);
|
||||
return this.mainFrame().$$eval(selector, pageFunction, ...args);
|
||||
}
|
||||
|
||||
@ -735,10 +742,7 @@ export class CDPPage extends Page {
|
||||
}
|
||||
|
||||
#handleException(exceptionDetails: Protocol.Runtime.ExceptionDetails): void {
|
||||
const message = getExceptionMessage(exceptionDetails);
|
||||
const err = new Error(message);
|
||||
err.stack = ''; // Don't report clientside error with a node stack attached
|
||||
this.emit(PageEmittedEvents.PageError, err);
|
||||
this.emit(PageEmittedEvents.PageError, createClientError(exceptionDetails));
|
||||
}
|
||||
|
||||
async #onConsoleAPI(
|
||||
@ -1257,6 +1261,10 @@ export class CDPPage extends Page {
|
||||
pageFunction: Func | string,
|
||||
...args: Params
|
||||
): Promise<Awaited<ReturnType<Func>>> {
|
||||
pageFunction = withSourcePuppeteerURLIfNone(
|
||||
this.evaluate.name,
|
||||
pageFunction
|
||||
);
|
||||
return this.#frameManager.mainFrame().evaluate(pageFunction, ...args);
|
||||
}
|
||||
|
||||
|
@ -23,7 +23,7 @@ import {EventEmitter} from './EventEmitter.js';
|
||||
import {ExecutionContext} from './ExecutionContext.js';
|
||||
import {CDPJSHandle} from './JSHandle.js';
|
||||
import {EvaluateFunc, HandleFor} from './types.js';
|
||||
import {debugError} from './util.js';
|
||||
import {debugError, withSourcePuppeteerURLIfNone} from './util.js';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -150,6 +150,10 @@ export class WebWorker extends EventEmitter {
|
||||
pageFunction: Func | string,
|
||||
...args: Params
|
||||
): Promise<Awaited<ReturnType<Func>>> {
|
||||
pageFunction = withSourcePuppeteerURLIfNone(
|
||||
this.evaluate.name,
|
||||
pageFunction
|
||||
);
|
||||
const context = await this.#executionContext;
|
||||
return context.evaluate(pageFunction, ...args);
|
||||
}
|
||||
@ -173,6 +177,10 @@ export class WebWorker extends EventEmitter {
|
||||
pageFunction: Func | string,
|
||||
...args: Params
|
||||
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
|
||||
pageFunction = withSourcePuppeteerURLIfNone(
|
||||
this.evaluateHandle.name,
|
||||
pageFunction
|
||||
);
|
||||
const context = await this.#executionContext;
|
||||
return context.evaluateHandle(pageFunction, ...args);
|
||||
}
|
||||
|
@ -26,12 +26,25 @@ import {EventEmitter} from '../EventEmitter.js';
|
||||
import {PuppeteerLifeCycleEvent} from '../LifecycleWatcher.js';
|
||||
import {TimeoutSettings} from '../TimeoutSettings.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 {ElementHandle} from './ElementHandle.js';
|
||||
import {JSHandle} from './JSHandle.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
|
||||
@ -120,18 +133,31 @@ export class Context extends EventEmitter {
|
||||
pageFunction: Func | string,
|
||||
...args: Params
|
||||
): Promise<HandleFor<Awaited<ReturnType<Func>>> | Awaited<ReturnType<Func>>> {
|
||||
const sourceUrlComment = getSourceUrlComment(
|
||||
getSourcePuppeteerURLIfAvailable(pageFunction)?.toString() ??
|
||||
PuppeteerURL.INTERNAL_URL
|
||||
);
|
||||
|
||||
let responsePromise;
|
||||
const resultOwnership = returnByValue ? 'none' : 'root';
|
||||
if (isString(pageFunction)) {
|
||||
const expression = SOURCE_URL_REGEX.test(pageFunction)
|
||||
? pageFunction
|
||||
: `${pageFunction}\n${sourceUrlComment}\n`;
|
||||
|
||||
responsePromise = this.#connection.send('script.evaluate', {
|
||||
expression: pageFunction,
|
||||
expression: expression,
|
||||
target: {context: this._contextId},
|
||||
resultOwnership,
|
||||
awaitPromise: true,
|
||||
});
|
||||
} else {
|
||||
let functionDeclaration = stringifyFunction(pageFunction);
|
||||
functionDeclaration = SOURCE_URL_REGEX.test(functionDeclaration)
|
||||
? functionDeclaration
|
||||
: `${functionDeclaration}\n${sourceUrlComment}\n`;
|
||||
responsePromise = this.#connection.send('script.callFunction', {
|
||||
functionDeclaration: stringifyFunction(pageFunction),
|
||||
functionDeclaration,
|
||||
arguments: await Promise.all(
|
||||
args.map(arg => {
|
||||
return BidiSerializer.serialize(arg, this);
|
||||
@ -146,7 +172,7 @@ export class Context extends EventEmitter {
|
||||
const {result} = await responsePromise;
|
||||
|
||||
if ('type' in result && result.type === 'exception') {
|
||||
throw new Error(result.exceptionDetails.text);
|
||||
throw createEvaluationError(result.exceptionDetails);
|
||||
}
|
||||
|
||||
return returnByValue
|
||||
|
@ -19,6 +19,7 @@ import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
||||
import {ElementHandle} from '../../api/ElementHandle.js';
|
||||
import {JSHandle as BaseJSHandle} from '../../api/JSHandle.js';
|
||||
import {EvaluateFuncWith, HandleFor, HandleOr} from '../../common/types.js';
|
||||
import {withSourcePuppeteerURLIfNone} from '../util.js';
|
||||
|
||||
import {Connection} from './Connection.js';
|
||||
import {Context} from './Context.js';
|
||||
@ -55,6 +56,10 @@ export class JSHandle<T = unknown> extends BaseJSHandle<T> {
|
||||
pageFunction: Func | string,
|
||||
...args: Params
|
||||
): Promise<Awaited<ReturnType<Func>>> {
|
||||
pageFunction = withSourcePuppeteerURLIfNone(
|
||||
this.evaluate.name,
|
||||
pageFunction
|
||||
);
|
||||
return await this.context().evaluate(pageFunction, this, ...args);
|
||||
}
|
||||
|
||||
@ -65,6 +70,10 @@ export class JSHandle<T = unknown> extends BaseJSHandle<T> {
|
||||
pageFunction: Func | string,
|
||||
...args: Params
|
||||
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
|
||||
pageFunction = withSourcePuppeteerURLIfNone(
|
||||
this.evaluateHandle.name,
|
||||
pageFunction
|
||||
);
|
||||
return await this.context().evaluateHandle(pageFunction, this, ...args);
|
||||
}
|
||||
|
||||
|
@ -31,7 +31,11 @@ import {Handler} from '../EventEmitter.js';
|
||||
import {PDFOptions} from '../PDFOptions.js';
|
||||
import {Viewport} from '../PuppeteerViewport.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 {BidiSerializer} from './Serializer.js';
|
||||
@ -151,6 +155,10 @@ export class Page extends PageBase {
|
||||
pageFunction: Func | string,
|
||||
...args: Params
|
||||
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
|
||||
pageFunction = withSourcePuppeteerURLIfNone(
|
||||
this.evaluateHandle.name,
|
||||
pageFunction
|
||||
);
|
||||
return this.#context.evaluateHandle(pageFunction, ...args);
|
||||
}
|
||||
|
||||
@ -161,6 +169,10 @@ export class Page extends PageBase {
|
||||
pageFunction: Func | string,
|
||||
...args: Params
|
||||
): Promise<Awaited<ReturnType<Func>>> {
|
||||
pageFunction = withSourcePuppeteerURLIfNone(
|
||||
this.evaluate.name,
|
||||
pageFunction
|
||||
);
|
||||
return this.#context.evaluate(pageFunction, ...args);
|
||||
}
|
||||
|
||||
|
@ -17,8 +17,10 @@
|
||||
import * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js';
|
||||
|
||||
import {debug} from '../Debug.js';
|
||||
import {PuppeteerURL} from '../util.js';
|
||||
|
||||
import {Context} from './Context.js';
|
||||
import {BidiSerializer} from './Serializer.js';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -45,3 +47,50 @@ export async function releaseReference(
|
||||
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;
|
||||
}
|
||||
|
@ -41,30 +41,213 @@ export const debugError = debug('puppeteer:error');
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
export function getExceptionMessage(
|
||||
exceptionDetails: Protocol.Runtime.ExceptionDetails
|
||||
): string {
|
||||
if (exceptionDetails.exception) {
|
||||
return (
|
||||
exceptionDetails.exception.description || exceptionDetails.exception.value
|
||||
export function createEvaluationError(
|
||||
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 = error.stack!.split('\n');
|
||||
const messageLines = stackLines.splice(0, messageHeight);
|
||||
|
||||
// The first line is this function which we ignore.
|
||||
stackLines.shift();
|
||||
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})`
|
||||
);
|
||||
}
|
||||
let message = exceptionDetails.text;
|
||||
if (exceptionDetails.stackTrace) {
|
||||
for (const callframe of exceptionDetails.stackTrace.callFrames) {
|
||||
const location =
|
||||
callframe.url +
|
||||
':' +
|
||||
callframe.lineNumber +
|
||||
':' +
|
||||
callframe.columnNumber;
|
||||
const functionName = callframe.functionName || '<anonymous>';
|
||||
message += `\n at ${functionName} (${location})`;
|
||||
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
|
||||
*/
|
||||
|
@ -1979,6 +1979,30 @@
|
||||
"parameters": ["cdp", "firefox"],
|
||||
"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",
|
||||
"platforms": ["darwin", "linux", "win32"],
|
||||
|
@ -230,8 +230,7 @@ describe('Evaluation specs', function () {
|
||||
.catch(error_ => {
|
||||
return (error = error_);
|
||||
});
|
||||
expect(error).toBeTruthy();
|
||||
expect(error.message).toContain('qwerty');
|
||||
expect(error).toEqual('qwerty');
|
||||
});
|
||||
it('should support thrown numbers as error messages', async () => {
|
||||
const {page} = getTestState();
|
||||
@ -244,8 +243,7 @@ describe('Evaluation specs', function () {
|
||||
.catch(error_ => {
|
||||
return (error = error_);
|
||||
});
|
||||
expect(error).toBeTruthy();
|
||||
expect(error.message).toContain('100500');
|
||||
expect(error).toEqual(100500);
|
||||
});
|
||||
it('should return complex objects', async () => {
|
||||
const {page} = getTestState();
|
||||
|
146
test/src/stacktrace.spec.ts
Normal file
146
test/src/stacktrace.spec.ts
Normal 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)',
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user