@remix-run/test 0.1.0 → 0.3.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 (159) hide show
  1. package/README.md +161 -50
  2. package/dist/app/client/entry.d.ts +2 -0
  3. package/dist/app/client/entry.d.ts.map +1 -0
  4. package/dist/app/client/entry.js +328 -0
  5. package/dist/app/client/iframe.d.ts +2 -0
  6. package/dist/app/client/iframe.d.ts.map +1 -0
  7. package/dist/app/client/iframe.js +22 -0
  8. package/dist/app/server.d.ts +6 -0
  9. package/dist/app/server.d.ts.map +1 -0
  10. package/dist/app/server.js +303 -0
  11. package/dist/cli-entry.d.ts +3 -0
  12. package/dist/cli-entry.d.ts.map +1 -0
  13. package/dist/cli-entry.js +14 -0
  14. package/dist/cli.d.ts +7 -2
  15. package/dist/cli.d.ts.map +1 -1
  16. package/dist/cli.js +319 -140
  17. package/dist/index.d.ts +2 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/lib/colors.d.ts +2 -0
  20. package/dist/lib/colors.d.ts.map +1 -0
  21. package/dist/lib/colors.js +2 -0
  22. package/dist/lib/config.d.ts +59 -14
  23. package/dist/lib/config.d.ts.map +1 -1
  24. package/dist/lib/config.js +181 -38
  25. package/dist/lib/context.d.ts +37 -13
  26. package/dist/lib/context.d.ts.map +1 -1
  27. package/dist/lib/context.js +19 -3
  28. package/dist/lib/coverage-loader.d.ts +16 -0
  29. package/dist/lib/coverage-loader.d.ts.map +1 -0
  30. package/dist/lib/coverage-loader.js +20 -0
  31. package/dist/lib/coverage.d.ts +28 -0
  32. package/dist/lib/coverage.d.ts.map +1 -0
  33. package/dist/lib/coverage.js +212 -0
  34. package/dist/lib/executor.d.ts +3 -26
  35. package/dist/lib/executor.d.ts.map +1 -1
  36. package/dist/lib/executor.js +11 -6
  37. package/dist/lib/fake-timers.d.ts +13 -0
  38. package/dist/lib/fake-timers.d.ts.map +1 -0
  39. package/dist/lib/fake-timers.js +64 -0
  40. package/dist/lib/import-module.d.ts +2 -0
  41. package/dist/lib/import-module.d.ts.map +1 -0
  42. package/dist/lib/import-module.js +38 -0
  43. package/dist/lib/normalize.d.ts +2 -0
  44. package/dist/lib/normalize.d.ts.map +1 -0
  45. package/dist/lib/{utils.js → normalize.js} +0 -9
  46. package/dist/lib/playwright.d.ts +1 -1
  47. package/dist/lib/playwright.d.ts.map +1 -1
  48. package/dist/lib/playwright.js +5 -8
  49. package/dist/lib/reporters/dot.d.ts +1 -2
  50. package/dist/lib/reporters/dot.d.ts.map +1 -1
  51. package/dist/lib/reporters/dot.js +12 -1
  52. package/dist/lib/reporters/files.d.ts +1 -2
  53. package/dist/lib/reporters/files.d.ts.map +1 -1
  54. package/dist/lib/reporters/files.js +12 -1
  55. package/dist/lib/reporters/index.d.ts +4 -5
  56. package/dist/lib/reporters/index.d.ts.map +1 -1
  57. package/dist/lib/reporters/index.js +3 -3
  58. package/dist/lib/reporters/results.d.ts +30 -0
  59. package/dist/lib/reporters/results.d.ts.map +1 -0
  60. package/dist/lib/reporters/results.js +1 -0
  61. package/dist/lib/reporters/spec.d.ts +1 -2
  62. package/dist/lib/reporters/spec.d.ts.map +1 -1
  63. package/dist/lib/reporters/spec.js +12 -1
  64. package/dist/lib/reporters/tap.d.ts +1 -2
  65. package/dist/lib/reporters/tap.d.ts.map +1 -1
  66. package/dist/lib/reporters/tap.js +11 -1
  67. package/dist/lib/runner-browser.d.ts +21 -0
  68. package/dist/lib/runner-browser.d.ts.map +1 -0
  69. package/dist/lib/runner-browser.js +123 -0
  70. package/dist/lib/runner.d.ts +24 -2
  71. package/dist/lib/runner.d.ts.map +1 -1
  72. package/dist/lib/runner.js +216 -38
  73. package/dist/lib/runtime.d.ts +2 -0
  74. package/dist/lib/runtime.d.ts.map +1 -0
  75. package/dist/lib/runtime.js +2 -0
  76. package/dist/lib/ts-transform.d.ts +4 -0
  77. package/dist/lib/ts-transform.d.ts.map +1 -0
  78. package/dist/lib/ts-transform.js +29 -0
  79. package/dist/lib/worker-e2e-file.d.ts +11 -0
  80. package/dist/lib/worker-e2e-file.d.ts.map +1 -0
  81. package/dist/lib/worker-e2e-file.js +69 -0
  82. package/dist/lib/worker-e2e.js +11 -46
  83. package/dist/lib/worker-process.d.ts +2 -0
  84. package/dist/lib/worker-process.d.ts.map +1 -0
  85. package/dist/lib/worker-process.js +55 -0
  86. package/dist/lib/worker-results.d.ts +3 -0
  87. package/dist/lib/worker-results.d.ts.map +1 -0
  88. package/dist/lib/worker-results.js +20 -0
  89. package/dist/lib/worker-server.d.ts +10 -0
  90. package/dist/lib/worker-server.d.ts.map +1 -0
  91. package/dist/lib/worker-server.js +113 -0
  92. package/dist/lib/worker.js +7 -28
  93. package/dist/test/coverage/fixture.d.ts +5 -0
  94. package/dist/test/coverage/fixture.d.ts.map +1 -0
  95. package/dist/test/coverage/fixture.js +32 -0
  96. package/dist/test/coverage/test-browser.d.ts +2 -0
  97. package/dist/test/coverage/test-browser.d.ts.map +1 -0
  98. package/dist/test/coverage/test-browser.js +24 -0
  99. package/dist/test/coverage/test-e2e.d.ts +2 -0
  100. package/dist/test/coverage/test-e2e.d.ts.map +1 -0
  101. package/dist/test/coverage/test-e2e.js +60 -0
  102. package/dist/test/coverage/test-unit.d.ts +2 -0
  103. package/dist/test/coverage/test-unit.d.ts.map +1 -0
  104. package/dist/test/coverage/test-unit.js +27 -0
  105. package/dist/test/framework.test.browser.d.ts +2 -0
  106. package/dist/test/framework.test.browser.d.ts.map +1 -0
  107. package/dist/test/framework.test.browser.js +107 -0
  108. package/dist/test/framework.test.e2e.d.ts.map +1 -0
  109. package/dist/test/framework.test.e2e.js +34 -0
  110. package/package.json +30 -9
  111. package/src/app/client/entry.ts +357 -0
  112. package/src/app/client/iframe.ts +18 -0
  113. package/src/app/server.ts +336 -0
  114. package/src/cli-entry.ts +15 -0
  115. package/src/cli.ts +382 -145
  116. package/src/index.ts +2 -1
  117. package/src/lib/colors.ts +3 -0
  118. package/src/lib/config.ts +266 -54
  119. package/src/lib/context.ts +59 -17
  120. package/src/lib/coverage-loader.ts +31 -0
  121. package/src/lib/coverage.ts +320 -0
  122. package/src/lib/executor.ts +18 -35
  123. package/src/lib/fake-timers.ts +89 -0
  124. package/src/lib/import-module.ts +39 -0
  125. package/src/lib/{utils.ts → normalize.ts} +0 -18
  126. package/src/lib/playwright.ts +5 -7
  127. package/src/lib/reporters/dot.ts +12 -2
  128. package/src/lib/reporters/files.ts +12 -2
  129. package/src/lib/reporters/index.ts +4 -5
  130. package/src/lib/reporters/results.ts +29 -0
  131. package/src/lib/reporters/spec.ts +12 -2
  132. package/src/lib/reporters/tap.ts +11 -2
  133. package/src/lib/runner-browser.ts +171 -0
  134. package/src/lib/runner.ts +308 -53
  135. package/src/lib/runtime.ts +2 -0
  136. package/src/lib/ts-transform.ts +36 -0
  137. package/src/lib/worker-e2e-file.ts +98 -0
  138. package/src/lib/worker-e2e.ts +14 -49
  139. package/src/lib/worker-process.ts +69 -0
  140. package/src/lib/worker-results.ts +22 -0
  141. package/src/lib/worker-server.ts +123 -0
  142. package/src/lib/worker.ts +8 -28
  143. package/src/test/coverage/fixture.ts +34 -0
  144. package/src/test/coverage/test-browser.ts +29 -0
  145. package/src/test/coverage/test-e2e.ts +70 -0
  146. package/src/test/coverage/test-unit.ts +32 -0
  147. package/tsconfig.json +3 -1
  148. package/dist/lib/e2e-server.d.ts +0 -11
  149. package/dist/lib/e2e-server.d.ts.map +0 -1
  150. package/dist/lib/e2e-server.js +0 -15
  151. package/dist/lib/framework.test.d.ts +0 -2
  152. package/dist/lib/framework.test.d.ts.map +0 -1
  153. package/dist/lib/framework.test.e2e.d.ts.map +0 -1
  154. package/dist/lib/framework.test.e2e.js +0 -29
  155. package/dist/lib/framework.test.js +0 -283
  156. package/dist/lib/utils.d.ts +0 -16
  157. package/dist/lib/utils.d.ts.map +0 -1
  158. package/src/lib/e2e-server.ts +0 -28
  159. /package/dist/{lib → test}/framework.test.e2e.d.ts +0 -0
