diff --git a/docs/src/test-reporter-api/class-testcase.md b/docs/src/test-reporter-api/class-testcase.md index b0df65f432..dbcb3c729d 100644 --- a/docs/src/test-reporter-api/class-testcase.md +++ b/docs/src/test-reporter-api/class-testcase.md @@ -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]> diff --git a/packages/playwright-test/src/dispatcher.ts b/packages/playwright-test/src/dispatcher.ts index 8f12892559..17cf36e853 100644 --- a/packages/playwright-test/src/dispatcher.ts +++ b/packages/playwright-test/src/dispatcher.ts @@ -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(); const onTestBegin = (params: TestBeginPayload) => { @@ -298,10 +298,10 @@ export class Dispatcher { const massSkipTestsFromRemaining = (testIds: Set, 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(); const serialSuitesWithFailures = new Set(); @@ -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 }); diff --git a/packages/playwright-test/src/ipc.ts b/packages/playwright-test/src/ipc.ts index 44663a377b..175ef067ef 100644 --- a/packages/playwright-test/src/ipc.ts +++ b/packages/playwright-test/src/ipc.ts @@ -34,7 +34,7 @@ export type WorkerInitParams = { workerIndex: number; parallelIndex: number; repeatEachIndex: number; - projectIndex: number; + projectId: string; loader: SerializedLoaderData; stdoutParams: TtyParams; stderrParams: TtyParams; diff --git a/packages/playwright-test/src/loader.ts b/packages/playwright-test/src/loader.ts index f0e8cf5205..827c336ed4 100644 --- a/packages/playwright-test/src/loader.ts +++ b/packages/playwright-test/src/loader.ts @@ -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(); private _testPools = new Map(); - 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 }); } diff --git a/packages/playwright-test/src/reporters/raw.ts b/packages/playwright-test/src/reporters/raw.ts index 67f994c6f0..4c320c3160 100644 --- a/packages/playwright-test/src/reporters/raw.ts +++ b/packages/playwright-test/src/reporters/raw.ts @@ -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, diff --git a/packages/playwright-test/src/runner.ts b/packages/playwright-test/src/runner.ts index 8d1d2c548a..3aa580ea80 100644 --- a/packages/playwright-test/src/runner.ts +++ b/packages/playwright-test/src/runner.ts @@ -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(); - 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(); 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()) { diff --git a/packages/playwright-test/src/test.ts b/packages/playwright-test/src/test.ts index 69339d1144..5f50fc2fcc 100644 --- a/packages/playwright-test/src/test.ts +++ b/packages/playwright-test/src/test.ts @@ -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; diff --git a/packages/playwright-test/src/testInfo.ts b/packages/playwright-test/src/testInfo.ts index 0dbd7c1dcd..d7c82d37ab 100644 --- a/packages/playwright-test/src/testInfo.ts +++ b/packages/playwright-test/src/testInfo.ts @@ -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) diff --git a/packages/playwright-test/src/testType.ts b/packages/playwright-test/src/testType.ts index 3c2a932e89..aa9ba2a73f 100644 --- a/packages/playwright-test/src/testType.ts +++ b/packages/playwright-test/src/testType.ts @@ -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); diff --git a/packages/playwright-test/src/types.ts b/packages/playwright-test/src/types.ts index eec3f47eac..921a216fb7 100644 --- a/packages/playwright-test/src/types.ts +++ b/packages/playwright-test/src/types.ts @@ -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']; diff --git a/packages/playwright-test/src/worker.ts b/packages/playwright-test/src/worker.ts index a6d149a98f..a4b96a7ef5 100644 --- a/packages/playwright-test/src/worker.ts +++ b/packages/playwright-test/src/worker.ts @@ -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); diff --git a/packages/playwright-test/src/workerRunner.ts b/packages/playwright-test/src/workerRunner.ts index 3e0fc66042..b704aca2a4 100644 --- a/packages/playwright-test/src/workerRunner.ts +++ b/packages/playwright-test/src/workerRunner.ts @@ -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, diff --git a/packages/playwright-test/types/testReporter.d.ts b/packages/playwright-test/types/testReporter.d.ts index b074712bce..beefad201c 100644 --- a/packages/playwright-test/types/testReporter.d.ts +++ b/packages/playwright-test/types/testReporter.d.ts @@ -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. */ diff --git a/tests/playwright-test/runner.spec.ts b/tests/playwright-test/runner.spec.ts index becf9ff28d..4e1f73c10d 100644 --- a/tests/playwright-test/runner.spec.ts +++ b/tests/playwright-test/runner.spec.ts @@ -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': ` diff --git a/tests/playwright-test/test-output-dir.spec.ts b/tests/playwright-test/test-output-dir.spec.ts index 0ddc29c511..3749954284 100644 --- a/tests/playwright-test/test-output-dir.spec.ts +++ b/tests/playwright-test/test-output-dir.spec.ts @@ -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