@remix-run/test 0.0.0 → 0.2.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 (150) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +430 -2
  3. package/dist/app/client/entry.d.ts +2 -0
  4. package/dist/app/client/entry.d.ts.map +1 -0
  5. package/dist/app/client/entry.js +324 -0
  6. package/dist/app/client/iframe.d.ts +2 -0
  7. package/dist/app/client/iframe.d.ts.map +1 -0
  8. package/dist/app/client/iframe.js +22 -0
  9. package/dist/app/server.d.ts +6 -0
  10. package/dist/app/server.d.ts.map +1 -0
  11. package/dist/app/server.js +303 -0
  12. package/dist/cli-entry.d.ts +3 -0
  13. package/dist/cli-entry.d.ts.map +1 -0
  14. package/dist/cli-entry.js +14 -0
  15. package/dist/cli.d.ts +8 -0
  16. package/dist/cli.d.ts.map +1 -0
  17. package/dist/cli.js +305 -0
  18. package/dist/index.d.ts +6 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +2 -0
  21. package/dist/lib/colors.d.ts +2 -0
  22. package/dist/lib/colors.d.ts.map +1 -0
  23. package/dist/lib/colors.js +2 -0
  24. package/dist/lib/config.d.ts +91 -0
  25. package/dist/lib/config.d.ts.map +1 -0
  26. package/dist/lib/config.js +255 -0
  27. package/dist/lib/context.d.ts +93 -0
  28. package/dist/lib/context.d.ts.map +1 -0
  29. package/dist/lib/context.js +65 -0
  30. package/dist/lib/coverage-loader.d.ts +16 -0
  31. package/dist/lib/coverage-loader.d.ts.map +1 -0
  32. package/dist/lib/coverage-loader.js +20 -0
  33. package/dist/lib/coverage.d.ts +28 -0
  34. package/dist/lib/coverage.d.ts.map +1 -0
  35. package/dist/lib/coverage.js +212 -0
  36. package/dist/lib/executor.d.ts +4 -0
  37. package/dist/lib/executor.d.ts.map +1 -0
  38. package/dist/lib/executor.js +128 -0
  39. package/dist/lib/fake-timers.d.ts +6 -0
  40. package/dist/lib/fake-timers.d.ts.map +1 -0
  41. package/dist/lib/fake-timers.js +45 -0
  42. package/dist/lib/framework.d.ts +107 -0
  43. package/dist/lib/framework.d.ts.map +1 -0
  44. package/dist/lib/framework.js +198 -0
  45. package/dist/lib/import-module.d.ts +2 -0
  46. package/dist/lib/import-module.d.ts.map +1 -0
  47. package/dist/lib/import-module.js +29 -0
  48. package/dist/lib/mock.d.ts +52 -0
  49. package/dist/lib/mock.d.ts.map +1 -0
  50. package/dist/lib/mock.js +61 -0
  51. package/dist/lib/normalize.d.ts +2 -0
  52. package/dist/lib/normalize.d.ts.map +1 -0
  53. package/dist/lib/normalize.js +18 -0
  54. package/dist/lib/playwright.d.ts +15 -0
  55. package/dist/lib/playwright.d.ts.map +1 -0
  56. package/dist/lib/playwright.js +81 -0
  57. package/dist/lib/reporters/dot.d.ts +9 -0
  58. package/dist/lib/reporters/dot.d.ts.map +1 -0
  59. package/dist/lib/reporters/dot.js +56 -0
  60. package/dist/lib/reporters/files.d.ts +9 -0
  61. package/dist/lib/reporters/files.d.ts.map +1 -0
  62. package/dist/lib/reporters/files.js +71 -0
  63. package/dist/lib/reporters/index.d.ts +13 -0
  64. package/dist/lib/reporters/index.d.ts.map +1 -0
  65. package/dist/lib/reporters/index.js +18 -0
  66. package/dist/lib/reporters/results.d.ts +30 -0
  67. package/dist/lib/reporters/results.d.ts.map +1 -0
  68. package/dist/lib/reporters/results.js +1 -0
  69. package/dist/lib/reporters/spec.d.ts +9 -0
  70. package/dist/lib/reporters/spec.d.ts.map +1 -0
  71. package/dist/lib/reporters/spec.js +153 -0
  72. package/dist/lib/reporters/tap.d.ts +9 -0
  73. package/dist/lib/reporters/tap.d.ts.map +1 -0
  74. package/dist/lib/reporters/tap.js +54 -0
  75. package/dist/lib/runner-browser.d.ts +21 -0
  76. package/dist/lib/runner-browser.d.ts.map +1 -0
  77. package/dist/lib/runner-browser.js +117 -0
  78. package/dist/lib/runner.d.ts +14 -0
  79. package/dist/lib/runner.d.ts.map +1 -0
  80. package/dist/lib/runner.js +118 -0
  81. package/dist/lib/runtime.d.ts +2 -0
  82. package/dist/lib/runtime.d.ts.map +1 -0
  83. package/dist/lib/runtime.js +2 -0
  84. package/dist/lib/ts-transform.d.ts +4 -0
  85. package/dist/lib/ts-transform.d.ts.map +1 -0
  86. package/dist/lib/ts-transform.js +29 -0
  87. package/dist/lib/watcher.d.ts +5 -0
  88. package/dist/lib/watcher.d.ts.map +1 -0
  89. package/dist/lib/watcher.js +39 -0
  90. package/dist/lib/worker-e2e.d.ts +2 -0
  91. package/dist/lib/worker-e2e.d.ts.map +1 -0
  92. package/dist/lib/worker-e2e.js +49 -0
  93. package/dist/lib/worker.d.ts +2 -0
  94. package/dist/lib/worker.d.ts.map +1 -0
  95. package/dist/lib/worker.js +57 -0
  96. package/dist/test/coverage/fixture.d.ts +5 -0
  97. package/dist/test/coverage/fixture.d.ts.map +1 -0
  98. package/dist/test/coverage/fixture.js +32 -0
  99. package/dist/test/coverage/test-browser.d.ts +2 -0
  100. package/dist/test/coverage/test-browser.d.ts.map +1 -0
  101. package/dist/test/coverage/test-browser.js +24 -0
  102. package/dist/test/coverage/test-e2e.d.ts +2 -0
  103. package/dist/test/coverage/test-e2e.d.ts.map +1 -0
  104. package/dist/test/coverage/test-e2e.js +60 -0
  105. package/dist/test/coverage/test-unit.d.ts +2 -0
  106. package/dist/test/coverage/test-unit.d.ts.map +1 -0
  107. package/dist/test/coverage/test-unit.js +27 -0
  108. package/dist/test/framework.test.browser.d.ts +2 -0
  109. package/dist/test/framework.test.browser.d.ts.map +1 -0
  110. package/dist/test/framework.test.browser.js +107 -0
  111. package/dist/test/framework.test.e2e.d.ts +2 -0
  112. package/dist/test/framework.test.e2e.d.ts.map +1 -0
  113. package/dist/test/framework.test.e2e.js +34 -0
  114. package/package.json +79 -5
  115. package/src/app/client/entry.ts +353 -0
  116. package/src/app/client/iframe.ts +18 -0
  117. package/src/app/server.ts +336 -0
  118. package/src/cli-entry.ts +15 -0
  119. package/src/cli.ts +384 -0
  120. package/src/index.ts +16 -0
  121. package/src/lib/colors.ts +3 -0
  122. package/src/lib/config.ts +377 -0
  123. package/src/lib/context.ts +168 -0
  124. package/src/lib/coverage-loader.ts +31 -0
  125. package/src/lib/coverage.ts +320 -0
  126. package/src/lib/executor.ts +145 -0
  127. package/src/lib/fake-timers.ts +64 -0
  128. package/src/lib/framework.ts +251 -0
  129. package/src/lib/import-module.ts +29 -0
  130. package/src/lib/mock.ts +89 -0
  131. package/src/lib/normalize.ts +22 -0
  132. package/src/lib/playwright.ts +100 -0
  133. package/src/lib/reporters/dot.ts +58 -0
  134. package/src/lib/reporters/files.ts +77 -0
  135. package/src/lib/reporters/index.ts +27 -0
  136. package/src/lib/reporters/results.ts +29 -0
  137. package/src/lib/reporters/spec.ts +174 -0
  138. package/src/lib/reporters/tap.ts +58 -0
  139. package/src/lib/runner-browser.ts +165 -0
  140. package/src/lib/runner.ts +189 -0
  141. package/src/lib/runtime.ts +2 -0
  142. package/src/lib/ts-transform.ts +36 -0
  143. package/src/lib/watcher.ts +46 -0
  144. package/src/lib/worker-e2e.ts +54 -0
  145. package/src/lib/worker.ts +50 -0
  146. package/src/test/coverage/fixture.ts +34 -0
  147. package/src/test/coverage/test-browser.ts +29 -0
  148. package/src/test/coverage/test-e2e.ts +70 -0
  149. package/src/test/coverage/test-unit.ts +32 -0
  150. package/tsconfig.json +16 -0
