chore: render timed out error message when expect timeouts (#18863)
Fixes https://github.com/microsoft/playwright/issues/18859
This commit is contained in:
parent
b40d0d2d83
commit
4e58b0c2ea
|
|
@ -306,7 +306,7 @@ export class Locator implements api.Locator {
|
||||||
await this._frame._channel.waitForSelector({ selector: this._selector, strict: true, omitReturnValue: true, ...options });
|
await this._frame._channel.waitForSelector({ selector: this._selector, strict: true, omitReturnValue: true, ...options });
|
||||||
}
|
}
|
||||||
|
|
||||||
async _expect(customStackTrace: ParsedStackTrace, expression: string, options: Omit<FrameExpectOptions, 'expectedValue'> & { expectedValue?: any }): Promise<{ matches: boolean, received?: any, log?: string[] }> {
|
async _expect(customStackTrace: ParsedStackTrace, expression: string, options: Omit<FrameExpectOptions, 'expectedValue'> & { expectedValue?: any }): Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean }> {
|
||||||
return this._frame._wrapApiCall(async () => {
|
return this._frame._wrapApiCall(async () => {
|
||||||
const params: channels.FrameExpectParams = { selector: this._selector, expression, ...options, isNot: !!options.isNot };
|
const params: channels.FrameExpectParams = { selector: this._selector, expression, ...options, isNot: !!options.isNot };
|
||||||
params.expectedValue = serializeArgument(options.expectedValue);
|
params.expectedValue = serializeArgument(options.expectedValue);
|
||||||
|
|
|
||||||
|
|
@ -1581,6 +1581,7 @@ scheme.FrameExpectParams = tObject({
|
||||||
scheme.FrameExpectResult = tObject({
|
scheme.FrameExpectResult = tObject({
|
||||||
matches: tBoolean,
|
matches: tBoolean,
|
||||||
received: tOptional(tType('SerializedValue')),
|
received: tOptional(tType('SerializedValue')),
|
||||||
|
timedOut: tOptional(tBoolean),
|
||||||
log: tOptional(tArray(tString)),
|
log: tOptional(tArray(tString)),
|
||||||
});
|
});
|
||||||
scheme.WorkerInitializer = tObject({
|
scheme.WorkerInitializer = tObject({
|
||||||
|
|
|
||||||
|
|
@ -1358,7 +1358,7 @@ export class Frame extends SdkObject {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async expect(metadata: CallMetadata, selector: string, options: FrameExpectParams): Promise<{ matches: boolean, received?: any, log?: string[] }> {
|
async expect(metadata: CallMetadata, selector: string, options: FrameExpectParams): Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean }> {
|
||||||
const controller = new ProgressController(metadata, this);
|
const controller = new ProgressController(metadata, this);
|
||||||
const isArray = options.expression === 'to.have.count' || options.expression.endsWith('.array');
|
const isArray = options.expression === 'to.have.count' || options.expression.endsWith('.array');
|
||||||
const mainWorld = options.expression === 'to.have.property';
|
const mainWorld = options.expression === 'to.have.property';
|
||||||
|
|
@ -1418,7 +1418,13 @@ export class Frame extends SdkObject {
|
||||||
// A: We want user to receive a friendly message containing the last intermediate result.
|
// A: We want user to receive a friendly message containing the last intermediate result.
|
||||||
if (js.isJavaScriptErrorInEvaluate(e) || isInvalidSelectorError(e))
|
if (js.isJavaScriptErrorInEvaluate(e) || isInvalidSelectorError(e))
|
||||||
throw e;
|
throw e;
|
||||||
return { received: controller.lastIntermediateResult(), matches: options.isNot, log: metadata.log };
|
const result: { matches: boolean, received?: any, log?: string[], timedOut?: boolean } = { matches: options.isNot, log: metadata.log };
|
||||||
|
const intermediateResult = controller.lastIntermediateResult();
|
||||||
|
if (intermediateResult)
|
||||||
|
result.received = intermediateResult.value;
|
||||||
|
else
|
||||||
|
result.timedOut = true;
|
||||||
|
return result;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ export class ProgressController {
|
||||||
private _state: 'before' | 'running' | 'aborted' | 'finished' = 'before';
|
private _state: 'before' | 'running' | 'aborted' | 'finished' = 'before';
|
||||||
private _deadline: number = 0;
|
private _deadline: number = 0;
|
||||||
private _timeout: number = 0;
|
private _timeout: number = 0;
|
||||||
private _lastIntermediateResult: any;
|
private _lastIntermediateResult: { value: any } | undefined;
|
||||||
readonly metadata: CallMetadata;
|
readonly metadata: CallMetadata;
|
||||||
readonly instrumentation: Instrumentation;
|
readonly instrumentation: Instrumentation;
|
||||||
readonly sdkObject: SdkObject;
|
readonly sdkObject: SdkObject;
|
||||||
|
|
@ -59,7 +59,7 @@ export class ProgressController {
|
||||||
this._logName = logName;
|
this._logName = logName;
|
||||||
}
|
}
|
||||||
|
|
||||||
lastIntermediateResult() {
|
lastIntermediateResult(): { value: any } | undefined {
|
||||||
return this._lastIntermediateResult;
|
return this._lastIntermediateResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -85,7 +85,7 @@ export class ProgressController {
|
||||||
this.instrumentation.onCallLog(this.sdkObject, this.metadata, this._logName, message);
|
this.instrumentation.onCallLog(this.sdkObject, this.metadata, this._logName, message);
|
||||||
}
|
}
|
||||||
if ('intermediateResult' in entry)
|
if ('intermediateResult' in entry)
|
||||||
this._lastIntermediateResult = entry.intermediateResult;
|
this._lastIntermediateResult = { value: entry.intermediateResult };
|
||||||
},
|
},
|
||||||
timeUntilDeadline: () => this._deadline ? this._deadline - monotonicTime() : 2147483647, // 2^31-1 safe setTimeout in Node.
|
timeUntilDeadline: () => this._deadline ? this._deadline - monotonicTime() : 2147483647, // 2^31-1 safe setTimeout in Node.
|
||||||
isRunning: () => this._state === 'running',
|
isRunning: () => this._state === 'running',
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
[*]
|
[*]
|
||||||
|
../types.ts
|
||||||
./utilsBundle.ts
|
./utilsBundle.ts
|
||||||
matchers/
|
matchers/
|
||||||
reporters/
|
reporters/
|
||||||
|
|
|
||||||
25
packages/playwright-test/src/matchers/matcherHint.ts
Normal file
25
packages/playwright-test/src/matchers/matcherHint.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
/**
|
||||||
|
* Copyright Microsoft Corporation. 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 { colors } from 'playwright-core/lib/utilsBundle';
|
||||||
|
import type { Expect } from '../types';
|
||||||
|
|
||||||
|
export function matcherHint(state: ReturnType<Expect['getState']>, matcherName: string, a: any, b: any, matcherOptions: any, timeout?: number) {
|
||||||
|
const message = state.utils.matcherHint(matcherName, a, b, matcherOptions);
|
||||||
|
if (timeout)
|
||||||
|
return colors.red(`Timed out ${timeout}ms waiting for `) + message;
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
@ -27,7 +27,7 @@ import type { ParsedStackTrace } from 'playwright-core/lib/utils/stackTrace';
|
||||||
import { isTextualMimeType } from 'playwright-core/lib/utils/mimeType';
|
import { isTextualMimeType } from 'playwright-core/lib/utils/mimeType';
|
||||||
|
|
||||||
interface LocatorEx extends Locator {
|
interface LocatorEx extends Locator {
|
||||||
_expect(customStackTrace: ParsedStackTrace, expression: string, options: Omit<FrameExpectOptions, 'expectedValue'> & { expectedValue?: any }): Promise<{ matches: boolean, received?: any, log?: string[] }>;
|
_expect(customStackTrace: ParsedStackTrace, expression: string, options: Omit<FrameExpectOptions, 'expectedValue'> & { expectedValue?: any }): Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface APIResponseEx extends APIResponse {
|
interface APIResponseEx extends APIResponse {
|
||||||
|
|
|
||||||
|
|
@ -17,13 +17,14 @@
|
||||||
import type { Expect } from '../types';
|
import type { Expect } from '../types';
|
||||||
import type { ParsedStackTrace } from '../util';
|
import type { ParsedStackTrace } from '../util';
|
||||||
import { expectTypes, callLogText, currentExpectTimeout, captureStackTrace } from '../util';
|
import { expectTypes, callLogText, currentExpectTimeout, captureStackTrace } from '../util';
|
||||||
|
import { matcherHint } from './matcherHint';
|
||||||
|
|
||||||
export async function toBeTruthy(
|
export async function toBeTruthy(
|
||||||
this: ReturnType<Expect['getState']>,
|
this: ReturnType<Expect['getState']>,
|
||||||
matcherName: string,
|
matcherName: string,
|
||||||
receiver: any,
|
receiver: any,
|
||||||
receiverType: string,
|
receiverType: string,
|
||||||
query: (isNot: boolean, timeout: number, customStackTrace: ParsedStackTrace) => Promise<{ matches: boolean, log?: string[] }>,
|
query: (isNot: boolean, timeout: number, customStackTrace: ParsedStackTrace) => Promise<{ matches: boolean, log?: string[], received?: any, timedOut?: boolean }>,
|
||||||
options: { timeout?: number } = {},
|
options: { timeout?: number } = {},
|
||||||
) {
|
) {
|
||||||
expectTypes(receiver, [receiverType], matcherName);
|
expectTypes(receiver, [receiverType], matcherName);
|
||||||
|
|
@ -35,10 +36,10 @@ export async function toBeTruthy(
|
||||||
|
|
||||||
const timeout = currentExpectTimeout(options);
|
const timeout = currentExpectTimeout(options);
|
||||||
|
|
||||||
const { matches, log } = await query(this.isNot, timeout, captureStackTrace('expect.' + matcherName));
|
const { matches, log, timedOut } = await query(this.isNot, timeout, captureStackTrace('expect.' + matcherName));
|
||||||
|
|
||||||
const message = () => {
|
const message = () => {
|
||||||
return this.utils.matcherHint(matcherName, undefined, '', matcherOptions) + callLogText(log);
|
return matcherHint(this, matcherName, undefined, '', matcherOptions, timedOut ? timeout : undefined) + callLogText(log);
|
||||||
};
|
};
|
||||||
|
|
||||||
return { message, pass: matches };
|
return { message, pass: matches };
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import { expectTypes } from '../util';
|
||||||
import { callLogText, currentExpectTimeout } from '../util';
|
import { callLogText, currentExpectTimeout } from '../util';
|
||||||
import type { ParsedStackTrace } from 'playwright-core/lib/utils/stackTrace';
|
import type { ParsedStackTrace } from 'playwright-core/lib/utils/stackTrace';
|
||||||
import { captureStackTrace } from 'playwright-core/lib/utils/stackTrace';
|
import { captureStackTrace } from 'playwright-core/lib/utils/stackTrace';
|
||||||
|
import { matcherHint } from './matcherHint';
|
||||||
|
|
||||||
// Omit colon and one or more spaces, so can call getLabelPrinter.
|
// Omit colon and one or more spaces, so can call getLabelPrinter.
|
||||||
const EXPECTED_LABEL = 'Expected';
|
const EXPECTED_LABEL = 'Expected';
|
||||||
|
|
@ -32,7 +33,7 @@ export async function toEqual<T>(
|
||||||
matcherName: string,
|
matcherName: string,
|
||||||
receiver: any,
|
receiver: any,
|
||||||
receiverType: string,
|
receiverType: string,
|
||||||
query: (isNot: boolean, timeout: number, customStackTrace: ParsedStackTrace) => Promise<{ matches: boolean, received?: any, log?: string[] }>,
|
query: (isNot: boolean, timeout: number, customStackTrace: ParsedStackTrace) => Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean }>,
|
||||||
expected: T,
|
expected: T,
|
||||||
options: { timeout?: number, contains?: boolean } = {},
|
options: { timeout?: number, contains?: boolean } = {},
|
||||||
) {
|
) {
|
||||||
|
|
@ -48,18 +49,18 @@ export async function toEqual<T>(
|
||||||
|
|
||||||
const customStackTrace = captureStackTrace();
|
const customStackTrace = captureStackTrace();
|
||||||
customStackTrace.apiName = 'expect.' + matcherName;
|
customStackTrace.apiName = 'expect.' + matcherName;
|
||||||
const { matches: pass, received, log } = await query(this.isNot, timeout, customStackTrace);
|
const { matches: pass, received, log, timedOut } = await query(this.isNot, timeout, customStackTrace);
|
||||||
|
|
||||||
const message = pass
|
const message = pass
|
||||||
? () =>
|
? () =>
|
||||||
this.utils.matcherHint(matcherName, undefined, undefined, matcherOptions) +
|
matcherHint(this, matcherName, undefined, undefined, matcherOptions, timedOut ? timeout : undefined) +
|
||||||
'\n\n' +
|
'\n\n' +
|
||||||
`Expected: not ${this.utils.printExpected(expected)}\n` +
|
`Expected: not ${this.utils.printExpected(expected)}\n` +
|
||||||
(this.utils.stringify(expected) !== this.utils.stringify(received)
|
(this.utils.stringify(expected) !== this.utils.stringify(received)
|
||||||
? `Received: ${this.utils.printReceived(received)}`
|
? `Received: ${this.utils.printReceived(received)}`
|
||||||
: '') + callLogText(log)
|
: '') + callLogText(log)
|
||||||
: () =>
|
: () =>
|
||||||
this.utils.matcherHint(matcherName, undefined, undefined, matcherOptions) +
|
matcherHint(this, matcherName, undefined, undefined, matcherOptions, timedOut ? timeout : undefined) +
|
||||||
'\n\n' +
|
'\n\n' +
|
||||||
this.utils.printDiffOrStringify(
|
this.utils.printDiffOrStringify(
|
||||||
expected,
|
expected,
|
||||||
|
|
|
||||||
|
|
@ -24,13 +24,14 @@ import {
|
||||||
printReceivedStringContainExpectedResult,
|
printReceivedStringContainExpectedResult,
|
||||||
printReceivedStringContainExpectedSubstring
|
printReceivedStringContainExpectedSubstring
|
||||||
} from '../expect';
|
} from '../expect';
|
||||||
|
import { matcherHint } from './matcherHint';
|
||||||
|
|
||||||
export async function toMatchText(
|
export async function toMatchText(
|
||||||
this: ReturnType<Expect['getState']>,
|
this: ReturnType<Expect['getState']>,
|
||||||
matcherName: string,
|
matcherName: string,
|
||||||
receiver: any,
|
receiver: any,
|
||||||
receiverType: string,
|
receiverType: string,
|
||||||
query: (isNot: boolean, timeout: number, customStackTrace: ParsedStackTrace) => Promise<{ matches: boolean, received?: string, log?: string[] }>,
|
query: (isNot: boolean, timeout: number, customStackTrace: ParsedStackTrace) => Promise<{ matches: boolean, received?: string, log?: string[], timedOut?: boolean }>,
|
||||||
expected: string | RegExp,
|
expected: string | RegExp,
|
||||||
options: { timeout?: number, matchSubstring?: boolean } = {},
|
options: { timeout?: number, matchSubstring?: boolean } = {},
|
||||||
) {
|
) {
|
||||||
|
|
@ -47,7 +48,7 @@ export async function toMatchText(
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
this.utils.matcherErrorMessage(
|
this.utils.matcherErrorMessage(
|
||||||
this.utils.matcherHint(matcherName, undefined, undefined, matcherOptions),
|
matcherHint(this, matcherName, undefined, undefined, matcherOptions),
|
||||||
`${this.utils.EXPECTED_COLOR(
|
`${this.utils.EXPECTED_COLOR(
|
||||||
'expected',
|
'expected',
|
||||||
)} value must be a string or regular expression`,
|
)} value must be a string or regular expression`,
|
||||||
|
|
@ -58,13 +59,13 @@ export async function toMatchText(
|
||||||
|
|
||||||
const timeout = currentExpectTimeout(options);
|
const timeout = currentExpectTimeout(options);
|
||||||
|
|
||||||
const { matches: pass, received, log } = await query(this.isNot, timeout, captureStackTrace('expect.' + matcherName));
|
const { matches: pass, received, log, timedOut } = await query(this.isNot, timeout, captureStackTrace('expect.' + matcherName));
|
||||||
const stringSubstring = options.matchSubstring ? 'substring' : 'string';
|
const stringSubstring = options.matchSubstring ? 'substring' : 'string';
|
||||||
const receivedString = received || '';
|
const receivedString = received || '';
|
||||||
const message = pass
|
const message = pass
|
||||||
? () =>
|
? () =>
|
||||||
typeof expected === 'string'
|
typeof expected === 'string'
|
||||||
? this.utils.matcherHint(matcherName, undefined, undefined, matcherOptions) +
|
? matcherHint(this, matcherName, undefined, undefined, matcherOptions, timedOut ? timeout : undefined) +
|
||||||
'\n\n' +
|
'\n\n' +
|
||||||
`Expected ${stringSubstring}: not ${this.utils.printExpected(expected)}\n` +
|
`Expected ${stringSubstring}: not ${this.utils.printExpected(expected)}\n` +
|
||||||
`Received string: ${printReceivedStringContainExpectedSubstring(
|
`Received string: ${printReceivedStringContainExpectedSubstring(
|
||||||
|
|
@ -72,7 +73,7 @@ export async function toMatchText(
|
||||||
receivedString.indexOf(expected),
|
receivedString.indexOf(expected),
|
||||||
expected.length,
|
expected.length,
|
||||||
)}` + callLogText(log)
|
)}` + callLogText(log)
|
||||||
: this.utils.matcherHint(matcherName, undefined, undefined, matcherOptions) +
|
: matcherHint(this, matcherName, undefined, undefined, matcherOptions, timedOut ? timeout : undefined) +
|
||||||
'\n\n' +
|
'\n\n' +
|
||||||
`Expected pattern: not ${this.utils.printExpected(expected)}\n` +
|
`Expected pattern: not ${this.utils.printExpected(expected)}\n` +
|
||||||
`Received string: ${printReceivedStringContainExpectedResult(
|
`Received string: ${printReceivedStringContainExpectedResult(
|
||||||
|
|
@ -87,7 +88,7 @@ export async function toMatchText(
|
||||||
const labelReceived = 'Received string';
|
const labelReceived = 'Received string';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
this.utils.matcherHint(matcherName, undefined, undefined, matcherOptions) +
|
matcherHint(this, matcherName, undefined, undefined, matcherOptions, timedOut ? timeout : undefined) +
|
||||||
'\n\n' +
|
'\n\n' +
|
||||||
this.utils.printDiffOrStringify(
|
this.utils.printDiffOrStringify(
|
||||||
expected,
|
expected,
|
||||||
|
|
|
||||||
|
|
@ -2843,6 +2843,7 @@ export type FrameExpectOptions = {
|
||||||
export type FrameExpectResult = {
|
export type FrameExpectResult = {
|
||||||
matches: boolean,
|
matches: boolean,
|
||||||
received?: SerializedValue,
|
received?: SerializedValue,
|
||||||
|
timedOut?: boolean,
|
||||||
log?: string[],
|
log?: string[],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2130,6 +2130,7 @@ Frame:
|
||||||
returns:
|
returns:
|
||||||
matches: boolean
|
matches: boolean
|
||||||
received: SerializedValue?
|
received: SerializedValue?
|
||||||
|
timedOut: boolean?
|
||||||
log:
|
log:
|
||||||
type: array?
|
type: array?
|
||||||
items: string
|
items: string
|
||||||
|
|
|
||||||
|
|
@ -504,7 +504,7 @@ test('should print pending operations for toHaveText', async ({ runInlineTest })
|
||||||
expect(result.exitCode).toBe(1);
|
expect(result.exitCode).toBe(1);
|
||||||
const output = stripAnsi(result.output);
|
const output = stripAnsi(result.output);
|
||||||
expect(output).toContain('Pending operations:');
|
expect(output).toContain('Pending operations:');
|
||||||
expect(output).toContain('Error: expect(received).toHaveText(expected)');
|
expect(output).toContain('expect(received).toHaveText(expected)');
|
||||||
expect(output).toContain('Expected string: "Text"');
|
expect(output).toContain('Expected string: "Text"');
|
||||||
expect(output).toContain('Received string: ""');
|
expect(output).toContain('Received string: ""');
|
||||||
expect(output).toContain('waiting for locator(\'no-such-thing\')');
|
expect(output).toContain('waiting for locator(\'no-such-thing\')');
|
||||||
|
|
@ -532,3 +532,20 @@ test('should print expected/received on Ctrl+C', async ({ runInlineTest }) => {
|
||||||
expect(stripAnsi(result.output)).toContain('Expected string: "Text 2"');
|
expect(stripAnsi(result.output)).toContain('Expected string: "Text 2"');
|
||||||
expect(stripAnsi(result.output)).toContain('Received string: "Text content"');
|
expect(stripAnsi(result.output)).toContain('Received string: "Text content"');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should print timed out error message', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'a.test.ts': `
|
||||||
|
const { test } = pwt;
|
||||||
|
|
||||||
|
test('fail', async ({ page }) => {
|
||||||
|
await page.setContent('<div id=node>Text content</div>');
|
||||||
|
await expect(page.locator('no-such-thing')).not.toBeVisible({ timeout: 1 });
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
}, { workers: 1 });
|
||||||
|
expect(result.failed).toBe(1);
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
const output = stripAnsi(result.output);
|
||||||
|
expect(output).toContain('Timed out 1ms waiting for expect(received).not.toBeVisible()');
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue