chore: allow matchers decorate step title (#13199)

This commit is contained in:
Pavel Feldman 2022-03-30 20:52:00 -08:00 committed by GitHub
parent ecc804d808
commit 923f74c5a6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 58 additions and 74 deletions

View file

@ -262,6 +262,8 @@ export class Dispatcher {
this._reporter.onStdErr?.('Internal error: step end without step begin: ' + params.stepId, data.test, result); this._reporter.onStdErr?.('Internal error: step end without step begin: ' + params.stepId, data.test, result);
return; return;
} }
if (params.refinedTitle)
step.title = params.refinedTitle;
step.duration = params.wallTime - step.startTime.getTime(); step.duration = params.wallTime - step.startTime.getTime();
if (params.error) if (params.error)
step.error = params.error; step.error = params.error;

View file

@ -44,13 +44,19 @@ import {
toHaveURL, toHaveURL,
toHaveValue toHaveValue
} from './matchers/matchers'; } from './matchers/matchers';
import { toMatchSnapshot, toHaveScreenshot, getSnapshotName } from './matchers/toMatchSnapshot'; import { toMatchSnapshot, toHaveScreenshot } from './matchers/toMatchSnapshot';
import type { Expect, TestError } from './types'; import type { Expect, TestError } from './types';
import matchers from 'expect/build/matchers'; import matchers from 'expect/build/matchers';
import { currentTestInfo } from './globals'; import { currentTestInfo } from './globals';
import { serializeError, captureStackTrace, currentExpectTimeout } from './util'; import { serializeError, captureStackTrace, currentExpectTimeout } from './util';
import { monotonicTime } from 'playwright-core/lib/utils/utils'; import { monotonicTime } from 'playwright-core/lib/utils/utils';
// from expect/build/types
export type SyncExpectationResult = {
pass: boolean;
message: () => string;
};
// #region // #region
// Mirrored from https://github.com/facebook/jest/blob/f13abff8df9a0e1148baf3584bcde6d1b479edc7/packages/expect/src/print.ts // Mirrored from https://github.com/facebook/jest/blob/f13abff8df9a0e1148baf3584bcde6d1b479edc7/packages/expect/src/print.ts
/** /**
@ -230,11 +236,6 @@ function wrap(matcherName: string, matcher: any) {
if (!testInfo) if (!testInfo)
return matcher.call(this, ...args); return matcher.call(this, ...args);
let titleSuffix = '';
if (matcherName === 'toHaveScreenshot' || matcherName === 'toMatchSnapshot') {
const [received, nameOrOptions, optOptions] = args;
titleSuffix = `(${getSnapshotName(testInfo, received, nameOrOptions, optOptions)})`;
}
const stackTrace = captureStackTrace(); const stackTrace = captureStackTrace();
const stackLines = stackTrace.frameTexts; const stackLines = stackTrace.frameTexts;
const frame = stackTrace.frames[0]; const frame = stackTrace.frames[0];
@ -242,15 +243,16 @@ function wrap(matcherName: string, matcher: any) {
const isSoft = expectCallMetaInfo?.isSoft ?? false; const isSoft = expectCallMetaInfo?.isSoft ?? false;
const isPoll = expectCallMetaInfo?.isPoll ?? false; const isPoll = expectCallMetaInfo?.isPoll ?? false;
const pollTimeout = expectCallMetaInfo?.pollTimeout; const pollTimeout = expectCallMetaInfo?.pollTimeout;
const defaultTitle = `expect${isPoll ? '.poll' : ''}${isSoft ? '.soft' : ''}${this.isNot ? '.not' : ''}.${matcherName}`;
const step = testInfo._addStep({ const step = testInfo._addStep({
location: frame && frame.file ? { file: path.resolve(process.cwd(), frame.file), line: frame.line || 0, column: frame.column || 0 } : undefined, location: frame && frame.file ? { file: path.resolve(process.cwd(), frame.file), line: frame.line || 0, column: frame.column || 0 } : undefined,
category: 'expect', category: 'expect',
title: customMessage || `expect${isPoll ? '.poll' : ''}${isSoft ? '.soft' : ''}${this.isNot ? '.not' : ''}.${matcherName}${titleSuffix}`, title: customMessage || defaultTitle,
canHaveChildren: true, canHaveChildren: true,
forceNoParent: false forceNoParent: false
}); });
const reportStepEnd = (result: any) => { const reportStepEnd = (result: any, options: { refinedTitle?: string }) => {
const success = result.pass !== this.isNot; const success = result.pass !== this.isNot;
let error: TestError | undefined; let error: TestError | undefined;
if (!success) { if (!success) {
@ -276,15 +278,19 @@ function wrap(matcherName: string, matcher: any) {
result.message = () => newMessage; result.message = () => newMessage;
} }
} }
step.complete(error); step.complete({ ...options, error });
return result; return result;
}; };
const reportStepError = (error: Error) => { const reportStepError = (error: Error) => {
step.complete(serializeError(error)); step.complete({ error: serializeError(error) });
throw error; throw error;
}; };
const refineTitle = (result: SyncExpectationResult & { titleSuffix?: string }): string | undefined => {
return !customMessage && result.titleSuffix ? defaultTitle + result.titleSuffix : undefined;
};
try { try {
let result; let result;
const [receivedOrGenerator, ...otherArgs] = args; const [receivedOrGenerator, ...otherArgs] = args;
@ -300,8 +306,8 @@ function wrap(matcherName: string, matcher: any) {
result = matcher.call(this, ...args); result = matcher.call(this, ...args);
} }
if (result instanceof Promise) if (result instanceof Promise)
return result.then(reportStepEnd).catch(reportStepError); return result.then(result => reportStepEnd(result, { refinedTitle: refineTitle(result) })).catch(reportStepError);
return reportStepEnd(result); return reportStepEnd(result, { refinedTitle: refineTitle(result) });
} catch (e) { } catch (e) {
reportStepError(e); reportStepError(e);
} }

View file

@ -264,7 +264,7 @@ export const test = _baseTest.extend<TestFixtures, WorkerFixtures>({
}, },
onApiCallEnd: (userData: any, error?: Error) => { onApiCallEnd: (userData: any, error?: Error) => {
const step = userData.userObject; const step = userData.userObject;
step?.complete(error); step?.complete({ error });
}, },
}; };
}; };

View file

@ -60,6 +60,7 @@ export type StepBeginPayload = {
export type StepEndPayload = { export type StepEndPayload = {
testId: string; testId: string;
stepId: string; stepId: string;
refinedTitle?: string;
wallTime: number; // milliseconds since unix epoch wallTime: number; // milliseconds since unix epoch
error?: TestError; error?: TestError;
}; };

View file

@ -31,38 +31,11 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import * as mime from 'mime'; import * as mime from 'mime';
import { TestInfoImpl } from '../testInfo'; import { TestInfoImpl } from '../testInfo';
import { SyncExpectationResult } from '../expect';
// from expect/build/types
type SyncExpectationResult = {
pass: boolean;
message: () => string;
};
type NameOrSegments = string | string[]; type NameOrSegments = string | string[];
const SNAPSHOT_COUNTER = Symbol('noname-snapshot-counter'); const SNAPSHOT_COUNTER = Symbol('noname-snapshot-counter');
export function getSnapshotName(
testInfo: TestInfoImpl,
received: any,
nameOrOptions: NameOrSegments | { name?: NameOrSegments } = {},
optOptions: any = {}
) {
const [
anonymousSnapshotExtension,
snapshotPathResolver,
] = typeof received === 'string' || Buffer.isBuffer(received) ? [
determineFileExtension(received),
testInfo.snapshotPath.bind(testInfo),
] : [
'png',
testInfo._screenshotPath.bind(testInfo),
];
const helper = new SnapshotHelper(
testInfo, snapshotPathResolver, anonymousSnapshotExtension, {},
nameOrOptions, optOptions, true /* dryRun */);
return path.basename(helper.snapshotPath);
}
class SnapshotHelper<T extends ImageComparatorOptions> { class SnapshotHelper<T extends ImageComparatorOptions> {
readonly testInfo: TestInfoImpl; readonly testInfo: TestInfoImpl;
readonly expectedPath: string; readonly expectedPath: string;
@ -84,7 +57,6 @@ class SnapshotHelper<T extends ImageComparatorOptions> {
configOptions: ImageComparatorOptions, configOptions: ImageComparatorOptions,
nameOrOptions: NameOrSegments | { name?: NameOrSegments } & T, nameOrOptions: NameOrSegments | { name?: NameOrSegments } & T,
optOptions: T, optOptions: T,
dryRun: boolean = false,
) { ) {
let options: T; let options: T;
let name: NameOrSegments | undefined; let name: NameOrSegments | undefined;
@ -102,8 +74,7 @@ class SnapshotHelper<T extends ImageComparatorOptions> {
...testInfo.titlePath.slice(1), ...testInfo.titlePath.slice(1),
(testInfo as any)[SNAPSHOT_COUNTER] + 1, (testInfo as any)[SNAPSHOT_COUNTER] + 1,
].join(' '); ].join(' ');
if (!dryRun) ++(testInfo as any)[SNAPSHOT_COUNTER];
++(testInfo as any)[SNAPSHOT_COUNTER];
name = sanitizeForFilePath(trimLongString(fullTitleWithoutSpec)) + '.' + anonymousSnapshotExtension; name = sanitizeForFilePath(trimLongString(fullTitleWithoutSpec)) + '.' + anonymousSnapshotExtension;
} }
@ -143,19 +114,23 @@ class SnapshotHelper<T extends ImageComparatorOptions> {
this.kind = this.mimeType.startsWith('image/') ? 'Screenshot' : 'Snapshot'; this.kind = this.mimeType.startsWith('image/') ? 'Screenshot' : 'Snapshot';
} }
decorateTitle(result: SyncExpectationResult): SyncExpectationResult & { titleSuffix: string } {
return { ...result, titleSuffix: `(${path.basename(this.snapshotPath)})` };
}
handleMissingNegated() { handleMissingNegated() {
const isWriteMissingMode = this.updateSnapshots === 'all' || this.updateSnapshots === 'missing'; const isWriteMissingMode = this.updateSnapshots === 'all' || this.updateSnapshots === 'missing';
const message = `${this.snapshotPath} is missing in snapshots${isWriteMissingMode ? ', matchers using ".not" won\'t write them automatically.' : '.'}`; const message = `${this.snapshotPath} is missing in snapshots${isWriteMissingMode ? ', matchers using ".not" won\'t write them automatically.' : '.'}`;
return { return this.decorateTitle({
// NOTE: 'isNot' matcher implies inversed value. // NOTE: 'isNot' matcher implies inversed value.
pass: true, pass: true,
message: () => message, message: () => message,
}; });
} }
handleDifferentNegated() { handleDifferentNegated() {
// NOTE: 'isNot' matcher implies inversed value. // NOTE: 'isNot' matcher implies inversed value.
return { pass: false, message: () => '' }; return this.decorateTitle({ pass: false, message: () => '' });
} }
handleMatchingNegated() { handleMatchingNegated() {
@ -165,7 +140,7 @@ class SnapshotHelper<T extends ImageComparatorOptions> {
indent('Expected result should be different from the actual one.', ' '), indent('Expected result should be different from the actual one.', ' '),
].join('\n'); ].join('\n');
// NOTE: 'isNot' matcher implies inversed value. // NOTE: 'isNot' matcher implies inversed value.
return { pass: true, message: () => message }; return this.decorateTitle({ pass: true, message: () => message });
} }
handleMissing(actual: Buffer | string) { handleMissing(actual: Buffer | string) {
@ -178,13 +153,13 @@ class SnapshotHelper<T extends ImageComparatorOptions> {
if (this.updateSnapshots === 'all') { if (this.updateSnapshots === 'all') {
/* eslint-disable no-console */ /* eslint-disable no-console */
console.log(message); console.log(message);
return { pass: true, message: () => message }; return this.decorateTitle({ pass: true, message: () => message });
} }
if (this.updateSnapshots === 'missing') { if (this.updateSnapshots === 'missing') {
this.testInfo._failWithError(serializeError(new Error(message)), false /* isHardError */); this.testInfo._failWithError(serializeError(new Error(message)), false /* isHardError */);
return { pass: true, message: () => '' }; return this.decorateTitle({ pass: true, message: () => '' });
} }
return { pass: false, message: () => message }; return this.decorateTitle({ pass: false, message: () => message });
} }
handleDifferent( handleDifferent(
@ -226,11 +201,11 @@ class SnapshotHelper<T extends ImageComparatorOptions> {
this.testInfo.attachments.push({ name: path.basename(this.diffPath), contentType: this.mimeType, path: this.diffPath }); this.testInfo.attachments.push({ name: path.basename(this.diffPath), contentType: this.mimeType, path: this.diffPath });
output.push(` Diff: ${colors.yellow(this.diffPath)}`); output.push(` Diff: ${colors.yellow(this.diffPath)}`);
} }
return { pass: false, message: () => output.join('\n'), }; return this.decorateTitle({ pass: false, message: () => output.join('\n'), });
} }
handleMatching() { handleMatching() {
return { pass: true, message: () => '' }; return this.decorateTitle({ pass: true, message: () => '' });
} }
} }
@ -239,7 +214,7 @@ export function toMatchSnapshot(
received: Buffer | string, received: Buffer | string,
nameOrOptions: NameOrSegments | { name?: NameOrSegments } & ImageComparatorOptions = {}, nameOrOptions: NameOrSegments | { name?: NameOrSegments } & ImageComparatorOptions = {},
optOptions: ImageComparatorOptions = {} optOptions: ImageComparatorOptions = {}
): SyncExpectationResult { ): SyncExpectationResult & { titleSuffix: string } {
const testInfo = currentTestInfo(); const testInfo = currentTestInfo();
if (!testInfo) if (!testInfo)
throw new Error(`toMatchSnapshot() must be called during the test`); throw new Error(`toMatchSnapshot() must be called during the test`);
@ -269,7 +244,7 @@ export function toMatchSnapshot(
writeFileSync(helper.snapshotPath, received); writeFileSync(helper.snapshotPath, received);
/* eslint-disable no-console */ /* eslint-disable no-console */
console.log(helper.snapshotPath + ' does not match, writing actual.'); console.log(helper.snapshotPath + ' does not match, writing actual.');
return { pass: true, message: () => helper.snapshotPath + ' running with --update-snapshots, writing actual.' }; return helper.decorateTitle({ pass: true, message: () => helper.snapshotPath + ' running with --update-snapshots, writing actual.' });
} }
return helper.handleDifferent(received, expected, undefined, result.diff, result.errorMessage, undefined); return helper.handleDifferent(received, expected, undefined, result.diff, result.errorMessage, undefined);

