Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion packages/injected/src/injectedScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ import type { Builtins } from './utilityScript';

export type FrameExpectParams = Omit<channels.FrameExpectParams, 'expectedValue' | 'timeout'> & {
expectedValue?: any;
noAutoWaiting?: boolean;
};

export type ElementState = 'visible' | 'hidden' | 'enabled' | 'disabled' | 'editable' | 'checked' | 'unchecked' | 'indeterminate' | 'stable';
Expand Down
8 changes: 6 additions & 2 deletions packages/playwright-core/src/client/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import { Stream } from './stream';
import { Tracing } from './tracing';
import { Worker } from './worker';
import { WritableStream } from './writableStream';
import { ValidationError, findValidator } from '../protocol/validator';
import { ValidationError, findValidator, maybeFindValidator } from '../protocol/validator';
import type { ClientInstrumentation } from './clientInstrumentation';
import type { HeadersArray } from './types';
import type { ValidatorContext } from '../protocol/validator';
Expand Down Expand Up @@ -214,7 +214,7 @@ export class Connection extends EventEmitter {
if (this._closedError)
return;

const { id, guid, method, params, result, error, log } = message as any;
const { id, guid, method, params, result, error, errorDetails, log } = message as any;
if (id) {
if (this._platform.isLogEnabled('channel'))
this._platform.log('channel', '<RECV ' + JSON.stringify(message));
Expand All @@ -224,7 +224,11 @@ export class Connection extends EventEmitter {
this._callbacks.delete(id);
if (error && !result) {
const parsedError = parseError(error);
parsedError.log = log || [];
rewriteErrorMessage(parsedError, parsedError.message + formatCallLog(this._platform, log));
const detailsValidator = maybeFindValidator(callback.type, callback.method, 'ErrorDetails');
if (detailsValidator)
parsedError.details = detailsValidator(errorDetails ?? {}, '', this._validatorFromWireContext());
callback.reject(parsedError);
} else {
const validator = findValidator(callback.type, callback.method, 'Result');
Expand Down
30 changes: 15 additions & 15 deletions packages/playwright-core/src/client/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,19 @@ import { parseSerializedValue, serializeValue } from '../protocol/serializers';

import type { SerializedError } from '@protocol/channels';

export class TimeoutError extends Error {
export class PlaywrightError extends Error {
log: string[] = [];
details?: any; // As declared in the protocol.
}

export class TimeoutError extends PlaywrightError {
constructor(message: string) {
super(message);
this.name = 'TimeoutError';
}
}

export class TargetClosedError extends Error {
export class TargetClosedError extends PlaywrightError {
constructor(cause?: string) {
super(cause || 'Target page, context or browser has been closed');
}
Expand All @@ -42,24 +47,19 @@ export function serializeError(e: any): SerializedError {
return { value: serializeValue(e, value => ({ fallThrough: value })) };
}

export function parseError(error: SerializedError): Error {
export function parseError(error: SerializedError): PlaywrightError {
if (!error.error) {
if (error.value === undefined)
throw new Error('Serialized error must have either an error or a value');
return parseSerializedValue(error.value, undefined);
}
if (error.error.name === 'TimeoutError') {
const e = new TimeoutError(error.error.message);
e.stack = error.error.stack || '';
return e;
}
if (error.error.name === 'TargetClosedError') {
const e = new TargetClosedError(error.error.message);
e.stack = error.error.stack || '';
return e;
}
const e = new Error(error.error.message);
let e: PlaywrightError;
if (error.error.name === 'TimeoutError')
e = new TimeoutError(error.error.message);
else if (error.error.name === 'TargetClosedError')
e = new TargetClosedError(error.error.message);
else
e = Object.assign(new PlaywrightError(error.error.message), { name: error.error.name });
e.stack = error.error.stack || '';
e.name = error.error.name;
return e;
}
30 changes: 18 additions & 12 deletions packages/playwright-core/src/client/frame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { EventEmitter } from './eventEmitter';
import { ChannelOwner } from './channelOwner';
import { addSourceUrlToScript } from './clientHelper';
import { ElementHandle, convertInputFiles, convertSelectOptionValues } from './elementHandle';
import { PlaywrightError } from './errors';
import { Events } from './events';
import { JSHandle, assertMaxArguments, parseResult, serializeArgument } from './jsHandle';
import { FrameLocator, Locator, testIdAttributeName } from './locator';
Expand Down Expand Up @@ -490,20 +491,25 @@ export class Frame extends ChannelOwner<channels.FrameChannel> implements api.Fr
async _expect(expression: string, options: Omit<channels.FrameExpectParams, 'expression'>): Promise<ExpectResult> {
const params: channels.FrameExpectParams = { expression, ...options, isNot: !!options.isNot };
params.expectedValue = serializeArgument(options.expectedValue);
const channelResult = await this._channel.expect(params);
const result: ExpectResult = {
matches: channelResult.matches,
log: channelResult.log,
timedOut: channelResult.timedOut,
errorMessage: channelResult.errorMessage,
};
if (channelResult.received !== undefined && channelResult.matches === !!options.isNot) {
result.received = {
value: channelResult.received.value !== undefined ? parseResult(channelResult.received.value) : undefined,
ariaSnapshot: channelResult.received.ariaSnapshot,
try {
await this._channel.expect(params);
return { matches: !params.isNot };
} catch (e) {
if (!(e instanceof PlaywrightError))
throw e;
Comment thread
dgozman marked this conversation as resolved.
const details = e.details as channels.FrameExpectErrorDetails;
Comment thread
dgozman marked this conversation as resolved.
const received = details.received ? {
value: details.received.value !== undefined ? parseResult(details.received.value) : undefined,
ariaSnapshot: details.received.ariaSnapshot,
} : undefined;
return {
matches: !!params.isNot,
received,
log: e.log,
timedOut: details.timedOut,
errorMessage: details.customErrorMessage ? 'Error: ' + details.customErrorMessage : undefined,
};
}
return result;
}
}

Expand Down
22 changes: 15 additions & 7 deletions packages/playwright-core/src/client/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { Coverage } from './coverage';
import { DisposableObject, DisposableStub } from './disposable';
import { Download } from './download';
import { ElementHandle, determineScreenshotType } from './elementHandle';
import { TargetClosedError, isTargetClosedError, parseError, serializeError } from './errors';
import { PlaywrightError, TargetClosedError, isTargetClosedError, parseError, serializeError } from './errors';
import { Events } from './events';
import { FileChooser } from './fileChooser';
import { Frame, verifyLoadState } from './frame';
Expand Down Expand Up @@ -625,12 +625,20 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
frame: (options.locator as Locator)._frame._channel,
selector: (options.locator as Locator)._selector,
} : undefined;
return await this._channel.expectScreenshot({
...options,
isNot: !!options.isNot,
locator,
mask,
});
try {
const result = await this._channel.expectScreenshot({
...options,
isNot: !!options.isNot,
locator,
mask,
});
return { actual: result.actual };
} catch (e) {
Comment thread
dgozman marked this conversation as resolved.
if (!(e instanceof PlaywrightError))
throw e;
const details = e.details as channels.PageExpectScreenshotErrorDetails;
return { ...details, errorMessage: details.customErrorMessage };
}
}

async title(): Promise<string> {
Expand Down
12 changes: 7 additions & 5 deletions packages/playwright-core/src/protocol/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1649,15 +1649,14 @@ scheme.FrameExpectParams = tObject({
isNot: tBoolean,
timeout: tFloat,
});
scheme.FrameExpectResult = tObject({
matches: tBoolean,
scheme.FrameExpectResult = tOptional(tObject({}));
scheme.FrameExpectErrorDetails = tObject({
received: tOptional(tObject({
value: tOptional(tType('SerializedValue')),
ariaSnapshot: tOptional(tString),
})),
timedOut: tOptional(tBoolean),
errorMessage: tOptional(tString),
log: tOptional(tArray(tString)),
customErrorMessage: tOptional(tString),
});
scheme.JSHandleInitializer = tObject({
preview: tString,
Expand Down Expand Up @@ -2412,8 +2411,11 @@ scheme.PageExpectScreenshotParams = tObject({
style: tOptional(tString),
});
scheme.PageExpectScreenshotResult = tObject({
actual: tOptional(tBinary),
});
scheme.PageExpectScreenshotErrorDetails = tObject({
diff: tOptional(tBinary),
errorMessage: tOptional(tString),
customErrorMessage: tOptional(tString),
actual: tOptional(tBinary),
previous: tOptional(tBinary),
timedOut: tOptional(tBoolean),
Expand Down
4 changes: 2 additions & 2 deletions packages/playwright-core/src/protocol/validatorPrimitives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ export type ValidatorContext = {
};
export const scheme: { [key: string]: Validator } = {};

export function findValidator(type: string, method: string, kind: 'Initializer' | 'Event' | 'Params' | 'Result'): Validator {
export function findValidator(type: string, method: string, kind: 'Initializer' | 'Event' | 'Params' | 'Result' | 'ErrorDetails'): Validator {
const validator = maybeFindValidator(type, method, kind);
if (!validator)
throw new ValidationError(`Unknown scheme for ${kind}: ${type}.${method}`);
return validator;
}
export function maybeFindValidator(type: string, method: string, kind: 'Initializer' | 'Event' | 'Params' | 'Result'): Validator | undefined {
export function maybeFindValidator(type: string, method: string, kind: 'Initializer' | 'Event' | 'Params' | 'Result' | 'ErrorDetails'): Validator | undefined {
const schemeName = type + (kind === 'Initializer' ? '' : method[0].toUpperCase() + method.substring(1)) + kind;
return scheme[schemeName];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { isUnderTest } from '@utils/debug';
import { assert } from '@isomorphic/assert';
import { monotonicTime } from '@isomorphic/time';
import { rewriteErrorMessage } from '@isomorphic/stackTrace';
import { ValidationError, createMetadataValidator, createWaitInfoValidator, findValidator } from '../../protocol/validator';
import { ValidationError, createMetadataValidator, createWaitInfoValidator, findValidator, maybeFindValidator } from '../../protocol/validator';
import { TargetClosedError, isTargetClosedError, serializeError } from '../errors';
import { createRootSdkObject, SdkObject } from '../instrumentation';
import { isProtocolError } from '../protocolError';
Expand Down Expand Up @@ -371,6 +371,9 @@ export class DispatcherConnection {
rewriteErrorMessage(e, 'Target crashed ' + e.browserLogMessage());
}
response.error = serializeError(e);
const detailsValidator = maybeFindValidator(dispatcher._type, method, 'ErrorDetails');
if (detailsValidator)
response.errorDetails = detailsValidator((e as any)?.details ?? {}, '', this._validatorToWireContext());
// The command handler could have set error in the metadata, do not reset it if there was no exception.
callMetadata.error = response.error;
} finally {
Expand Down
27 changes: 8 additions & 19 deletions packages/playwright-core/src/server/dispatchers/frameDispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,8 @@
* limitations under the License.
*/

import yaml from 'yaml';
import { parseAriaSnapshotUnsafe } from '@isomorphic/ariaSnapshot';
import { renderTitleForCall } from '@isomorphic/protocolFormatter';
import { Frame } from '../frames';
import { ExpectError, Frame } from '../frames';
import { Dispatcher } from './dispatcher';
import { ElementHandleDispatcher } from './elementHandlerDispatcher';
import { parseArgument, serializeResult } from './jsHandleDispatcher';
Expand Down Expand Up @@ -273,23 +271,14 @@ export class FrameDispatcher extends Dispatcher<Frame, channels.FrameChannel, Br
}

async expect(params: channels.FrameExpectParams, progress: Progress): Promise<channels.FrameExpectResult> {
let expectedValue = params.expectedValue ? parseArgument(params.expectedValue) : undefined;
if (params.expression === 'to.match.aria' && expectedValue)
expectedValue = parseAriaSnapshotUnsafe(yaml, expectedValue);
progress.log(`${renderTitleForCall(progress.metadata)}${params.timeout ? ` with timeout ${params.timeout}ms` : ''}`);
const result = await this._frame.expect(progress, params.selector, { ...params, expectedValue });
const channelResult: channels.FrameExpectResult = {
matches: result.matches,
log: result.log,
timedOut: result.timedOut,
errorMessage: result.errorMessage,
};
if (result.received !== undefined) {
channelResult.received = {
value: result.received.value !== undefined ? serializeResult(result.received.value) : undefined,
ariaSnapshot: result.received.ariaSnapshot,
};
const expectedValue = params.expectedValue ? parseArgument(params.expectedValue) : undefined;
try {
await this._frame.expect(progress, params.selector, { ...params, expectedValue });
} catch (e) {
if (e instanceof ExpectError && e.details.received && 'value' in e.details.received)
e.details.received.value = serializeResult(e.details.received.value);
throw e;
}
return channelResult;
}
}
Loading
Loading