chore: render successful toPass as such (#23411)

Fixes https://github.com/microsoft/playwright/issues/23302
This commit is contained in:
Pavel Feldman 2023-06-01 13:22:08 -07:00 committed by GitHub
parent aca4afc3a9
commit 84942aa992
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 143 additions and 30 deletions

View file

@ -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.

View file

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

View file

@ -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);

View file

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