diff --git a/packages/playwright-core/src/client/connection.ts b/packages/playwright-core/src/client/connection.ts index 2c64ebb2a4d12..8c25949ddb514 100644 --- a/packages/playwright-core/src/client/connection.ts +++ b/packages/playwright-core/src/client/connection.ts @@ -39,7 +39,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 { rewriteErrorMessage } from '../utils/isomorphic/stackTrace'; import type { ClientInstrumentation } from './clientInstrumentation'; @@ -159,7 +159,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', ' ({ 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'); @@ -58,7 +63,7 @@ export function parseError(error: SerializedError): Error { e.stack = error.error.stack || ''; return e; } - const e = new Error(error.error.message); + const e = new PlaywrightError(error.error.message); e.stack = error.error.stack || ''; e.name = error.error.name; return e; diff --git a/packages/playwright-core/src/client/locator.ts b/packages/playwright-core/src/client/locator.ts index 9e759c2f34220..16823592241ca 100644 --- a/packages/playwright-core/src/client/locator.ts +++ b/packages/playwright-core/src/client/locator.ts @@ -15,6 +15,7 @@ */ import { ElementHandle } from './elementHandle'; +import { TargetClosedError, TimeoutError } from './errors'; import { parseResult, serializeArgument } from './jsHandle'; import { asLocator } from '../utils/isomorphic/locatorGenerators'; import { getByAltTextSelector, getByLabelSelector, getByPlaceholderSelector, getByRoleSelector, getByTestIdSelector, getByTextSelector, getByTitleSelector } from '../utils/isomorphic/locatorUtils'; @@ -374,12 +375,23 @@ export class Locator implements api.Locator { } async _expect(expression: string, options: FrameExpectParams): Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean }> { - const params: channels.FrameExpectParams = { selector: this._selector, expression, ...options, isNot: !!options.isNot }; - params.expectedValue = serializeArgument(options.expectedValue); - const result = (await this._frame._channel.expect(params)); - if (result.received !== undefined) - result.received = parseResult(result.received); - return result; + try { + const params: channels.FrameExpectParams = { selector: this._selector, expression, ...options, isNot: !!options.isNot }; + params.expectedValue = serializeArgument(options.expectedValue); + const result = await this._frame._channel.expect(params); + if (result.received !== undefined) + result.received = parseResult(result.received); + return { matches: !options.isNot, received: result.received }; + } catch (error) { + if (error instanceof TimeoutError || error instanceof TargetClosedError) { + // Timeout error comes with extra details. + const details = error.details as channels.FrameExpectErrorDetails; + const received = details.received !== undefined ? parseResult(details.received) : undefined; + return { matches: options.isNot, received, log: error.log, timedOut: error instanceof TimeoutError }; + } + // Any other error, e.g. "invalid selector", should be thrown directly. + throw error; + } } private _inspect() { diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index 26a925f680721..a300394787660 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -22,7 +22,7 @@ import { evaluationScript } from './clientHelper'; import { Coverage } from './coverage'; import { Download } from './download'; import { ElementHandle, determineScreenshotType } from './elementHandle'; -import { TargetClosedError, isTargetClosedError, serializeError } from './errors'; +import { PlaywrightError, TargetClosedError, TimeoutError, isTargetClosedError, serializeError } from './errors'; import { Events } from './events'; import { FileChooser } from './fileChooser'; import { Frame, verifyLoadState } from './frame'; @@ -598,19 +598,30 @@ export class Page extends ChannelOwner implements api.Page } async _expectScreenshot(options: ExpectScreenshotOptions): Promise<{ actual?: Buffer, previous?: Buffer, diff?: Buffer, errorMessage?: string, log?: string[], timedOut?: boolean}> { - const mask = options?.mask ? options?.mask.map(locator => ({ - frame: (locator as Locator)._frame._channel, - selector: (locator as Locator)._selector, - })) : undefined; - const locator = options.locator ? { - 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, + // This extra wrapApiCall avoids prepending apiName to the error message. + return await this._wrapApiCall(async () => { + try { + const mask = options?.mask ? options?.mask.map(locator => ({ + frame: (locator as Locator)._frame._channel, + selector: (locator as Locator)._selector, + })) : undefined; + const locator = options.locator ? { + 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, + }); + } catch (error) { + if (error instanceof PlaywrightError) { + const details = error.details as channels.PageExpectScreenshotErrorDetails; + return { ...details, errorMessage: error.message, log: error.log, timedOut: error instanceof TimeoutError }; + } + throw error; + } }); } diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index f3de08b065e4f..0a4615311bca6 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -1300,12 +1300,12 @@ scheme.PageExpectScreenshotParams = tObject({ style: tOptional(tString), }); scheme.PageExpectScreenshotResult = tObject({ + actual: tOptional(tBinary), +}); +scheme.PageExpectScreenshotErrorDetails = tObject({ diff: tOptional(tBinary), - errorMessage: tOptional(tString), actual: tOptional(tBinary), previous: tOptional(tBinary), - timedOut: tOptional(tBoolean), - log: tOptional(tArray(tString)), }); scheme.PageScreenshotParams = tObject({ timeout: tNumber, @@ -1889,10 +1889,10 @@ scheme.FrameExpectParams = tObject({ timeout: tNumber, }); scheme.FrameExpectResult = tObject({ - matches: tBoolean, received: tOptional(tType('SerializedValue')), - timedOut: tOptional(tBoolean), - log: tOptional(tArray(tString)), +}); +scheme.FrameExpectErrorDetails = tObject({ + received: tOptional(tType('SerializedValue')), }); scheme.WorkerInitializer = tObject({ url: tString, diff --git a/packages/playwright-core/src/protocol/validatorPrimitives.ts b/packages/playwright-core/src/protocol/validatorPrimitives.ts index eadd2e014e67d..ad7da6d2b204d 100644 --- a/packages/playwright-core/src/protocol/validatorPrimitives.ts +++ b/packages/playwright-core/src/protocol/validatorPrimitives.ts @@ -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]; } diff --git a/packages/playwright-core/src/server/dispatchers/dispatcher.ts b/packages/playwright-core/src/server/dispatchers/dispatcher.ts index ca1db256e9ed8..89795b036c8bb 100644 --- a/packages/playwright-core/src/server/dispatchers/dispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/dispatcher.ts @@ -17,7 +17,7 @@ import { EventEmitter } from 'events'; import { eventsHelper } from '../utils/eventsHelper'; -import { ValidationError, createMetadataValidator, findValidator } from '../../protocol/validator'; +import { ValidationError, createMetadataValidator, findValidator, maybeFindValidator } from '../../protocol/validator'; import { LongStandingScope, assert, monotonicTime, rewriteErrorMessage } from '../../utils'; import { isUnderTest } from '../utils/debug'; import { TargetClosedError, isTargetClosedError, serializeError } from '../errors'; @@ -376,9 +376,11 @@ export class DispatcherConnection { rewriteErrorMessage(e, 'Target crashed ' + e.browserLogMessage()); } } - response.error = serializeError(e); - // The command handler could have set error in the metadata, do not reset it if there was no exception. - callMetadata.error = response.error; + callMetadata.error = serializeError(e); + response.error = callMetadata.error; + const validator = maybeFindValidator(dispatcher._type, method, 'ErrorDetails'); + if (validator) + response.errorDetails = validator(callMetadata.errorDetails || {}, '', this._validatorToWireContext()); } finally { callMetadata.endTime = monotonicTime(); await sdkObject?.instrumentation.onAfterCall(sdkObject, callMetadata); diff --git a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts index 3afab78c254f7..4c996d0dfee06 100644 --- a/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/frameDispatcher.ts @@ -262,10 +262,16 @@ export class FrameDispatcher extends Dispatcher { diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index c6f47914108a5..0f4a074065831 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -33,7 +33,6 @@ import { debugLogger } from './utils/debugLogger'; import { eventsHelper } from './utils/eventsHelper'; import { isInvalidSelectorError } from '../utils/isomorphic/selectorParser'; import { ManualPromise } from '../utils/isomorphic/manualPromise'; -import { compressCallLog } from './callLog'; import type { ConsoleMessage } from './console'; import type { ElementStateWithoutStable, FrameExpectParams, InjectedScript } from '@injected/injectedScript'; @@ -1382,77 +1381,55 @@ export class Frame extends SdkObject { }, options.timeout); } - async expect(metadata: CallMetadata, selector: string, options: FrameExpectParams): Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean }> { - const result = await this._expectImpl(metadata, selector, options); - // Library mode special case for the expect errors which are return values, not exceptions. - if (result.matches === options.isNot) - metadata.error = { error: { name: 'Expect', message: 'Expect failed' } }; - return result; - } - - private async _expectImpl(metadata: CallMetadata, selector: string, options: FrameExpectParams): Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean }> { - const lastIntermediateResult: { received?: any, isSet: boolean } = { isSet: false }; - try { - let timeout = options.timeout; - const start = timeout > 0 ? monotonicTime() : 0; + async expect(metadata: CallMetadata, selector: string, options: FrameExpectParams): Promise<{ received?: any }> { + let timeout = options.timeout; + const start = timeout > 0 ? monotonicTime() : 0; - // Step 1: perform locator handlers checkpoint with a specified timeout. - await (new ProgressController(metadata, this)).run(async progress => { - progress.log(`${renderTitleForCall(metadata)}${timeout ? ` with timeout ${timeout}ms` : ''}`); - progress.log(`waiting for ${this._asLocator(selector)}`); - await this._page.performActionPreChecks(progress); - }, timeout); + // Step 1: perform locator handlers checkpoint with a specified timeout. + await (new ProgressController(metadata, this)).run(async progress => { + progress.log(`${renderTitleForCall(metadata)}${timeout ? ` with timeout ${timeout}ms` : ''}`); + progress.log(`waiting for ${this._asLocator(selector)}`); + await this._page.performActionPreChecks(progress); + }, timeout); - // Step 2: perform one-shot expect check without a timeout. - // Supports the case of `expect(locator).toBeVisible({ timeout: 1 })` - // that should succeed when the locator is already visible. - try { - const resultOneShot = await (new ProgressController(metadata, this)).run(async progress => { - return await this._expectInternal(progress, selector, options, lastIntermediateResult); - }); - if (resultOneShot.matches !== options.isNot) - return resultOneShot; - } catch (e) { - if (js.isJavaScriptErrorInEvaluate(e) || isInvalidSelectorError(e)) - throw e; - // Ignore any other errors from one-shot, we'll handle them during retries. - } - if (timeout > 0) { - const elapsed = monotonicTime() - start; - timeout -= elapsed; - } - if (timeout < 0) - return { matches: options.isNot, log: compressCallLog(metadata.log), timedOut: true, received: lastIntermediateResult.received }; - - // Step 3: auto-retry expect with increasing timeouts. Bounded by the total remaining time. - return await (new ProgressController(metadata, this)).run(async progress => { - return await this.retryWithProgressAndTimeouts(progress, [100, 250, 500, 1000], async continuePolling => { - await this._page.performActionPreChecks(progress); - const { matches, received } = await this._expectInternal(progress, selector, options, lastIntermediateResult); - if (matches === options.isNot) { - // Keep waiting in these cases: - // expect(locator).conditionThatDoesNotMatch - // expect(locator).not.conditionThatDoesMatch - return continuePolling; - } - return { matches, received }; - }); - }, timeout); + // Step 2: perform one-shot expect check without a timeout. + // Supports the case of `expect(locator).toBeVisible({ timeout: 1 })` + // that should succeed when the locator is already visible. + try { + const resultOneShot = await (new ProgressController(metadata, this)).run(async progress => { + return await this._expectInternal(progress, selector, options); + }); + if (resultOneShot.matches !== options.isNot) + return { received: resultOneShot.received }; } catch (e) { - // Q: Why not throw upon isSessionClosedError(e) as in other places? - // A: We want user to receive a friendly message containing the last intermediate result. if (js.isJavaScriptErrorInEvaluate(e) || isInvalidSelectorError(e)) throw e; - const result: { matches: boolean, received?: any, log?: string[], timedOut?: boolean } = { matches: options.isNot, log: compressCallLog(metadata.log) }; - if (lastIntermediateResult.isSet) - result.received = lastIntermediateResult.received; - if (e instanceof TimeoutError) - result.timedOut = true; - return result; + // Ignore any other errors from one-shot, we'll handle them during retries. + } + if (timeout > 0) { + const elapsed = monotonicTime() - start; + timeout -= elapsed; } + if (timeout < 0) + throw new TimeoutError(`Timeout ${options.timeout}ms exceeded.`); + + // Step 3: auto-retry expect with increasing timeouts. Bounded by the total remaining time. + return await (new ProgressController(metadata, this)).run(async progress => { + return await this.retryWithProgressAndTimeouts(progress, [100, 250, 500, 1000], async continuePolling => { + await this._page.performActionPreChecks(progress); + const { matches, received } = await this._expectInternal(progress, selector, options); + if (matches === options.isNot) { + // Keep waiting in these cases: + // expect(locator).conditionThatDoesNotMatch + // expect(locator).not.conditionThatDoesMatch + return continuePolling; + } + return { received }; + }); + }, timeout); } - private async _expectInternal(progress: Progress, selector: string, options: FrameExpectParams, lastIntermediateResult: { received?: any, isSet: boolean }) { + private async _expectInternal(progress: Progress, selector: string, options: FrameExpectParams): Promise<{ matches: boolean, received?: any }> { const selectorInFrame = await this.selectors.resolveFrameForSelector(selector, { strict: true }); progress.throwIfAborted(); @@ -1481,12 +1458,17 @@ export class Frame extends SdkObject { progress.log(log); // Note: missingReceived avoids `unexpected value "undefined"` when element was not found. if (matches === options.isNot) { - lastIntermediateResult.received = missingReceived ? '' : received; - lastIntermediateResult.isSet = true; + progress.metadata.errorDetails = { received: missingReceived ? '' : received }; if (!missingReceived && !Array.isArray(received)) progress.log(` unexpected value "${renderUnexpectedValue(options.expression, received)}"`); } - return { matches, received }; + + let returnedReceived = received; + if (matches !== options.isNot && options.expression !== 'to.match.aria') { + // When expect is passing, we only need the received value for aria snapshots to perform the rebaseline. + returnedReceived = undefined; + } + return { matches, received: returnedReceived }; } async _waitForFunctionExpression(metadata: CallMetadata, expression: string, isFunction: boolean | undefined, arg: any, options: types.WaitForFunctionOptions, world: types.World = 'main'): Promise> { diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index 01392f7b1b219..e93f70eb85888 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -18,7 +18,7 @@ import * as accessibility from './accessibility'; import { BrowserContext } from './browserContext'; import { ConsoleMessage } from './console'; -import { TargetClosedError, TimeoutError } from './errors'; +import { TargetClosedError } from './errors'; import { FileChooser } from './fileChooser'; import * as frames from './frames'; import { helper } from './helper'; @@ -31,10 +31,8 @@ import { LongStandingScope, assert, renderTitleForCall, trimStringWithEllipsis } import { asLocator } from '../utils'; import { getComparator } from './utils/comparators'; import { debugLogger } from './utils/debugLogger'; -import { isInvalidSelectorError } from '../utils/isomorphic/selectorParser'; import { ManualPromise } from '../utils/isomorphic/manualPromise'; import { parseEvaluationResultValue } from '../utils/isomorphic/utilityScriptSerializers'; -import { compressCallLog } from './callLog'; import * as rawBindingsControllerSource from '../generated/bindingsControllerSource'; import type { Artifact } from './artifact'; @@ -588,7 +586,7 @@ export class Page extends SdkObject { await this.delegate.updateRequestInterception(); } - async expectScreenshot(metadata: CallMetadata, options: ExpectScreenshotOptions): Promise<{ actual?: Buffer, previous?: Buffer, diff?: Buffer, errorMessage?: string, log?: string[] }> { + async expectScreenshot(metadata: CallMetadata, options: ExpectScreenshotOptions): Promise<{ actual?: Buffer }> { const locator = options.locator; const rafrafScreenshot = locator ? async (progress: Progress, timeout: number) => { return await locator.frame.rafrafTimeoutScreenshotElementWithProgress(progress, locator.selector, timeout, options || {}); @@ -601,27 +599,17 @@ export class Page extends SdkObject { const comparator = getComparator('image/png'); const controller = new ProgressController(metadata, this); if (!options.expected && options.isNot) - return { errorMessage: '"not" matcher requires expected result' }; - try { - const format = validateScreenshotOptions(options || {}); - if (format !== 'png') - throw new Error('Only PNG screenshots are supported'); - } catch (error) { - return { errorMessage: error.message }; - } - let intermediateResult: { - actual?: Buffer, - previous?: Buffer, - errorMessage: string, - diff?: Buffer, - } | undefined = undefined; - const areEqualScreenshots = (actual: Buffer | undefined, expected: Buffer | undefined, previous: Buffer | undefined) => { + throw new Error('"not" matcher requires expected result'); + const format = validateScreenshotOptions(options || {}); + if (format !== 'png') + throw new Error('Only PNG screenshots are supported'); + const compareScreenshots = (actual: Buffer | undefined, expected: Buffer | undefined, previous: Buffer | undefined): string | undefined => { const comparatorResult = actual && expected ? comparator(actual, expected, options) : undefined; if (comparatorResult !== undefined && !!comparatorResult === !!options.isNot) - return true; + return; if (comparatorResult) - intermediateResult = { errorMessage: comparatorResult.errorMessage, diff: comparatorResult.diff, actual, previous }; - return false; + metadata.errorDetails = { diff: comparatorResult.diff, actual, previous }; + return comparatorResult?.errorMessage || ''; }; const callTimeout = options.timeout; return controller.run(async progress => { @@ -650,10 +638,11 @@ export class Page extends SdkObject { continue; // Compare against expectation for the first iteration. const expectation = options.expected && isFirstIteration ? options.expected : previous; - if (areEqualScreenshots(actual, expectation, previous)) + const comparatorError = compareScreenshots(actual, expectation, previous); + if (comparatorError === undefined) break; - if (intermediateResult) - progress.log(intermediateResult.errorMessage); + if (comparatorError) + progress.log(comparatorError); isFirstIteration = false; } @@ -668,26 +657,13 @@ export class Page extends SdkObject { return {}; } - if (areEqualScreenshots(actual, options.expected, undefined)) { + const comparatorError = compareScreenshots(actual, options.expected, undefined); + if (comparatorError === undefined) { progress.log(`screenshot matched expectation`); return {}; } - throw new Error(intermediateResult!.errorMessage); - }, callTimeout).catch(e => { - // Q: Why not throw upon isSessionClosedError(e) as in other places? - // A: We want user to receive a friendly diff between actual and expected/previous. - if (js.isJavaScriptErrorInEvaluate(e) || isInvalidSelectorError(e)) - throw e; - let errorMessage = e.message; - if (e instanceof TimeoutError && intermediateResult?.previous) - errorMessage = `Failed to take two consecutive stable screenshots.`; - return { - log: compressCallLog(e.message ? [...metadata.log, e.message] : metadata.log), - ...intermediateResult, - errorMessage, - timedOut: (e instanceof TimeoutError), - }; - }); + throw new Error(comparatorError); + }, callTimeout); } async screenshot(metadata: CallMetadata, options: ScreenshotOptions & types.TimeoutOptions): Promise { diff --git a/packages/playwright/src/matchers/toMatchSnapshot.ts b/packages/playwright/src/matchers/toMatchSnapshot.ts index 98fc72a6f2f85..20053fa0187db 100644 --- a/packages/playwright/src/matchers/toMatchSnapshot.ts +++ b/packages/playwright/src/matchers/toMatchSnapshot.ts @@ -382,7 +382,8 @@ export async function toHaveScreenshot( // This can be due to e.g. spinning animation, so we want to show it as a diff. if (errorMessage) { const header = matcherHint(this, locator, 'toHaveScreenshot', receiver, undefined, undefined, timedOut ? timeout : undefined); - return helper.handleDifferent(actual, undefined, previous, diff, header, errorMessage, log, this._stepInfo); + const errorText = timedOut && previous ? `Failed to take two consecutive stable screenshots.` : errorMessage; + return helper.handleDifferent(actual, undefined, previous, diff, header, errorText, log, this._stepInfo); } // We successfully generated new screenshot. @@ -416,8 +417,9 @@ export async function toHaveScreenshot( if (helper.updateSnapshots === 'changed' || helper.updateSnapshots === 'all') return writeFiles(); + const errorText = timedOut && previous ? `Failed to take two consecutive stable screenshots.` : errorMessage; const header = matcherHint(this, undefined, 'toHaveScreenshot', receiver, undefined, undefined, timedOut ? timeout : undefined); - return helper.handleDifferent(actual, expectScreenshotOptions.expected, previous, diff, header, errorMessage, log, this._stepInfo); + return helper.handleDifferent(actual, expectScreenshotOptions.expected, previous, diff, header, errorText, log, this._stepInfo); } function writeFileSync(aPath: string, content: Buffer | string) { diff --git a/packages/protocol/src/callMetadata.d.ts b/packages/protocol/src/callMetadata.d.ts index 076abd2dc656a..53dc4e146ec4e 100644 --- a/packages/protocol/src/callMetadata.d.ts +++ b/packages/protocol/src/callMetadata.d.ts @@ -37,6 +37,7 @@ export type CallMetadata = { location?: { file: string, line?: number, column?: number }; log: string[]; error?: SerializedError; + errorDetails?: any; result?: any; point?: Point; objectId?: string; diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index 7831b4e26eea5..06fd293a392d4 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -2290,12 +2290,12 @@ export type PageExpectScreenshotOptions = { style?: string, }; export type PageExpectScreenshotResult = { + actual?: Binary, +}; +export type PageExpectScreenshotErrorDetails = { diff?: Binary, - errorMessage?: string, actual?: Binary, previous?: Binary, - timedOut?: boolean, - log?: string[], }; export type PageScreenshotParams = { timeout: number, @@ -3252,10 +3252,10 @@ export type FrameExpectOptions = { useInnerText?: boolean, }; export type FrameExpectResult = { - matches: boolean, received?: SerializedValue, - timedOut?: boolean, - log?: string[], +}; +export type FrameExpectErrorDetails = { + received?: SerializedValue, }; export interface FrameEvents { diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 805c0396d6334..9194014ff0cb9 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -1676,14 +1676,11 @@ Page: clip: Rect? $mixin: CommonScreenshotOptions returns: + actual: binary? + errorDetails: diff: binary? - errorMessage: string? actual: binary? previous: binary? - timedOut: boolean? - log: - type: array? - items: string flags: snapshot: true @@ -2667,12 +2664,9 @@ Frame: isNot: boolean timeout: number returns: - matches: boolean received: SerializedValue? - timedOut: boolean? - log: - type: array? - items: string + errorDetails: + received: SerializedValue? flags: snapshot: true diff --git a/tests/library/trace-viewer.spec.ts b/tests/library/trace-viewer.spec.ts index ff50a454a5e16..38641e17ef3a3 100644 --- a/tests/library/trace-viewer.spec.ts +++ b/tests/library/trace-viewer.spec.ts @@ -311,8 +311,6 @@ test('should show params and return value', async ({ showTraceViewer }) => { /locator:locator\('button'\)/, /expression:"to.have.text"/, /timeout:10000/, - /matches:true/, - /received:"Click"/, ]); }); diff --git a/tests/page/expect-timeout.spec.ts b/tests/page/expect-timeout.spec.ts index d8a062dd25b7a..3d4685627b90c 100644 --- a/tests/page/expect-timeout.spec.ts +++ b/tests/page/expect-timeout.spec.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import { kTargetClosedErrorMessage } from '../config/errors'; import { stripAnsi } from '../config/utils'; import { test, expect } from './pageTest'; @@ -47,7 +48,7 @@ test('should not print timed out error message when page closes', async ({ page expect(page.locator('div')).toHaveText('hey', { timeout: 100000 }).catch(e => e), page.close(), ]); - expect(stripAnsi(error.message)).toContain(`expect(locator).toHaveText(expected)`); + expect(stripAnsi(error.message)).toContain(`expect.toHaveText: ${kTargetClosedErrorMessage}`); expect(stripAnsi(error.message)).not.toContain('Timed out'); }); diff --git a/tests/playwright-test/to-have-screenshot.spec.ts b/tests/playwright-test/to-have-screenshot.spec.ts index 330a7991d995a..0be374d0d651f 100644 --- a/tests/playwright-test/to-have-screenshot.spec.ts +++ b/tests/playwright-test/to-have-screenshot.spec.ts @@ -49,7 +49,7 @@ test('should fail to screenshot a page with infinite animation', async ({ runInl ` }); expect(result.exitCode).toBe(1); - expect(result.output).toContain(`Timeout 2000ms exceeded`); + expect(result.output).toContain(`Timed out 2000ms waiting for expect(page).toHaveScreenshot(expected)`); expect(result.output).toContain(`Expect "toHaveScreenshot" with timeout 2000ms`); expect(result.output).toContain(`generating new stable screenshot expectation`); expect(fs.existsSync(testInfo.outputPath('test-results', 'a-is-a-test', 'is-a-test-1-actual.png'))).toBe(true); @@ -388,7 +388,7 @@ test('should fail to screenshot an element with infinite animation', async ({ ru ` }); expect(result.exitCode).toBe(1); - expect(result.output).toContain(`Timeout 2000ms exceeded`); + expect(result.output).toContain(`Timed out 2000ms waiting for expect(locator).toHaveScreenshot(expected)`); expect(result.output).toContain(`Expect "toHaveScreenshot" with timeout 2000ms`); expect(fs.existsSync(testInfo.outputPath('test-results', 'a-is-a-test', 'is-a-test-1-previous.png'))).toBe(true); expect(fs.existsSync(testInfo.outputPath('test-results', 'a-is-a-test', 'is-a-test-1-actual.png'))).toBe(true); diff --git a/utils/generate_channels.js b/utils/generate_channels.js index 3389f16275f06..61b6c5dfd0611 100755 --- a/utils/generate_channels.js +++ b/utils/generate_channels.js @@ -304,6 +304,15 @@ for (const [name, item] of Object.entries(protocol)) { addScheme(`${derived}${titleCase(methodName)}Result`, `tType('${resultName}')`); channels_ts.push(` ${methodName}(params${method.parameters ? '' : '?'}: ${paramsName}, metadata?: CallMetadata): Promise<${resultName}>;`); + + if (method.errorDetails) { + const errorDetailsName = `${channelName}${titleCase(methodName)}ErrorDetails`; + const details = objectType(method.errorDetails, ''); + ts_types.set(errorDetailsName, details.ts); + addScheme(errorDetailsName, details.scheme); + for (const derived of derivedClasses.get(channelName) || []) + addScheme(`${derived}${titleCase(methodName)}ErrorDetails`, `tType('${errorDetailsName}')`); + } } channels_ts.push(`}`);