fix(runner): Properly attribute error messages to locator handlers
This commit is contained in:
parent
aabbcbf41d
commit
1ac1ca64bf
|
|
@ -25,6 +25,7 @@ import { zones } from '../utils/zones';
|
||||||
import type { ClientInstrumentation } from './clientInstrumentation';
|
import type { ClientInstrumentation } from './clientInstrumentation';
|
||||||
import type { Connection } from './connection';
|
import type { Connection } from './connection';
|
||||||
import type { Logger } from './types';
|
import type { Logger } from './types';
|
||||||
|
import { isOverriddenAPIError } from './errors';
|
||||||
|
|
||||||
type Listener = (...args: any[]) => void;
|
type Listener = (...args: any[]) => void;
|
||||||
|
|
||||||
|
|
@ -203,15 +204,18 @@ export abstract class ChannelOwner<T extends channels.Channel = channels.Channel
|
||||||
return result;
|
return result;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const innerError = ((process.env.PWDEBUGIMPL || isUnderTest()) && e.stack) ? '\n<inner error>\n' + e.stack : '';
|
const innerError = ((process.env.PWDEBUGIMPL || isUnderTest()) && e.stack) ? '\n<inner error>\n' + e.stack : '';
|
||||||
if (apiName && !apiName.includes('<anonymous>'))
|
|
||||||
e.message = apiName + ': ' + e.message;
|
const computedAPIName = isOverriddenAPIError(e) ? e.apiNameOverride : apiName;
|
||||||
|
|
||||||
|
if (computedAPIName && !computedAPIName.includes('<anonymous>'))
|
||||||
|
e.message = computedAPIName + ': ' + e.message;
|
||||||
const stackFrames = '\n' + stringifyStackFrames(stackTrace.frames).join('\n') + innerError;
|
const stackFrames = '\n' + stringifyStackFrames(stackTrace.frames).join('\n') + innerError;
|
||||||
if (stackFrames.trim())
|
if (stackFrames.trim())
|
||||||
e.stack = e.message + stackFrames;
|
e.stack = e.message + stackFrames;
|
||||||
else
|
else
|
||||||
e.stack = '';
|
e.stack = '';
|
||||||
csi?.onApiCallEnd(callCookie, e);
|
csi?.onApiCallEnd(callCookie, e);
|
||||||
logApiCall(logger, `<= ${apiName} failed`, isInternal);
|
logApiCall(logger, `<= ${computedAPIName} failed`, isInternal);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,21 @@ export function isTargetClosedError(error: Error) {
|
||||||
return error instanceof TargetClosedError;
|
return error instanceof TargetClosedError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class OverriddenAPIError extends Error {
|
||||||
|
public apiNameOverride: string;
|
||||||
|
|
||||||
|
constructor(error: Exclude<SerializedError['error'], undefined>, apiNameOverride: string) {
|
||||||
|
super(error.message);
|
||||||
|
this.name = error.name;
|
||||||
|
this.stack = error.stack;
|
||||||
|
this.apiNameOverride = apiNameOverride;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isOverriddenAPIError(error: Error) {
|
||||||
|
return error instanceof OverriddenAPIError;
|
||||||
|
}
|
||||||
|
|
||||||
export function serializeError(e: any): SerializedError {
|
export function serializeError(e: any): SerializedError {
|
||||||
if (isError(e))
|
if (isError(e))
|
||||||
return { error: { message: e.message, stack: e.stack, name: e.name } };
|
return { error: { message: e.message, stack: e.stack, name: e.name } };
|
||||||
|
|
@ -47,6 +62,12 @@ export function parseError(error: SerializedError): Error {
|
||||||
throw new Error('Serialized error must have either an error or a value');
|
throw new Error('Serialized error must have either an error or a value');
|
||||||
return parseSerializedValue(error.value, undefined);
|
return parseSerializedValue(error.value, undefined);
|
||||||
}
|
}
|
||||||
|
if (error.error.apiNameOverride) {
|
||||||
|
const e = new OverriddenAPIError(error.error, error.error.apiNameOverride);
|
||||||
|
e.stack = error.error.stack;
|
||||||
|
e.name = error.error.name;
|
||||||
|
return e;
|
||||||
|
}
|
||||||
if (error.error.name === 'TimeoutError') {
|
if (error.error.name === 'TimeoutError') {
|
||||||
const e = new TimeoutError(error.error.message);
|
const e = new TimeoutError(error.error.message);
|
||||||
e.stack = error.error.stack || '';
|
e.stack = error.error.stack || '';
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
import type { SerializedError } from '@protocol/channels';
|
import type { SerializedError } from '@protocol/channels';
|
||||||
import { isError } from '../utils';
|
import { isError } from '../utils';
|
||||||
import { parseSerializedValue, serializeValue } from '../protocol/serializers';
|
import { parseSerializedValue, serializeValue } from '../protocol/serializers';
|
||||||
|
import { JavaScriptErrorInEvaluate } from './javascript';
|
||||||
|
|
||||||
class CustomError extends Error {
|
class CustomError extends Error {
|
||||||
constructor(message: string) {
|
constructor(message: string) {
|
||||||
|
|
@ -37,9 +38,43 @@ export function isTargetClosedError(error: Error) {
|
||||||
return error instanceof TargetClosedError || error.name === 'TargetClosedError';
|
return error instanceof TargetClosedError || error.name === 'TargetClosedError';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class OverriddenAPIError extends JavaScriptErrorInEvaluate {
|
||||||
|
public apiNameOverride: string;
|
||||||
|
|
||||||
|
constructor(error: JavaScriptErrorInEvaluate, apiNameOverride: string) {
|
||||||
|
super(error.message);
|
||||||
|
this.name = error.name;
|
||||||
|
this.stack = error.stack;
|
||||||
|
this.apiNameOverride = apiNameOverride;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isOverriddenAPIError(error: Error) {
|
||||||
|
return error instanceof OverriddenAPIError;
|
||||||
|
}
|
||||||
|
|
||||||
export function serializeError(e: any): SerializedError {
|
export function serializeError(e: any): SerializedError {
|
||||||
if (isError(e))
|
if (isError(e)) {
|
||||||
return { error: { message: e.message, stack: e.stack, name: e.name } };
|
const serializedError: {
|
||||||
|
error: {
|
||||||
|
message: any,
|
||||||
|
stack: any,
|
||||||
|
name: string,
|
||||||
|
apiNameOverride?: string;
|
||||||
|
}
|
||||||
|
} = {
|
||||||
|
error: {
|
||||||
|
message: e.message,
|
||||||
|
stack: e.stack,
|
||||||
|
name: e.name
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isOverriddenAPIError(e))
|
||||||
|
serializedError.error.apiNameOverride = e.apiNameOverride;
|
||||||
|
|
||||||
|
return serializedError;
|
||||||
|
}
|
||||||
return { value: serializeValue(e, value => ({ fallThrough: value })) };
|
return { value: serializeValue(e, value => ({ fallThrough: value })) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ import type { TimeoutOptions } from '../common/types';
|
||||||
import { isInvalidSelectorError } from '../utils/isomorphic/selectorParser';
|
import { isInvalidSelectorError } from '../utils/isomorphic/selectorParser';
|
||||||
import { parseEvaluationResultValue, source } from './isomorphic/utilityScriptSerializers';
|
import { parseEvaluationResultValue, source } from './isomorphic/utilityScriptSerializers';
|
||||||
import type { SerializedValue } from './isomorphic/utilityScriptSerializers';
|
import type { SerializedValue } from './isomorphic/utilityScriptSerializers';
|
||||||
import { TargetClosedError, TimeoutError } from './errors';
|
import { OverriddenAPIError, TargetClosedError, TimeoutError } from './errors';
|
||||||
import { asLocator } from '../utils';
|
import { asLocator } from '../utils';
|
||||||
import { helper } from './helper';
|
import { helper } from './helper';
|
||||||
|
|
||||||
|
|
@ -494,30 +494,36 @@ export class Page extends SdkObject {
|
||||||
// Do not run locator handlers from inside locator handler callbacks to avoid deadlocks.
|
// Do not run locator handlers from inside locator handler callbacks to avoid deadlocks.
|
||||||
if (this._locatorHandlerRunningCounter)
|
if (this._locatorHandlerRunningCounter)
|
||||||
return;
|
return;
|
||||||
for (const [uid, handler] of this._locatorHandlers) {
|
try {
|
||||||
if (!handler.resolved) {
|
for (const [uid, handler] of this._locatorHandlers) {
|
||||||
if (await this.mainFrame().isVisibleInternal(handler.selector, { strict: true })) {
|
if (!handler.resolved) {
|
||||||
handler.resolved = new ManualPromise();
|
if (await this.mainFrame().isVisibleInternal(handler.selector, { strict: true })) {
|
||||||
this.emit(Page.Events.LocatorHandlerTriggered, uid);
|
handler.resolved = new ManualPromise();
|
||||||
|
this.emit(Page.Events.LocatorHandlerTriggered, uid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (handler.resolved) {
|
||||||
|
++this._locatorHandlerRunningCounter;
|
||||||
|
progress.log(` found ${asLocator(this.attribution.playwright.options.sdkLanguage, handler.selector)}, intercepting action to run the handler`);
|
||||||
|
const promise = handler.resolved.then(async () => {
|
||||||
|
progress.throwIfAborted();
|
||||||
|
if (!handler.noWaitAfter) {
|
||||||
|
progress.log(` locator handler has finished, waiting for ${asLocator(this.attribution.playwright.options.sdkLanguage, handler.selector)} to be hidden`);
|
||||||
|
await this.mainFrame().waitForSelectorInternal(progress, handler.selector, false, { state: 'hidden' });
|
||||||
|
} else {
|
||||||
|
progress.log(` locator handler has finished`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await this.openScope.race(promise).finally(() => --this._locatorHandlerRunningCounter);
|
||||||
|
// Avoid side-effects after long-running operation.
|
||||||
|
progress.throwIfAborted();
|
||||||
|
progress.log(` interception handler has finished, continuing`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (handler.resolved) {
|
} catch (e) {
|
||||||
++this._locatorHandlerRunningCounter;
|
// Since this checkpoint occurs during the evaluation of a different public API call, any errors are automatically attributed
|
||||||
progress.log(` found ${asLocator(this.attribution.playwright.options.sdkLanguage, handler.selector)}, intercepting action to run the handler`);
|
// to that API call, rather than the locator handler. Mark the error from the locator handler
|
||||||
const promise = handler.resolved.then(async () => {
|
throw new OverriddenAPIError(e, 'page.addLocatorHandler');
|
||||||
progress.throwIfAborted();
|
|
||||||
if (!handler.noWaitAfter) {
|
|
||||||
progress.log(` locator handler has finished, waiting for ${asLocator(this.attribution.playwright.options.sdkLanguage, handler.selector)} to be hidden`);
|
|
||||||
await this.mainFrame().waitForSelectorInternal(progress, handler.selector, false, { state: 'hidden' });
|
|
||||||
} else {
|
|
||||||
progress.log(` locator handler has finished`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
await this.openScope.race(promise).finally(() => --this._locatorHandlerRunningCounter);
|
|
||||||
// Avoid side-effects after long-running operation.
|
|
||||||
progress.throwIfAborted();
|
|
||||||
progress.log(` interception handler has finished, continuing`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -281,6 +281,7 @@ export type SerializedError = {
|
||||||
message: string,
|
message: string,
|
||||||
name: string,
|
name: string,
|
||||||
stack?: string,
|
stack?: string,
|
||||||
|
apiNameOverride?: string,
|
||||||
},
|
},
|
||||||
value?: SerializedValue,
|
value?: SerializedValue,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -371,3 +371,19 @@ test('should removeLocatorHandler', async ({ page, server }) => {
|
||||||
await expect(page.locator('#interstitial')).toBeVisible();
|
await expect(page.locator('#interstitial')).toBeVisible();
|
||||||
expect(error.message).toContain('Timeout 3000ms exceeded');
|
expect(error.message).toContain('Timeout 3000ms exceeded');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should attribute errors to the locator handler', async ({ page }) => {
|
||||||
|
await page.setContent(`<html><body><script>setTimeout(() => {document.body.innerHTML = '<div><ul><li>Foo</li></ul><ul><li>Bar</li></ul></div>'}, 100)</script></body></html>`);
|
||||||
|
|
||||||
|
await page.addLocatorHandler(page.locator('ul'), async locator => Promise.resolve());
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Perform an operation that will trigger the locator handler
|
||||||
|
await page.locator('ul > li').first().boundingBox();
|
||||||
|
|
||||||
|
// Unreachable
|
||||||
|
expect(false).toBeTruthy();
|
||||||
|
} catch (e) {
|
||||||
|
expect(e.message).toContain(`page.addLocatorHandler: Error: strict mode violation: locator('ul') resolved to 2 elements:`);
|
||||||
|
}
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue