chore: render successful toPass as such (#23411)
Fixes https://github.com/microsoft/playwright/issues/23302
This commit is contained in:
parent
aca4afc3a9
commit
84942aa992
|
|
@ -249,13 +249,13 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
|
||||||
|
|
||||||
const defaultTitle = `expect${this._info.isPoll ? '.poll' : ''}${this._info.isSoft ? '.soft' : ''}${this._info.isNot ? '.not' : ''}.${matcherName}${argsSuffix}`;
|
const defaultTitle = `expect${this._info.isPoll ? '.poll' : ''}${this._info.isSoft ? '.soft' : ''}${this._info.isNot ? '.not' : ''}.${matcherName}${argsSuffix}`;
|
||||||
const wallTime = Date.now();
|
const wallTime = Date.now();
|
||||||
const step = testInfo._addStep({
|
const step = matcherName !== 'toPass' ? testInfo._addStep({
|
||||||
location: stackFrames[0],
|
location: stackFrames[0],
|
||||||
category: 'expect',
|
category: 'expect',
|
||||||
title: trimLongString(customMessage || defaultTitle, 1024),
|
title: trimLongString(customMessage || defaultTitle, 1024),
|
||||||
params: args[0] ? { expected: args[0] } : undefined,
|
params: args[0] ? { expected: args[0] } : undefined,
|
||||||
wallTime
|
wallTime
|
||||||
});
|
}) : undefined;
|
||||||
|
|
||||||
const reportStepError = (jestError: Error) => {
|
const reportStepError = (jestError: Error) => {
|
||||||
const message = jestError.message;
|
const message = jestError.message;
|
||||||
|
|
@ -283,7 +283,7 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
|
||||||
const serializedError = serializeError(jestError);
|
const serializedError = serializeError(jestError);
|
||||||
// Serialized error has filtered stack trace.
|
// Serialized error has filtered stack trace.
|
||||||
jestError.stack = serializedError.stack;
|
jestError.stack = serializedError.stack;
|
||||||
step.complete({ error: serializedError });
|
step?.complete({ error: serializedError });
|
||||||
if (this._info.isSoft)
|
if (this._info.isSoft)
|
||||||
testInfo._failWithError(serializedError, false /* isHardError */);
|
testInfo._failWithError(serializedError, false /* isHardError */);
|
||||||
else
|
else
|
||||||
|
|
@ -291,7 +291,7 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
|
||||||
};
|
};
|
||||||
|
|
||||||
const finalizer = () => {
|
const finalizer = () => {
|
||||||
step.complete({});
|
step?.complete({});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Process the async matchers separately to preserve the zones in the stacks.
|
// Process the async matchers separately to preserve the zones in the stacks.
|
||||||
|
|
|
||||||
|
|
@ -18,12 +18,13 @@ import type { Locator, Page, APIResponse } from 'playwright-core';
|
||||||
import type { FrameExpectOptions } from 'playwright-core/lib/client/types';
|
import type { FrameExpectOptions } from 'playwright-core/lib/client/types';
|
||||||
import { colors } from 'playwright-core/lib/utilsBundle';
|
import { colors } from 'playwright-core/lib/utilsBundle';
|
||||||
import type { Expect } from '../../types/test';
|
import type { Expect } from '../../types/test';
|
||||||
import { expectTypes, callLogText } from '../util';
|
import { expectTypes, callLogText, filteredStackTrace } from '../util';
|
||||||
import { toBeTruthy } from './toBeTruthy';
|
import { toBeTruthy } from './toBeTruthy';
|
||||||
import { toEqual } from './toEqual';
|
import { toEqual } from './toEqual';
|
||||||
import { toExpectedTextValues, toMatchText } from './toMatchText';
|
import { toExpectedTextValues, toMatchText } from './toMatchText';
|
||||||
import { constructURLBasedOnBaseURL, isTextualMimeType, pollAgainstTimeout } from 'playwright-core/lib/utils';
|
import { captureRawStack, constructURLBasedOnBaseURL, isTextualMimeType, pollAgainstTimeout } from 'playwright-core/lib/utils';
|
||||||
import { currentTestInfo } from '../common/globals';
|
import { currentTestInfo } from '../common/globals';
|
||||||
|
import type { TestStepInternal } from '../worker/testInfo';
|
||||||
|
|
||||||
interface LocatorEx extends Locator {
|
interface LocatorEx extends Locator {
|
||||||
_expect(expression: string, options: Omit<FrameExpectOptions, 'expectedValue'> & { expectedValue?: any }): Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean }>;
|
_expect(expression: string, options: Omit<FrameExpectOptions, 'expectedValue'> & { expectedValue?: any }): Promise<{ matches: boolean, received?: any, log?: string[], timedOut?: boolean }>;
|
||||||
|
|
@ -341,27 +342,43 @@ export async function toPass(
|
||||||
const testInfo = currentTestInfo();
|
const testInfo = currentTestInfo();
|
||||||
const timeout = options.timeout !== undefined ? options.timeout : 0;
|
const timeout = options.timeout !== undefined ? options.timeout : 0;
|
||||||
|
|
||||||
const result = await pollAgainstTimeout<Error|undefined>(async () => {
|
const rawStack = captureRawStack();
|
||||||
if (testInfo && currentTestInfo() !== testInfo)
|
const stackFrames = filteredStackTrace(rawStack);
|
||||||
return { continuePolling: false, result: undefined };
|
|
||||||
try {
|
const runWithOrWithoutStep = async (callback: (step: TestStepInternal | undefined) => Promise<{ pass: boolean; message: () => string; }>) => {
|
||||||
await callback();
|
if (!testInfo)
|
||||||
return { continuePolling: this.isNot, result: undefined };
|
return await callback(undefined);
|
||||||
} catch (e) {
|
return await testInfo._runAsStep({
|
||||||
return { continuePolling: !this.isNot, result: e };
|
title: 'expect.toPass',
|
||||||
|
category: 'expect',
|
||||||
|
location: stackFrames[0],
|
||||||
|
insulateChildErrors: true,
|
||||||
|
}, callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
return await runWithOrWithoutStep(async (step: TestStepInternal | undefined) => {
|
||||||
|
const result = await pollAgainstTimeout<Error|undefined>(async () => {
|
||||||
|
if (testInfo && currentTestInfo() !== testInfo)
|
||||||
|
return { continuePolling: false, result: undefined };
|
||||||
|
try {
|
||||||
|
await callback();
|
||||||
|
return { continuePolling: this.isNot, result: undefined };
|
||||||
|
} catch (e) {
|
||||||
|
return { continuePolling: !this.isNot, result: e };
|
||||||
|
}
|
||||||
|
}, timeout, options.intervals || [100, 250, 500, 1000]);
|
||||||
|
|
||||||
|
if (result.timedOut) {
|
||||||
|
const timeoutMessage = `Timeout ${timeout}ms exceeded while waiting on the predicate`;
|
||||||
|
const message = result.result ? [
|
||||||
|
result.result.message,
|
||||||
|
'',
|
||||||
|
`Call Log:`,
|
||||||
|
`- ${timeoutMessage}`,
|
||||||
|
].join('\n') : timeoutMessage;
|
||||||
|
step?.complete({ error: { message } });
|
||||||
|
return { message: () => message, pass: this.isNot };
|
||||||
}
|
}
|
||||||
}, timeout, options.intervals || [100, 250, 500, 1000]);
|
return { pass: !this.isNot, message: () => '' };
|
||||||
|
});
|
||||||
if (result.timedOut) {
|
|
||||||
const timeoutMessage = `Timeout ${timeout}ms exceeded while waiting on the predicate`;
|
|
||||||
const message = () => result.result ? [
|
|
||||||
result.result.message,
|
|
||||||
'',
|
|
||||||
`Call Log:`,
|
|
||||||
`- ${timeoutMessage}`,
|
|
||||||
].join('\n') : timeoutMessage;
|
|
||||||
|
|
||||||
return { message, pass: this.isNot };
|
|
||||||
}
|
|
||||||
return { pass: !this.isNot, message: () => '' };
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ export interface TestStepInternal {
|
||||||
apiName?: string;
|
apiName?: string;
|
||||||
params?: Record<string, any>;
|
params?: Record<string, any>;
|
||||||
error?: TestInfoError;
|
error?: TestInfoError;
|
||||||
|
insulateChildErrors?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TestInfoImpl implements TestInfo {
|
export class TestInfoImpl implements TestInfo {
|
||||||
|
|
@ -252,7 +253,7 @@ export class TestInfoImpl implements TestInfo {
|
||||||
} else if (result.error) {
|
} else if (result.error) {
|
||||||
// Internal API step reported an error.
|
// Internal API step reported an error.
|
||||||
error = result.error;
|
error = result.error;
|
||||||
} else {
|
} else if (!data.insulateChildErrors) {
|
||||||
// One of the child steps failed (probably soft expect).
|
// One of the child steps failed (probably soft expect).
|
||||||
// Report this step as failed to make it easier to spot.
|
// Report this step as failed to make it easier to spot.
|
||||||
error = step.steps.map(s => s.error).find(e => !!e);
|
error = step.steps.map(s => s.error).find(e => !!e);
|
||||||
|
|
|
||||||
|
|
@ -951,7 +951,7 @@ test('should not mark page.close as failed when page.click fails', async ({ runI
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should nest page.continue insize page.goto steps', async ({ runInlineTest }) => {
|
test('should nest page.continue inside page.goto steps', async ({ runInlineTest }) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'reporter.ts': stepHierarchyReporter,
|
'reporter.ts': stepHierarchyReporter,
|
||||||
'playwright.config.ts': `module.exports = { reporter: './reporter', };`,
|
'playwright.config.ts': `module.exports = { reporter: './reporter', };`,
|
||||||
|
|
@ -1033,3 +1033,98 @@ test('should nest page.continue insize page.goto steps', async ({ runInlineTest
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should not propagate errors from within toPass', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'reporter.ts': stepHierarchyReporter,
|
||||||
|
'playwright.config.ts': `module.exports = { reporter: './reporter', };`,
|
||||||
|
'a.test.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('pass', async () => {
|
||||||
|
let i = 0;
|
||||||
|
await expect(() => {
|
||||||
|
expect(i++).toBe(2);
|
||||||
|
}).toPass();
|
||||||
|
});
|
||||||
|
`
|
||||||
|
}, { reporter: '' });
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
const objects = result.outputLines.map(line => JSON.parse(line));
|
||||||
|
expect(objects).toEqual([
|
||||||
|
{
|
||||||
|
title: 'Before Hooks',
|
||||||
|
category: 'hook',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'expect.toPass',
|
||||||
|
category: 'expect',
|
||||||
|
location: { file: 'a.test.ts', line: 'number', column: 'number' },
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
category: 'expect',
|
||||||
|
error: '<error>',
|
||||||
|
location: { file: 'a.test.ts', line: 'number', column: 'number' },
|
||||||
|
title: 'expect.toBe',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'expect',
|
||||||
|
error: '<error>',
|
||||||
|
location: { file: 'a.test.ts', line: 'number', column: 'number' },
|
||||||
|
title: 'expect.toBe',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: 'expect',
|
||||||
|
location: { file: 'a.test.ts', line: 'number', column: 'number' },
|
||||||
|
title: 'expect.toBe',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'After Hooks',
|
||||||
|
category: 'hook',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show final toPass error', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'reporter.ts': stepHierarchyReporter,
|
||||||
|
'playwright.config.ts': `module.exports = { reporter: './reporter', };`,
|
||||||
|
'a.test.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('fail', async () => {
|
||||||
|
await expect(() => {
|
||||||
|
expect(true).toBe(false);
|
||||||
|
}).toPass({ timeout: 1 });
|
||||||
|
});
|
||||||
|
`
|
||||||
|
}, { reporter: '' });
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
const objects = result.outputLines.map(line => JSON.parse(line));
|
||||||
|
expect(objects).toEqual([
|
||||||
|
{
|
||||||
|
title: 'Before Hooks',
|
||||||
|
category: 'hook',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'expect.toPass',
|
||||||
|
category: 'expect',
|
||||||
|
error: '<error>',
|
||||||
|
location: { file: 'a.test.ts', line: 'number', column: 'number' },
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
category: 'expect',
|
||||||
|
error: '<error>',
|
||||||
|
location: { file: 'a.test.ts', line: 'number', column: 'number' },
|
||||||
|
title: 'expect.toBe',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'After Hooks',
|
||||||
|
category: 'hook',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue