@remix-run/test 0.1.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 (143) hide show
  1. package/README.md +140 -35
  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 +324 -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 +273 -139
  17. package/dist/index.d.ts +1 -0
  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 +32 -1
  23. package/dist/lib/config.d.ts.map +1 -1
  24. package/dist/lib/config.js +125 -22
  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 +6 -0
  38. package/dist/lib/fake-timers.d.ts.map +1 -0
  39. package/dist/lib/fake-timers.js +45 -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 +29 -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 +2 -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 +2 -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 +2 -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 +1 -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 +117 -0
  70. package/dist/lib/runner.d.ts +7 -2
  71. package/dist/lib/runner.d.ts.map +1 -1
  72. package/dist/lib/runner.js +33 -4
  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.js +5 -4
  80. package/dist/lib/worker.js +31 -3
  81. package/dist/test/coverage/fixture.d.ts +5 -0
  82. package/dist/test/coverage/fixture.d.ts.map +1 -0
  83. package/dist/test/coverage/fixture.js +32 -0
  84. package/dist/test/coverage/test-browser.d.ts +2 -0
  85. package/dist/test/coverage/test-browser.d.ts.map +1 -0
  86. package/dist/test/coverage/test-browser.js +24 -0
  87. package/dist/test/coverage/test-e2e.d.ts +2 -0
  88. package/dist/test/coverage/test-e2e.d.ts.map +1 -0
  89. package/dist/test/coverage/test-e2e.js +60 -0
  90. package/dist/test/coverage/test-unit.d.ts +2 -0
  91. package/dist/test/coverage/test-unit.d.ts.map +1 -0
  92. package/dist/test/coverage/test-unit.js +27 -0
  93. package/dist/test/framework.test.browser.d.ts +2 -0
  94. package/dist/test/framework.test.browser.d.ts.map +1 -0
  95. package/dist/test/framework.test.browser.js +107 -0
  96. package/dist/test/framework.test.e2e.d.ts.map +1 -0
  97. package/dist/test/framework.test.e2e.js +34 -0
  98. package/package.json +30 -9
  99. package/src/app/client/entry.ts +353 -0
  100. package/src/app/client/iframe.ts +18 -0
  101. package/src/app/server.ts +336 -0
  102. package/src/cli-entry.ts +15 -0
  103. package/src/cli.ts +322 -148
  104. package/src/index.ts +1 -0
  105. package/src/lib/colors.ts +3 -0
  106. package/src/lib/config.ts +169 -23
  107. package/src/lib/context.ts +59 -17
  108. package/src/lib/coverage-loader.ts +31 -0
  109. package/src/lib/coverage.ts +320 -0
  110. package/src/lib/executor.ts +18 -35
  111. package/src/lib/fake-timers.ts +64 -0
  112. package/src/lib/import-module.ts +29 -0
  113. package/src/lib/{utils.ts → normalize.ts} +0 -18
  114. package/src/lib/playwright.ts +5 -7
  115. package/src/lib/reporters/dot.ts +3 -2
  116. package/src/lib/reporters/files.ts +3 -2
  117. package/src/lib/reporters/index.ts +4 -5
  118. package/src/lib/reporters/results.ts +29 -0
  119. package/src/lib/reporters/spec.ts +3 -2
  120. package/src/lib/reporters/tap.ts +2 -2
  121. package/src/lib/runner-browser.ts +165 -0
  122. package/src/lib/runner.ts +62 -10
  123. package/src/lib/runtime.ts +2 -0
  124. package/src/lib/ts-transform.ts +36 -0
  125. package/src/lib/worker-e2e.ts +7 -5
  126. package/src/lib/worker.ts +24 -4
  127. package/src/test/coverage/fixture.ts +34 -0
  128. package/src/test/coverage/test-browser.ts +29 -0
  129. package/src/test/coverage/test-e2e.ts +70 -0
  130. package/src/test/coverage/test-unit.ts +32 -0
  131. package/tsconfig.json +3 -1
  132. package/dist/lib/e2e-server.d.ts +0 -11
  133. package/dist/lib/e2e-server.d.ts.map +0 -1
  134. package/dist/lib/e2e-server.js +0 -15
  135. package/dist/lib/framework.test.d.ts +0 -2
  136. package/dist/lib/framework.test.d.ts.map +0 -1
  137. package/dist/lib/framework.test.e2e.d.ts.map +0 -1
  138. package/dist/lib/framework.test.e2e.js +0 -29
  139. package/dist/lib/framework.test.js +0 -283
  140. package/dist/lib/utils.d.ts +0 -16
  141. package/dist/lib/utils.d.ts.map +0 -1
  142. package/src/lib/e2e-server.ts +0 -28
  143. /package/dist/{lib → test}/framework.test.e2e.d.ts +0 -0
