@remix-run/test 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +325 -2
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +171 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/lib/config.d.ts +60 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +152 -0
- package/dist/lib/context.d.ts +69 -0
- package/dist/lib/context.d.ts.map +1 -0
- package/dist/lib/context.js +49 -0
- package/dist/lib/e2e-server.d.ts +11 -0
- package/dist/lib/e2e-server.d.ts.map +1 -0
- package/dist/lib/e2e-server.js +15 -0
- package/dist/lib/executor.d.ts +27 -0
- package/dist/lib/executor.d.ts.map +1 -0
- package/dist/lib/executor.js +123 -0
- package/dist/lib/framework.d.ts +107 -0
- package/dist/lib/framework.d.ts.map +1 -0
- package/dist/lib/framework.js +198 -0
- package/dist/lib/framework.test.d.ts +2 -0
- package/dist/lib/framework.test.d.ts.map +1 -0
- package/dist/lib/framework.test.e2e.d.ts +2 -0
- package/dist/lib/framework.test.e2e.d.ts.map +1 -0
- package/dist/lib/framework.test.e2e.js +29 -0
- package/dist/lib/framework.test.js +283 -0
- package/dist/lib/mock.d.ts +52 -0
- package/dist/lib/mock.d.ts.map +1 -0
- package/dist/lib/mock.js +61 -0
- package/dist/lib/playwright.d.ts +15 -0
- package/dist/lib/playwright.d.ts.map +1 -0
- package/dist/lib/playwright.js +84 -0
- package/dist/lib/reporters/dot.d.ts +10 -0
- package/dist/lib/reporters/dot.d.ts.map +1 -0
- package/dist/lib/reporters/dot.js +55 -0
- package/dist/lib/reporters/files.d.ts +10 -0
- package/dist/lib/reporters/files.d.ts.map +1 -0
- package/dist/lib/reporters/files.js +70 -0
- package/dist/lib/reporters/index.d.ts +14 -0
- package/dist/lib/reporters/index.d.ts.map +1 -0
- package/dist/lib/reporters/index.js +18 -0
- package/dist/lib/reporters/spec.d.ts +10 -0
- package/dist/lib/reporters/spec.d.ts.map +1 -0
- package/dist/lib/reporters/spec.js +152 -0
- package/dist/lib/reporters/tap.d.ts +10 -0
- package/dist/lib/reporters/tap.d.ts.map +1 -0
- package/dist/lib/reporters/tap.js +54 -0
- package/dist/lib/runner.d.ts +9 -0
- package/dist/lib/runner.d.ts.map +1 -0
- package/dist/lib/runner.js +89 -0
- package/dist/lib/utils.d.ts +16 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +27 -0
- package/dist/lib/watcher.d.ts +5 -0
- package/dist/lib/watcher.d.ts.map +1 -0
- package/dist/lib/watcher.js +39 -0
- package/dist/lib/worker-e2e.d.ts +2 -0
- package/dist/lib/worker-e2e.d.ts.map +1 -0
- package/dist/lib/worker-e2e.js +48 -0
- package/dist/lib/worker.d.ts +2 -0
- package/dist/lib/worker.d.ts.map +1 -0
- package/dist/lib/worker.js +29 -0
- package/package.json +58 -5
- package/src/cli.ts +210 -0
- package/src/index.ts +15 -0
- package/src/lib/config.ts +231 -0
- package/src/lib/context.ts +126 -0
- package/src/lib/e2e-server.ts +28 -0
- package/src/lib/executor.ts +162 -0
- package/src/lib/framework.ts +251 -0
- package/src/lib/mock.ts +89 -0
- package/src/lib/playwright.ts +102 -0
- package/src/lib/reporters/dot.ts +57 -0
- package/src/lib/reporters/files.ts +76 -0
- package/src/lib/reporters/index.ts +28 -0
- package/src/lib/reporters/spec.ts +173 -0
- package/src/lib/reporters/tap.ts +58 -0
- package/src/lib/runner.ts +137 -0
- package/src/lib/utils.ts +40 -0
- package/src/lib/watcher.ts +46 -0
- package/src/lib/worker-e2e.ts +52 -0
- package/src/lib/worker.ts +30 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
import * as fs from 'node:fs/promises';
|
|
3
|
+
import { chromium, firefox, webkit } from 'playwright';
|
|
4
|
+
import { tsImport } from 'tsx/esm/api';
|
|
5
|
+
export async function loadPlaywrightConfig(input) {
|
|
6
|
+
let candidates = input
|
|
7
|
+
? [path.resolve(process.cwd(), input)]
|
|
8
|
+
: [
|
|
9
|
+
path.join(process.cwd(), 'playwright.config.ts'),
|
|
10
|
+
path.join(process.cwd(), 'playwright.config.js'),
|
|
11
|
+
];
|
|
12
|
+
for (let configPath of candidates) {
|
|
13
|
+
try {
|
|
14
|
+
await fs.access(configPath);
|
|
15
|
+
let mod = await tsImport(configPath, { parentURL: import.meta.url });
|
|
16
|
+
return mod.default ?? mod;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// not found or failed to load — try next
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const launchers = {
|
|
24
|
+
chromium,
|
|
25
|
+
firefox,
|
|
26
|
+
webkit,
|
|
27
|
+
};
|
|
28
|
+
export function getBrowserLauncher(playwrightUseOpts) {
|
|
29
|
+
if (playwrightUseOpts?.browserName) {
|
|
30
|
+
let launcher = launchers[playwrightUseOpts.browserName];
|
|
31
|
+
if (!launcher) {
|
|
32
|
+
let supportedBrowsers = Object.keys(launchers).join(', ');
|
|
33
|
+
throw new Error(`Unsupported browser "${playwrightUseOpts.browserName}". ` +
|
|
34
|
+
`Supported browsers are: ${supportedBrowsers}`);
|
|
35
|
+
}
|
|
36
|
+
return launcher;
|
|
37
|
+
}
|
|
38
|
+
return chromium;
|
|
39
|
+
}
|
|
40
|
+
export function resolveProjects(config) {
|
|
41
|
+
if (config?.projects?.length) {
|
|
42
|
+
return config.projects.map((p) => ({
|
|
43
|
+
name: p.name,
|
|
44
|
+
playwrightUseOpts: { ...config.use, ...p.use },
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
return [
|
|
48
|
+
{
|
|
49
|
+
name: 'chromium',
|
|
50
|
+
playwrightUseOpts: config?.use,
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
export function getPlaywrightLaunchOptions(playwrightUseOpts) {
|
|
55
|
+
return {
|
|
56
|
+
headless: playwrightUseOpts?.headless,
|
|
57
|
+
channel: playwrightUseOpts?.channel,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export function getPlaywrightPageOptions(playwrightUseOpts) {
|
|
61
|
+
return {
|
|
62
|
+
// Context options passed to browser.newPage()
|
|
63
|
+
bypassCSP: playwrightUseOpts?.bypassCSP,
|
|
64
|
+
colorScheme: playwrightUseOpts?.colorScheme,
|
|
65
|
+
deviceScaleFactor: playwrightUseOpts?.deviceScaleFactor,
|
|
66
|
+
extraHTTPHeaders: playwrightUseOpts?.extraHTTPHeaders,
|
|
67
|
+
geolocation: playwrightUseOpts?.geolocation,
|
|
68
|
+
hasTouch: playwrightUseOpts?.hasTouch,
|
|
69
|
+
httpCredentials: playwrightUseOpts?.httpCredentials,
|
|
70
|
+
ignoreHTTPSErrors: playwrightUseOpts?.ignoreHTTPSErrors,
|
|
71
|
+
isMobile: playwrightUseOpts?.isMobile,
|
|
72
|
+
javaScriptEnabled: playwrightUseOpts?.javaScriptEnabled,
|
|
73
|
+
locale: playwrightUseOpts?.locale,
|
|
74
|
+
offline: playwrightUseOpts?.offline,
|
|
75
|
+
permissions: playwrightUseOpts?.permissions,
|
|
76
|
+
storageState: playwrightUseOpts?.storageState,
|
|
77
|
+
timezoneId: playwrightUseOpts?.timezoneId,
|
|
78
|
+
userAgent: playwrightUseOpts?.userAgent,
|
|
79
|
+
viewport: playwrightUseOpts?.viewport,
|
|
80
|
+
// Additional options set on the page instance
|
|
81
|
+
navigationTimeout: playwrightUseOpts?.navigationTimeout,
|
|
82
|
+
actionTimeout: playwrightUseOpts?.actionTimeout,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type Counts } from '../utils.ts';
|
|
2
|
+
import type { TestResults } from '../executor.ts';
|
|
3
|
+
import type { Reporter } from './index.ts';
|
|
4
|
+
export declare class DotReporter implements Reporter {
|
|
5
|
+
#private;
|
|
6
|
+
onSectionStart(_label: string): void;
|
|
7
|
+
onResult(results: TestResults, _env?: string): void;
|
|
8
|
+
onSummary(counts: Counts, durationMs: number): void;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=dot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dot.d.ts","sourceRoot":"","sources":["../../../src/lib/reporters/dot.ts"],"names":[],"mappings":"AAAA,OAAO,EAAyB,KAAK,MAAM,EAAE,MAAM,aAAa,CAAA;AAChE,OAAO,KAAK,EAAc,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC7D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAE1C,qBAAa,WAAY,YAAW,QAAQ;;IAI1C,cAAc,CAAC,MAAM,EAAE,MAAM,QAAI;IAEjC,QAAQ,CAAC,OAAO,EAAE,WAAW,EAAE,IAAI,CAAC,EAAE,MAAM,QAc3C;IAED,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,QA6B3C;CACF"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { colors, normalizeLine } from "../utils.js";
|
|
2
|
+
export class DotReporter {
|
|
3
|
+
#failures = [];
|
|
4
|
+
#dotCount = 0;
|
|
5
|
+
onSectionStart(_label) { }
|
|
6
|
+
onResult(results, _env) {
|
|
7
|
+
for (let test of results.tests) {
|
|
8
|
+
if (test.status === 'passed') {
|
|
9
|
+
process.stdout.write(colors.green('.'));
|
|
10
|
+
}
|
|
11
|
+
else if (test.status === 'skipped') {
|
|
12
|
+
process.stdout.write(colors.dim('S'));
|
|
13
|
+
}
|
|
14
|
+
else if (test.status === 'todo') {
|
|
15
|
+
process.stdout.write(colors.dim('T'));
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
process.stdout.write(colors.red('F'));
|
|
19
|
+
this.#failures.push({ name: `${test.suiteName} > ${test.name}`, error: test.error });
|
|
20
|
+
}
|
|
21
|
+
this.#dotCount++;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
onSummary(counts, durationMs) {
|
|
25
|
+
if (this.#dotCount > 0)
|
|
26
|
+
console.log();
|
|
27
|
+
for (let i = 0; i < this.#failures.length; i++) {
|
|
28
|
+
let { name, error } = this.#failures[i];
|
|
29
|
+
console.log(`\n ${colors.red(`${i + 1})`)} ${name}`);
|
|
30
|
+
if (error) {
|
|
31
|
+
console.log(` ${colors.red(error.message)}`);
|
|
32
|
+
if (error.stack) {
|
|
33
|
+
let frames = error.stack
|
|
34
|
+
.split('\n')
|
|
35
|
+
.slice(1, 4)
|
|
36
|
+
.map((l) => ` ${normalizeLine(l).trim()}`)
|
|
37
|
+
.join('\n');
|
|
38
|
+
console.log(frames);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
let { passed, failed, skipped, todo } = counts;
|
|
43
|
+
let info = colors.cyan('ℹ');
|
|
44
|
+
console.log();
|
|
45
|
+
console.log(`${info} tests ${passed + failed + skipped + todo}`);
|
|
46
|
+
console.log(`${info} pass ${passed}`);
|
|
47
|
+
console.log(`${info} fail ${failed}`);
|
|
48
|
+
if (skipped > 0)
|
|
49
|
+
console.log(`${info} skipped ${skipped}`);
|
|
50
|
+
if (todo > 0)
|
|
51
|
+
console.log(`${info} todo ${todo}`);
|
|
52
|
+
console.log(`${info} duration_ms ${durationMs.toFixed(5)}`);
|
|
53
|
+
console.log();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type Counts } from '../utils.ts';
|
|
2
|
+
import type { TestResults } from '../executor.ts';
|
|
3
|
+
import type { Reporter } from './index.ts';
|
|
4
|
+
export declare class FilesReporter implements Reporter {
|
|
5
|
+
#private;
|
|
6
|
+
onSectionStart(_label: string): void;
|
|
7
|
+
onResult(results: TestResults, env?: string): void;
|
|
8
|
+
onSummary(counts: Counts, durationMs: number): void;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=files.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"files.d.ts","sourceRoot":"","sources":["../../../src/lib/reporters/files.ts"],"names":[],"mappings":"AACA,OAAO,EAAyB,KAAK,MAAM,EAAE,MAAM,aAAa,CAAA;AAChE,OAAO,KAAK,EAAc,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC7D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAE1C,qBAAa,aAAc,YAAW,QAAQ;;IAG5C,cAAc,CAAC,MAAM,EAAE,MAAM,QAAI;IAEjC,QAAQ,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,CAAC,EAAE,MAAM,QA8B1C;IAED,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,QAgC3C;CACF"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
import { colors, normalizeLine } from "../utils.js";
|
|
3
|
+
export class FilesReporter {
|
|
4
|
+
#failures = [];
|
|
5
|
+
onSectionStart(_label) { }
|
|
6
|
+
onResult(results, env) {
|
|
7
|
+
let filePath = results.tests[0]?.filePath;
|
|
8
|
+
let fileName = filePath ? path.relative(process.cwd(), filePath) : '(unknown)';
|
|
9
|
+
let envLabel = env ? ` ${colors.dim(`[${env}]`)}` : '';
|
|
10
|
+
let totalDuration = results.tests.reduce((sum, t) => sum + t.duration, 0);
|
|
11
|
+
let hasFailed = results.tests.some((t) => t.status === 'failed');
|
|
12
|
+
let fileColor = hasFailed ? colors.red : colors.green;
|
|
13
|
+
let duration = hasFailed ? '' : ` (${totalDuration.toFixed(2)}ms)`;
|
|
14
|
+
console.log(`${colors.dim('▶')} ${fileColor(fileName)}${duration}${envLabel}`);
|
|
15
|
+
if (hasFailed) {
|
|
16
|
+
// Print failing tests with suite/test nesting using > separators
|
|
17
|
+
for (let test of results.tests) {
|
|
18
|
+
if (test.status !== 'failed')
|
|
19
|
+
continue;
|
|
20
|
+
let fullName = test.name ? `${test.suiteName} > ${test.name}` : test.suiteName;
|
|
21
|
+
console.log(` ${colors.red('✗')} ${fullName}`);
|
|
22
|
+
if (test.error) {
|
|
23
|
+
console.log(` ${colors.red(`Error: ${test.error.message}`)}`);
|
|
24
|
+
if (test.error.stack) {
|
|
25
|
+
let stack = test.error.stack
|
|
26
|
+
.split('\n')
|
|
27
|
+
.map((line) => normalizeLine(line))
|
|
28
|
+
.join('\n');
|
|
29
|
+
console.log(` ${stack.split('\n').slice(1, 5).join(`\n `)}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
this.#failures.push({ suiteName: test.suiteName, name: test.name, error: test.error });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
onSummary(counts, durationMs) {
|
|
37
|
+
if (this.#failures.length > 0) {
|
|
38
|
+
console.log();
|
|
39
|
+
console.log(colors.red('Failed tests:'));
|
|
40
|
+
for (let i = 0; i < this.#failures.length; i++) {
|
|
41
|
+
let { suiteName, name, error } = this.#failures[i];
|
|
42
|
+
let fullName = name ? `${suiteName} > ${name}` : suiteName;
|
|
43
|
+
console.log(`\n ${colors.red(`${i + 1})`)} ${fullName}`);
|
|
44
|
+
if (error) {
|
|
45
|
+
console.log(` ${colors.red(error.message)}`);
|
|
46
|
+
if (error.stack) {
|
|
47
|
+
let frames = error.stack
|
|
48
|
+
.split('\n')
|
|
49
|
+
.slice(1, 4)
|
|
50
|
+
.map((l) => ` ${normalizeLine(l).trim()}`)
|
|
51
|
+
.join('\n');
|
|
52
|
+
console.log(frames);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
let { passed, failed, skipped, todo } = counts;
|
|
58
|
+
let info = colors.cyan('ℹ');
|
|
59
|
+
console.log();
|
|
60
|
+
console.log(`${info} tests ${passed + failed + skipped + todo}`);
|
|
61
|
+
console.log(`${info} pass ${passed}`);
|
|
62
|
+
console.log(`${info} fail ${failed}`);
|
|
63
|
+
if (skipped > 0)
|
|
64
|
+
console.log(`${info} skipped ${skipped}`);
|
|
65
|
+
if (todo > 0)
|
|
66
|
+
console.log(`${info} todo ${todo}`);
|
|
67
|
+
console.log(`${info} duration_ms ${durationMs.toFixed(5)}`);
|
|
68
|
+
console.log();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Counts } from '../utils.ts';
|
|
2
|
+
import type { TestResults } from '../executor.ts';
|
|
3
|
+
import { SpecReporter } from './spec.ts';
|
|
4
|
+
import { TapReporter } from './tap.ts';
|
|
5
|
+
import { DotReporter } from './dot.ts';
|
|
6
|
+
import { FilesReporter } from './files.ts';
|
|
7
|
+
export interface Reporter {
|
|
8
|
+
onResult(results: TestResults, env?: string): void;
|
|
9
|
+
onSummary(counts: Counts, durationMs: number): void;
|
|
10
|
+
onSectionStart(label: string): void;
|
|
11
|
+
}
|
|
12
|
+
export { SpecReporter, TapReporter, DotReporter, FilesReporter };
|
|
13
|
+
export declare function createReporter(type: string): Reporter;
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/reporters/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACzC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAA;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAA;AACtC,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE1C,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAClD,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACnD,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CACpC;AAED,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,aAAa,EAAE,CAAA;AAEhE,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CAYrD"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { SpecReporter } from "./spec.js";
|
|
2
|
+
import { TapReporter } from "./tap.js";
|
|
3
|
+
import { DotReporter } from "./dot.js";
|
|
4
|
+
import { FilesReporter } from "./files.js";
|
|
5
|
+
export { SpecReporter, TapReporter, DotReporter, FilesReporter };
|
|
6
|
+
export function createReporter(type) {
|
|
7
|
+
switch (type) {
|
|
8
|
+
case 'tap':
|
|
9
|
+
return new TapReporter();
|
|
10
|
+
case 'dot':
|
|
11
|
+
return new DotReporter();
|
|
12
|
+
case 'files':
|
|
13
|
+
return new FilesReporter();
|
|
14
|
+
case 'spec':
|
|
15
|
+
default:
|
|
16
|
+
return new SpecReporter();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type Counts } from '../utils.ts';
|
|
2
|
+
import type { TestResults } from '../executor.ts';
|
|
3
|
+
import type { Reporter } from './index.ts';
|
|
4
|
+
export declare class SpecReporter implements Reporter {
|
|
5
|
+
#private;
|
|
6
|
+
onSectionStart(label: string): void;
|
|
7
|
+
onResult(results: TestResults, env?: string): void;
|
|
8
|
+
onSummary(counts: Counts, durationMs: number): void;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=spec.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spec.d.ts","sourceRoot":"","sources":["../../../src/lib/reporters/spec.ts"],"names":[],"mappings":"AAAA,OAAO,EAAyB,KAAK,MAAM,EAAE,MAAM,aAAa,CAAA;AAChE,OAAO,KAAK,EAAc,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC7D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAE1C,qBAAa,YAAa,YAAW,QAAQ;;IAG3C,cAAc,CAAC,KAAK,EAAE,MAAM,QAE3B;IAED,QAAQ,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,CAAC,EAAE,MAAM,QA8H1C;IAED,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,QAgC3C;CACF"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { colors, normalizeLine } from "../utils.js";
|
|
2
|
+
export class SpecReporter {
|
|
3
|
+
#failures = [];
|
|
4
|
+
onSectionStart(label) {
|
|
5
|
+
console.log(label);
|
|
6
|
+
}
|
|
7
|
+
onResult(results, env) {
|
|
8
|
+
let suiteMap = new Map();
|
|
9
|
+
for (let test of results.tests) {
|
|
10
|
+
let suite = test.suiteName || 'Global';
|
|
11
|
+
if (!suiteMap.has(suite))
|
|
12
|
+
suiteMap.set(suite, []);
|
|
13
|
+
suiteMap.get(suite).push(test);
|
|
14
|
+
}
|
|
15
|
+
let envLabel = env ? ` ${colors.dim(`[${env}]`)}` : '';
|
|
16
|
+
let lastParts = [];
|
|
17
|
+
// Pre-compute aggregate test results for each path prefix so non-leaf
|
|
18
|
+
// suite headings can be colored the same way as leaf headings.
|
|
19
|
+
let prefixTests = new Map();
|
|
20
|
+
for (let [suiteName, tests] of suiteMap) {
|
|
21
|
+
let parts = suiteName.split(' > ');
|
|
22
|
+
for (let i = 0; i < parts.length; i++) {
|
|
23
|
+
let prefix = parts.slice(0, i + 1).join(' > ');
|
|
24
|
+
if (!prefixTests.has(prefix))
|
|
25
|
+
prefixTests.set(prefix, []);
|
|
26
|
+
prefixTests.get(prefix).push(...tests);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
for (let [suiteName, suiteTests] of suiteMap) {
|
|
30
|
+
let parts = suiteName.split(' > ');
|
|
31
|
+
// Find where this path diverges from the last rendered path
|
|
32
|
+
let commonLen = 0;
|
|
33
|
+
while (commonLen < lastParts.length &&
|
|
34
|
+
commonLen < parts.length &&
|
|
35
|
+
lastParts[commonLen] === parts[commonLen]) {
|
|
36
|
+
commonLen++;
|
|
37
|
+
}
|
|
38
|
+
// Print each new path component
|
|
39
|
+
for (let i = commonLen; i < parts.length; i++) {
|
|
40
|
+
let indent = ' '.repeat(i);
|
|
41
|
+
let isLeaf = i === parts.length - 1;
|
|
42
|
+
if (isLeaf) {
|
|
43
|
+
let totalDuration = suiteTests.reduce((sum, t) => sum + t.duration, 0);
|
|
44
|
+
let suiteHasFailed = suiteTests.some((t) => t.status === 'failed');
|
|
45
|
+
let suiteAllSkipped = suiteTests.every((t) => t.status === 'skipped');
|
|
46
|
+
let suiteAllTodo = suiteTests.every((t) => t.status === 'todo');
|
|
47
|
+
let label = suiteHasFailed
|
|
48
|
+
? colors.red(parts[i])
|
|
49
|
+
: suiteAllSkipped
|
|
50
|
+
? colors.dim(parts[i])
|
|
51
|
+
: suiteAllTodo
|
|
52
|
+
? colors.yellow(parts[i])
|
|
53
|
+
: colors.green(parts[i]);
|
|
54
|
+
let suiteComment = suiteAllSkipped
|
|
55
|
+
? colors.dim(' # skipped')
|
|
56
|
+
: suiteAllTodo
|
|
57
|
+
? colors.yellow(' # todo')
|
|
58
|
+
: '';
|
|
59
|
+
let duration = suiteComment ? '' : ` (${totalDuration.toFixed(2)}ms)`;
|
|
60
|
+
let label2 = envLabel;
|
|
61
|
+
console.log(`${indent}${colors.dim('▶')} ${label}${duration}${suiteComment}${label2}`);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
let prefix = parts.slice(0, i + 1).join(' > ');
|
|
65
|
+
let prefixTestList = prefixTests.get(prefix) ?? [];
|
|
66
|
+
let prefixHasFailed = prefixTestList.some((t) => t.status === 'failed');
|
|
67
|
+
let prefixAllSkipped = prefixTestList.length > 0 && prefixTestList.every((t) => t.status === 'skipped');
|
|
68
|
+
let prefixAllTodo = prefixTestList.length > 0 && prefixTestList.every((t) => t.status === 'todo');
|
|
69
|
+
let nameColor = prefixHasFailed
|
|
70
|
+
? colors.red
|
|
71
|
+
: prefixAllSkipped
|
|
72
|
+
? colors.dim
|
|
73
|
+
: prefixAllTodo
|
|
74
|
+
? colors.yellow
|
|
75
|
+
: colors.green;
|
|
76
|
+
let prefixDuration = prefixTestList.reduce((sum, t) => sum + t.duration, 0);
|
|
77
|
+
let prefixComment = prefixAllSkipped
|
|
78
|
+
? colors.dim(' # skipped')
|
|
79
|
+
: prefixAllTodo
|
|
80
|
+
? colors.yellow(' # todo')
|
|
81
|
+
: '';
|
|
82
|
+
let prefixDurationStr = prefixComment ? '' : ` (${prefixDuration.toFixed(2)}ms)`;
|
|
83
|
+
console.log(`${indent}${colors.dim('▶')} ${nameColor(parts[i])}${prefixDurationStr}${prefixComment}${envLabel}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
lastParts = parts;
|
|
87
|
+
// Print tests indented to the suite's depth
|
|
88
|
+
let testIndent = ' '.repeat(parts.length);
|
|
89
|
+
for (let test of suiteTests) {
|
|
90
|
+
if (test.status === 'passed') {
|
|
91
|
+
console.log(`${testIndent}${colors.green('✓')} ${test.name} (${test.duration.toFixed(2)}ms)`);
|
|
92
|
+
}
|
|
93
|
+
else if (test.status === 'failed') {
|
|
94
|
+
console.log(`${testIndent}${colors.red('✗')} ${test.name} (${test.duration.toFixed(2)}ms)`);
|
|
95
|
+
if (test.error) {
|
|
96
|
+
console.log(`${testIndent} ${colors.red(`Error: ${test.error.message}`)}`);
|
|
97
|
+
if (test.error.stack) {
|
|
98
|
+
let stack = test.error.stack
|
|
99
|
+
.split('\n')
|
|
100
|
+
.map((line) => normalizeLine(line))
|
|
101
|
+
.join('\n');
|
|
102
|
+
console.log(`${testIndent} ${stack.split('\n').slice(1, 5).join(`\n${testIndent} `)}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
this.#failures.push({ suiteName: test.suiteName, name: test.name, error: test.error });
|
|
106
|
+
}
|
|
107
|
+
else if (test.status === 'skipped') {
|
|
108
|
+
if (test.name)
|
|
109
|
+
console.log(`${testIndent}${colors.dim('↓')} ${colors.dim(`${test.name} # skipped`)}`);
|
|
110
|
+
}
|
|
111
|
+
else if (test.status === 'todo') {
|
|
112
|
+
if (test.name)
|
|
113
|
+
console.log(`${testIndent}${colors.yellow('…')} ${colors.yellow(`${test.name} # todo`)}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
onSummary(counts, durationMs) {
|
|
119
|
+
if (this.#failures.length > 0) {
|
|
120
|
+
console.log();
|
|
121
|
+
console.log(colors.red('Failed tests:'));
|
|
122
|
+
for (let i = 0; i < this.#failures.length; i++) {
|
|
123
|
+
let { suiteName, name, error } = this.#failures[i];
|
|
124
|
+
let fullName = name ? `${suiteName} > ${name}` : suiteName;
|
|
125
|
+
console.log(`\n ${colors.red(`${i + 1})`)} ${fullName}`);
|
|
126
|
+
if (error) {
|
|
127
|
+
console.log(` ${colors.red(error.message)}`);
|
|
128
|
+
if (error.stack) {
|
|
129
|
+
let frames = error.stack
|
|
130
|
+
.split('\n')
|
|
131
|
+
.slice(1, 4)
|
|
132
|
+
.map((l) => ` ${normalizeLine(l).trim()}`)
|
|
133
|
+
.join('\n');
|
|
134
|
+
console.log(frames);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
let { passed, failed, skipped, todo } = counts;
|
|
140
|
+
let info = colors.cyan('ℹ');
|
|
141
|
+
console.log();
|
|
142
|
+
console.log(`${info} tests ${passed + failed + skipped + todo}`);
|
|
143
|
+
console.log(`${info} pass ${passed}`);
|
|
144
|
+
console.log(`${info} fail ${failed}`);
|
|
145
|
+
if (skipped > 0)
|
|
146
|
+
console.log(`${info} skipped ${skipped}`);
|
|
147
|
+
if (todo > 0)
|
|
148
|
+
console.log(`${info} todo ${todo}`);
|
|
149
|
+
console.log(`${info} duration_ms ${durationMs.toFixed(5)}`);
|
|
150
|
+
console.log();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type Counts } from '../utils.ts';
|
|
2
|
+
import type { TestResults } from '../executor.ts';
|
|
3
|
+
import type { Reporter } from './index.ts';
|
|
4
|
+
export declare class TapReporter implements Reporter {
|
|
5
|
+
#private;
|
|
6
|
+
onSectionStart(_label: string): void;
|
|
7
|
+
onResult(results: TestResults, env?: string): void;
|
|
8
|
+
onSummary(counts: Counts, durationMs: number): void;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=tap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tap.d.ts","sourceRoot":"","sources":["../../../src/lib/reporters/tap.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,MAAM,EAAE,MAAM,aAAa,CAAA;AACxD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AACjD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAE1C,qBAAa,WAAY,YAAW,QAAQ;;IAI1C,cAAc,CAAC,MAAM,EAAE,MAAM,QAAI;IAEjC,QAAQ,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,CAAC,EAAE,MAAM,QAmC1C;IAED,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,QAS3C;CACF"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { normalizeLine } from "../utils.js";
|
|
2
|
+
export class TapReporter {
|
|
3
|
+
#counter = 0;
|
|
4
|
+
#total = 0;
|
|
5
|
+
onSectionStart(_label) { }
|
|
6
|
+
onResult(results, env) {
|
|
7
|
+
if (this.#counter === 0) {
|
|
8
|
+
console.log('TAP version 14');
|
|
9
|
+
}
|
|
10
|
+
let envComment = env ? ` # ${env}` : '';
|
|
11
|
+
for (let test of results.tests) {
|
|
12
|
+
this.#counter++;
|
|
13
|
+
this.#total++;
|
|
14
|
+
let fullName = test.name
|
|
15
|
+
? `${test.suiteName} > ${test.name}${envComment}`
|
|
16
|
+
: `${test.suiteName}${envComment}`;
|
|
17
|
+
if (test.status === 'passed') {
|
|
18
|
+
console.log(`ok ${this.#counter} - ${fullName}`);
|
|
19
|
+
}
|
|
20
|
+
else if (test.status === 'skipped') {
|
|
21
|
+
console.log(`ok ${this.#counter} - ${fullName} # SKIP`);
|
|
22
|
+
}
|
|
23
|
+
else if (test.status === 'todo') {
|
|
24
|
+
console.log(`ok ${this.#counter} - ${fullName} # TODO`);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
console.log(`not ok ${this.#counter} - ${fullName}`);
|
|
28
|
+
console.log(' ---');
|
|
29
|
+
console.log(` message: ${test.error?.message ?? 'unknown error'}`);
|
|
30
|
+
if (test.error?.stack) {
|
|
31
|
+
let frames = test.error.stack
|
|
32
|
+
.split('\n')
|
|
33
|
+
.slice(1, 4)
|
|
34
|
+
.map((l) => normalizeLine(l).trim())
|
|
35
|
+
.join('\n ');
|
|
36
|
+
console.log(` stack: |\n ${frames}`);
|
|
37
|
+
}
|
|
38
|
+
console.log(' ...');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
onSummary(counts, durationMs) {
|
|
43
|
+
let { passed, failed, skipped, todo } = counts;
|
|
44
|
+
console.log(`1..${this.#total}`);
|
|
45
|
+
console.log(`# tests ${passed + failed + skipped + todo}`);
|
|
46
|
+
console.log(`# pass ${passed}`);
|
|
47
|
+
console.log(`# fail ${failed}`);
|
|
48
|
+
if (skipped > 0)
|
|
49
|
+
console.log(`# skipped ${skipped}`);
|
|
50
|
+
if (todo > 0)
|
|
51
|
+
console.log(`# todo ${todo}`);
|
|
52
|
+
console.log(`# duration_ms ${durationMs.toFixed(5)}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type PlaywrightUseOpts } from './playwright.ts';
|
|
2
|
+
import type { Reporter } from './reporters/index.ts';
|
|
3
|
+
import type { Counts } from './utils.ts';
|
|
4
|
+
export declare function runServerTests(files: string[], reporter: Reporter, concurrency: number, type: 'server' | 'e2e', options?: {
|
|
5
|
+
open?: boolean;
|
|
6
|
+
playwrightUseOpts?: PlaywrightUseOpts;
|
|
7
|
+
projectName?: string;
|
|
8
|
+
}): Promise<Counts>;
|
|
9
|
+
//# sourceMappingURL=runner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/lib/runner.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AACxD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AACpD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AAMxC,wBAAsB,cAAc,CAClC,KAAK,EAAE,MAAM,EAAE,EACf,QAAQ,EAAE,QAAQ,EAClB,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,QAAQ,GAAG,KAAK,EACtB,OAAO,GAAE;IACP,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;IACrC,WAAW,CAAC,EAAE,MAAM,CAAA;CAChB,GACL,OAAO,CAAC,MAAM,CAAC,CAoCjB"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
import { Worker } from 'node:worker_threads';
|
|
4
|
+
import {} from "./playwright.js";
|
|
5
|
+
const ext = path.extname(import.meta.url);
|
|
6
|
+
const workerUrl = new URL(`./worker${ext}`, import.meta.url);
|
|
7
|
+
const workerE2EUrl = new URL(`./worker-e2e${ext}`, import.meta.url);
|
|
8
|
+
export async function runServerTests(files, reporter, concurrency, type, options = {}) {
|
|
9
|
+
let counts = { passed: 0, failed: 0, skipped: 0, todo: 0 };
|
|
10
|
+
let envLabel = options.projectName ? `${type}:${options.projectName}` : type;
|
|
11
|
+
function accumulate(results, file) {
|
|
12
|
+
reporter.onResult({ ...results, tests: results.tests.map((t) => ({ ...t, filePath: file })) }, envLabel);
|
|
13
|
+
counts.passed += results.passed;
|
|
14
|
+
counts.failed += results.failed;
|
|
15
|
+
counts.skipped += results.skipped;
|
|
16
|
+
counts.todo += results.todo;
|
|
17
|
+
}
|
|
18
|
+
if (type === 'e2e') {
|
|
19
|
+
await runInConcurrentWorkers(files, concurrency, (file) => runFileInWorker(file, type, (results) => accumulate(results, file), {
|
|
20
|
+
...options,
|
|
21
|
+
playwrightUseOpts: options.playwrightUseOpts,
|
|
22
|
+
}), () => counts.failed++);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
await runInConcurrentWorkers(files, concurrency, (file) => runFileInWorker(file, type, (results) => accumulate(results, file)), () => counts.failed++);
|
|
26
|
+
}
|
|
27
|
+
return { ...counts };
|
|
28
|
+
}
|
|
29
|
+
async function runInConcurrentWorkers(files, concurrency, runFile, onError) {
|
|
30
|
+
let index = 0;
|
|
31
|
+
let active = 0;
|
|
32
|
+
await new Promise((resolve) => {
|
|
33
|
+
function dispatch() {
|
|
34
|
+
while (active < concurrency && index < files.length) {
|
|
35
|
+
let file = files[index];
|
|
36
|
+
index++;
|
|
37
|
+
active++;
|
|
38
|
+
runFile(file).then(() => {
|
|
39
|
+
active--;
|
|
40
|
+
if (index < files.length) {
|
|
41
|
+
dispatch();
|
|
42
|
+
}
|
|
43
|
+
else if (active === 0) {
|
|
44
|
+
resolve();
|
|
45
|
+
}
|
|
46
|
+
}, (err) => {
|
|
47
|
+
console.error(`Error running ${file}:`, err.message);
|
|
48
|
+
console.error(err);
|
|
49
|
+
onError();
|
|
50
|
+
active--;
|
|
51
|
+
if (active === 0 && index >= files.length)
|
|
52
|
+
resolve();
|
|
53
|
+
else
|
|
54
|
+
dispatch();
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
if (index >= files.length && active === 0)
|
|
58
|
+
resolve();
|
|
59
|
+
}
|
|
60
|
+
dispatch();
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function runFileInWorker(file, type, onResults, options = {}) {
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
let worker = type === 'e2e'
|
|
66
|
+
? new Worker(workerE2EUrl, {
|
|
67
|
+
workerData: {
|
|
68
|
+
file: pathToFileURL(file).href,
|
|
69
|
+
type,
|
|
70
|
+
open: options.open,
|
|
71
|
+
playwrightUseOpts: options.playwrightUseOpts,
|
|
72
|
+
},
|
|
73
|
+
})
|
|
74
|
+
: new Worker(workerUrl, {
|
|
75
|
+
workerData: {
|
|
76
|
+
file: pathToFileURL(file).href,
|
|
77
|
+
type,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
worker.once('message', (msg) => onResults(msg));
|
|
81
|
+
worker.once('error', reject);
|
|
82
|
+
worker.once('exit', (code) => {
|
|
83
|
+
if (code !== 0)
|
|
84
|
+
reject(new Error(`Worker exited with code ${code}`));
|
|
85
|
+
else
|
|
86
|
+
resolve();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type Counts = {
|
|
2
|
+
passed: number;
|
|
3
|
+
failed: number;
|
|
4
|
+
skipped: number;
|
|
5
|
+
todo: number;
|
|
6
|
+
};
|
|
7
|
+
export declare const colors: {
|
|
8
|
+
reset: string;
|
|
9
|
+
dim: (s: string) => string;
|
|
10
|
+
green: (s: string) => string;
|
|
11
|
+
red: (s: string) => string;
|
|
12
|
+
cyan: (s: string) => string;
|
|
13
|
+
yellow: (s: string) => string;
|
|
14
|
+
};
|
|
15
|
+
export declare function normalizeLine(line: string): string;
|
|
16
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/lib/utils.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,MAAM,GAAG;IACnB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;CACb,CAAA;AAID,eAAO,MAAM,MAAM;;;;;;;CAOlB,CAAA;AAcD,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CASlD"}
|