@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.
- package/README.md +140 -35
- package/dist/app/client/entry.d.ts +2 -0
- package/dist/app/client/entry.d.ts.map +1 -0
- package/dist/app/client/entry.js +324 -0
- package/dist/app/client/iframe.d.ts +2 -0
- package/dist/app/client/iframe.d.ts.map +1 -0
- package/dist/app/client/iframe.js +22 -0
- package/dist/app/server.d.ts +6 -0
- package/dist/app/server.d.ts.map +1 -0
- package/dist/app/server.js +303 -0
- package/dist/cli-entry.d.ts +3 -0
- package/dist/cli-entry.d.ts.map +1 -0
- package/dist/cli-entry.js +14 -0
- package/dist/cli.d.ts +7 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +273 -139
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/lib/colors.d.ts +2 -0
- package/dist/lib/colors.d.ts.map +1 -0
- package/dist/lib/colors.js +2 -0
- package/dist/lib/config.d.ts +32 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +125 -22
- package/dist/lib/context.d.ts +37 -13
- package/dist/lib/context.d.ts.map +1 -1
- package/dist/lib/context.js +19 -3
- package/dist/lib/coverage-loader.d.ts +16 -0
- package/dist/lib/coverage-loader.d.ts.map +1 -0
- package/dist/lib/coverage-loader.js +20 -0
- package/dist/lib/coverage.d.ts +28 -0
- package/dist/lib/coverage.d.ts.map +1 -0
- package/dist/lib/coverage.js +212 -0
- package/dist/lib/executor.d.ts +3 -26
- package/dist/lib/executor.d.ts.map +1 -1
- package/dist/lib/executor.js +11 -6
- package/dist/lib/fake-timers.d.ts +6 -0
- package/dist/lib/fake-timers.d.ts.map +1 -0
- package/dist/lib/fake-timers.js +45 -0
- package/dist/lib/import-module.d.ts +2 -0
- package/dist/lib/import-module.d.ts.map +1 -0
- package/dist/lib/import-module.js +29 -0
- package/dist/lib/normalize.d.ts +2 -0
- package/dist/lib/normalize.d.ts.map +1 -0
- package/dist/lib/{utils.js → normalize.js} +0 -9
- package/dist/lib/playwright.d.ts +1 -1
- package/dist/lib/playwright.d.ts.map +1 -1
- package/dist/lib/playwright.js +5 -8
- package/dist/lib/reporters/dot.d.ts +1 -2
- package/dist/lib/reporters/dot.d.ts.map +1 -1
- package/dist/lib/reporters/dot.js +2 -1
- package/dist/lib/reporters/files.d.ts +1 -2
- package/dist/lib/reporters/files.d.ts.map +1 -1
- package/dist/lib/reporters/files.js +2 -1
- package/dist/lib/reporters/index.d.ts +4 -5
- package/dist/lib/reporters/index.d.ts.map +1 -1
- package/dist/lib/reporters/index.js +3 -3
- package/dist/lib/reporters/results.d.ts +30 -0
- package/dist/lib/reporters/results.d.ts.map +1 -0
- package/dist/lib/reporters/results.js +1 -0
- package/dist/lib/reporters/spec.d.ts +1 -2
- package/dist/lib/reporters/spec.d.ts.map +1 -1
- package/dist/lib/reporters/spec.js +2 -1
- package/dist/lib/reporters/tap.d.ts +1 -2
- package/dist/lib/reporters/tap.d.ts.map +1 -1
- package/dist/lib/reporters/tap.js +1 -1
- package/dist/lib/runner-browser.d.ts +21 -0
- package/dist/lib/runner-browser.d.ts.map +1 -0
- package/dist/lib/runner-browser.js +117 -0
- package/dist/lib/runner.d.ts +7 -2
- package/dist/lib/runner.d.ts.map +1 -1
- package/dist/lib/runner.js +33 -4
- package/dist/lib/runtime.d.ts +2 -0
- package/dist/lib/runtime.d.ts.map +1 -0
- package/dist/lib/runtime.js +2 -0
- package/dist/lib/ts-transform.d.ts +4 -0
- package/dist/lib/ts-transform.d.ts.map +1 -0
- package/dist/lib/ts-transform.js +29 -0
- package/dist/lib/worker-e2e.js +5 -4
- package/dist/lib/worker.js +31 -3
- package/dist/test/coverage/fixture.d.ts +5 -0
- package/dist/test/coverage/fixture.d.ts.map +1 -0
- package/dist/test/coverage/fixture.js +32 -0
- package/dist/test/coverage/test-browser.d.ts +2 -0
- package/dist/test/coverage/test-browser.d.ts.map +1 -0
- package/dist/test/coverage/test-browser.js +24 -0
- package/dist/test/coverage/test-e2e.d.ts +2 -0
- package/dist/test/coverage/test-e2e.d.ts.map +1 -0
- package/dist/test/coverage/test-e2e.js +60 -0
- package/dist/test/coverage/test-unit.d.ts +2 -0
- package/dist/test/coverage/test-unit.d.ts.map +1 -0
- package/dist/test/coverage/test-unit.js +27 -0
- package/dist/test/framework.test.browser.d.ts +2 -0
- package/dist/test/framework.test.browser.d.ts.map +1 -0
- package/dist/test/framework.test.browser.js +107 -0
- package/dist/test/framework.test.e2e.d.ts.map +1 -0
- package/dist/test/framework.test.e2e.js +34 -0
- package/package.json +30 -9
- package/src/app/client/entry.ts +353 -0
- package/src/app/client/iframe.ts +18 -0
- package/src/app/server.ts +336 -0
- package/src/cli-entry.ts +15 -0
- package/src/cli.ts +322 -148
- package/src/index.ts +1 -0
- package/src/lib/colors.ts +3 -0
- package/src/lib/config.ts +169 -23
- package/src/lib/context.ts +59 -17
- package/src/lib/coverage-loader.ts +31 -0
- package/src/lib/coverage.ts +320 -0
- package/src/lib/executor.ts +18 -35
- package/src/lib/fake-timers.ts +64 -0
- package/src/lib/import-module.ts +29 -0
- package/src/lib/{utils.ts → normalize.ts} +0 -18
- package/src/lib/playwright.ts +5 -7
- package/src/lib/reporters/dot.ts +3 -2
- package/src/lib/reporters/files.ts +3 -2
- package/src/lib/reporters/index.ts +4 -5
- package/src/lib/reporters/results.ts +29 -0
- package/src/lib/reporters/spec.ts +3 -2
- package/src/lib/reporters/tap.ts +2 -2
- package/src/lib/runner-browser.ts +165 -0
- package/src/lib/runner.ts +62 -10
- package/src/lib/runtime.ts +2 -0
- package/src/lib/ts-transform.ts +36 -0
- package/src/lib/worker-e2e.ts +7 -5
- package/src/lib/worker.ts +24 -4
- package/src/test/coverage/fixture.ts +34 -0
- package/src/test/coverage/test-browser.ts +29 -0
- package/src/test/coverage/test-e2e.ts +70 -0
- package/src/test/coverage/test-unit.ts +32 -0
- package/tsconfig.json +3 -1
- package/dist/lib/e2e-server.d.ts +0 -11
- package/dist/lib/e2e-server.d.ts.map +0 -1
- package/dist/lib/e2e-server.js +0 -15
- package/dist/lib/framework.test.d.ts +0 -2
- package/dist/lib/framework.test.d.ts.map +0 -1
- package/dist/lib/framework.test.e2e.d.ts.map +0 -1
- package/dist/lib/framework.test.e2e.js +0 -29
- package/dist/lib/framework.test.js +0 -283
- package/dist/lib/utils.d.ts +0 -16
- package/dist/lib/utils.d.ts.map +0 -1
- package/src/lib/e2e-server.ts +0 -28
- /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
|
|
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
|
|
41
|
+
remix test
|
|
40
42
|
```
|
|
41
43
|
|
|
42
|
-
By default, `remix
|
|
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
|
|
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
|
|
72
|
-
test: '**/*.test
|
|
73
|
-
//
|
|
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
|
|
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
|
-
//
|
|
198
|
-
|
|
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()`
|
|
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
|
|
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
|
|
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
|
|
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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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"}
|