chore: render timed out error message when expect timeouts (#18863)
Fixes https://github.com/microsoft/playwright/issues/18859
This commit is contained in:
Родитель
b40d0d2d83
Коммит
4e58b0c2ea
|
@ -306,7 +306,7 @@ export class Locator implements api.Locator {
|
|||
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 () => {
|
||||
const params: channels.FrameExpectParams = { selector: this._selector, expression, ...options, isNot: !!options.isNot };
|
||||
params.expectedValue = serializeArgument(options.expectedValue);
|
||||
|
|
|
@ -1581,6 +1581,7 @@ scheme.FrameExpectParams = tObject({
|
|||
scheme.FrameExpectResult = tObject({
|
||||
matches: tBoolean,
|
||||
received: tOptional(tType('SerializedValue')),
|
||||
timedOut: tOptional(tBoolean),
|
||||
log: tOptional(tArray(tString)),
|
||||
});
|
||||
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 isArray = options.expression === 'to.have.count' || options.expression.endsWith('.array');
|
||||
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.
|
||||
if (js.isJavaScriptErrorInEvaluate(e) || isInvalidSelectorError(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 _deadline: number = 0;
|
||||
private _timeout: number = 0;
|
||||
private _lastIntermediateResult: any;
|
||||
private _lastIntermediateResult: { value: any } | undefined;
|
||||
readonly metadata: CallMetadata;
|
||||
readonly instrumentation: Instrumentation;
|
||||
readonly sdkObject: SdkObject;
|
||||
|
@ -59,7 +59,7 @@ export class ProgressController {
|
|||
this._logName = logName;
|
||||
}
|
||||
|
||||
lastIntermediateResult() {
|
||||
lastIntermediateResult(): { value: any } | undefined {
|
||||
return this._lastIntermediateResult;
|
||||
}
|
||||
|
||||
|
@ -85,7 +85,7 @@ export class ProgressController {
|
|||
this.instrumentation.onCallLog(this.sdkObject, this.metadata, this._logName, message);
|
||||
}
|
||||
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.
|
||||
isRunning: () => this._state === 'running',
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
[*]
|
||||
../types.ts
|
||||
./utilsBundle.ts
|
||||
matchers/
|
||||
reporters/
|
||||
|
|
|
@ -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';
|
||||
|
||||
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 {
|
||||
|
|
|
@ -17,13 +17,14 @@
|
|||
import type { Expect } from '../types';
|
||||
import type { ParsedStackTrace } from '../util';
|
||||
import { expectTypes, callLogText, currentExpectTimeout, captureStackTrace } from '../util';
|
||||
import { matcherHint } from './matcherHint';
|
||||
|
||||
export async function toBeTruthy(
|
||||
this: ReturnType<Expect['getState']>,
|
||||
matcherName: string,
|
||||
receiver: any,
|
||||
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 } = {},
|
||||
) {
|
||||
expectTypes(receiver, [receiverType], matcherName);
|
||||
|
@ -35,10 +36,10 @@ export async function toBeTruthy(
|
|||
|
||||
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 = () => {
|
||||
return this.utils.matcherHint(matcherName, undefined, '', matcherOptions) + callLogText(log);
|
||||
return matcherHint(this, matcherName, undefined, '', matcherOptions, timedOut ? timeout : undefined) + callLogText(log);
|
||||
};
|
||||
|
||||
return { message, pass: matches };
|
||||
|
|
|
@ -19,6 +19,7 @@ import { expectTypes } from '../util';
|
|||
import { callLogText, currentExpectTimeout } from '../util';
|
||||
import type { ParsedStackTrace } 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.
|
||||
const EXPECTED_LABEL = 'Expected';
|
||||
|
@ -32,7 +33,7 @@ export async function toEqual<T>(
|
|||
matcherName: string,
|
||||
receiver: any,
|
||||
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,
|
||||
options: { timeout?: number, contains?: boolean } = {},
|
||||
) {
|
||||
|
@ -48,18 +49,18 @@ export async function toEqual<T>(
|
|||
|
||||
const customStackTrace = captureStackTrace();
|
||||
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
|
||||
? () =>
|
||||
this.utils.matcherHint(matcherName, undefined, undefined, matcherOptions) +
|
||||
matcherHint(this, matcherName, undefined, undefined, matcherOptions, timedOut ? timeout : undefined) +
|
||||
'\n\n' +
|
||||
`Expected: not ${this.utils.printExpected(expected)}\n` +
|
||||
(this.utils.stringify(expected) !== this.utils.stringify(received)
|
||||
? `Received: ${this.utils.printReceived(received)}`
|
||||
: '') + callLogText(log)
|
||||
: () =>
|
||||
this.utils.matcherHint(matcherName, undefined, undefined, matcherOptions) +
|
||||
matcherHint(this, matcherName, undefined, undefined, matcherOptions, timedOut ? timeout : undefined) +
|
||||
'\n\n' +
|
||||
this.utils.printDiffOrStringify(
|
||||
expected,
|
||||
|
|
|
@ -24,13 +24,14 @@ import {
|
|||
printReceivedStringContainExpectedResult,
|
||||
printReceivedStringContainExpectedSubstring
|
||||
} from '../expect';
|
||||
import { matcherHint } from './matcherHint';
|
||||
|
||||
export async function toMatchText(
|
||||
this: ReturnType<Expect['getState']>,
|
||||
matcherName: string,
|
||||
receiver: any,
|
||||
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,
|
||||
options: { timeout?: number, matchSubstring?: boolean } = {},
|
||||
) {
|
||||
|
@ -47,7 +48,7 @@ export async function toMatchText(
|
|||
) {
|
||||
throw new Error(
|
||||
this.utils.matcherErrorMessage(
|
||||
this.utils.matcherHint(matcherName, undefined, undefined, matcherOptions),
|
||||
matcherHint(this, matcherName, undefined, undefined, matcherOptions),
|
||||
`${this.utils.EXPECTED_COLOR(
|
||||
'expected',
|
||||
)} value must be a string or regular expression`,
|
||||
|
@ -58,13 +59,13 @@ export async function toMatchText(
|
|||
|
||||
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 receivedString = received || '';
|
||||
const message = pass
|
||||
? () =>
|
||||
typeof expected === 'string'
|
||||
? this.utils.matcherHint(matcherName, undefined, undefined, matcherOptions) +
|
||||
? matcherHint(this, matcherName, undefined, undefined, matcherOptions, timedOut ? timeout : undefined) +
|
||||
'\n\n' +
|
||||
`Expected ${stringSubstring}: not ${this.utils.printExpected(expected)}\n` +
|
||||
`Received string: ${printReceivedStringContainExpectedSubstring(
|
||||
|
@ -72,7 +73,7 @@ export async function toMatchText(
|
|||
receivedString.indexOf(expected),
|
||||
expected.length,
|
||||
)}` + callLogText(log)
|
||||
: this.utils.matcherHint(matcherName, undefined, undefined, matcherOptions) +
|
||||
: matcherHint(this, matcherName, undefined, undefined, matcherOptions, timedOut ? timeout : undefined) +
|
||||
'\n\n' +
|
||||
`Expected pattern: not ${this.utils.printExpected(expected)}\n` +
|
||||
`Received string: ${printReceivedStringContainExpectedResult(
|
||||
|
@ -87,7 +88,7 @@ export async function toMatchText(
|
|||
const labelReceived = 'Received string';
|
||||
|
||||
return (
|
||||
this.utils.matcherHint(matcherName, undefined, undefined, matcherOptions) +
|
||||
matcherHint(this, matcherName, undefined, undefined, matcherOptions, timedOut ? timeout : undefined) +
|
||||
'\n\n' +
|
||||
this.utils.printDiffOrStringify(
|
||||
expected,
|
||||
|
|
|
@ -2843,6 +2843,7 @@ export type FrameExpectOptions = {
|
|||
export type FrameExpectResult = {
|
||||
matches: boolean,
|
||||
received?: SerializedValue,
|
||||
timedOut?: boolean,
|
||||
log?: string[],
|
||||
};
|
||||
|
||||
|
|
|
@ -2130,6 +2130,7 @@ Frame:
|
|||
returns:
|
||||
matches: boolean
|
||||
received: SerializedValue?
|
||||
timedOut: boolean?
|
||||
log:
|
||||
type: array?
|
||||
items: string
|
||||
|
|
|
@ -504,7 +504,7 @@ test('should print pending operations for toHaveText', async ({ runInlineTest })
|
|||
expect(result.exitCode).toBe(1);
|
||||
const output = stripAnsi(result.output);
|
||||
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('Received string: ""');
|
||||
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('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()');
|
||||
});
|
||||
|
|
Загрузка…
Ссылка в новой задаче