feat(testId): expose persistent test id (#16003)

This commit is contained in:
Pavel Feldman 2022-07-27 20:17:19 -07:00 committed by GitHub
parent a128dda84e
commit 8c190c3e09
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 110 additions and 104 deletions

View File

@ -27,6 +27,12 @@ Expected test status.
See also [`property: TestResult.status`] for the actual status.
## property: TestCase.id
* since: v1.25
- type: <[string]>
Unique test ID that is computed based on the test file name, test title and project name. Test ID can be used as a history ID.
## property: TestCase.location
* since: v1.10
- type: <[Location]>

View File

@ -27,7 +27,7 @@ export type TestGroup = {
workerHash: string;
requireFile: string;
repeatEachIndex: number;
projectIndex: number;
projectId: string;
tests: TestCase[];
};
@ -67,7 +67,7 @@ export class Dispatcher {
for (const group of testGroups) {
this._queuedOrRunningHashCount.set(group.workerHash, 1 + (this._queuedOrRunningHashCount.get(group.workerHash) || 0));
for (const test of group.tests)
this._testById.set(test._id, { test, resultByWorkerIndex: new Map() });
this._testById.set(test.id, { test, resultByWorkerIndex: new Map() });
}
}
@ -181,7 +181,7 @@ export class Dispatcher {
doneCallback();
};
const remainingByTestId = new Map(testGroup.tests.map(e => [ e._id, e ]));
const remainingByTestId = new Map(testGroup.tests.map(e => [ e.id, e ]));
const failedTestIds = new Set<string>();
const onTestBegin = (params: TestBeginPayload) => {
@ -298,10 +298,10 @@ export class Dispatcher {
const massSkipTestsFromRemaining = (testIds: Set<string>, errors: TestError[], onlyStartedTests?: boolean) => {
remaining = remaining.filter(test => {
if (!testIds.has(test._id))
if (!testIds.has(test.id))
return true;
if (!this._hasReachedMaxFailures()) {
const data = this._testById.get(test._id)!;
const data = this._testById.get(test.id)!;
const runData = data.resultByWorkerIndex.get(worker.workerIndex);
// There might be a single test that has started but has not finished yet.
let result: TestResult;
@ -317,7 +317,7 @@ export class Dispatcher {
result.error = result.errors[0];
result.status = errors.length ? 'failed' : 'skipped';
this._reportTestEnd(test, result);
failedTestIds.add(test._id);
failedTestIds.add(test.id);
errors = []; // Only report errors for the first test.
}
return false;
@ -341,13 +341,13 @@ export class Dispatcher {
if (params.fatalErrors.length) {
// In case of fatal errors, report first remaining test as failing with these errors,
// and all others as skipped.
massSkipTestsFromRemaining(new Set(remaining.map(test => test._id)), params.fatalErrors);
massSkipTestsFromRemaining(new Set(remaining.map(test => test.id)), params.fatalErrors);
}
// Handle tests that should be skipped because of the setup failure.
massSkipTestsFromRemaining(new Set(params.skipTestsDueToSetupFailure), []);
// Handle unexpected worker exit.
if (params.unexpectedExitError)
massSkipTestsFromRemaining(new Set(remaining.map(test => test._id)), [params.unexpectedExitError], true /* onlyStartedTests */);
massSkipTestsFromRemaining(new Set(remaining.map(test => test.id)), [params.unexpectedExitError], true /* onlyStartedTests */);
const retryCandidates = new Set<string>();
const serialSuitesWithFailures = new Set<Suite>();
@ -387,7 +387,7 @@ export class Dispatcher {
// Add all tests from faiiled serial suites for possible retry.
// These will only be retried together, because they have the same
// "retries" setting and the same number of previous runs.
serialSuite.allTests().forEach(test => retryCandidates.add(test._id));
serialSuite.allTests().forEach(test => retryCandidates.add(test.id));
}
for (const testId of retryCandidates) {
@ -527,7 +527,7 @@ class Worker extends EventEmitter {
workerIndex: this.workerIndex,
parallelIndex: this.parallelIndex,
repeatEachIndex: testGroup.repeatEachIndex,
projectIndex: testGroup.projectIndex,
projectId: testGroup.projectId,
loader: loaderData,
stdoutParams: {
rows: process.stdout.rows,
@ -547,7 +547,7 @@ class Worker extends EventEmitter {
const runPayload: RunPayload = {
file: testGroup.requireFile,
entries: testGroup.tests.map(test => {
return { testId: test._id, retry: test.results.length };
return { testId: test.id, retry: test.results.length };
}),
};
this.send({ method: 'run', params: runPayload });

View File

@ -34,7 +34,7 @@ export type WorkerInitParams = {
workerIndex: number;
parallelIndex: number;
repeatEachIndex: number;
projectIndex: number;
projectId: string;
loader: SerializedLoaderData;
stdoutParams: TtyParams;
stderrParams: TtyParams;

View File

@ -152,12 +152,28 @@ export class Loader {
}
this._fullConfig.metadata = takeFirst(config.metadata, baseFullConfig.metadata);
this._fullConfig.projects = (config.projects || [config]).map(p => this._resolveProject(config, this._fullConfig, p, throwawayArtifactsPath));
this._assignUniqueProjectIds(this._fullConfig.projects);
}
private _assignUniqueProjectIds(projects: FullProjectInternal[]) {
const usedNames = new Set();
for (const p of projects) {
const name = p.name || '';
for (let i = 0; i < projects.length; ++i) {
const candidate = name + (i ? i : '');
if (usedNames.has(candidate))
continue;
p._id = candidate;
usedNames.add(candidate);
break;
}
}
}
async loadTestFile(file: string, environment: 'runner' | 'worker') {
if (cachedFileSuites.has(file))
return cachedFileSuites.get(file)!;
const suite = new Suite(path.relative(this._fullConfig.rootDir, file) || path.basename(file));
const suite = new Suite(path.relative(this._fullConfig.rootDir, file) || path.basename(file), 'file');
suite._requireFile = file;
suite.location = { file, line: 0, column: 0 };
@ -210,7 +226,7 @@ export class Loader {
buildFileSuiteForProject(project: FullProjectInternal, suite: Suite, repeatEachIndex: number, filter: (test: TestCase) => boolean): Suite | undefined {
if (!this._projectSuiteBuilders.has(project))
this._projectSuiteBuilders.set(project, new ProjectSuiteBuilder(project, this._fullConfig.projects.indexOf(project)));
this._projectSuiteBuilders.set(project, new ProjectSuiteBuilder(project));
const builder = this._projectSuiteBuilders.get(project)!;
return builder.cloneFileSuite(suite, repeatEachIndex, filter);
}
@ -254,6 +270,7 @@ export class Loader {
const name = takeFirst(projectConfig.name, config.name, '');
const screenshotsDir = takeFirst((projectConfig as any).screenshotsDir, (config as any).screenshotsDir, path.join(testDir, '__screenshots__', process.platform, name));
return {
_id: '',
_fullConfig: fullConfig,
_fullyParallel: takeFirst(projectConfig.fullyParallel, config.fullyParallel, undefined),
_expect: takeFirst(projectConfig.expect, config.expect, {}),
@ -307,13 +324,11 @@ export class Loader {
class ProjectSuiteBuilder {
private _project: FullProjectInternal;
private _index: number;
private _testTypePools = new Map<TestTypeImpl, FixturePool>();
private _testPools = new Map<TestCase, FixturePool>();
constructor(project: FullProjectInternal, index: number) {
constructor(project: FullProjectInternal) {
this._project = project;
this._index = index;
}
private _buildTestTypePool(testType: TestTypeImpl): FixturePool {
@ -337,7 +352,7 @@ class ProjectSuiteBuilder {
for (const parent of parents) {
if (parent._use.length)
pool = new FixturePool(parent._use, pool, parent._isDescribe);
pool = new FixturePool(parent._use, pool, parent._type === 'describe');
for (const hook of parent._hooks)
pool.validateFunction(hook.fn, hook.type + ' hook', hook.location);
for (const modifier of parent._modifiers)
@ -350,32 +365,34 @@ class ProjectSuiteBuilder {
return this._testPools.get(test)!;
}
private _cloneEntries(from: Suite, to: Suite, repeatEachIndex: number, filter: (test: TestCase) => boolean, relativeTitlePath: string): boolean {
private _cloneEntries(from: Suite, to: Suite, repeatEachIndex: number, filter: (test: TestCase) => boolean): boolean {
for (const entry of from._entries) {
if (entry instanceof Suite) {
const suite = entry._clone();
suite._fileId = to._fileId;
to._addSuite(suite);
// Ignore empty titles, similar to Suite.titlePath().
const childTitlePath = relativeTitlePath + (suite.title ? ' ' + suite.title : '');
if (!this._cloneEntries(entry, suite, repeatEachIndex, filter, childTitlePath)) {
if (!this._cloneEntries(entry, suite, repeatEachIndex, filter)) {
to._entries.pop();
to.suites.pop();
}
} else {
const test = entry._clone();
test.retries = this._project.retries;
// We rely upon relative paths being unique.
// See `getClashingTestsPerSuite()` in `runner.ts`.
test._id = `${calculateSha1(relativeTitlePath + ' ' + entry.title)}@${entry._requireFile}#run${this._index}-repeat${repeatEachIndex}`;
test.repeatEachIndex = repeatEachIndex;
test._projectIndex = this._index;
to._addTest(test);
test.retries = this._project.retries;
const repeatEachIndexSuffix = repeatEachIndex ? ` (repeat:${repeatEachIndex})` : '';
// At the point of the query, suite is not yet attached to the project, so we only get file, describe and test titles.
const testIdExpression = `[project=${this._project._id}]${test.titlePath().join('\x1e')}${repeatEachIndexSuffix}`;
const testId = to._fileId + '-' + calculateSha1(testIdExpression).slice(0, 20);
test.id = testId;
test.repeatEachIndex = repeatEachIndex;
test._projectId = this._project._id;
if (!filter(test)) {
to._entries.pop();
to.tests.pop();
} else {
const pool = this._buildPool(entry);
test._workerHash = `run${this._index}-${pool.digest}-repeat${repeatEachIndex}`;
test._workerHash = `run${this._project._id}-${pool.digest}-repeat${repeatEachIndex}`;
test._pool = pool;
}
}
@ -387,7 +404,9 @@ class ProjectSuiteBuilder {
cloneFileSuite(suite: Suite, repeatEachIndex: number, filter: (test: TestCase) => boolean): Suite | undefined {
const result = suite._clone();
return this._cloneEntries(suite, result, repeatEachIndex, filter, '') ? result : undefined;
const relativeFile = path.relative(this._project.testDir, suite.location!.file).split(path.sep).join('/');
result._fileId = calculateSha1(relativeFile).slice(0, 20);
return this._cloneEntries(suite, result, repeatEachIndex, filter) ? result : undefined;
}
private _applyConfigUseOptions(testType: TestTypeImpl, configUse: Fixtures): FixturesWithLocation[] {
@ -405,7 +424,7 @@ class ProjectSuiteBuilder {
(originalFixtures as any)[key] = value;
}
if (Object.entries(optionsFromConfig).length)
result.push({ fixtures: optionsFromConfig, location: { file: `project#${this._index}`, line: 1, column: 1 } });
result.push({ fixtures: optionsFromConfig, location: { file: `project#${this._project._id}`, line: 1, column: 1 } });
if (Object.entries(originalFixtures).length)
result.push({ fixtures: originalFixtures, location: f.location });
}

View File

@ -17,7 +17,7 @@
import fs from 'fs';
import path from 'path';
import type { FullConfig, Location, Suite, TestCase, TestResult, TestStatus, TestStep } from '../../types/testReporter';
import { assert, calculateSha1 } from 'playwright-core/lib/utils';
import { assert } from 'playwright-core/lib/utils';
import { sanitizeForFilePath } from '../util';
import { formatResultFailure } from './base';
import { toPosixPath, serializePatterns } from './json';
@ -180,11 +180,7 @@ class RawReporter {
timeout: project.timeout,
},
suites: suite.suites.map(fileSuite => {
// fileId is based on the location of the enclosing file suite.
// Don't use the file in test/suite location, it can be different
// due to the source map / require.
const fileId = calculateSha1(fileSuite.location!.file.split(path.sep).join('/'));
return this._serializeSuite(fileSuite, fileId);
return this._serializeSuite(fileSuite);
})
};
for (const file of this.stepsInFile.keys()) {
@ -215,23 +211,21 @@ class RawReporter {
return report;
}
private _serializeSuite(suite: Suite, fileId: string): JsonSuite {
private _serializeSuite(suite: Suite): JsonSuite {
const location = this._relativeLocation(suite.location);
return {
const result = {
title: suite.title,
fileId,
fileId: (suite as any)._fileId,
location,
suites: suite.suites.map(s => this._serializeSuite(s, fileId)),
tests: suite.tests.map(t => this._serializeTest(t, fileId)),
suites: suite.suites.map(s => this._serializeSuite(s)),
tests: suite.tests.map(t => this._serializeTest(t)),
};
return result;
}
private _serializeTest(test: TestCase, fileId: string): JsonTestCase {
const [, projectName, , ...titles] = test.titlePath();
const testIdExpression = `project:${projectName}|path:${titles.join('>')}|repeat:${test.repeatEachIndex}`;
const testId = fileId + '-' + calculateSha1(testIdExpression);
private _serializeTest(test: TestCase): JsonTestCase {
return {
testId,
testId: test.id,
title: test.title,
location: this._relativeLocation(test.location)!,
expectedStatus: test.expectedStatus,

View File

@ -266,7 +266,7 @@ export class Runner {
const fatalErrors: TestError[] = [];
// 1. Add all tests.
const preprocessRoot = new Suite('');
const preprocessRoot = new Suite('', 'root');
for (const file of allTestFiles) {
const fileSuite = await this._loader.loadTestFile(file, 'runner');
if (fileSuite._loadError)
@ -299,11 +299,11 @@ export class Runner {
fileSuites.set(fileSuite._requireFile, fileSuite);
const outputDirs = new Set<string>();
const rootSuite = new Suite('');
const rootSuite = new Suite('', 'root');
for (const [project, files] of filesByProject) {
const grepMatcher = createTitleMatcher(project.grep);
const grepInvertMatcher = project.grepInvert ? createTitleMatcher(project.grepInvert) : null;
const projectSuite = new Suite(project.name);
const projectSuite = new Suite(project.name, 'project');
projectSuite._projectConfig = project;
if (project._fullyParallel)
projectSuite._parallelMode = 'parallel';
@ -671,7 +671,7 @@ function createTestGroups(rootSuite: Suite, workers: number): TestGroup[] {
workerHash: test._workerHash,
requireFile: test._requireFile,
repeatEachIndex: test.repeatEachIndex,
projectIndex: test._projectIndex,
projectId: test._projectId,
tests: [],
};
};
@ -794,7 +794,7 @@ function createDuplicateTitlesError(config: FullConfigInternal, rootSuite: Suite
for (const fileSuite of rootSuite.suites) {
const testsByFullTitle = new MultiMap<string, TestCase>();
for (const test of fileSuite.allTests()) {
const fullTitle = test.titlePath().slice(2).join(' ');
const fullTitle = test.titlePath().slice(2).join('\x1e');
testsByFullTitle.set(fullTitle, test);
}
for (const fullTitle of testsByFullTitle.keys()) {

View File

@ -42,7 +42,6 @@ export class Suite extends Base implements reporterTypes.Suite {
location?: Location;
parent?: Suite;
_use: FixturesWithLocation[] = [];
_isDescribe = false;
_skipped = false;
_entries: (Suite | TestCase)[] = [];
_hooks: { type: 'beforeEach' | 'afterEach' | 'beforeAll' | 'afterAll', fn: Function, location: Location }[] = [];
@ -52,6 +51,13 @@ export class Suite extends Base implements reporterTypes.Suite {
_parallelMode: 'default' | 'serial' | 'parallel' = 'default';
_projectConfig: FullProjectInternal | undefined;
_loadError?: reporterTypes.TestError;
_fileId: string | undefined;
readonly _type: 'root' | 'project' | 'file' | 'describe';
constructor(title: string, type: 'root' | 'project' | 'file' | 'describe') {
super(title);
this._type = type;
}
_addTest(test: TestCase) {
test.parent = this;
@ -82,7 +88,7 @@ export class Suite extends Base implements reporterTypes.Suite {
titlePath(): string[] {
const titlePath = this.parent ? this.parent.titlePath() : [];
// Ignore anonymous describe blocks.
if (this.title || !this._isDescribe)
if (this.title || this._type !== 'describe')
titlePath.push(this.title);
return titlePath;
}
@ -98,7 +104,7 @@ export class Suite extends Base implements reporterTypes.Suite {
}
_clone(): Suite {
const suite = new Suite(this.title);
const suite = new Suite(this.title, this._type);
suite._only = this._only;
suite.location = this.location;
suite._requireFile = this._requireFile;
@ -107,7 +113,6 @@ export class Suite extends Base implements reporterTypes.Suite {
suite._timeout = this._timeout;
suite._annotations = this._annotations.slice();
suite._modifiers = this._modifiers.slice();
suite._isDescribe = this._isDescribe;
suite._parallelMode = this._parallelMode;
suite._projectConfig = this._projectConfig;
suite._skipped = this._skipped;
@ -132,10 +137,10 @@ export class TestCase extends Base implements reporterTypes.TestCase {
repeatEachIndex = 0;
_testType: TestTypeImpl;
_id = '';
id = '';
_workerHash = '';
_pool: FixturePool | undefined;
_projectIndex = 0;
_projectId = '';
// Annotations that are not added from within a test (like fixme and skip), should not
// be re-added each time we retry a test.
_alreadyInheritedAnnotations: boolean = false;

View File

@ -113,20 +113,13 @@ export class TestInfoImpl implements TestInfo {
this._timeoutManager = new TimeoutManager(this.project.timeout);
this.outputDir = (() => {
const sameName = loader.fullConfig().projects.filter(project => project.name === this.project.name);
let uniqueProjectNamePathSegment: string;
if (sameName.length > 1)
uniqueProjectNamePathSegment = this.project.name + (sameName.indexOf(this.project) + 1);
else
uniqueProjectNamePathSegment = this.project.name;
const relativeTestFilePath = path.relative(this.project.testDir, test._requireFile.replace(/\.(spec|test)\.(js|ts|mjs)$/, ''));
const sanitizedRelativePath = relativeTestFilePath.replace(process.platform === 'win32' ? new RegExp('\\\\', 'g') : new RegExp('/', 'g'), '-');
const fullTitleWithoutSpec = test.titlePath().slice(1).join(' ');
let testOutputDir = trimLongString(sanitizedRelativePath + '-' + sanitizeForFilePath(fullTitleWithoutSpec));
if (uniqueProjectNamePathSegment)
testOutputDir += '-' + sanitizeForFilePath(uniqueProjectNamePathSegment);
if (project._id)
testOutputDir += '-' + sanitizeForFilePath(project._id);
if (this.retry)
testOutputDir += '-retry' + this.retry;
if (this.repeatEachIndex)

View File

@ -107,9 +107,8 @@ export class TestTypeImpl {
title = '';
}
const child = new Suite(title);
const child = new Suite(title, 'describe');
child._requireFile = suite._requireFile;
child._isDescribe = true;
child.location = location;
suite._addSuite(child);

View File

@ -59,6 +59,7 @@ export interface FullConfigInternal extends FullConfigPublic {
* increasing the surface area of the public API type called FullProject.
*/
export interface FullProjectInternal extends FullProjectPublic {
_id: string;
_fullConfig: FullConfigInternal;
_fullyParallel: boolean;
_expect: Project['expect'];

View File

@ -27,7 +27,7 @@ sendMessageToParent('ready');
process.stdout.write = (chunk: string | Buffer) => {
const outPayload: TestOutputPayload = {
testId: workerRunner?._currentTest?._test._id,
testId: workerRunner?._currentTest?._test.id,
...chunkToParams(chunk)
};
sendMessageToParent('stdOut', outPayload);
@ -37,7 +37,7 @@ process.stdout.write = (chunk: string | Buffer) => {
if (!process.env.PW_RUNNER_DEBUG) {
process.stderr.write = (chunk: string | Buffer) => {
const outPayload: TestOutputPayload = {
testId: workerRunner?._currentTest?._test._id,
testId: workerRunner?._currentTest?._test.id,
...chunkToParams(chunk)
};
sendMessageToParent('stdErr', outPayload);

View File

@ -150,7 +150,7 @@ export class WorkerRunner extends EventEmitter {
return;
this._loader = await Loader.deserialize(this._params.loader);
this._project = this._loader.fullConfig().projects[this._params.projectIndex];
this._project = this._loader.fullConfig().projects.find(p => p._id === this._params.projectId)!;
}
async runTestGroup(runPayload: RunPayload) {
@ -161,7 +161,7 @@ export class WorkerRunner extends EventEmitter {
await this._loadIfNeeded();
const fileSuite = await this._loader.loadTestFile(runPayload.file, 'worker');
const suite = this._loader.buildFileSuiteForProject(this._project, fileSuite, this._params.repeatEachIndex, test => {
if (!entries.has(test._id))
if (!entries.has(test.id))
return false;
return true;
});
@ -169,13 +169,13 @@ export class WorkerRunner extends EventEmitter {
this._extraSuiteAnnotations = new Map();
this._activeSuites = new Set();
this._didRunFullCleanup = false;
const tests = suite.allTests().filter(test => entries.has(test._id));
const tests = suite.allTests().filter(test => entries.has(test.id));
for (let i = 0; i < tests.length; i++) {
// Do not run tests after full cleanup, because we are entirely done.
if (this._isStopped && this._didRunFullCleanup)
break;
const entry = entries.get(tests[i]._id)!;
entries.delete(tests[i]._id);
const entry = entries.get(tests[i].id)!;
entries.delete(tests[i].id);
await this._runTest(tests[i], entry.retry, tests[i + 1]);
}
} else {
@ -194,8 +194,8 @@ export class WorkerRunner extends EventEmitter {
fatalUnknownTestIds
};
for (const test of this._skipRemainingTestsInSuite?.allTests() || []) {
if (entries.has(test._id))
donePayload.skipTestsDueToSetupFailure.push(test._id);
if (entries.has(test.id))
donePayload.skipTestsDueToSetupFailure.push(test.id);
}
this.emit('done', donePayload);
this._fatalErrors = [];
@ -217,7 +217,7 @@ export class WorkerRunner extends EventEmitter {
callbackHandled = true;
const error = result.error instanceof Error ? serializeError(result.error) : result.error;
const payload: StepEndPayload = {
testId: test._id,
testId: test.id,
refinedTitle: step.refinedTitle,
stepId,
wallTime: Date.now(),
@ -230,7 +230,7 @@ export class WorkerRunner extends EventEmitter {
// Sanitize location that comes from user land, it might have extra properties.
const location = data.location && hasLocation ? { file: data.location.file, line: data.location.line, column: data.location.column } : undefined;
const payload: StepBeginPayload = {
testId: test._id,
testId: test.id,
stepId,
...data,
location,
@ -567,14 +567,14 @@ export class WorkerRunner extends EventEmitter {
function buildTestBeginPayload(testInfo: TestInfoImpl): TestBeginPayload {
return {
testId: testInfo._test._id,
testId: testInfo._test.id,
startWallTime: testInfo._startWallTime,
};
}
function buildTestEndPayload(testInfo: TestInfoImpl): TestEndPayload {
return {
testId: testInfo._test._id,
testId: testInfo._test.id,
duration: testInfo.duration,
status: testInfo.status!,
errors: testInfo.errors,

View File

@ -126,6 +126,12 @@ export interface TestCase {
description?: string;
}>;
/**
* Unique test ID that is computed based on the test file name, test title and project name. Test ID can be used as a
* history ID.
*/
id: string;
/**
* Location in the source where the test is defined.
*/

View File

@ -31,23 +31,6 @@ test('it should not allow multiple tests with the same name per suite', async ({
expect(result.output).toContain(` - tests${path.sep}example.spec.js:7`);
});
test('it should enforce unique test names based on the describe block name', async ({ runInlineTest }) => {
const result = await runInlineTest({
'tests/example.spec.js': `
const { test } = pwt;
test.describe('hello', () => { test('my world', () => {}) });
test.describe('hello my', () => { test('world', () => {}) });
test('hello my world', () => {});
`
});
expect(result.exitCode).toBe(1);
expect(result.output).toContain('duplicate test titles are not allowed');
expect(result.output).toContain(`- title: hello my world`);
expect(result.output).toContain(` - tests${path.sep}example.spec.js:6`);
expect(result.output).toContain(` - tests${path.sep}example.spec.js:7`);
expect(result.output).toContain(` - tests${path.sep}example.spec.js:8`);
});
test('it should not allow multiple tests with the same name in multiple files', async ({ runInlineTest }) => {
const result = await runInlineTest({
'tests/example1.spec.js': `

View File

@ -182,15 +182,15 @@ test('should include the project name', async ({ runInlineTest }) => {
expect(result.output).toContain('my-test.spec.js-snapshots/bar.txt');
// test1, run with foo #1
expect(result.output).toContain('test-results/my-test-test-1-foo1/bar.txt');
expect(result.output).toContain('test-results/my-test-test-1-foo/bar.txt');
expect(result.output).toContain('my-test.spec.js-snapshots/bar-foo.txt');
expect(result.output).toContain('test-results/my-test-test-1-foo1-retry1/bar.txt');
expect(result.output).toContain('test-results/my-test-test-1-foo-retry1/bar.txt');
expect(result.output).toContain('my-test.spec.js-snapshots/bar-foo.txt');
// test1, run with foo #2
expect(result.output).toContain('test-results/my-test-test-1-foo2/bar.txt');
expect(result.output).toContain('test-results/my-test-test-1-foo1/bar.txt');
expect(result.output).toContain('my-test.spec.js-snapshots/bar-foo.txt');
expect(result.output).toContain('test-results/my-test-test-1-foo2-retry1/bar.txt');
expect(result.output).toContain('test-results/my-test-test-1-foo1-retry1/bar.txt');
expect(result.output).toContain('my-test.spec.js-snapshots/bar-foo.txt');
// test1, run with bar
@ -204,11 +204,11 @@ test('should include the project name', async ({ runInlineTest }) => {
expect(result.output).toContain('my-test.spec.js-snapshots/bar-suffix.txt');
// test2, run with foo #1
expect(result.output).toContain('test-results/my-test-test-2-foo1/bar.txt');
expect(result.output).toContain('test-results/my-test-test-2-foo/bar.txt');
expect(result.output).toContain('my-test.spec.js-snapshots/bar-foo-suffix.txt');
// test2, run with foo #2
expect(result.output).toContain('test-results/my-test-test-2-foo2/bar.txt');
expect(result.output).toContain('test-results/my-test-test-2-foo1/bar.txt');
expect(result.output).toContain('my-test.spec.js-snapshots/bar-foo-suffix.txt');
// test2, run with bar