@@ -1,6 +1,5 @@
1
- import { type Counts } from '../utils.ts';
2
- import type { TestResults } from '../executor.ts';
3
1
  import type { Reporter } from './index.ts';
2
+ import type { Counts, TestResults } from './results.ts';
4
3
  export declare class SpecReporter implements Reporter {
5
4
  #private;
6
5
  onSectionStart(label: string): void;
@@ -1 +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"}
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;;IAK3C,cAAc,CAAC,KAAK,EAAE,MAAM,QAE3B;IAED,QAAQ,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,CAAC,EAAE,MAAM,QAmI1C;IAED,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,QAkC3C;CACF"}
@@ -1,10 +1,19 @@
1
- import { colors, normalizeLine } from "../utils.js";
1
+ import { colors } from "../colors.js";
2
+ import { normalizeLine } from "../normalize.js";
2
3
  export class SpecReporter {
3
4
  #failures = [];
5
+ #files = new Set();
6
+ #suites = new Set();
4
7
  onSectionStart(label) {
5
8
  console.log(label);
6
9
  }
7
10
  onResult(results, env) {
11
+ for (let test of results.tests) {
12
+ if (test.filePath)
13
+ this.#files.add(test.filePath);
14
+ if (test.suiteName)
15
+ this.#suites.add(test.suiteName);
16
+ }
8
17
  let suiteMap = new Map();
9
18
  for (let test of results.tests) {
10
19
  let suite = test.suiteName || 'Global';
@@ -139,6 +148,8 @@ export class SpecReporter {
139
148
  let { passed, failed, skipped, todo } = counts;
140
149
  let info = colors.cyan('ℹ');
141
150
  console.log();
151
+ console.log(`${info} files ${this.#files.size}`);
152
+ console.log(`${info} suites ${this.#suites.size}`);
142
153
  console.log(`${info} tests ${passed + failed + skipped + todo}`);
143
154
  console.log(`${info} pass ${passed}`);
144
155
  console.log(`${info} fail ${failed}`);
@@ -1,6 +1,5 @@
1
- import { type Counts } from '../utils.ts';
2
- import type { TestResults } from '../executor.ts';
3
1
  import type { Reporter } from './index.ts';
2
+ import type { Counts, TestResults } from './results.ts';
4
3
  export declare class TapReporter implements Reporter {
5
4
  #private;
6
5
  onSectionStart(_label: string): void;
@@ -1 +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"}
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;;IAM1C,cAAc,CAAC,MAAM,EAAE,MAAM,QAAI;IAEjC,QAAQ,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,CAAC,EAAE,MAAM,QAwC1C;IAED,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,QAW3C;CACF"}
@@ -1,13 +1,21 @@
1
- import { normalizeLine } from "../utils.js";
1
+ import { normalizeLine } from "../normalize.js";
2
2
  export class TapReporter {
3
3
  #counter = 0;
4
4
  #total = 0;
5
+ #files = new Set();
6
+ #suites = new Set();
5
7
  onSectionStart(_label) { }
6
8
  onResult(results, env) {
7
9
  if (this.#counter === 0) {
8
10
  console.log('TAP version 14');
9
11
  }
10
12
  let envComment = env ? ` # ${env}` : '';
13
+ for (let test of results.tests) {
14
+ if (test.filePath)
15
+ this.#files.add(test.filePath);
16
+ if (test.suiteName)
17
+ this.#suites.add(test.suiteName);
18
+ }
11
19
  for (let test of results.tests) {
12
20
  this.#counter++;
13
21
  this.#total++;
@@ -42,6 +50,8 @@ export class TapReporter {
42
50
  onSummary(counts, durationMs) {
43
51
  let { passed, failed, skipped, todo } = counts;
44
52
  console.log(`1..${this.#total}`);
53
+ console.log(`# files ${this.#files.size}`);
54
+ console.log(`# suites ${this.#suites.size}`);
45
55
  console.log(`# tests ${passed + failed + skipped + todo}`);
46
56
  console.log(`# pass ${passed}`);
47
57
  console.log(`# fail ${failed}`);
@@ -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,CA6HD"}
@@ -0,0 +1,123 @@
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
+ // Cap how long we'll wait for a browser-test file to signal completion.
31
+ // Playwright's default is 30s; bumping to 60s buys headroom for slower
32
+ // suites without letting a hung test hide forever. Plumb this through
33
+ // config later if anyone needs to tune it.
34
+ page.setDefaultTimeout(90_000);
35
+ page.setDefaultNavigationTimeout(90_000);
36
+ if (options.console) {
37
+ page.on('console', (msg) => console.log(`${colors.dim('[browser console]')} ${msg.text()}`));
38
+ }
39
+ // Playwright's JS coverage is Chromium-only. Start before navigation so
40
+ // the harness scripts and test modules are instrumented from first parse.
41
+ let coverageEnabled = options.coverage && browser.browserType().name() === 'chromium';
42
+ if (coverageEnabled) {
43
+ await page.coverage.startJSCoverage({ resetOnNavigation: false });
44
+ }
45
+ let totalPassed = 0;
46
+ let totalFailed = 0;
47
+ let totalSkipped = 0;
48
+ let totalTodo = 0;
49
+ let rootDir = getBrowserTestRootDir();
50
+ await page.route('**/file-results', async (route) => {
51
+ let results = route.request().postDataJSON();
52
+ for (let test of results.tests) {
53
+ if (test.filePath)
54
+ test.filePath = urlPathToFilePath(test.filePath, rootDir);
55
+ }
56
+ options.reporter.onResult(results, envLabel);
57
+ totalPassed += results.passed;
58
+ totalFailed += results.failed;
59
+ totalSkipped += results.skipped;
60
+ totalTodo += results.todo;
61
+ await route.fulfill({ status: 200 });
62
+ });
63
+ // Fail the tests if any /scripts/ request fails (harness scripts, test
64
+ // modules, or their transitive imports — all served via the same prefix).
65
+ let errorPromise = new Promise((_, reject) => {
66
+ let isScriptRequest = (request) => new URL(request.url()).pathname.startsWith('/scripts/');
67
+ page.on('response', (response) => {
68
+ if (!response.ok() && isScriptRequest(response.request())) {
69
+ reject(new Error(`Failed to load script: ${response.request().url()}`));
70
+ }
71
+ });
72
+ page.on('requestfailed', (request) => {
73
+ if (isScriptRequest(request)) {
74
+ reject(new Error(`Failed to load script: ${request.url()}`));
75
+ }
76
+ });
77
+ });
78
+ // Prevent unhandled rejection if we fail before setting up the listener
79
+ errorPromise.catch(() => { });
80
+ await page.goto(options.baseUrl);
81
+ await Promise.race([page.waitForFunction('window.__testsDone'), errorPromise]);
82
+ if (coverageEnabled) {
83
+ let entries = (await page.coverage.stopJSCoverage());
84
+ if (entries.length > 0) {
85
+ coverageMap = await collectCoverageMapFromPlaywright(entries, getBrowserTestRootDir(), new Set(options.testFiles ?? []), async (urlPath) => urlPath.startsWith('/scripts/') ? urlPath.slice('/scripts/'.length) : null);
86
+ }
87
+ }
88
+ results = {
89
+ passed: totalPassed,
90
+ failed: totalFailed,
91
+ skipped: totalSkipped,
92
+ todo: totalTodo,
93
+ tests: [],
94
+ };
95
+ }
96
+ catch (error) {
97
+ console.error('Browser tests failed to run:', error);
98
+ results = {
99
+ passed: 0,
100
+ failed: 1,
101
+ skipped: 0,
102
+ todo: 0,
103
+ tests: [],
104
+ };
105
+ }
106
+ if (options.open) {
107
+ return {
108
+ results,
109
+ coverageMap,
110
+ close,
111
+ disconnected: new Promise((r) => browser.on('disconnected', () => r())),
112
+ };
113
+ }
114
+ else {
115
+ await close();
116
+ return {
117
+ results,
118
+ coverageMap,
119
+ close,
120
+ disconnected: Promise.resolve(),
121
+ };
122
+ }
123
+ }
@@ -1,9 +1,31 @@
1
+ import { type RemixTestPool } from './config.ts';
2
+ import { type CoverageConfig, type CoverageMap } from './coverage.ts';
1
3
  import { type PlaywrightUseOpts } from './playwright.ts';
2
4
  import type { Reporter } from './reporters/index.ts';
3
- import type { Counts } from './utils.ts';
5
+ import type { Counts, TestResults } from './reporters/results.ts';
6
+ interface WorkerRun {
7
+ finished: Promise<void>;
8
+ exited: Promise<number | null>;
9
+ terminate(): Promise<boolean>;
10
+ }
11
+ interface RunFileOptions {
12
+ cwd?: string;
13
+ coverage?: CoverageConfig;
14
+ open?: boolean;
15
+ playwrightUseOpts?: PlaywrightUseOpts;
16
+ pool?: RemixTestPool;
17
+ }
4
18
  export declare function runServerTests(files: string[], reporter: Reporter, concurrency: number, type: 'server' | 'e2e', options?: {
19
+ cwd?: string;
5
20
  open?: boolean;
6
21
  playwrightUseOpts?: PlaywrightUseOpts;
7
22
  projectName?: string;
8
- }): Promise<Counts>;
23
+ coverage?: CoverageConfig;
24
+ workerShutdownTimeoutMs?: number;
25
+ pool?: RemixTestPool;
26
+ }): Promise<Counts & {
27
+ coverageMap: CoverageMap | null;
28
+ }>;
29
+ export declare function runFileInWorker(file: string, type: 'server' | 'e2e', onResults: (results: TestResults) => void, options?: RunFileOptions): WorkerRun;
30
+ export {};
9
31
  //# sourceMappingURL=runner.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/lib/runner.ts"],"names":[],"mappings":"AAKA,OAAO,EAAuB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAA;AACrE,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,WAAW,EAAE,MAAM,wBAAwB,CAAA;AAUjE,UAAU,SAAS;IACjB,QAAQ,EAAE,OAAO,CAAC,IAAI,CAAC,CAAA;IACvB,MAAM,EAAE,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IAC9B,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC,CAAA;CAC9B;AAED,UAAU,cAAc;IACtB,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,cAAc,CAAA;IACzB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;IACrC,IAAI,CAAC,EAAE,aAAa,CAAA;CACrB;AAED,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;IACzB,uBAAuB,CAAC,EAAE,MAAM,CAAA;IAChC,IAAI,CAAC,EAAE,aAAa,CAAA;CAChB,GACL,OAAO,CAAC,MAAM,GAAG;IAAE,WAAW,EAAE,WAAW,GAAG,IAAI,CAAA;CAAE,CAAC,CAkFvD;AAwFD,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,QAAQ,GAAG,KAAK,EACtB,SAAS,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,EACzC,OAAO,GAAE,cAAmB,GAC3B,SAAS,CAiEX"}
@@ -1,13 +1,24 @@
1
+ import { fork } from 'node:child_process';
2
+ import * as fsp from 'node:fs/promises';
1
3
  import * as path from 'node:path';
2
- import { pathToFileURL } from 'node:url';
4
+ import { fileURLToPath, pathToFileURL } from 'node:url';
3
5
  import { Worker } from 'node:worker_threads';
6
+ import { IS_RUNNING_FROM_SRC } from "./config.js";
7
+ import { collectCoverageMapFromPlaywright, collectServerCoverageMap, } from "./coverage.js";
4
8
  import {} from "./playwright.js";
5
- const ext = path.extname(import.meta.url);
9
+ // Ensure we load the right file whether we're running in the monorepo (TS) or
10
+ // from a published package (JS)
11
+ const ext = IS_RUNNING_FROM_SRC ? '.ts' : '.js';
6
12
  const workerUrl = new URL(`./worker${ext}`, import.meta.url);
7
13
  const workerE2EUrl = new URL(`./worker-e2e${ext}`, import.meta.url);
14
+ const workerProcessUrl = new URL(`./worker-process${ext}`, import.meta.url);
15
+ const DEFAULT_WORKER_SHUTDOWN_TIMEOUT_MS = 10_000;
8
16
  export async function runServerTests(files, reporter, concurrency, type, options = {}) {
9
17
  let counts = { passed: 0, failed: 0, skipped: 0, todo: 0 };
18
+ let coverageMap = null;
19
+ let cwd = options.cwd ?? process.cwd();
10
20
  let envLabel = options.projectName ? `${type}:${options.projectName}` : type;
21
+ let pool = options.pool ?? 'forks';
11
22
  function accumulate(results, file) {
12
23
  reporter.onResult({ ...results, tests: results.tests.map((t) => ({ ...t, filePath: file })) }, envLabel);
13
24
  counts.passed += results.passed;
@@ -16,17 +27,41 @@ export async function runServerTests(files, reporter, concurrency, type, options
16
27
  counts.todo += results.todo;
17
28
  }
18
29
  if (type === 'e2e') {
19
- await runInConcurrentWorkers(files, concurrency, (file) => runFileInWorker(file, type, (results) => accumulate(results, file), {
30
+ let allBrowserCoverageEntries = [];
31
+ await runInConcurrentWorkers(files, concurrency, (file) => runFileInPool(file, type, (results) => {
32
+ accumulate(results, file);
33
+ if (results.e2eBrowserCoverageEntries) {
34
+ allBrowserCoverageEntries.push(...results.e2eBrowserCoverageEntries);
35
+ }
36
+ }, {
20
37
  ...options,
38
+ pool,
21
39
  playwrightUseOpts: options.playwrightUseOpts,
22
- }), () => counts.failed++);
40
+ }), () => counts.failed++, !options.open, options.workerShutdownTimeoutMs ?? DEFAULT_WORKER_SHUTDOWN_TIMEOUT_MS);
41
+ if (options.coverage && allBrowserCoverageEntries.length > 0) {
42
+ coverageMap = await collectCoverageMapFromPlaywright(allBrowserCoverageEntries.flatMap((e) => e.entries), cwd, new Set(files), async (urlPath) => (urlPath.startsWith('/') ? urlPath.slice(1) : urlPath));
43
+ }
23
44
  }
24
45
  else {
25
- await runInConcurrentWorkers(files, concurrency, (file) => runFileInWorker(file, type, (results) => accumulate(results, file)), () => counts.failed++);
46
+ let coverageDataDir;
47
+ if (options.coverage) {
48
+ coverageDataDir = path.resolve(cwd, options.coverage.dir);
49
+ await fsp.mkdir(coverageDataDir, { recursive: true });
50
+ process.env.NODE_V8_COVERAGE = coverageDataDir;
51
+ }
52
+ await runInConcurrentWorkers(files, concurrency, (file) => runFileInPool(file, type, (results) => accumulate(results, file), {
53
+ ...options,
54
+ pool,
55
+ }), () => counts.failed++, true, options.workerShutdownTimeoutMs ?? DEFAULT_WORKER_SHUTDOWN_TIMEOUT_MS);
56
+ if (coverageDataDir) {
57
+ delete process.env.NODE_V8_COVERAGE;
58
+ let serverMap = await collectServerCoverageMap(coverageDataDir, cwd, new Set(files));
59
+ coverageMap = serverMap;
60
+ }
26
61
  }
27
- return { ...counts };
62
+ return { ...counts, coverageMap };
28
63
  }
29
- async function runInConcurrentWorkers(files, concurrency, runFile, onError) {
64
+ async function runInConcurrentWorkers(files, concurrency, runFile, onError, terminateWhenFinished, workerShutdownTimeoutMs) {
30
65
  let index = 0;
31
66
  let active = 0;
32
67
  await new Promise((resolve) => {
@@ -35,7 +70,8 @@ async function runInConcurrentWorkers(files, concurrency, runFile, onError) {
35
70
  let file = files[index];
36
71
  index++;
37
72
  active++;
38
- runFile(file).then(() => {
73
+ let run = runFile(file);
74
+ function complete() {
39
75
  active--;
40
76
  if (index < files.length) {
41
77
  dispatch();
@@ -43,15 +79,32 @@ async function runInConcurrentWorkers(files, concurrency, runFile, onError) {
43
79
  else if (active === 0) {
44
80
  resolve();
45
81
  }
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();
82
+ }
83
+ run.finished.then(async () => {
84
+ try {
85
+ if (terminateWhenFinished) {
86
+ let exited = await waitForWorkerExit(run.exited, workerShutdownTimeoutMs);
87
+ if (!exited) {
88
+ let terminated = await run.terminate();
89
+ if (!terminated) {
90
+ onError();
91
+ }
92
+ }
93
+ }
94
+ }
95
+ finally {
96
+ complete();
97
+ }
98
+ }, async (err) => {
99
+ try {
100
+ console.error(`Error running ${file}:`, err instanceof Error ? err.message : err);
101
+ console.error(err);
102
+ onError();
103
+ await run.terminate();
104
+ }
105
+ finally {
106
+ complete();
107
+ }
55
108
  });
56
109
  }
57
110
  if (index >= files.length && active === 0)
@@ -60,30 +113,155 @@ async function runInConcurrentWorkers(files, concurrency, runFile, onError) {
60
113
  dispatch();
61
114
  });
62
115
  }
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));
116
+ function waitForWorkerExit(exited, timeoutMs) {
117
+ return new Promise((resolve) => {
118
+ let timeout = setTimeout(() => resolve(false), timeoutMs);
119
+ exited.then(() => {
120
+ clearTimeout(timeout);
121
+ resolve(true);
122
+ });
123
+ });
124
+ }
125
+ function runFileInPool(file, type, onResults, options) {
126
+ return options.pool === 'threads'
127
+ ? runFileInWorker(file, type, onResults, options)
128
+ : runFileInProcess(file, type, onResults, options);
129
+ }
130
+ export function runFileInWorker(file, type, onResults, options = {}) {
131
+ let receivedResults = false;
132
+ let worker = type === 'e2e'
133
+ ? new Worker(workerE2EUrl, {
134
+ workerData: {
135
+ file: pathToFileURL(file).href,
136
+ type,
137
+ coverage: options.coverage,
138
+ open: options.open,
139
+ playwrightUseOpts: options.playwrightUseOpts,
140
+ },
141
+ })
142
+ : new Worker(workerUrl, {
143
+ workerData: {
144
+ file: pathToFileURL(file).href,
145
+ type,
146
+ coverage: options.coverage,
147
+ },
148
+ });
149
+ let exited = new Promise((resolve) => {
150
+ worker.once('exit', (code) => resolve(code));
151
+ });
152
+ let finished = new Promise((resolve, reject) => {
153
+ worker.once('message', (msg) => {
154
+ receivedResults = true;
155
+ try {
156
+ onResults(msg);
157
+ }
158
+ catch (error) {
159
+ reject(error);
160
+ return;
161
+ }
162
+ if (!options.open) {
163
+ resolve();
164
+ }
165
+ });
81
166
  worker.once('error', reject);
82
- worker.once('exit', (code) => {
83
- if (code !== 0)
167
+ exited.then((code) => {
168
+ if (receivedResults || code === 0) {
169
+ resolve();
170
+ }
171
+ else {
84
172
  reject(new Error(`Worker exited with code ${code}`));
85
- else
173
+ }
174
+ });
175
+ });
176
+ return {
177
+ finished,
178
+ exited,
179
+ async terminate() {
180
+ try {
181
+ await worker.terminate();
182
+ return true;
183
+ }
184
+ catch (err) {
185
+ console.error(`Error terminating worker for ${file}:`, err instanceof Error ? err.message : err);
186
+ console.error(err);
187
+ return false;
188
+ }
189
+ },
190
+ };
191
+ }
192
+ function runFileInProcess(file, type, onResults, options = {}) {
193
+ let receivedResults = false;
194
+ let child = fork(fileURLToPath(workerProcessUrl), [], {
195
+ serialization: 'advanced',
196
+ stdio: ['ignore', 'inherit', 'inherit', 'ipc'],
197
+ });
198
+ let exited = new Promise((resolve) => {
199
+ child.once('exit', (code) => resolve(code));
200
+ });
201
+ let finished = new Promise((resolve, reject) => {
202
+ child.once('message', (msg) => {
203
+ if (!isTestResults(msg)) {
204
+ reject(new Error('Test worker process sent invalid results'));
205
+ return;
206
+ }
207
+ receivedResults = true;
208
+ try {
209
+ onResults(msg);
210
+ }
211
+ catch (error) {
212
+ reject(error);
213
+ return;
214
+ }
215
+ if (!options.open) {
86
216
  resolve();
217
+ }
218
+ });
219
+ child.once('error', reject);
220
+ exited.then((code) => {
221
+ if (receivedResults || code === 0) {
222
+ resolve();
223
+ }
224
+ else {
225
+ reject(new Error(`Worker process exited with code ${code}`));
226
+ }
227
+ });
228
+ child.send({
229
+ file: pathToFileURL(file).href,
230
+ type,
231
+ coverage: options.coverage,
232
+ open: options.open,
233
+ playwrightUseOpts: options.playwrightUseOpts,
234
+ }, (error) => {
235
+ if (error) {
236
+ reject(error);
237
+ }
87
238
  });
88
239
  });
240
+ return {
241
+ finished,
242
+ exited,
243
+ terminate: () => terminateChildProcess(child, exited),
244
+ };
245
+ }
246
+ async function terminateChildProcess(child, exited) {
247
+ if (child.exitCode !== null || child.signalCode !== null) {
248
+ return true;
249
+ }
250
+ if (!child.kill()) {
251
+ return false;
252
+ }
253
+ return await waitForWorkerExit(exited, 5_000);
254
+ }
255
+ function isTestResults(value) {
256
+ if (!isRecord(value)) {
257
+ return false;
258
+ }
259
+ return (typeof value.passed === 'number' &&
260
+ typeof value.failed === 'number' &&
261
+ typeof value.skipped === 'number' &&
262
+ typeof value.todo === 'number' &&
263
+ Array.isArray(value.tests));
264
+ }
265
+ function isRecord(value) {
266
+ return typeof value === 'object' && value !== null;
89
267
  }
@@ -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,11 @@
1
+ import { type PlaywrightUseOpts } from './playwright.ts';
2
+ import type { CoverageConfig } from './coverage.ts';
3
+ import type { TestResults } from './reporters/results.ts';
4
+ export interface E2ETestWorkerData {
5
+ file: string;
6
+ coverage?: CoverageConfig;
7
+ open?: boolean;
8
+ playwrightUseOpts?: PlaywrightUseOpts;
9
+ }
10
+ export declare function runE2ETestFile(value: unknown, onOpenResults?: (results: TestResults) => void | Promise<void>): Promise<TestResults | undefined>;
11
+ //# sourceMappingURL=worker-e2e-file.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker-e2e-file.d.ts","sourceRoot":"","sources":["../../src/lib/worker-e2e-file.ts"],"names":[],"mappings":"AAEA,OAAO,EAIL,KAAK,iBAAiB,EACvB,MAAM,iBAAiB,CAAA;AACxB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AACnD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAA;AAIzD,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,cAAc,CAAA;IACzB,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;CACtC;AAED,wBAAsB,cAAc,CAClC,KAAK,EAAE,OAAO,EACd,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAC7D,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAqClC"}