diff --git a/src/test/cli.ts b/src/test/cli.ts
index 48b08f9fe8..000fccc421 100644
--- a/src/test/cli.ts
+++ b/src/test/cli.ts
@@ -20,13 +20,12 @@ import * as commander from 'commander';
import * as fs from 'fs';
import * as path from 'path';
import type { Config } from './types';
-import { Runner } from './runner';
+import { Runner, builtInReporters, BuiltInReporter } from './runner';
import { stopProfiling, startProfiling } from './profiler';
-import type { FilePatternFilter } from './util';
+import { FilePatternFilter } from './util';
const defaultTimeout = 30000;
-const defaultReporter = process.env.CI ? 'dot' : 'list';
-const builtinReporters = ['list', 'line', 'dot', 'json', 'junit', 'null'];
+const defaultReporter: BuiltInReporter = process.env.CI ? 'dot' : 'list';
const tsConfig = 'playwright.config.ts';
const jsConfig = 'playwright.config.js';
const mjsConfig = 'playwright.config.mjs';
@@ -55,7 +54,7 @@ export function addTestCommand(program: commander.CommanderStatic) {
command.option('--output
', `Folder for output artifacts (default: "test-results")`);
command.option('--quiet', `Suppress stdio`);
command.option('--repeat-each ', `Run each test N times (default: 1)`);
- command.option('--reporter ', `Reporter to use, comma-separated, can be ${builtinReporters.map(name => `"${name}"`).join(', ')} (default: "${defaultReporter}")`);
+ command.option('--reporter ', `Reporter to use, comma-separated, can be ${builtInReporters.map(name => `"${name}"`).join(', ')} (default: "${defaultReporter}")`);
command.option('--retries ', `Maximum retry count for flaky tests, zero for no retries (default: no retries)`);
command.option('--shard ', `Shard tests and execute only the selected shard, specify in the form "current/all", 1-based, for example "3/5"`);
command.option('--project ', `Only run tests from the specified project (default: run all projects)`);
@@ -177,10 +176,19 @@ function overridesFromOptions(options: { [key: string]: any }): Config {
quiet: options.quiet ? options.quiet : undefined,
repeatEach: options.repeatEach ? parseInt(options.repeatEach, 10) : undefined,
retries: options.retries ? parseInt(options.retries, 10) : undefined,
- reporter: (options.reporter && options.reporter.length) ? options.reporter.split(',').map((r: string) => [r]) : undefined,
+ reporter: (options.reporter && options.reporter.length) ? options.reporter.split(',').map((r: string) => [resolveReporter(r)]) : undefined,
shard: shardPair ? { current: shardPair[0] - 1, total: shardPair[1] } : undefined,
timeout: isDebuggerAttached ? 0 : (options.timeout ? parseInt(options.timeout, 10) : undefined),
updateSnapshots: options.updateSnapshots ? 'all' as const : undefined,
workers: options.workers ? parseInt(options.workers, 10) : undefined,
};
}
+
+function resolveReporter(id: string) {
+ if (builtInReporters.includes(id as any))
+ return id;
+ const localPath = path.resolve(process.cwd(), id);
+ if (fs.existsSync(localPath))
+ return localPath;
+ return require.resolve(id, { paths: [ process.cwd() ] });
+}
diff --git a/src/test/loader.ts b/src/test/loader.ts
index d43937071e..1102d4ae82 100644
--- a/src/test/loader.ts
+++ b/src/test/loader.ts
@@ -25,6 +25,7 @@ import * as url from 'url';
import { ProjectImpl } from './project';
import { Reporter } from '../../types/testReporter';
import { LaunchConfig } from '../../types/test';
+import { BuiltInReporter, builtInReporters } from './runner';
export class Loader {
private _defaultConfig: Config;
@@ -80,7 +81,7 @@ export class Loader {
const configUse = mergeObjects(this._defaultConfig.use, this._config.use);
this._config = mergeObjects(mergeObjects(this._defaultConfig, this._config), { use: configUse });
- if (('testDir' in this._config) && this._config.testDir !== undefined && !path.isAbsolute(this._config.testDir))
+ if (this._config.testDir !== undefined)
this._config.testDir = path.resolve(rootDir, this._config.testDir);
const projects: Project[] = ('projects' in this._config) && this._config.projects !== undefined ? this._config.projects : [this._config];
@@ -93,7 +94,7 @@ export class Loader {
this._fullConfig.grepInvert = takeFirst(this._configOverrides.grepInvert, this._config.grepInvert, baseFullConfig.grepInvert);
this._fullConfig.maxFailures = takeFirst(this._configOverrides.maxFailures, this._config.maxFailures, baseFullConfig.maxFailures);
this._fullConfig.preserveOutput = takeFirst(this._configOverrides.preserveOutput, this._config.preserveOutput, baseFullConfig.preserveOutput);
- this._fullConfig.reporter = takeFirst(toReporters(this._configOverrides.reporter), toReporters(this._config.reporter), baseFullConfig.reporter);
+ this._fullConfig.reporter = takeFirst(toReporters(this._configOverrides.reporter as any), resolveReporters(this._config.reporter, rootDir), baseFullConfig.reporter);
this._fullConfig.reportSlowTests = takeFirst(this._configOverrides.reportSlowTests, this._config.reportSlowTests, baseFullConfig.reportSlowTests);
this._fullConfig.quiet = takeFirst(this._configOverrides.quiet, this._config.quiet, baseFullConfig.quiet);
this._fullConfig.shard = takeFirst(this._configOverrides.shard, this._config.shard, baseFullConfig.shard);
@@ -220,7 +221,7 @@ function takeFirst(...args: (T | undefined)[]): T {
return undefined as any as T;
}
-function toReporters(reporters: 'dot' | 'line' | 'list' | 'junit' | 'json' | 'null' | ReporterDescription[] | undefined): ReporterDescription[] | undefined {
+function toReporters(reporters: BuiltInReporter | ReporterDescription[] | undefined): ReporterDescription[] | undefined {
if (!reporters)
return;
if (typeof reporters === 'string')
@@ -313,10 +314,8 @@ function validateConfig(file: string, config: Config) {
if (!Array.isArray(item) || item.length <= 0 || item.length > 2 || typeof item[0] !== 'string')
throw errorWithFile(file, `config.reporter[${index}] must be a tuple [name, optionalArgument]`);
});
- } else {
- const builtinReporters = ['dot', 'line', 'list', 'junit', 'json', 'null'];
- if (typeof config.reporter !== 'string' || !builtinReporters.includes(config.reporter))
- throw errorWithFile(file, `config.reporter must be one of ${builtinReporters.map(name => `"${name}"`).join(', ')}`);
+ } else if (typeof config.reporter !== 'string') {
+ throw errorWithFile(file, `config.reporter must be a string`);
}
}
@@ -437,3 +436,11 @@ const baseFullConfig: FullConfig = {
workers: 1,
launch: [],
};
+
+function resolveReporters(reporters: Config['reporter'], rootDir: string): ReporterDescription[]|undefined {
+ return toReporters(reporters as any)?.map(([id, arg]) => {
+ if (builtInReporters.includes(id as any))
+ return [id, arg];
+ return [require.resolve(id, { paths: [ rootDir ] }), arg];
+ });
+}
diff --git a/src/test/runner.ts b/src/test/runner.ts
index 74b968d0cc..54baa57891 100644
--- a/src/test/runner.ts
+++ b/src/test/runner.ts
@@ -64,7 +64,7 @@ export class Runner {
private async _createReporter(list: boolean) {
const reporters: Reporter[] = [];
- const defaultReporters = {
+ const defaultReporters: {[key in BuiltInReporter]: new(arg: any) => Reporter} = {
dot: list ? ListModeReporter : DotReporter,
line: list ? ListModeReporter : LineReporter,
list: list ? ListModeReporter : ListReporter,
@@ -427,3 +427,6 @@ class ListModeReporter implements Reporter {
console.log(`Total: ${tests.length} ${tests.length === 1 ? 'test' : 'tests'} in ${files.size} ${files.size === 1 ? 'file' : 'files'}`);
}
}
+
+export const builtInReporters = ['list', 'line', 'dot', 'json', 'junit', 'null'] as const;
+export type BuiltInReporter = typeof builtInReporters[number];
diff --git a/tests/playwright-test/global-setup.spec.ts b/tests/playwright-test/global-setup.spec.ts
index ddbbad7ea4..2898e04c08 100644
--- a/tests/playwright-test/global-setup.spec.ts
+++ b/tests/playwright-test/global-setup.spec.ts
@@ -21,7 +21,7 @@ test('globalSetup and globalTeardown should work', async ({ runInlineTest }) =>
'playwright.config.ts': `
import * as path from 'path';
module.exports = {
- globalSetup: 'globalSetup.ts',
+ globalSetup: './globalSetup.ts',
globalTeardown: path.join(__dirname, 'globalTeardown.ts'),
};
`,
diff --git a/tests/playwright-test/reporter.spec.ts b/tests/playwright-test/reporter.spec.ts
index 0bb36fb423..23b23d8a89 100644
--- a/tests/playwright-test/reporter.spec.ts
+++ b/tests/playwright-test/reporter.spec.ts
@@ -16,6 +16,24 @@
import { test, expect } from './playwright-test-fixtures';
+const smallReporterJS = `
+class Reporter {
+ onBegin(config, suite) {
+ console.log('\\n%%begin');
+ }
+ onTestBegin(test) {}
+ onStdOut() {}
+ onStdErr() {}
+ onTestEnd(test, result) {}
+ onTimeout() {}
+ onError() {}
+ onEnd() {
+ console.log('\\n%%end');
+ }
+}
+module.exports = Reporter;
+`;
+
test('should work with custom reporter', async ({ runInlineTest }) => {
const result = await runInlineTest({
'reporter.ts': `
@@ -91,3 +109,48 @@ test('should work with custom reporter', async ({ runInlineTest }) => {
'%%reporter-end-end%%',
]);
});
+
+
+test('should work without a file extension', async ({ runInlineTest }) => {
+ const result = await runInlineTest({
+ 'reporter.ts': smallReporterJS,
+ 'playwright.config.ts': `
+ module.exports = {
+ reporter: './reporter',
+ };
+ `,
+ 'a.test.ts': `
+ const { test } = pwt;
+ test('pass', async ({}) => {
+ });
+ `
+ }, { reporter: '', workers: 1 });
+
+ expect(result.exitCode).toBe(0);
+ expect(result.output.split('\n').filter(line => line.startsWith('%%'))).toEqual([
+ '%%begin',
+ '%%end',
+ ]);
+});
+
+test('should load reporter from node_modules', async ({ runInlineTest }) => {
+ const result = await runInlineTest({
+ 'node_modules/my-reporter/index.js': smallReporterJS,
+ 'playwright.config.ts': `
+ module.exports = {
+ reporter: 'my-reporter',
+ };
+ `,
+ 'a.test.ts': `
+ const { test } = pwt;
+ test('pass', async ({}) => {
+ });
+ `
+ }, { reporter: '', workers: 1 });
+
+ expect(result.exitCode).toBe(0);
+ expect(result.output.split('\n').filter(line => line.startsWith('%%'))).toEqual([
+ '%%begin',
+ '%%end',
+ ]);
+});
diff --git a/types/test.d.ts b/types/test.d.ts
index 960b117d74..3c7d09cf47 100644
--- a/types/test.d.ts
+++ b/types/test.d.ts
@@ -146,6 +146,9 @@ export type LaunchConfig = {
cwd?: string,
};
+type LiteralUnion = T | (U & { zz_IGNORE_ME?: never })
+
+
/**
* Testing configuration.
*/
@@ -213,7 +216,7 @@ interface ConfigBase {
* It is possible to pass multiple reporters. A common pattern is using one terminal reporter
* like `'line'` or `'list'`, and one file reporter like `'json'` or `'junit'`.
*/
- reporter?: 'dot' | 'line' | 'list' | 'junit' | 'json' | 'null' | ReporterDescription[];
+ reporter?: LiteralUnion<'list'|'dot'|'line'|'json'|'junit'|'null', string> | ReporterDescription[];
/**
* Whether to report slow tests. When `null`, slow tests are not reported.