@@ -0,0 +1,18 @@
1
+ import { DotReporter } from "./dot.js";
2
+ import { FilesReporter } from "./files.js";
3
+ import { SpecReporter } from "./spec.js";
4
+ import { TapReporter } from "./tap.js";
5
+ export { DotReporter, FilesReporter, SpecReporter, TapReporter };
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,30 @@
1
+ import type { V8CoverageEntry } from '../coverage';
2
+ export interface TestResult {
3
+ name: string;
4
+ suiteName: string;
5
+ filePath?: string;
6
+ status: 'passed' | 'failed' | 'skipped' | 'todo';
7
+ error?: {
8
+ message: string;
9
+ stack?: string;
10
+ };
11
+ duration: number;
12
+ }
13
+ export interface TestResults {
14
+ passed: number;
15
+ failed: number;
16
+ skipped: number;
17
+ todo: number;
18
+ tests: TestResult[];
19
+ e2eBrowserCoverageEntries?: Array<{
20
+ entries: V8CoverageEntry[];
21
+ baseUrl: string;
22
+ }>;
23
+ }
24
+ export type Counts = {
25
+ passed: number;
26
+ failed: number;
27
+ skipped: number;
28
+ todo: number;
29
+ };
30
+ //# sourceMappingURL=results.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"results.d.ts","sourceRoot":"","sources":["../../../src/lib/reporters/results.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAElD,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,CAAA;IAChD,KAAK,CAAC,EAAE;QACN,OAAO,EAAE,MAAM,CAAA;QACf,KAAK,CAAC,EAAE,MAAM,CAAA;KACf,CAAA;IACD,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,UAAU,EAAE,CAAA;IACnB,yBAAyB,CAAC,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,eAAe,EAAE,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACnF;AAED,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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ import type { Reporter } from './index.ts';
2
+ import type { Counts, TestResults } from './results.ts';
3
+ export declare class SpecReporter implements Reporter {
4
+ #private;
5
+ onSectionStart(label: string): void;
6
+ onResult(results: TestResults, env?: string): void;
7
+ onSummary(counts: Counts, durationMs: number): void;
8
+ }
9
+ //# sourceMappingURL=spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spec.d.ts","sourceRoot":"","sources":["../../../src/lib/reporters/spec.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAC1C,OAAO,KAAK,EAAE,MAAM,EAAc,WAAW,EAAE,MAAM,cAAc,CAAA;AAEnE,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,153 @@
1
+ import { colors } from "../colors.js";
2
+ import { normalizeLine } from "../normalize.js";
3
+ export class SpecReporter {
4
+ #failures = [];
5
+ onSectionStart(label) {
6
+ console.log(label);
7
+ }
8
+ onResult(results, env) {
9
+ let suiteMap = new Map();
10
+ for (let test of results.tests) {
11
+ let suite = test.suiteName || 'Global';
12
+ if (!suiteMap.has(suite))
13
+ suiteMap.set(suite, []);
14
+ suiteMap.get(suite).push(test);
15
+ }
16
+ let envLabel = env ? ` ${colors.dim(`[${env}]`)}` : '';
17
+ let lastParts = [];
18
+ // Pre-compute aggregate test results for each path prefix so non-leaf
19
+ // suite headings can be colored the same way as leaf headings.
20
+ let prefixTests = new Map();
21
+ for (let [suiteName, tests] of suiteMap) {
22
+ let parts = suiteName.split(' > ');
23
+ for (let i = 0; i < parts.length; i++) {
24
+ let prefix = parts.slice(0, i + 1).join(' > ');
25
+ if (!prefixTests.has(prefix))
26
+ prefixTests.set(prefix, []);
27
+ prefixTests.get(prefix).push(...tests);
28
+ }
29
+ }
30
+ for (let [suiteName, suiteTests] of suiteMap) {
31
+ let parts = suiteName.split(' > ');
32
+ // Find where this path diverges from the last rendered path
33
+ let commonLen = 0;
34
+ while (commonLen < lastParts.length &&
35
+ commonLen < parts.length &&
36
+ lastParts[commonLen] === parts[commonLen]) {
37
+ commonLen++;
38
+ }
39
+ // Print each new path component
40
+ for (let i = commonLen; i < parts.length; i++) {
41
+ let indent = ' '.repeat(i);
42
+ let isLeaf = i === parts.length - 1;
43
+ if (isLeaf) {
44
+ let totalDuration = suiteTests.reduce((sum, t) => sum + t.duration, 0);
45
+ let suiteHasFailed = suiteTests.some((t) => t.status === 'failed');
46
+ let suiteAllSkipped = suiteTests.every((t) => t.status === 'skipped');
47
+ let suiteAllTodo = suiteTests.every((t) => t.status === 'todo');
48
+ let label = suiteHasFailed
49
+ ? colors.red(parts[i])
50
+ : suiteAllSkipped
51
+ ? colors.dim(parts[i])
52
+ : suiteAllTodo
53
+ ? colors.yellow(parts[i])
54
+ : colors.green(parts[i]);
55
+ let suiteComment = suiteAllSkipped
56
+ ? colors.dim(' # skipped')
57
+ : suiteAllTodo
58
+ ? colors.yellow(' # todo')
59
+ : '';
60
+ let duration = suiteComment ? '' : ` (${totalDuration.toFixed(2)}ms)`;
61
+ let label2 = envLabel;
62
+ console.log(`${indent}${colors.dim('▶')} ${label}${duration}${suiteComment}${label2}`);
63
+ }
64
+ else {
65
+ let prefix = parts.slice(0, i + 1).join(' > ');
66
+ let prefixTestList = prefixTests.get(prefix) ?? [];
67
+ let prefixHasFailed = prefixTestList.some((t) => t.status === 'failed');
68
+ let prefixAllSkipped = prefixTestList.length > 0 && prefixTestList.every((t) => t.status === 'skipped');
69
+ let prefixAllTodo = prefixTestList.length > 0 && prefixTestList.every((t) => t.status === 'todo');
70
+ let nameColor = prefixHasFailed
71
+ ? colors.red
72
+ : prefixAllSkipped
73
+ ? colors.dim
74
+ : prefixAllTodo
75
+ ? colors.yellow
76
+ : colors.green;
77
+ let prefixDuration = prefixTestList.reduce((sum, t) => sum + t.duration, 0);
78
+ let prefixComment = prefixAllSkipped
79
+ ? colors.dim(' # skipped')
80
+ : prefixAllTodo
81
+ ? colors.yellow(' # todo')
82
+ : '';
83
+ let prefixDurationStr = prefixComment ? '' : ` (${prefixDuration.toFixed(2)}ms)`;
84
+ console.log(`${indent}${colors.dim('▶')} ${nameColor(parts[i])}${prefixDurationStr}${prefixComment}${envLabel}`);
85
+ }
86
+ }
87
+ lastParts = parts;
88
+ // Print tests indented to the suite's depth
89
+ let testIndent = ' '.repeat(parts.length);
90
+ for (let test of suiteTests) {
91
+ if (test.status === 'passed') {
92
+ console.log(`${testIndent}${colors.green('✓')} ${test.name} (${test.duration.toFixed(2)}ms)`);
93
+ }
94
+ else if (test.status === 'failed') {
95
+ console.log(`${testIndent}${colors.red('✗')} ${test.name} (${test.duration.toFixed(2)}ms)`);
96
+ if (test.error) {
97
+ console.log(`${testIndent} ${colors.red(`Error: ${test.error.message}`)}`);
98
+ if (test.error.stack) {
99
+ let stack = test.error.stack
100
+ .split('\n')
101
+ .map((line) => normalizeLine(line))
102
+ .join('\n');
103
+ console.log(`${testIndent} ${stack.split('\n').slice(1, 5).join(`\n${testIndent} `)}`);
104
+ }
105
+ }
106
+ this.#failures.push({ suiteName: test.suiteName, name: test.name, error: test.error });
107
+ }
108
+ else if (test.status === 'skipped') {
109
+ if (test.name)
110
+ console.log(`${testIndent}${colors.dim('↓')} ${colors.dim(`${test.name} # skipped`)}`);
111
+ }
112
+ else if (test.status === 'todo') {
113
+ if (test.name)
114
+ console.log(`${testIndent}${colors.yellow('…')} ${colors.yellow(`${test.name} # todo`)}`);
115
+ }
116
+ }
117
+ }
118
+ }
119
+ onSummary(counts, durationMs) {
120
+ if (this.#failures.length > 0) {
121
+ console.log();
122
+ console.log(colors.red('Failed tests:'));
123
+ for (let i = 0; i < this.#failures.length; i++) {
124
+ let { suiteName, name, error } = this.#failures[i];
125
+ let fullName = name ? `${suiteName} > ${name}` : suiteName;
126
+ console.log(`\n ${colors.red(`${i + 1})`)} ${fullName}`);
127
+ if (error) {
128
+ console.log(` ${colors.red(error.message)}`);
129
+ if (error.stack) {
130
+ let frames = error.stack
131
+ .split('\n')
132
+ .slice(1, 4)
133
+ .map((l) => ` ${normalizeLine(l).trim()}`)
134
+ .join('\n');
135
+ console.log(frames);
136
+ }
137
+ }
138
+ }
139
+ }
140
+ let { passed, failed, skipped, todo } = counts;
141
+ let info = colors.cyan('ℹ');
142
+ console.log();
143
+ console.log(`${info} tests ${passed + failed + skipped + todo}`);
144
+ console.log(`${info} pass ${passed}`);
145
+ console.log(`${info} fail ${failed}`);
146
+ if (skipped > 0)
147
+ console.log(`${info} skipped ${skipped}`);
148
+ if (todo > 0)
149
+ console.log(`${info} todo ${todo}`);
150
+ console.log(`${info} duration_ms ${durationMs.toFixed(5)}`);
151
+ console.log();
152
+ }
153
+ }
@@ -0,0 +1,9 @@
1
+ import type { Reporter } from './index.ts';
2
+ import type { Counts, TestResults } from './results.ts';
3
+ export declare class TapReporter implements Reporter {
4
+ #private;
5
+ onSectionStart(_label: string): void;
6
+ onResult(results: TestResults, env?: string): void;
7
+ onSummary(counts: Counts, durationMs: number): void;
8
+ }
9
+ //# sourceMappingURL=tap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tap.d.ts","sourceRoot":"","sources":["../../../src/lib/reporters/tap.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAC1C,OAAO,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAEvD,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 "../normalize.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,21 @@
1
+ import { type CoverageMap } from './coverage.ts';
2
+ import { type PlaywrightUseOpts } from './playwright.ts';
3
+ import type { Reporter } from './reporters/index.ts';
4
+ import type { TestResults } from './reporters/results.ts';
5
+ export interface TestRunOptions {
6
+ baseUrl: string;
7
+ console?: boolean;
8
+ coverage?: boolean;
9
+ open?: boolean;
10
+ playwrightUseOpts?: PlaywrightUseOpts;
11
+ projectName?: string;
12
+ reporter: Reporter;
13
+ testFiles?: string[];
14
+ }
15
+ export declare function runBrowserTests(options: TestRunOptions): Promise<{
16
+ results: TestResults;
17
+ coverageMap: CoverageMap | null;
18
+ close: () => Promise<void>;
19
+ disconnected: Promise<void>;
20
+ }>;
21
+ //# sourceMappingURL=runner-browser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runner-browser.d.ts","sourceRoot":"","sources":["../../src/lib/runner-browser.ts"],"names":[],"mappings":"AAIA,OAAO,EAEL,KAAK,WAAW,EAEjB,MAAM,eAAe,CAAA;AACtB,OAAO,EAIL,KAAK,iBAAiB,EACvB,MAAM,iBAAiB,CAAA;AACxB,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AACpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAA;AAWzD,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;IACrC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,EAAE,QAAQ,CAAA;IAGlB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAA;CACrB;AAED,wBAAsB,eAAe,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC;IACtE,OAAO,EAAE,WAAW,CAAA;IACpB,WAAW,EAAE,WAAW,GAAG,IAAI,CAAA;IAC/B,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC1B,YAAY,EAAE,OAAO,CAAC,IAAI,CAAC,CAAA;CAC5B,CAAC,CAuHD"}
@@ -0,0 +1,117 @@
1
+ import * as path from 'node:path';
2
+ import { colors } from "./colors.js";
3
+ import { getBrowserTestRootDir } from "./config.js";
4
+ import { collectCoverageMapFromPlaywright, } from "./coverage.js";
5
+ import { getBrowserLauncher, getPlaywrightLaunchOptions, getPlaywrightPageOptions, } from "./playwright.js";
6
+ // The harness reports each test result with `filePath` set to the
7
+ // `/scripts/<rel>` URL the iframe loaded. Reporters expect a real filesystem
8
+ // path so they can compute `path.relative(cwd, ...)` cleanly; otherwise they
9
+ // produce noisy `../../../scripts/...` strings.
10
+ function urlPathToFilePath(urlPath, rootDir) {
11
+ if (!urlPath.startsWith('/scripts/'))
12
+ return urlPath;
13
+ return path.resolve(rootDir, urlPath.slice('/scripts/'.length));
14
+ }
15
+ export async function runBrowserTests(options) {
16
+ let envLabel = options.projectName ? `browser:${options.projectName}` : 'browser';
17
+ let browser;
18
+ let page;
19
+ let close = async () => {
20
+ await page?.close();
21
+ await browser?.close();
22
+ browser = undefined;
23
+ page = undefined;
24
+ };
25
+ let results;
26
+ let coverageMap = null;
27
+ try {
28
+ browser = await getBrowserLauncher(options.playwrightUseOpts).launch(getPlaywrightLaunchOptions(options.playwrightUseOpts));
29
+ page = await browser.newPage(getPlaywrightPageOptions(options.playwrightUseOpts));
30
+ if (options.console) {
31
+ page.on('console', (msg) => console.log(`${colors.dim('[browser console]')} ${msg.text()}`));
32
+ }
33
+ // Playwright's JS coverage is Chromium-only. Start before navigation so
34
+ // the harness scripts and test modules are instrumented from first parse.
35
+ let coverageEnabled = options.coverage && browser.browserType().name() === 'chromium';
36
+ if (coverageEnabled) {
37
+ await page.coverage.startJSCoverage({ resetOnNavigation: false });
38
+ }
39
+ let totalPassed = 0;
40
+ let totalFailed = 0;
41
+ let totalSkipped = 0;
42
+ let totalTodo = 0;
43
+ let rootDir = getBrowserTestRootDir();
44
+ await page.route('**/file-results', async (route) => {
45
+ let results = route.request().postDataJSON();
46
+ for (let test of results.tests) {
47
+ if (test.filePath)
48
+ test.filePath = urlPathToFilePath(test.filePath, rootDir);
49
+ }
50
+ options.reporter.onResult(results, envLabel);
51
+ totalPassed += results.passed;
52
+ totalFailed += results.failed;
53
+ totalSkipped += results.skipped;
54
+ totalTodo += results.todo;
55
+ await route.fulfill({ status: 200 });
56
+ });
57
+ // Fail the tests if any /scripts/ request fails (harness scripts, test
58
+ // modules, or their transitive imports — all served via the same prefix).
59
+ let errorPromise = new Promise((_, reject) => {
60
+ let isScriptRequest = (request) => new URL(request.url()).pathname.startsWith('/scripts/');
61
+ page.on('response', (response) => {
62
+ if (!response.ok() && isScriptRequest(response.request())) {
63
+ reject(new Error(`Failed to load script: ${response.request().url()}`));
64
+ }
65
+ });
66
+ page.on('requestfailed', (request) => {
67
+ if (isScriptRequest(request)) {
68
+ reject(new Error(`Failed to load script: ${request.url()}`));
69
+ }
70
+ });
71
+ });
72
+ // Prevent unhandled rejection if we fail before setting up the listener
73
+ errorPromise.catch(() => { });
74
+ await page.goto(options.baseUrl);
75
+ await Promise.race([page.waitForFunction('window.__testsDone'), errorPromise]);
76
+ if (coverageEnabled) {
77
+ let entries = (await page.coverage.stopJSCoverage());
78
+ if (entries.length > 0) {
79
+ coverageMap = await collectCoverageMapFromPlaywright(entries, getBrowserTestRootDir(), new Set(options.testFiles ?? []), async (urlPath) => urlPath.startsWith('/scripts/') ? urlPath.slice('/scripts/'.length) : null);
80
+ }
81
+ }
82
+ results = {
83
+ passed: totalPassed,
84
+ failed: totalFailed,
85
+ skipped: totalSkipped,
86
+ todo: totalTodo,
87
+ tests: [],
88
+ };
89
+ }
90
+ catch (error) {
91
+ console.error('Browser tests failed to run:', error);
92
+ results = {
93
+ passed: 0,
94
+ failed: 1,
95
+ skipped: 0,
96
+ todo: 0,
97
+ tests: [],
98
+ };
99
+ }
100
+ if (options.open) {
101
+ return {
102
+ results,
103
+ coverageMap,
104
+ close,
105
+ disconnected: new Promise((r) => browser.on('disconnected', () => r())),
106
+ };
107
+ }
108
+ else {
109
+ await close();
110
+ return {
111
+ results,
112
+ coverageMap,
113
+ close,
114
+ disconnected: Promise.resolve(),
115
+ };
116
+ }
117
+ }
@@ -0,0 +1,14 @@
1
+ import { type CoverageConfig, type CoverageMap } from './coverage.ts';
2
+ import { type PlaywrightUseOpts } from './playwright.ts';
3
+ import type { Reporter } from './reporters/index.ts';
4
+ import type { Counts } from './reporters/results.ts';
5
+ export declare function runServerTests(files: string[], reporter: Reporter, concurrency: number, type: 'server' | 'e2e', options?: {
6
+ cwd?: string;
7
+ open?: boolean;
8
+ playwrightUseOpts?: PlaywrightUseOpts;
9
+ projectName?: string;
10
+ coverage?: CoverageConfig;
11
+ }): Promise<Counts & {
12
+ coverageMap: CoverageMap | null;
13
+ }>;
14
+ //# sourceMappingURL=runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/lib/runner.ts"],"names":[],"mappings":"AAKA,OAAO,EAGL,KAAK,cAAc,EACnB,KAAK,WAAW,EAEjB,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,KAAK,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AACxD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAA;AACpD,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,wBAAwB,CAAA;AAQjE,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,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;IACrC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,cAAc,CAAA;CACrB,GACL,OAAO,CAAC,MAAM,GAAG;IAAE,WAAW,EAAE,WAAW,GAAG,IAAI,CAAA;CAAE,CAAC,CAwEvD"}
@@ -0,0 +1,118 @@
1
+ import * as fsp from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { Worker } from 'node:worker_threads';
5
+ import { IS_RUNNING_FROM_SRC } from "./config.js";
6
+ import { collectCoverageMapFromPlaywright, collectServerCoverageMap, } from "./coverage.js";
7
+ import {} from "./playwright.js";
8
+ // Ensure we load the right file whether we're running in the monorepo (TS) or
9
+ // from a published package (JS)
10
+ const ext = IS_RUNNING_FROM_SRC ? '.ts' : '.js';
11
+ const workerUrl = new URL(`./worker${ext}`, import.meta.url);
12
+ const workerE2EUrl = new URL(`./worker-e2e${ext}`, import.meta.url);
13
+ export async function runServerTests(files, reporter, concurrency, type, options = {}) {
14
+ let counts = { passed: 0, failed: 0, skipped: 0, todo: 0 };
15
+ let coverageMap = null;
16
+ let cwd = options.cwd ?? process.cwd();
17
+ let envLabel = options.projectName ? `${type}:${options.projectName}` : type;
18
+ function accumulate(results, file) {
19
+ reporter.onResult({ ...results, tests: results.tests.map((t) => ({ ...t, filePath: file })) }, envLabel);
20
+ counts.passed += results.passed;
21
+ counts.failed += results.failed;
22
+ counts.skipped += results.skipped;
23
+ counts.todo += results.todo;
24
+ }
25
+ if (type === 'e2e') {
26
+ let allBrowserCoverageEntries = [];
27
+ await runInConcurrentWorkers(files, concurrency, (file) => runFileInWorker(file, type, (results) => {
28
+ accumulate(results, file);
29
+ if (results.e2eBrowserCoverageEntries) {
30
+ allBrowserCoverageEntries.push(...results.e2eBrowserCoverageEntries);
31
+ }
32
+ }, {
33
+ ...options,
34
+ playwrightUseOpts: options.playwrightUseOpts,
35
+ }), () => counts.failed++);
36
+ if (options.coverage && allBrowserCoverageEntries.length > 0) {
37
+ coverageMap = await collectCoverageMapFromPlaywright(allBrowserCoverageEntries.flatMap((e) => e.entries), cwd, new Set(files), async (urlPath) => (urlPath.startsWith('/') ? urlPath.slice(1) : urlPath));
38
+ }
39
+ }
40
+ else {
41
+ let coverageDataDir;
42
+ if (options.coverage) {
43
+ coverageDataDir = path.resolve(cwd, options.coverage.dir);
44
+ await fsp.mkdir(coverageDataDir, { recursive: true });
45
+ process.env.NODE_V8_COVERAGE = coverageDataDir;
46
+ }
47
+ await runInConcurrentWorkers(files, concurrency, (file) => runFileInWorker(file, type, (results) => accumulate(results, file), options), () => counts.failed++);
48
+ if (coverageDataDir) {
49
+ delete process.env.NODE_V8_COVERAGE;
50
+ let serverMap = await collectServerCoverageMap(coverageDataDir, cwd, new Set(files));
51
+ coverageMap = serverMap;
52
+ }
53
+ }
54
+ return { ...counts, coverageMap };
55
+ }
56
+ async function runInConcurrentWorkers(files, concurrency, runFile, onError) {
57
+ let index = 0;
58
+ let active = 0;
59
+ await new Promise((resolve) => {
60
+ function dispatch() {
61
+ while (active < concurrency && index < files.length) {
62
+ let file = files[index];
63
+ index++;
64
+ active++;
65
+ runFile(file).then(() => {
66
+ active--;
67
+ if (index < files.length) {
68
+ dispatch();
69
+ }
70
+ else if (active === 0) {
71
+ resolve();
72
+ }
73
+ }, (err) => {
74
+ console.error(`Error running ${file}:`, err.message);
75
+ console.error(err);
76
+ onError();
77
+ active--;
78
+ if (active === 0 && index >= files.length)
79
+ resolve();
80
+ else
81
+ dispatch();
82
+ });
83
+ }
84
+ if (index >= files.length && active === 0)
85
+ resolve();
86
+ }
87
+ dispatch();
88
+ });
89
+ }
90
+ function runFileInWorker(file, type, onResults, options = {}) {
91
+ return new Promise((resolve, reject) => {
92
+ let worker = type === 'e2e'
93
+ ? new Worker(workerE2EUrl, {
94
+ workerData: {
95
+ file: pathToFileURL(file).href,
96
+ type,
97
+ coverage: options.coverage,
98
+ open: options.open,
99
+ playwrightUseOpts: options.playwrightUseOpts,
100
+ },
101
+ })
102
+ : new Worker(workerUrl, {
103
+ workerData: {
104
+ file: pathToFileURL(file).href,
105
+ type,
106
+ coverage: options.coverage,
107
+ },
108
+ });
109
+ worker.once('message', (msg) => onResults(msg));
110
+ worker.once('error', reject);
111
+ worker.once('exit', (code) => {
112
+ if (code !== 0)
113
+ reject(new Error(`Worker exited with code ${code}`));
114
+ else
115
+ resolve();
116
+ });
117
+ });
118
+ }
@@ -0,0 +1,2 @@
1
+ export declare const IS_BUN: boolean;
2
+ //# sourceMappingURL=runtime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/lib/runtime.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,MAAM,SAA2C,CAAA"}
@@ -0,0 +1,2 @@
1
+ // https://bun.com/docs/guides/util/detect-bun
2
+ export const IS_BUN = typeof process.versions.bun === 'string';
@@ -0,0 +1,4 @@
1
+ export declare function transformTypeScript(source: string, filePath: string): Promise<{
2
+ code: string;
3
+ }>;
4
+ //# sourceMappingURL=ts-transform.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ts-transform.d.ts","sourceRoot":"","sources":["../../src/lib/ts-transform.ts"],"names":[],"mappings":"AAmBA,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAa3B"}
@@ -0,0 +1,29 @@
1
+ import { transform } from 'esbuild';
2
+ import { getTsconfig } from 'get-tsconfig';
3
+ import * as path from 'node:path';
4
+ const tsconfigCache = new Map();
5
+ /*
6
+ * Transform a TypeScript file to JavaScript using esbuild with an inline
7
+ * source map and no minification. Used by the coverage ESM loader hook (so V8
8
+ * instruments readable JS), the coverage collector (so byte offsets can be
9
+ * re-derived and mapped back to TypeScript lines), and the browser harness
10
+ * server (so the bytes V8 sees in the browser match what the collector
11
+ * re-derives). Identical inputs must produce identical outputs across all
12
+ * call sites or coverage offsets won't line up.
13
+ *
14
+ * Compiler options (notably JSX) are taken from the nearest `tsconfig.json`
15
+ * walking up from the file's directory, so each project picks up its own
16
+ * `jsxImportSource` etc. Discovery results are cached by directory.
17
+ */
18
+ export async function transformTypeScript(source, filePath) {
19
+ let loader = filePath.endsWith('.tsx') ? 'tsx' : 'ts';
20
+ let tsConfig = getTsconfig(path.dirname(filePath), 'tsconfig.json', tsconfigCache);
21
+ let result = await transform(source, {
22
+ loader,
23
+ sourcemap: 'inline',
24
+ sourcesContent: true,
25
+ sourcefile: filePath,
26
+ tsconfigRaw: { compilerOptions: tsConfig?.config.compilerOptions ?? {} },
27
+ });
28
+ return { code: result.code };
29
+ }
@@ -0,0 +1,5 @@
1
+ export declare function createWatcher(onChange: (file: string) => void): {
2
+ update: (files: string[]) => void;
3
+ close: () => void;
4
+ };
5
+ //# sourceMappingURL=watcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watcher.d.ts","sourceRoot":"","sources":["../../src/lib/watcher.ts"],"names":[],"mappings":"AAUA,wBAAgB,aAAa,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI;;;EAmC7D"}