View file

@ -219,10 +219,10 @@ export class TestInfoImpl implements TestInfo {
const step = this._addStep(stepInfo); const step = this._addStep(stepInfo);
try { try {
const result = await cb(); const result = await cb();
step.complete(); step.complete({});
return result; return result;
} catch (e) { } catch (e) {
step.complete(e instanceof SkipError ? undefined : serializeError(e)); step.complete({ error: e instanceof SkipError ? undefined : serializeError(e) });
throw e; throw e;
} }
} }

View file

@ -216,9 +216,9 @@ export class TestTypeImpl {
}); });
try { try {
await body(); await body();
step.complete(); step.complete({});
} catch (e) { } catch (e) {
step.complete(serializeError(e)); step.complete({ error: serializeError(e) });
throw e; throw e;
} }
} }

View file

@ -27,7 +27,7 @@ export type FixturesWithLocation = {
export type Annotation = { type: string, description?: string }; export type Annotation = { type: string, description?: string };
export interface TestStepInternal { export interface TestStepInternal {
complete(error?: Error | TestError): void; complete(result: { refinedTitle?: string, error?: Error | TestError }): void;
title: string; title: string;
category: string; category: string;
canHaveChildren: boolean; canHaveChildren: boolean;

View file

@ -207,14 +207,14 @@ export class WorkerRunner extends EventEmitter {
let callbackHandled = false; let callbackHandled = false;
const step: TestStepInternal = { const step: TestStepInternal = {
...data, ...data,
complete: (error?: Error | TestError) => { complete: result => {
if (callbackHandled) if (callbackHandled)
return; return;
callbackHandled = true; callbackHandled = true;
if (error instanceof Error) const error = result.error instanceof Error ? serializeError(result.error) : result.error;
error = serializeError(error);
const payload: StepEndPayload = { const payload: StepEndPayload = {
testId: test._id, testId: test._id,
refinedTitle: result.refinedTitle,
stepId, stepId,
wallTime: Date.now(), wallTime: Date.now(),
error, error,
@ -346,14 +346,14 @@ export class WorkerRunner extends EventEmitter {
// Setup fixtures required by the test. // Setup fixtures required by the test.
testInfo._timeoutManager.setCurrentRunnable({ type: 'test' }); testInfo._timeoutManager.setCurrentRunnable({ type: 'test' });
const params = await this._fixtureRunner.resolveParametersForFunction(test.fn, testInfo); const params = await this._fixtureRunner.resolveParametersForFunction(test.fn, testInfo);
beforeHooksStep.complete(); // Report fixture hooks step as completed. beforeHooksStep.complete({}); // Report fixture hooks step as completed.
// Now run the test itself. // Now run the test itself.
const fn = test.fn; // Extract a variable to get a better stack trace ("myTest" vs "TestCase.myTest [as fn]"). const fn = test.fn; // Extract a variable to get a better stack trace ("myTest" vs "TestCase.myTest [as fn]").
await fn(params, testInfo); await fn(params, testInfo);
}, 'allowSkips'); }, 'allowSkips');
beforeHooksStep.complete(maybeError); // Second complete is a no-op. beforeHooksStep.complete({ error: maybeError }); // Second complete is a no-op.
}); });
if (didFailBeforeAllForSuite) { if (didFailBeforeAllForSuite) {
@ -425,7 +425,7 @@ export class WorkerRunner extends EventEmitter {
}); });
} }
afterHooksStep.complete(firstAfterHooksError); afterHooksStep.complete({ error: firstAfterHooksError });
this._currentTest = null; this._currentTest = null;
setCurrentTestInfo(null); setCurrentTestInfo(null);
this.emit('testEnd', buildTestEndPayload(testInfo)); this.emit('testEnd', buildTestEndPayload(testInfo));

