chore: render timed out error message when expect timeouts (#18863)

Fixes https://github.com/microsoft/playwright/issues/18859
This commit is contained in:
Pavel Feldman 2022-11-16 17:00:42 -08:00 коммит произвёл GitHub
Родитель b40d0d2d83
Коммит 4e58b0c2ea
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
13 изменённых файлов: 76 добавлений и 21 удалений

Просмотреть файл

@ -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()');
});