feat(reporter): expose more apis (#9603)

This commit is contained in:
Dmitry Gozman 2021-10-19 08:38:04 -07:00 committed by GitHub
parent 6d727401bf
commit 6d554a5e30
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 69 additions and 24 deletions

View file

@ -29,6 +29,16 @@ Returns the list of all test cases in this suite and its descendants, as opposit
Location in the source where the suite is defined. Missing for root and project suites. Location in the source where the suite is defined. Missing for root and project suites.
## property: Suite.parent
- type: <[void]|[Suite]>
Parent suite or [void] for the root suite.
## property: Suite.project
- type: <[void]|[TestProject]>
Configuration of the project this suite belongs to, or [void] for the root suite.
## property: Suite.suites ## property: Suite.suites
- type: <[Array]<[Suite]>> - type: <[Array]<[Suite]>>

View file

@ -53,6 +53,11 @@ The maximum number of retries given to this test in the configuration.
Learn more about [test retries](./test-retries.md#retries). Learn more about [test retries](./test-retries.md#retries).
## property: TestCase.suite
- type: <[Suite]>
Suite this test case belongs to.
## property: TestCase.timeout ## property: TestCase.timeout
- type: <[float]> - type: <[float]>

View file

@ -230,7 +230,7 @@ export class Dispatcher {
retryCandidates.add(failedTestId); retryCandidates.add(failedTestId);
let outermostSerialSuite: Suite | undefined; let outermostSerialSuite: Suite | undefined;
for (let parent = this._testById.get(failedTestId)!.test.parent; parent; parent = parent.parent) { for (let parent: Suite | undefined = this._testById.get(failedTestId)!.test.parent; parent; parent = parent.parent) {
if (parent._parallelMode === 'serial') if (parent._parallelMode === 'serial')
outermostSerialSuite = parent; outermostSerialSuite = parent;
} }
@ -241,7 +241,7 @@ export class Dispatcher {
// We have failed tests that belong to a serial suite. // We have failed tests that belong to a serial suite.
// We should skip all future tests from the same serial suite. // We should skip all future tests from the same serial suite.
remaining = remaining.filter(test => { remaining = remaining.filter(test => {
let parent = test.parent; let parent: Suite | undefined = test.parent;
while (parent && !serialSuitesWithFailures.has(parent)) while (parent && !serialSuitesWithFailures.has(parent))
parent = parent.parent; parent = parent.parent;

View file

@ -40,7 +40,7 @@ export class Loader {
constructor(defaultConfig: Config, configOverrides: Config) { constructor(defaultConfig: Config, configOverrides: Config) {
this._defaultConfig = defaultConfig; this._defaultConfig = defaultConfig;
this._configOverrides = configOverrides; this._configOverrides = configOverrides;
this._fullConfig = baseFullConfig; this._fullConfig = { ...baseFullConfig };
} }
static async deserialize(data: SerializedLoaderData): Promise<Loader> { static async deserialize(data: SerializedLoaderData): Promise<Loader> {
@ -426,6 +426,7 @@ const baseFullConfig: FullConfig = {
quiet: false, quiet: false,
shard: null, shard: null,
updateSnapshots: 'missing', updateSnapshots: 'missing',
version: require('../package.json').version,
workers: 1, workers: 1,
webServer: null, webServer: null,
}; };

View file

@ -58,7 +58,7 @@ export class ProjectImpl {
let pool = this.buildTestTypePool(test._testType); let pool = this.buildTestTypePool(test._testType);
const parents: Suite[] = []; const parents: Suite[] = [];
for (let parent = test.parent; parent; parent = parent.parent) for (let parent: Suite | undefined = test.parent; parent; parent = parent.parent)
parents.push(parent); parents.push(parent);
parents.reverse(); parents.reverse();
@ -82,7 +82,6 @@ export class ProjectImpl {
private _cloneEntries(from: Suite, to: Suite, repeatEachIndex: number, filter: (test: TestCase) => boolean): boolean { private _cloneEntries(from: Suite, to: Suite, repeatEachIndex: number, filter: (test: TestCase) => boolean): boolean {
for (const hook of from._allHooks) { for (const hook of from._allHooks) {
const clone = hook._clone(); const clone = hook._clone();
clone.projectName = this.config.name;
clone._pool = this.buildPool(hook); clone._pool = this.buildPool(hook);
clone._projectIndex = this.index; clone._projectIndex = this.index;
to._addAllHook(clone); to._addAllHook(clone);
@ -98,7 +97,6 @@ export class ProjectImpl {
} else { } else {
const pool = this.buildPool(entry); const pool = this.buildPool(entry);
const test = entry._clone(); const test = entry._clone();
test.projectName = this.config.name;
test.retries = this.config.retries; test.retries = this.config.retries;
test._workerHash = `run${this.index}-${pool.digest}-repeat${repeatEachIndex}`; test._workerHash = `run${this.index}-${pool.digest}-repeat${repeatEachIndex}`;
test._id = `${entry._ordinalInFile}@${entry._requireFile}#run${this.index}-repeat${repeatEachIndex}`; test._id = `${entry._ordinalInFile}@${entry._requireFile}#run${this.index}-repeat${repeatEachIndex}`;

View file

@ -16,7 +16,6 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { FullProject } from '../types';
import { FullConfig, Location, Suite, TestCase, TestResult, TestStatus, TestStep } from '../../types/testReporter'; import { FullConfig, Location, Suite, TestCase, TestResult, TestStatus, TestStep } from '../../types/testReporter';
import { assert, calculateSha1 } from 'playwright-core/src/utils/utils'; import { assert, calculateSha1 } from 'playwright-core/src/utils/utils';
import { sanitizeForFilePath } from '../util'; import { sanitizeForFilePath } from '../util';
@ -109,7 +108,7 @@ class RawReporter {
async onEnd() { async onEnd() {
const projectSuites = this.suite.suites; const projectSuites = this.suite.suites;
for (const suite of projectSuites) { for (const suite of projectSuites) {
const project = (suite as any)._projectConfig as FullProject; const project = suite.project();
assert(project, 'Internal Error: Invalid project structure'); assert(project, 'Internal Error: Invalid project structure');
const reportFolder = path.join(project.outputDir, 'report'); const reportFolder = path.join(project.outputDir, 'report');
fs.mkdirSync(reportFolder, { recursive: true }); fs.mkdirSync(reportFolder, { recursive: true });
@ -132,7 +131,7 @@ class RawReporter {
generateProjectReport(config: FullConfig, suite: Suite): JsonReport { generateProjectReport(config: FullConfig, suite: Suite): JsonReport {
this.config = config; this.config = config;
const project = (suite as any)._projectConfig as FullProject; const project = suite.project();
assert(project, 'Internal Error: Invalid project structure'); assert(project, 'Internal Error: Invalid project structure');
const report: JsonReport = { const report: JsonReport = {
config, config,

View file

@ -509,7 +509,7 @@ function createTestGroups(rootSuite: Suite): TestGroup[] {
} }
let insideParallel = false; let insideParallel = false;
for (let parent = test.parent; parent; parent = parent.parent) for (let parent: Suite | undefined = test.parent; parent; parent = parent.parent)
insideParallel = insideParallel || parent._parallelMode === 'parallel'; insideParallel = insideParallel || parent._parallelMode === 'parallel';
if (insideParallel) { if (insideParallel) {

View file

@ -22,20 +22,12 @@ import { FullProject } from './types';
class Base { class Base {
title: string; title: string;
parent?: Suite;
_only = false; _only = false;
_requireFile: string = ''; _requireFile: string = '';
constructor(title: string) { constructor(title: string) {
this.title = title; this.title = title;
} }
titlePath(): string[] {
const titlePath = this.parent ? this.parent.titlePath() : [];
titlePath.push(this.title);
return titlePath;
}
} }
export type Modifier = { export type Modifier = {
@ -49,6 +41,7 @@ export class Suite extends Base implements reporterTypes.Suite {
suites: Suite[] = []; suites: Suite[] = [];
tests: TestCase[] = []; tests: TestCase[] = [];
location?: Location; location?: Location;
parent?: Suite;
_use: FixturesWithLocation[] = []; _use: FixturesWithLocation[] = [];
_isDescribe = false; _isDescribe = false;
_entries: (Suite | TestCase)[] = []; _entries: (Suite | TestCase)[] = [];
@ -91,6 +84,12 @@ export class Suite extends Base implements reporterTypes.Suite {
return result; return result;
} }
titlePath(): string[] {
const titlePath = this.parent ? this.parent.titlePath() : [];
titlePath.push(this.title);
return titlePath;
}
_getOnlyItems(): (TestCase | Suite)[] { _getOnlyItems(): (TestCase | Suite)[] {
const items: (TestCase | Suite)[] = []; const items: (TestCase | Suite)[] = [];
if (this._only) if (this._only)
@ -113,19 +112,24 @@ export class Suite extends Base implements reporterTypes.Suite {
suite._modifiers = this._modifiers.slice(); suite._modifiers = this._modifiers.slice();
suite._isDescribe = this._isDescribe; suite._isDescribe = this._isDescribe;
suite._parallelMode = this._parallelMode; suite._parallelMode = this._parallelMode;
suite._projectConfig = this._projectConfig;
return suite; return suite;
} }
project(): FullProject | undefined {
return this._projectConfig || this.parent?.project();
}
} }
export class TestCase extends Base implements reporterTypes.TestCase { export class TestCase extends Base implements reporterTypes.TestCase {
fn: Function; fn: Function;
results: reporterTypes.TestResult[] = []; results: reporterTypes.TestResult[] = [];
location: Location; location: Location;
parent!: Suite;
expectedStatus: reporterTypes.TestStatus = 'passed'; expectedStatus: reporterTypes.TestStatus = 'passed';
timeout = 0; timeout = 0;
annotations: Annotations = []; annotations: Annotations = [];
projectName = '';
retries = 0; retries = 0;
_type: 'beforeAll' | 'afterAll' | 'test'; _type: 'beforeAll' | 'afterAll' | 'test';
@ -146,6 +150,12 @@ export class TestCase extends Base implements reporterTypes.TestCase {
this.location = location; this.location = location;
} }
titlePath(): string[] {
const titlePath = this.parent ? this.parent.titlePath() : [];
titlePath.push(this.title);
return titlePath;
}
outcome(): 'skipped' | 'expected' | 'unexpected' | 'flaky' { outcome(): 'skipped' | 'expected' | 'unexpected' | 'flaky' {
const nonSkipped = this.results.filter(result => result.status !== 'skipped'); const nonSkipped = this.results.filter(result => result.status !== 'skipped');
if (!nonSkipped.length) if (!nonSkipped.length)

View file

@ -323,7 +323,7 @@ export class WorkerRunner extends EventEmitter {
}; };
// Inherit test.setTimeout() from parent suites. // Inherit test.setTimeout() from parent suites.
for (let suite = test.parent; suite; suite = suite.parent) { for (let suite: Suite | undefined = test.parent; suite; suite = suite.parent) {
if (suite._timeout !== undefined) { if (suite._timeout !== undefined) {
testInfo.setTimeout(suite._timeout); testInfo.setTimeout(suite._timeout);
break; break;
@ -420,7 +420,7 @@ export class WorkerRunner extends EventEmitter {
private async _runBeforeHooks(test: TestCase, testInfo: TestInfoImpl) { private async _runBeforeHooks(test: TestCase, testInfo: TestInfoImpl) {
try { try {
const beforeEachModifiers: Modifier[] = []; const beforeEachModifiers: Modifier[] = [];
for (let s = test.parent; s; s = s.parent) { for (let s: Suite | undefined = test.parent; s; s = s.parent) {
const modifiers = s._modifiers.filter(modifier => !this._fixtureRunner.dependsOnWorkerFixturesOnly(modifier.fn, modifier.location)); const modifiers = s._modifiers.filter(modifier => !this._fixtureRunner.dependsOnWorkerFixturesOnly(modifier.fn, modifier.location));
beforeEachModifiers.push(...modifiers.reverse()); beforeEachModifiers.push(...modifiers.reverse());
} }

View file

@ -698,6 +698,7 @@ export interface FullConfig<TestArgs = {}, WorkerArgs = {}> {
* Also available in the [command line](https://playwright.dev/docs/test-cli) with the `--max-failures` and `-x` options. * Also available in the [command line](https://playwright.dev/docs/test-cli) with the `--max-failures` and `-x` options.
*/ */
maxFailures: number; maxFailures: number;
version: string;
/** /**
* Whether to preserve test output in the * Whether to preserve test output in the
* [testConfig.outputDir](https://playwright.dev/docs/api/class-testconfig#test-config-output-dir). Defaults to `'always'`. * [testConfig.outputDir](https://playwright.dev/docs/api/class-testconfig#test-config-output-dir). Defaults to `'always'`.

View file

@ -15,7 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
import type { FullConfig, TestStatus, TestError } from './test'; import type { FullConfig, FullProject, TestStatus, TestError } from './test';
export type { FullConfig, TestStatus, TestError } from './test'; export type { FullConfig, TestStatus, TestError } from './test';
/** /**
@ -57,6 +57,10 @@ export interface Location {
* [reporter.onBegin(config, suite)](https://playwright.dev/docs/api/class-reporter#reporter-on-begin) method. * [reporter.onBegin(config, suite)](https://playwright.dev/docs/api/class-reporter#reporter-on-begin) method.
*/ */
export interface Suite { export interface Suite {
/**
* Parent suite or [void] for the root suite.
*/
parent?: Suite;
/** /**
* Suite title. * Suite title.
* - Empty for root suite. * - Empty for root suite.
@ -89,6 +93,10 @@ export interface Suite {
* [suite.tests](https://playwright.dev/docs/api/class-suite#suite-tests). * [suite.tests](https://playwright.dev/docs/api/class-suite#suite-tests).
*/ */
allTests(): TestCase[]; allTests(): TestCase[];
/**
* Configuration of the project this suite belongs to, or [void] for the root suite.
*/
project(): FullProject | undefined;
} }
/** /**
@ -98,6 +106,7 @@ export interface Suite {
* or repeated multiple times, it will have multiple `TestCase` objects in corresponding projects' suites. * or repeated multiple times, it will have multiple `TestCase` objects in corresponding projects' suites.
*/ */
export interface TestCase { export interface TestCase {
parent: Suite;
/** /**
* Test title as passed to the [test.(call)(title, testFunction)](https://playwright.dev/docs/api/class-test#test-call) * Test title as passed to the [test.(call)(title, testFunction)](https://playwright.dev/docs/api/class-test#test-call)
* call. * call.

View file

@ -71,9 +71,16 @@ test('should work with custom reporter', async ({ runInlineTest }) => {
} }
onBegin(config, suite) { onBegin(config, suite) {
console.log('\\n%%reporter-begin-' + this.options.begin + '%%'); console.log('\\n%%reporter-begin-' + this.options.begin + '%%');
console.log('\\n%%version-' + config.version);
} }
onTestBegin(test) { onTestBegin(test) {
console.log('\\n%%reporter-testbegin-' + test.title + '-' + test.titlePath()[1] + '%%'); const projectName = test.titlePath()[1];
console.log('\\n%%reporter-testbegin-' + test.title + '-' + projectName + '%%');
const suite = test.parent;
if (!suite.tests.includes(test))
console.log('\\n%%error-inconsistent-parent');
if (test.parent.project().name !== projectName)
console.log('\\n%%error-inconsistent-project-name');
} }
onStdOut() { onStdOut() {
console.log('\\n%%reporter-stdout%%'); console.log('\\n%%reporter-stdout%%');
@ -126,6 +133,7 @@ test('should work with custom reporter', async ({ runInlineTest }) => {
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([
'%%reporter-begin-begin%%', '%%reporter-begin-begin%%',
'%%version-' + require('../../packages/playwright-test/package.json').version,
'%%reporter-testbegin-is run-foo%%', '%%reporter-testbegin-is run-foo%%',
'%%reporter-stdout%%', '%%reporter-stdout%%',
'%%reporter-stderr%%', '%%reporter-stderr%%',

View file

@ -142,6 +142,7 @@ export interface FullConfig<TestArgs = {}, WorkerArgs = {}> {
grep: RegExp | RegExp[]; grep: RegExp | RegExp[];
grepInvert: RegExp | RegExp[] | null; grepInvert: RegExp | RegExp[] | null;
maxFailures: number; maxFailures: number;
version: string;
preserveOutput: PreserveOutput; preserveOutput: PreserveOutput;
projects: FullProject<TestArgs, WorkerArgs>[]; projects: FullProject<TestArgs, WorkerArgs>[];
reporter: ReporterDescription[]; reporter: ReporterDescription[];

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import type { FullConfig, TestStatus, TestError } from './test'; import type { FullConfig, FullProject, TestStatus, TestError } from './test';
export type { FullConfig, TestStatus, TestError } from './test'; export type { FullConfig, TestStatus, TestError } from './test';
export interface Location { export interface Location {
@ -24,15 +24,18 @@ export interface Location {
} }
export interface Suite { export interface Suite {
parent?: Suite;
title: string; title: string;
location?: Location; location?: Location;
suites: Suite[]; suites: Suite[];
tests: TestCase[]; tests: TestCase[];
titlePath(): string[]; titlePath(): string[];
allTests(): TestCase[]; allTests(): TestCase[];
project(): FullProject | undefined;
} }
export interface TestCase { export interface TestCase {
parent: Suite;
title: string; title: string;
location: Location; location: Location;
titlePath(): string[]; titlePath(): string[];