View file

@ -150,8 +150,8 @@ test('should report toHaveScreenshot step with expectation name in title', async
const result = await runInlineTest({ const result = await runInlineTest({
'reporter.ts': ` 'reporter.ts': `
class Reporter { class Reporter {
onStepBegin(test, result, step) { onStepEnd(test, result, step) {
console.log('%% begin ' + step.title); console.log('%% end ' + step.title);
} }
} }
module.exports = Reporter; module.exports = Reporter;
@ -173,12 +173,12 @@ test('should report toHaveScreenshot step with expectation name in title', async
expect(result.exitCode).toBe(0); expect(result.exitCode).toBe(0);
expect(result.output.split('\n').filter(line => line.startsWith('%%'))).toEqual([ expect(result.output.split('\n').filter(line => line.startsWith('%%'))).toEqual([
`%% begin Before Hooks`, `%% end browserContext.newPage`,
`%% begin browserContext.newPage`, `%% end Before Hooks`,
`%% begin expect.toHaveScreenshot(foo.png)`, `%% end expect.toHaveScreenshot(foo.png)`,
`%% begin expect.toHaveScreenshot(is-a-test-1.png)`, `%% end expect.toHaveScreenshot(is-a-test-1.png)`,
`%% begin After Hooks`, `%% end browserContext.close`,
`%% begin browserContext.close`, `%% end After Hooks`,
]); ]);
}); });