mirror of
https://github.com/microsoft/playwright.git
synced 2024-10-27 21:58:52 +03:00
feat(testId): expose persistent test id (#16003)
This commit is contained in:
parent
a128dda84e
commit
8c190c3e09
@ -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]>
|
||||
|
@ -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 });
|
||||
|
@ -34,7 +34,7 @@ export type WorkerInitParams = {
|
||||
workerIndex: number;
|
||||
parallelIndex: number;
|
||||
repeatEachIndex: number;
|
||||
projectIndex: number;
|
||||
projectId: string;
|
||||
loader: SerializedLoaderData;
|
||||
stdoutParams: TtyParams;
|
||||
stderrParams: TtyParams;
|
||||
|
@ -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 });
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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()) {
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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'];
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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': `
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user