@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.
Files changed (86) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +325 -2
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +171 -0
  6. package/dist/index.d.ts +5 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +2 -0
  9. package/dist/lib/config.d.ts +60 -0
  10. package/dist/lib/config.d.ts.map +1 -0
  11. package/dist/lib/config.js +152 -0
  12. package/dist/lib/context.d.ts +69 -0
  13. package/dist/lib/context.d.ts.map +1 -0
  14. package/dist/lib/context.js +49 -0
  15. package/dist/lib/e2e-server.d.ts +11 -0
  16. package/dist/lib/e2e-server.d.ts.map +1 -0
  17. package/dist/lib/e2e-server.js +15 -0
  18. package/dist/lib/executor.d.ts +27 -0
  19. package/dist/lib/executor.d.ts.map +1 -0
  20. package/dist/lib/executor.js +123 -0
  21. package/dist/lib/framework.d.ts +107 -0
  22. package/dist/lib/framework.d.ts.map +1 -0
  23. package/dist/lib/framework.js +198 -0
  24. package/dist/lib/framework.test.d.ts +2 -0
  25. package/dist/lib/framework.test.d.ts.map +1 -0
  26. package/dist/lib/framework.test.e2e.d.ts +2 -0
  27. package/dist/lib/framework.test.e2e.d.ts.map +1 -0
  28. package/dist/lib/framework.test.e2e.js +29 -0
  29. package/dist/lib/framework.test.js +283 -0
  30. package/dist/lib/mock.d.ts +52 -0
  31. package/dist/lib/mock.d.ts.map +1 -0
  32. package/dist/lib/mock.js +61 -0
  33. package/dist/lib/playwright.d.ts +15 -0
  34. package/dist/lib/playwright.d.ts.map +1 -0
  35. package/dist/lib/playwright.js +84 -0
  36. package/dist/lib/reporters/dot.d.ts +10 -0
  37. package/dist/lib/reporters/dot.d.ts.map +1 -0
  38. package/dist/lib/reporters/dot.js +55 -0
  39. package/dist/lib/reporters/files.d.ts +10 -0
  40. package/dist/lib/reporters/files.d.ts.map +1 -0
  41. package/dist/lib/reporters/files.js +70 -0
  42. package/dist/lib/reporters/index.d.ts +14 -0
  43. package/dist/lib/reporters/index.d.ts.map +1 -0
  44. package/dist/lib/reporters/index.js +18 -0
  45. package/dist/lib/reporters/spec.d.ts +10 -0
  46. package/dist/lib/reporters/spec.d.ts.map +1 -0
  47. package/dist/lib/reporters/spec.js +152 -0
  48. package/dist/lib/reporters/tap.d.ts +10 -0
  49. package/dist/lib/reporters/tap.d.ts.map +1 -0
  50. package/dist/lib/reporters/tap.js +54 -0
  51. package/dist/lib/runner.d.ts +9 -0
  52. package/dist/lib/runner.d.ts.map +1 -0
  53. package/dist/lib/runner.js +89 -0
  54. package/dist/lib/utils.d.ts +16 -0
  55. package/dist/lib/utils.d.ts.map +1 -0
  56. package/dist/lib/utils.js +27 -0
  57. package/dist/lib/watcher.d.ts +5 -0
  58. package/dist/lib/watcher.d.ts.map +1 -0
  59. package/dist/lib/watcher.js +39 -0
  60. package/dist/lib/worker-e2e.d.ts +2 -0
  61. package/dist/lib/worker-e2e.d.ts.map +1 -0
  62. package/dist/lib/worker-e2e.js +48 -0
  63. package/dist/lib/worker.d.ts +2 -0
  64. package/dist/lib/worker.d.ts.map +1 -0
  65. package/dist/lib/worker.js +29 -0
  66. package/package.json +58 -5
  67. package/src/cli.ts +210 -0
  68. package/src/index.ts +15 -0
  69. package/src/lib/config.ts +231 -0
  70. package/src/lib/context.ts +126 -0
  71. package/src/lib/e2e-server.ts +28 -0
  72. package/src/lib/executor.ts +162 -0
  73. package/src/lib/framework.ts +251 -0
  74. package/src/lib/mock.ts +89 -0
  75. package/src/lib/playwright.ts +102 -0
  76. package/src/lib/reporters/dot.ts +57 -0
  77. package/src/lib/reporters/files.ts +76 -0
  78. package/src/lib/reporters/index.ts +28 -0
  79. package/src/lib/reporters/spec.ts +173 -0
  80. package/src/lib/reporters/tap.ts +58 -0
  81. package/src/lib/runner.ts +137 -0
  82. package/src/lib/utils.ts +40 -0
  83. package/src/lib/watcher.ts +46 -0
  84. package/src/lib/worker-e2e.ts +52 -0
  85. package/src/lib/worker.ts +30 -0
  86. 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"}