package/README.md CHANGED
@@ -1,13 +1,15 @@
1
1
  # `test`
2
2
 
3
- A test framework for Remix applications
3
+ A test framework for JavaScript and TypeScript projects.
4
4
 
5
5
  ## Features
6
6
 
7
7
  - `describe`/`it` test structure with `before`/`after`/`beforeEach`/`afterEach` hooks
8
8
  - Server-side unit testing
9
9
  - Playwright E2E testing via `t.serve`
10
+ - In-browser component testing (pair with `render` from `remix/ui/test`)
10
11
  - Mock functions and method spies via `t.mock.fn` / `t.mock.method`
12
+ - Unified code coverage reporting across unit and E2E tests
11
13
  - Watch mode
12
14
  - Config file support (`remix-test.config.ts`)
13
15
 
@@ -36,17 +38,24 @@ describe('My Test Suite', () => {
36
38
  Run tests with the CLI:
37
39
 
38
40
  ```sh
39
- remix-test
41
+ remix test
40
42
  ```
41
43
 
42
- By default, `remix-test` discovers all files matching `**/*.test.{ts,tsx}`. Pass a glob as the first positional argument to override:
44
+ By default, `remix test` discovers all files matching `**/*.test{,.e2e}.{ts,tsx}`. Pass a glob as the first positional argument to override:
43
45
 
44
46
  ```sh
45
- remix-test "src/**/*.test.ts"
47
+ remix test "src/**/*.test.ts"
46
48
  ```
47
49
 
48
50
  Or, you may control via the `glob.test` config field/CLI arg.
49
51
 
52
+ If you install `@remix-run/test` directly instead of the umbrella `remix` package, the same runner is available as `remix-test`:
53
+
54
+ ```sh
55
+ npm i @remix-run/test
56
+ remix-test
57
+ ```
58
+
50
59
  ### Config File
51
60
 
52
61
  Create a `remix-test.config.ts` (or `.js`) file at the root of your project (shown with default values):
@@ -67,10 +76,28 @@ export default {
67
76
  // Max number of concurrent test workers (default `os.availableParallelism()`)
68
77
  concurrency: 2,
69
78
 
79
+ // Code coverage options
80
+ coverage: {
81
+ // Enable coverage reporting
82
+ enabled: true,
83
+ // Output directory (default: ".coverage")
84
+ dir: '.coverage',
85
+ // Glob patterns to include/exclude
86
+ include: ['src/**'],
87
+ exclude: ['src/**/*.test.ts'],
88
+ // Minimum thresholds (%)
89
+ statements: 80,
90
+ lines: 80,
91
+ branches: 80,
92
+ functions: 80,
93
+ },
94
+
70
95
  glob: {
71
- // Glob pattern identifying all test files (default: "**/*.test?(.e2e).{ts,tsx}")
72
- test: '**/*.test?(.e2e).ts',
73
- // Global pattern identifying the subset of E2E test files{ts,tsx}")
96
+ // Glob pattern identifying all test files (default: "**/*.test{,.browser,.e2e}.{ts,tsx}")
97
+ test: '**/*.test{,.browser,.e2e}.ts',
98
+ // Glob pattern identifying browser test files (default: "**/*.test.browser.{ts,tsx}")
99
+ browser: '**/*.test.browser.ts',
100
+ // Glob pattern identifying E2E test files (default: "**/*.test.e2e.{ts,tsx}")
74
101
  e2e: '**/*.test.e2e.ts',
75
102
  },
76
103
 
@@ -96,8 +123,8 @@ export default {
96
123
  // Path to a setup module (see Setup section below)
97
124
  setup: './test/setup.ts',
98
125
 
99
- // Comma-separated list of test types to run ("server", "e2e")
100
- type: 'server,e2e',
126
+ // Comma-separated list of test types to run ("server", "browser", "e2e")
127
+ type: 'server,browser,e2e',
101
128
 
102
129
  // Watch for file changes and re-run
103
130
  watch: false,
@@ -109,7 +136,7 @@ export default {
109
136
  You can point to a different config file location with the `--config` flag:
110
137
 
111
138
  ```sh
112
- remix-test --config ./tests/config.ts
139
+ remix test --config ./tests/config.ts
113
140
  ```
114
141
 
115
142
  You may also specify any config field as a CLI flag which will take precedence over config file values:
@@ -119,7 +146,16 @@ You may also specify any config field as a CLI flag which will take precedence o
119
146
  | `--browser.echo` | |
120
147
  | `--browser.open` | |
121
148
  | `--concurrency <n>` | `-c` |
149
+ | `--coverage` | |
150
+ | `--coverage.dir <path>` | |
151
+ | `--coverage.include` | |
152
+ | `--coverage.exclude` | |
153
+ | `--coverage.statements` | |
154
+ | `--coverage.lines` | |
155
+ | `--coverage.branches` | |
156
+ | `--coverage.functions` | |
122
157
  | `--glob.test` | |
158
+ | `--glob.browser` | |
123
159
  | `--glob.e2e` | |
124
160
  | `--playwrightConfig <path>` | |
125
161
  | `--project <name>` | `-p` |
@@ -172,11 +208,30 @@ suite('My Test Suite', () => {
172
208
  })
173
209
  ```
174
210
 
211
+ ### Programmatic runner
212
+
213
+ `@remix-run/test/cli` exports `runRemixTest()` for tools that want to run the test runner without
214
+ exiting the current process:
215
+
216
+ ```ts
217
+ import { runRemixTest } from '@remix-run/test/cli'
218
+
219
+ let exitCode = await runRemixTest({
220
+ argv: ['--type', 'server'],
221
+ cwd: process.cwd(),
222
+ })
223
+ ```
224
+
225
+ `runRemixTest()` returns the runner exit code. The `remix test` and `remix-test` bin wrappers call
226
+ `process.exit()` with that code when the run finishes so open workers, browsers, or project handles
227
+ cannot keep the CLI alive.
228
+
175
229
  ### Test Context
176
230
 
177
231
  Each test callback receives a `TestContext` (`t`) as its first argument with helpful test utilities.
178
232
 
179
233
  ```ts
234
+ // from 'remix/test'
180
235
  interface TestContext {
181
236
  // Register a cleanup function to run after the test completes
182
237
  after(fn: () => void): void
@@ -194,8 +249,11 @@ interface TestContext {
194
249
  ): MockFunction
195
250
  }
196
251
 
197
- // E2E only: start a server with the given request handler, returns a Playwright Page
198
- serve(handler: (req: Request) => Promise<Response>): Promise<Page>
252
+ // Replace global timer functions with controllable fakes
253
+ useFakeTimers(): FakeTimers
254
+
255
+ // E2E only: connect a running test server to a Playwright Page
256
+ serve(server: { baseUrl: string; close(): Promise<void> }): Promise<Page>
199
257
  }
200
258
  ```
201
259
 
@@ -230,14 +288,40 @@ it('cleanup', (t) => {
230
288
  })
231
289
  ```
232
290
 
291
+ #### Fake Timers
292
+
293
+ `t.useFakeTimers()` replaces the global timer functions (`setTimeout`, `setInterval`, etc.) with controllable fakes that are automatically restored after the test. It works in any test environment — server unit tests, browser tests, or E2E setup code.
294
+
295
+ ```ts
296
+ it('debounces a callback', (t) => {
297
+ let timers = t.useFakeTimers()
298
+ let calls = 0
299
+ let debounced = debounce(() => calls++, 300)
300
+
301
+ debounced()
302
+ timers.advance(299)
303
+ assert.equal(calls, 0)
304
+ timers.advance(1)
305
+ assert.equal(calls, 1)
306
+ })
307
+ ```
308
+
309
+ | Method | Description |
310
+ | ------------- | --------------------------------------------------------------------------- |
311
+ | `advance(ms)` | Advance the clock by `ms` milliseconds, firing any elapsed timers |
312
+ | `restore()` | Restore the original timer functions (called automatically after each test) |
313
+
233
314
  #### E2E
234
315
 
235
- In E2E test files, `t.serve()` starts an HTTP server and returns a Playwright `Page`. See [E2E Testing](#e2e-testing) for details.
316
+ In E2E test files, `t.serve()` connects a running test server to a Playwright `Page`. See [E2E Testing](#e2e-testing) for details.
236
317
 
237
318
  ```ts
319
+ import { createTestServer } from 'remix/node-fetch-server/test'
320
+
238
321
  it('navigates to home', async (t) => {
239
322
  let router = createRouter()
240
- let page = await t.serve(router.fetch)
323
+ let server = await createTestServer(router.fetch)
324
+ let page = await t.serve(server)
241
325
  await page.goto('/')
242
326
  })
243
327
  ```
@@ -254,21 +338,60 @@ let spy = mock.method(console, 'log')
254
338
  spy.mock.restore?.()
255
339
  ```
256
340
 
341
+ ### Browser Testing
342
+
343
+ Browser tests run components in an actual browser environment via Playwright and are discovered by the `**/*.test.browser.{ts,tsx}` glob pattern (configurable via `glob.browser`). They use the same `describe`/`it` API as unit tests. Each in-browser test suite runs in an isolated `iframe` so it has access to its own `document` instance.
344
+
345
+ #### `render()`
346
+
347
+ `render`, exported from `remix/ui/test`, mounts a component into the DOM and returns a `RenderResult`:
348
+
349
+ ```ts
350
+ import * as assert from 'remix/assert'
351
+ import { describe, it } from 'remix/test'
352
+ import { render } from 'remix/ui/test'
353
+ import { Counter } from './counter.tsx'
354
+
355
+ describe('Counter', () => {
356
+ it('increments on click', async (t) => {
357
+ let { $, act, cleanup } = render(<Counter />)
358
+ t.after(cleanup)
359
+
360
+ assert.equal($('[data-count]')?.textContent, '0')
361
+ await act(() => $('[data-action="increment"]')?.click())
362
+ assert.equal($('[data-count]')?.textContent, '1')
363
+ })
364
+ })
365
+ ```
366
+
367
+ `RenderResult` provides:
368
+
369
+ | Property/Method | Description |
370
+ | --------------- | ----------------------------------------------------------------------- |
371
+ | `container` | The `HTMLElement` the component is mounted into |
372
+ | `root` | The Remix `VirtualRoot` the component is rendered in |
373
+ | `$(selector)` | Alias for `container.querySelector()` |
374
+ | `$$(selector)` | Alias for `container.querySelectorAll()` |
375
+ | `act(fn)` | Runs `fn` and flushes pending component updates |
376
+ | `cleanup()` | Unmounts and removes the container (pass to `t.after` for auto-cleanup) |
377
+
257
378
  ### E2E Testing
258
379
 
259
- E2E tests use [Playwright](https://playwright.dev) and are discovered by the `**/*.test.e2e.{ts,tsx}` glob pattern (configurable via `glob.e2e`). They use the same `describe`/`it` API as unit tests.
380
+ End-to-end (E2E) tests use [Playwright](https://playwright.dev) and are discovered by the `**/*.test.e2e.{ts,tsx}` glob pattern (configurable via `glob.e2e`). They use the same `describe`/`it` API as unit tests.
260
381
 
261
- E2E tests receive `t.serve()` on the test context, which starts an HTTP server with the given request handler and returns a Playwright [`Page`](https://playwright.dev/docs/api/class-page). The server and page are automatically closed after each test.
382
+ E2E tests receive `t.serve()` on the test context, which accepts a running test server and returns a Playwright [`Page`](https://playwright.dev/docs/api/class-page) whose `baseURL` points at that server. The server and page are automatically closed after each test.
262
383
 
263
384
  ```ts
264
385
  import * as assert from 'remix/assert'
386
+ import { createTestServer } from 'remix/node-fetch-server/test'
265
387
  import { describe, it } from 'remix/test'
266
388
  import { createRouter } from './router.ts'
267
389
 
268
390
  describe('checkout', () => {
269
391
  it('adds an item to the cart', async (t) => {
270
392
  let router = createRouter()
271
- let page = await t.serve(router.fetch)
393
+ let server = await createTestServer(router.fetch)
394
+ let page = await t.serve(server)
272
395
 
273
396
  await page.goto('/')
274
397
  await page.getByRole('button', { name: 'Add to Cart' }).click()
@@ -303,24 +426,6 @@ export default {
303
426
 
304
427
  Set `browser.open: true` to keep the browser open after tests finish — useful for debugging failures.
305
428
 
306
- ### Assertions
307
-
308
- `remix/test` re-exports `remix/assert`. See the [`@remix-run/assert` README](../assert/README.md) for full API documentation.
309
-
310
- ```ts
311
- import * as assert from 'remix/assert'
312
-
313
- assert.ok(value)
314
- assert.equal(actual, expected)
315
- assert.notEqual(actual, expected)
316
- assert.deepEqual(actual, expected)
317
- assert.notDeepEqual(actual, expected)
318
- assert.match(string, regexp)
319
- assert.throws(fn)
320
- await assert.rejects(asyncFn)
321
- assert.fail('message')
322
- ```
323
-
324
429
  ## License
325
430
 
326
431
  See [LICENSE](https://github.com/remix-run/remix/blob/main/LICENSE)
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=entry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entry.d.ts","sourceRoot":"","sources":["../../../src/app/client/entry.ts"],"names":[],"mappings":""}
@@ -0,0 +1,324 @@
1
+ import { normalizeLine } from "../../lib/normalize.js";
2
+ const STYLES = `
3
+ .rt-container {
4
+ font-family: monospace;
5
+ padding: 16px;
6
+ max-width: 900px;
7
+ }
8
+ .rt-summary {
9
+ margin-bottom: 16px;
10
+ line-height: 1.6;
11
+ }
12
+ .rt-summary-row {
13
+ display: block;
14
+ }
15
+ .rt-info {
16
+ color: #0ea5e9;
17
+ }
18
+ .rt-indent {
19
+ margin-left: 16px;
20
+ margin-top: 4px;
21
+ }
22
+ .rt-suite-details {
23
+ margin-bottom: 8px;
24
+ }
25
+ .rt-suite-summary {
26
+ cursor: pointer;
27
+ padding: 2px 0;
28
+ user-select: none;
29
+ }
30
+ .rt-suite-icon {
31
+ margin-left: 6px;
32
+ }
33
+ .rt-test-item {
34
+ padding: 3px 18px;
35
+ }
36
+ .rt-test-duration {
37
+ color: #999;
38
+ font-size: 0.85em;
39
+ }
40
+ .rt-error-pre {
41
+ margin: 4px 0 4px 16px;
42
+ padding: 8px 12px;
43
+ font-size: 12px;
44
+ color: #dc2626;
45
+ background: #fff5f5;
46
+ border-left: 3px solid #dc2626;
47
+ white-space: pre-wrap;
48
+ word-break: break-word;
49
+ }
50
+ .rt-error-stack {
51
+ color: #999;
52
+ margin-top: 6px;
53
+ }
54
+ .rt-button {
55
+ margin-top: 8px;
56
+ padding: 6px 12px;
57
+ cursor: pointer;
58
+ }
59
+ .rt-stack-link {
60
+ color: inherit;
61
+ text-decoration: underline;
62
+ text-decoration-color: #aaa;
63
+ }
64
+ .rt-passed {
65
+ color: #16a34a;
66
+ }
67
+ .rt-failed {
68
+ color: #dc2626;
69
+ }
70
+ .rt-muted {
71
+ color: #666;
72
+ }
73
+ .rt-todo {
74
+ color: #a16207;
75
+ }
76
+ `;
77
+ const styleEl = document.createElement('style');
78
+ styleEl.textContent = STYLES;
79
+ document.head.appendChild(styleEl);
80
+ const setupEl = document.getElementById('test-setup');
81
+ if (!setupEl?.textContent) {
82
+ throw new Error('Test runner: missing #test-setup payload');
83
+ }
84
+ const setup = JSON.parse(setupEl.textContent);
85
+ const root = document.getElementById('test-root');
86
+ if (!root) {
87
+ throw new Error('Test runner: missing #test-root mount point');
88
+ }
89
+ mountTests(root, setup);
90
+ function mountTests(host, setup) {
91
+ let startTime = performance.now();
92
+ let totals = { passed: 0, failed: 0, skipped: 0, todo: 0 };
93
+ let container = el('div', { id: 'test-status', className: 'rt-container' });
94
+ host.appendChild(container);
95
+ let summary = el('div', { className: 'rt-summary' });
96
+ container.appendChild(summary);
97
+ let testsRow = summaryRow();
98
+ let passRow = summaryRow();
99
+ let failRow = summaryRow();
100
+ let skippedRow = summaryRow();
101
+ let todoRow = summaryRow();
102
+ let durationRow = summaryRow();
103
+ summary.append(testsRow.el, passRow.el, failRow.el);
104
+ let suitesContainer = el('div');
105
+ container.appendChild(suitesContainer);
106
+ function renderSummary(done) {
107
+ let total = totals.passed + totals.failed + totals.skipped + totals.todo;
108
+ testsRow.text(`tests ${total}`);
109
+ passRow.text(`pass ${totals.passed}`);
110
+ failRow.text(`fail ${totals.failed}`);
111
+ if (totals.skipped > 0) {
112
+ if (!skippedRow.el.parentNode)
113
+ summary.appendChild(skippedRow.el);
114
+ skippedRow.text(`skipped ${totals.skipped}`);
115
+ }
116
+ if (totals.todo > 0) {
117
+ if (!todoRow.el.parentNode)
118
+ summary.appendChild(todoRow.el);
119
+ todoRow.text(`todo ${totals.todo}`);
120
+ }
121
+ if (done) {
122
+ if (!durationRow.el.parentNode)
123
+ summary.appendChild(durationRow.el);
124
+ durationRow.text(`duration_ms ${(performance.now() - startTime).toFixed(5)}`);
125
+ }
126
+ }
127
+ function appendFileSuites(fileResults) {
128
+ let suiteMap = new Map();
129
+ for (let test of fileResults.tests) {
130
+ let suite = test.suiteName || 'Tests';
131
+ if (!suiteMap.has(suite))
132
+ suiteMap.set(suite, []);
133
+ suiteMap.get(suite).push(test);
134
+ }
135
+ for (let [suiteName, tests] of suiteMap) {
136
+ suitesContainer.appendChild(buildSuite(suiteName, tests, setup.baseDir));
137
+ }
138
+ }
139
+ function appendRerunButton() {
140
+ let button = el('button', { className: 'rt-button', textContent: 'Re-run' });
141
+ button.type = 'button';
142
+ button.addEventListener('click', () => window.location.reload());
143
+ container.appendChild(button);
144
+ }
145
+ renderSummary(false);
146
+ void (async () => {
147
+ for (let testFile of setup.testPaths) {
148
+ let fileResults = await runInIframe(testFile);
149
+ await fetch('/file-results', {
150
+ method: 'POST',
151
+ headers: { 'Content-Type': 'application/json' },
152
+ body: JSON.stringify(fileResults),
153
+ });
154
+ totals.passed += fileResults.passed;
155
+ totals.failed += fileResults.failed;
156
+ totals.skipped += fileResults.skipped;
157
+ totals.todo += fileResults.todo;
158
+ appendFileSuites(fileResults);
159
+ renderSummary(false);
160
+ }
161
+ renderSummary(true);
162
+ appendRerunButton();
163
+ window.__testsDone = true;
164
+ })();
165
+ }
166
+ function runInIframe(testFile) {
167
+ return new Promise((resolve) => {
168
+ let iframe = document.createElement('iframe');
169
+ iframe.src = `/iframe?file=${encodeURIComponent(testFile)}`;
170
+ document.body.appendChild(iframe);
171
+ function onMessage(event) {
172
+ if (event.source !== iframe.contentWindow)
173
+ return;
174
+ window.removeEventListener('message', onMessage);
175
+ // Hide instead of remove so when coverage is enabled the iframe remains attached
176
+ // so V8 retains its scripts and Playwright can collect coverage at run end.
177
+ iframe.style.display = 'none';
178
+ if (event.data.type === 'test-results') {
179
+ let { passed, failed, skipped, todo, tests } = event.data.results;
180
+ resolve({
181
+ passed,
182
+ failed,
183
+ skipped,
184
+ todo,
185
+ tests: tests.map((t) => ({ ...t, filePath: testFile })),
186
+ });
187
+ }
188
+ else {
189
+ let { message, stack } = event.data.error;
190
+ resolve({
191
+ passed: 0,
192
+ failed: 1,
193
+ skipped: 0,
194
+ todo: 0,
195
+ tests: [
196
+ {
197
+ name: '',
198
+ suiteName: testFile,
199
+ filePath: testFile,
200
+ status: 'failed',
201
+ error: { message, stack },
202
+ duration: 0,
203
+ },
204
+ ],
205
+ });
206
+ }
207
+ }
208
+ window.addEventListener('message', onMessage);
209
+ });
210
+ }
211
+ function buildSuite(suiteName, tests, baseDir) {
212
+ let suiteFailed = tests.some((t) => t.status === 'failed');
213
+ let suiteAllSkipped = tests.every((t) => t.status === 'skipped');
214
+ let suiteAllTodo = tests.every((t) => t.status === 'todo');
215
+ let stateClass = suiteFailed
216
+ ? 'rt-failed'
217
+ : suiteAllSkipped
218
+ ? 'rt-muted'
219
+ : suiteAllTodo
220
+ ? 'rt-todo'
221
+ : 'rt-passed';
222
+ let icon = suiteFailed ? '✗' : suiteAllSkipped ? '↓' : suiteAllTodo ? '…' : '✓';
223
+ let suffix = suiteAllSkipped ? ' # skipped' : suiteAllTodo ? ' # todo' : '';
224
+ let details = el('details', { className: 'rt-suite-details' });
225
+ if (suiteFailed)
226
+ details.open = true;
227
+ let summary = el('summary', { className: `rt-suite-summary ${stateClass}` });
228
+ summary.appendChild(el('span', { className: 'rt-suite-icon', textContent: `${icon} ${suiteName}${suffix}` }));
229
+ details.appendChild(summary);
230
+ let body = el('div', { className: 'rt-indent' });
231
+ for (let test of tests) {
232
+ let item = buildTestItem(test, baseDir);
233
+ if (item)
234
+ body.appendChild(el('div', { className: 'rt-test-item' }, item));
235
+ }
236
+ details.appendChild(body);
237
+ return details;
238
+ }
239
+ function buildTestItem(test, baseDir) {
240
+ if (test.status === 'passed') {
241
+ let row = el('div', { className: 'rt-passed' });
242
+ row.append(`✓ ${test.name} `);
243
+ row.appendChild(el('span', {
244
+ className: 'rt-test-duration',
245
+ textContent: `(${test.duration.toFixed(2)}ms)`,
246
+ }));
247
+ return row;
248
+ }
249
+ if (test.status === 'failed') {
250
+ let row = el('div', { className: 'rt-failed' });
251
+ row.append(`✗ ${test.name} `);
252
+ row.appendChild(el('span', {
253
+ className: 'rt-test-duration',
254
+ textContent: `(${test.duration.toFixed(2)}ms)`,
255
+ }));
256
+ if (test.error) {
257
+ let pre = el('pre', { className: 'rt-error-pre' });
258
+ pre.append(test.error.message);
259
+ if (test.error.stack) {
260
+ let stackDiv = el('div', { className: 'rt-error-stack' });
261
+ stackDiv.appendChild(buildStack(test.error.stack, baseDir));
262
+ pre.appendChild(stackDiv);
263
+ }
264
+ row.appendChild(pre);
265
+ }
266
+ return row;
267
+ }
268
+ if (test.status === 'skipped' && test.name) {
269
+ return el('div', { className: 'rt-muted', textContent: `↓ ${test.name} # skipped` });
270
+ }
271
+ if (test.status === 'todo' && test.name) {
272
+ return el('div', { className: 'rt-todo', textContent: `… ${test.name} # todo` });
273
+ }
274
+ return null;
275
+ }
276
+ function buildStack(stack, baseDir) {
277
+ let frameLocRe = /([^():\s][^():]*\.[jt]sx?):(\d+):(\d+)/;
278
+ let frag = document.createDocumentFragment();
279
+ for (let raw of stack.split('\n')) {
280
+ let isTestModule = raw.includes('/@test/');
281
+ let line = normalizeLine(raw);
282
+ let match = isTestModule ? frameLocRe.exec(line) : null;
283
+ let div = document.createElement('div');
284
+ if (match) {
285
+ let [full, file, row, col] = match;
286
+ let abs = `${baseDir}/${file}`;
287
+ let href = `vscode://file/${abs}:${row}:${col}`;
288
+ div.append(line.slice(0, match.index));
289
+ let a = el('a', { className: 'rt-stack-link', textContent: full });
290
+ a.href = href;
291
+ div.appendChild(a);
292
+ div.append(line.slice(match.index + full.length));
293
+ }
294
+ else {
295
+ div.textContent = line;
296
+ }
297
+ frag.appendChild(div);
298
+ }
299
+ return frag;
300
+ }
301
+ function summaryRow() {
302
+ let row = el('span', { className: 'rt-summary-row' });
303
+ let icon = el('span', { className: 'rt-info', textContent: 'ℹ' });
304
+ let textNode = document.createTextNode('');
305
+ row.append(icon, ' ', textNode);
306
+ return {
307
+ el: row,
308
+ text(s) {
309
+ textNode.data = ' ' + s;
310
+ },
311
+ };
312
+ }
313
+ function el(tag, props, ...children) {
314
+ let node = document.createElement(tag);
315
+ if (props?.id)
316
+ node.id = props.id;
317
+ if (props?.className)
318
+ node.className = props.className;
319
+ if (props?.textContent != null)
320
+ node.textContent = props.textContent;
321
+ if (children.length)
322
+ node.append(...children);
323
+ return node;
324
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=iframe.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"iframe.d.ts","sourceRoot":"","sources":["../../../src/app/client/iframe.ts"],"names":[],"mappings":""}
@@ -0,0 +1,22 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ import { runTests } from "../../lib/executor.js";
10
+ const params = new URLSearchParams(location.search);
11
+ const testFile = params.get('file');
12
+ try {
13
+ await import(__rewriteRelativeImportExtension(testFile));
14
+ let results = await runTests();
15
+ window.parent.postMessage({ type: 'test-results', results }, '*');
16
+ }
17
+ catch (error) {
18
+ window.parent.postMessage({
19
+ type: 'test-error',
20
+ error: { message: error?.message ?? String(error), stack: error?.stack },
21
+ }, '*');
22
+ }
@@ -0,0 +1,6 @@
1
+ import * as http from 'node:http';
2
+ export declare function startServer(browserFiles: string[]): Promise<{
3
+ server: http.Server;
4
+ port: number;
5
+ }>;
6
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/app/server.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,IAAI,MAAM,WAAW,CAAA;AAQjC,wBAAsB,WAAW,CAC/B,YAAY,EAAE,MAAM,EAAE,GACrB,OAAO,CAAC;IAAE,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAkChD"}