@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.
- package/README.md +161 -50
- 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 +328 -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 +319 -140
- package/dist/index.d.ts +2 -1
- 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 +59 -14
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +181 -38
- 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 +13 -0
- package/dist/lib/fake-timers.d.ts.map +1 -0
- package/dist/lib/fake-timers.js +64 -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 +38 -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 +12 -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 +12 -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 +12 -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 +11 -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 +123 -0
- package/dist/lib/runner.d.ts +24 -2
- package/dist/lib/runner.d.ts.map +1 -1
- package/dist/lib/runner.js +216 -38
- 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-file.d.ts +11 -0
- package/dist/lib/worker-e2e-file.d.ts.map +1 -0
- package/dist/lib/worker-e2e-file.js +69 -0
- package/dist/lib/worker-e2e.js +11 -46
- package/dist/lib/worker-process.d.ts +2 -0
- package/dist/lib/worker-process.d.ts.map +1 -0
- package/dist/lib/worker-process.js +55 -0
- package/dist/lib/worker-results.d.ts +3 -0
- package/dist/lib/worker-results.d.ts.map +1 -0
- package/dist/lib/worker-results.js +20 -0
- package/dist/lib/worker-server.d.ts +10 -0
- package/dist/lib/worker-server.d.ts.map +1 -0
- package/dist/lib/worker-server.js +113 -0
- package/dist/lib/worker.js +7 -28
- 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 +357 -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 +382 -145
- package/src/index.ts +2 -1
- package/src/lib/colors.ts +3 -0
- package/src/lib/config.ts +266 -54
- 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 +89 -0
- package/src/lib/import-module.ts +39 -0
- package/src/lib/{utils.ts → normalize.ts} +0 -18
- package/src/lib/playwright.ts +5 -7
- package/src/lib/reporters/dot.ts +12 -2
- package/src/lib/reporters/files.ts +12 -2
- package/src/lib/reporters/index.ts +4 -5
- package/src/lib/reporters/results.ts +29 -0
- package/src/lib/reporters/spec.ts +12 -2
- package/src/lib/reporters/tap.ts +11 -2
- package/src/lib/runner-browser.ts +171 -0
- package/src/lib/runner.ts +308 -53
- package/src/lib/runtime.ts +2 -0
- package/src/lib/ts-transform.ts +36 -0
- package/src/lib/worker-e2e-file.ts +98 -0
- package/src/lib/worker-e2e.ts +14 -49
- package/src/lib/worker-process.ts +69 -0
- package/src/lib/worker-results.ts +22 -0
- package/src/lib/worker-server.ts +123 -0
- package/src/lib/worker.ts +8 -28
- 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,16 +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 one or more globs as positional arguments to override:
|
|
43
45
|
|
|
44
46
|
```sh
|
|
45
|
-
remix
|
|
47
|
+
remix test "src/**/*.test.ts"
|
|
48
|
+
remix test "src/**/*.test.ts" "tests/**/*.test.tsx"
|
|
46
49
|
```
|
|
47
50
|
|
|
48
|
-
Or, you may control via the `glob.test` config field/CLI arg.
|
|
51
|
+
Or, you may control via the `glob.test` config field/CLI arg. Each `glob.*` field accepts a single string or an array of patterns, and `--glob.*` flags can be repeated on the CLI.
|
|
52
|
+
|
|
53
|
+
If you install `@remix-run/test` directly instead of the umbrella `remix` package, the same runner is available as `remix-test`:
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
npm i @remix-run/test
|
|
57
|
+
remix-test
|
|
58
|
+
```
|
|
49
59
|
|
|
50
60
|
### Config File
|
|
51
61
|
|
|
@@ -67,10 +77,32 @@ export default {
|
|
|
67
77
|
// Max number of concurrent test workers (default `os.availableParallelism()`)
|
|
68
78
|
concurrency: 2,
|
|
69
79
|
|
|
80
|
+
// Pool for server and E2E test files ("forks", "threads")
|
|
81
|
+
pool: 'forks',
|
|
82
|
+
|
|
83
|
+
// Code coverage options
|
|
84
|
+
coverage: {
|
|
85
|
+
// Enable coverage reporting
|
|
86
|
+
enabled: true,
|
|
87
|
+
// Output directory (default: ".coverage")
|
|
88
|
+
dir: '.coverage',
|
|
89
|
+
// Glob pattern(s) to include/exclude
|
|
90
|
+
include: 'src/**',
|
|
91
|
+
exclude: 'src/**/*.test.ts',
|
|
92
|
+
// Minimum thresholds (%)
|
|
93
|
+
statements: 80,
|
|
94
|
+
lines: 80,
|
|
95
|
+
branches: 80,
|
|
96
|
+
functions: 80,
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
// Glob pattern(s) identifying test files
|
|
70
100
|
glob: {
|
|
71
|
-
//
|
|
72
|
-
test: '**/*.test
|
|
73
|
-
//
|
|
101
|
+
// All test files (default: "**/*.test{,.browser,.e2e}.{ts,tsx}").
|
|
102
|
+
test: '**/*.test{,.browser,.e2e}.ts',
|
|
103
|
+
// Browser test files (default: "**/*.test.browser.{ts,tsx}")
|
|
104
|
+
browser: '**/*.test.browser.ts',
|
|
105
|
+
// E2E test files (default: "**/*.test.e2e.{ts,tsx}")
|
|
74
106
|
e2e: '**/*.test.e2e.ts',
|
|
75
107
|
},
|
|
76
108
|
|
|
@@ -87,7 +119,7 @@ export default {
|
|
|
87
119
|
},
|
|
88
120
|
},
|
|
89
121
|
|
|
90
|
-
//
|
|
122
|
+
// Playwright project(s) to run E2E tests for
|
|
91
123
|
project: 'chromium',
|
|
92
124
|
|
|
93
125
|
// Test reporter ("spec", "files", "tap", "dot")
|
|
@@ -96,8 +128,8 @@ export default {
|
|
|
96
128
|
// Path to a setup module (see Setup section below)
|
|
97
129
|
setup: './test/setup.ts',
|
|
98
130
|
|
|
99
|
-
//
|
|
100
|
-
type: 'server,e2e',
|
|
131
|
+
// Test type(s) to run ("server", "browser", "e2e")
|
|
132
|
+
type: ['server', 'browser', 'e2e'],
|
|
101
133
|
|
|
102
134
|
// Watch for file changes and re-run
|
|
103
135
|
watch: false,
|
|
@@ -109,24 +141,34 @@ export default {
|
|
|
109
141
|
You can point to a different config file location with the `--config` flag:
|
|
110
142
|
|
|
111
143
|
```sh
|
|
112
|
-
remix
|
|
144
|
+
remix test --config ./tests/config.ts
|
|
113
145
|
```
|
|
114
146
|
|
|
115
147
|
You may also specify any config field as a CLI flag which will take precedence over config file values:
|
|
116
148
|
|
|
117
|
-
| Flag | Short
|
|
118
|
-
| --------------------------- |
|
|
119
|
-
| `--browser.echo` |
|
|
120
|
-
| `--browser.open` |
|
|
121
|
-
| `--concurrency <n>` | `-c`
|
|
122
|
-
| `--
|
|
123
|
-
| `--
|
|
124
|
-
| `--
|
|
125
|
-
| `--
|
|
126
|
-
| `--
|
|
127
|
-
| `--
|
|
128
|
-
| `--
|
|
129
|
-
| `--
|
|
149
|
+
| Flag | Short |
|
|
150
|
+
| --------------------------- | --------- | --- |
|
|
151
|
+
| `--browser.echo` | |
|
|
152
|
+
| `--browser.open` | |
|
|
153
|
+
| `--concurrency <n>` | `-c` |
|
|
154
|
+
| `--coverage` | |
|
|
155
|
+
| `--coverage.dir <path>` | |
|
|
156
|
+
| `--coverage.include` | |
|
|
157
|
+
| `--coverage.exclude` | |
|
|
158
|
+
| `--coverage.statements` | |
|
|
159
|
+
| `--coverage.lines` | |
|
|
160
|
+
| `--coverage.branches` | |
|
|
161
|
+
| `--coverage.functions` | |
|
|
162
|
+
| `--glob.test` | |
|
|
163
|
+
| `--glob.browser` | |
|
|
164
|
+
| `--glob.e2e` | |
|
|
165
|
+
| `--playwrightConfig <path>` | |
|
|
166
|
+
| `--pool <forks | threads>` | |
|
|
167
|
+
| `--project <name>` | `-p` |
|
|
168
|
+
| `--reporter <name>` | `-r` |
|
|
169
|
+
| `--setup <path>` | `-s` |
|
|
170
|
+
| `--type <name>` | `-t` |
|
|
171
|
+
| `--watch` | `-w` |
|
|
130
172
|
|
|
131
173
|
### Setup
|
|
132
174
|
|
|
@@ -172,11 +214,30 @@ suite('My Test Suite', () => {
|
|
|
172
214
|
})
|
|
173
215
|
```
|
|
174
216
|
|
|
217
|
+
### Programmatic runner
|
|
218
|
+
|
|
219
|
+
`@remix-run/test/cli` exports `runRemixTest()` for tools that want to run the test runner without
|
|
220
|
+
exiting the current process:
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
import { runRemixTest } from '@remix-run/test/cli'
|
|
224
|
+
|
|
225
|
+
let exitCode = await runRemixTest({
|
|
226
|
+
argv: ['--type', 'server'],
|
|
227
|
+
cwd: process.cwd(),
|
|
228
|
+
})
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
`runRemixTest()` returns the runner exit code. The `remix test` and `remix-test` bin wrappers call
|
|
232
|
+
`process.exit()` with that code when the run finishes so open workers, browsers, or project handles
|
|
233
|
+
cannot keep the CLI alive.
|
|
234
|
+
|
|
175
235
|
### Test Context
|
|
176
236
|
|
|
177
237
|
Each test callback receives a `TestContext` (`t`) as its first argument with helpful test utilities.
|
|
178
238
|
|
|
179
239
|
```ts
|
|
240
|
+
// from 'remix/test'
|
|
180
241
|
interface TestContext {
|
|
181
242
|
// Register a cleanup function to run after the test completes
|
|
182
243
|
after(fn: () => void): void
|
|
@@ -194,8 +255,11 @@ interface TestContext {
|
|
|
194
255
|
): MockFunction
|
|
195
256
|
}
|
|
196
257
|
|
|
197
|
-
//
|
|
198
|
-
|
|
258
|
+
// Replace global timer functions with controllable fakes
|
|
259
|
+
useFakeTimers(): FakeTimers
|
|
260
|
+
|
|
261
|
+
// E2E only: connect a running test server to a Playwright Page
|
|
262
|
+
serve(server: { baseUrl: string; close(): Promise<void> }): Promise<Page>
|
|
199
263
|
}
|
|
200
264
|
```
|
|
201
265
|
|
|
@@ -230,14 +294,40 @@ it('cleanup', (t) => {
|
|
|
230
294
|
})
|
|
231
295
|
```
|
|
232
296
|
|
|
297
|
+
#### Fake Timers
|
|
298
|
+
|
|
299
|
+
`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.
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
it('debounces a callback', (t) => {
|
|
303
|
+
let timers = t.useFakeTimers()
|
|
304
|
+
let calls = 0
|
|
305
|
+
let debounced = debounce(() => calls++, 300)
|
|
306
|
+
|
|
307
|
+
debounced()
|
|
308
|
+
timers.advance(299)
|
|
309
|
+
assert.equal(calls, 0)
|
|
310
|
+
timers.advance(1)
|
|
311
|
+
assert.equal(calls, 1)
|
|
312
|
+
})
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
| Method | Description |
|
|
316
|
+
| ------------- | --------------------------------------------------------------------------- |
|
|
317
|
+
| `advance(ms)` | Advance the clock by `ms` milliseconds, firing any elapsed timers |
|
|
318
|
+
| `restore()` | Restore the original timer functions (called automatically after each test) |
|
|
319
|
+
|
|
233
320
|
#### E2E
|
|
234
321
|
|
|
235
|
-
In E2E test files, `t.serve()`
|
|
322
|
+
In E2E test files, `t.serve()` connects a running test server to a Playwright `Page`. See [E2E Testing](#e2e-testing) for details.
|
|
236
323
|
|
|
237
324
|
```ts
|
|
325
|
+
import { createTestServer } from 'remix/node-fetch-server/test'
|
|
326
|
+
|
|
238
327
|
it('navigates to home', async (t) => {
|
|
239
328
|
let router = createRouter()
|
|
240
|
-
let
|
|
329
|
+
let server = await createTestServer(router.fetch)
|
|
330
|
+
let page = await t.serve(server)
|
|
241
331
|
await page.goto('/')
|
|
242
332
|
})
|
|
243
333
|
```
|
|
@@ -254,21 +344,60 @@ let spy = mock.method(console, 'log')
|
|
|
254
344
|
spy.mock.restore?.()
|
|
255
345
|
```
|
|
256
346
|
|
|
347
|
+
### Browser Testing
|
|
348
|
+
|
|
349
|
+
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.
|
|
350
|
+
|
|
351
|
+
#### `render()`
|
|
352
|
+
|
|
353
|
+
`render`, exported from `remix/ui/test`, mounts a component into the DOM and returns a `RenderResult`:
|
|
354
|
+
|
|
355
|
+
```ts
|
|
356
|
+
import * as assert from 'remix/assert'
|
|
357
|
+
import { describe, it } from 'remix/test'
|
|
358
|
+
import { render } from 'remix/ui/test'
|
|
359
|
+
import { Counter } from './counter.tsx'
|
|
360
|
+
|
|
361
|
+
describe('Counter', () => {
|
|
362
|
+
it('increments on click', async (t) => {
|
|
363
|
+
let { $, act, cleanup } = render(<Counter />)
|
|
364
|
+
t.after(cleanup)
|
|
365
|
+
|
|
366
|
+
assert.equal($('[data-count]')?.textContent, '0')
|
|
367
|
+
await act(() => $('[data-action="increment"]')?.click())
|
|
368
|
+
assert.equal($('[data-count]')?.textContent, '1')
|
|
369
|
+
})
|
|
370
|
+
})
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
`RenderResult` provides:
|
|
374
|
+
|
|
375
|
+
| Property/Method | Description |
|
|
376
|
+
| --------------- | ----------------------------------------------------------------------- |
|
|
377
|
+
| `container` | The `HTMLElement` the component is mounted into |
|
|
378
|
+
| `root` | The Remix `VirtualRoot` the component is rendered in |
|
|
379
|
+
| `$(selector)` | Alias for `container.querySelector()` |
|
|
380
|
+
| `$$(selector)` | Alias for `container.querySelectorAll()` |
|
|
381
|
+
| `act(fn)` | Runs `fn` and flushes pending component updates |
|
|
382
|
+
| `cleanup()` | Unmounts and removes the container (pass to `t.after` for auto-cleanup) |
|
|
383
|
+
|
|
257
384
|
### E2E Testing
|
|
258
385
|
|
|
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.
|
|
386
|
+
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
387
|
|
|
261
|
-
E2E tests receive `t.serve()` on the test context, which
|
|
388
|
+
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
389
|
|
|
263
390
|
```ts
|
|
264
391
|
import * as assert from 'remix/assert'
|
|
392
|
+
import { createTestServer } from 'remix/node-fetch-server/test'
|
|
265
393
|
import { describe, it } from 'remix/test'
|
|
266
394
|
import { createRouter } from './router.ts'
|
|
267
395
|
|
|
268
396
|
describe('checkout', () => {
|
|
269
397
|
it('adds an item to the cart', async (t) => {
|
|
270
398
|
let router = createRouter()
|
|
271
|
-
let
|
|
399
|
+
let server = await createTestServer(router.fetch)
|
|
400
|
+
let page = await t.serve(server)
|
|
272
401
|
|
|
273
402
|
await page.goto('/')
|
|
274
403
|
await page.getByRole('button', { name: 'Add to Cart' }).click()
|
|
@@ -303,24 +432,6 @@ export default {
|
|
|
303
432
|
|
|
304
433
|
Set `browser.open: true` to keep the browser open after tests finish — useful for debugging failures.
|
|
305
434
|
|
|
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
435
|
## License
|
|
325
436
|
|
|
326
437
|
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,328 @@
|
|
|
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
|
+
// Make the iframe as big so we don't get unintentional scrolling in test UIs
|
|
171
|
+
let parentBody = iframe.contentWindow?.document.body;
|
|
172
|
+
iframe.width = Math.max(parentBody?.scrollWidth ?? 0, 800).toString();
|
|
173
|
+
iframe.height = Math.max(Math.round((parentBody?.scrollHeight ?? 0) / 2), 400).toString();
|
|
174
|
+
document.body.appendChild(iframe);
|
|
175
|
+
function onMessage(event) {
|
|
176
|
+
if (event.source !== iframe.contentWindow)
|
|
177
|
+
return;
|
|
178
|
+
window.removeEventListener('message', onMessage);
|
|
179
|
+
// Hide instead of remove so when coverage is enabled the iframe remains attached
|
|
180
|
+
// so V8 retains its scripts and Playwright can collect coverage at run end.
|
|
181
|
+
iframe.style.display = 'none';
|
|
182
|
+
if (event.data.type === 'test-results') {
|
|
183
|
+
let { passed, failed, skipped, todo, tests } = event.data.results;
|
|
184
|
+
resolve({
|
|
185
|
+
passed,
|
|
186
|
+
failed,
|
|
187
|
+
skipped,
|
|
188
|
+
todo,
|
|
189
|
+
tests: tests.map((t) => ({ ...t, filePath: testFile })),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
let { message, stack } = event.data.error;
|
|
194
|
+
resolve({
|
|
195
|
+
passed: 0,
|
|
196
|
+
failed: 1,
|
|
197
|
+
skipped: 0,
|
|
198
|
+
todo: 0,
|
|
199
|
+
tests: [
|
|
200
|
+
{
|
|
201
|
+
name: '',
|
|
202
|
+
suiteName: testFile,
|
|
203
|
+
filePath: testFile,
|
|
204
|
+
status: 'failed',
|
|
205
|
+
error: { message, stack },
|
|
206
|
+
duration: 0,
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
window.addEventListener('message', onMessage);
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
function buildSuite(suiteName, tests, baseDir) {
|
|
216
|
+
let suiteFailed = tests.some((t) => t.status === 'failed');
|
|
217
|
+
let suiteAllSkipped = tests.every((t) => t.status === 'skipped');
|
|
218
|
+
let suiteAllTodo = tests.every((t) => t.status === 'todo');
|
|
219
|
+
let stateClass = suiteFailed
|
|
220
|
+
? 'rt-failed'
|
|
221
|
+
: suiteAllSkipped
|
|
222
|
+
? 'rt-muted'
|
|
223
|
+
: suiteAllTodo
|
|
224
|
+
? 'rt-todo'
|
|
225
|
+
: 'rt-passed';
|
|
226
|
+
let icon = suiteFailed ? '✗' : suiteAllSkipped ? '↓' : suiteAllTodo ? '…' : '✓';
|
|
227
|
+
let suffix = suiteAllSkipped ? ' # skipped' : suiteAllTodo ? ' # todo' : '';
|
|
228
|
+
let details = el('details', { className: 'rt-suite-details' });
|
|
229
|
+
if (suiteFailed)
|
|
230
|
+
details.open = true;
|
|
231
|
+
let summary = el('summary', { className: `rt-suite-summary ${stateClass}` });
|
|
232
|
+
summary.appendChild(el('span', { className: 'rt-suite-icon', textContent: `${icon} ${suiteName}${suffix}` }));
|
|
233
|
+
details.appendChild(summary);
|
|
234
|
+
let body = el('div', { className: 'rt-indent' });
|
|
235
|
+
for (let test of tests) {
|
|
236
|
+
let item = buildTestItem(test, baseDir);
|
|
237
|
+
if (item)
|
|
238
|
+
body.appendChild(el('div', { className: 'rt-test-item' }, item));
|
|
239
|
+
}
|
|
240
|
+
details.appendChild(body);
|
|
241
|
+
return details;
|
|
242
|
+
}
|
|
243
|
+
function buildTestItem(test, baseDir) {
|
|
244
|
+
if (test.status === 'passed') {
|
|
245
|
+
let row = el('div', { className: 'rt-passed' });
|
|
246
|
+
row.append(`✓ ${test.name} `);
|
|
247
|
+
row.appendChild(el('span', {
|
|
248
|
+
className: 'rt-test-duration',
|
|
249
|
+
textContent: `(${test.duration.toFixed(2)}ms)`,
|
|
250
|
+
}));
|
|
251
|
+
return row;
|
|
252
|
+
}
|
|
253
|
+
if (test.status === 'failed') {
|
|
254
|
+
let row = el('div', { className: 'rt-failed' });
|
|
255
|
+
row.append(`✗ ${test.name} `);
|
|
256
|
+
row.appendChild(el('span', {
|
|
257
|
+
className: 'rt-test-duration',
|
|
258
|
+
textContent: `(${test.duration.toFixed(2)}ms)`,
|
|
259
|
+
}));
|
|
260
|
+
if (test.error) {
|
|
261
|
+
let pre = el('pre', { className: 'rt-error-pre' });
|
|
262
|
+
pre.append(test.error.message);
|
|
263
|
+
if (test.error.stack) {
|
|
264
|
+
let stackDiv = el('div', { className: 'rt-error-stack' });
|
|
265
|
+
stackDiv.appendChild(buildStack(test.error.stack, baseDir));
|
|
266
|
+
pre.appendChild(stackDiv);
|
|
267
|
+
}
|
|
268
|
+
row.appendChild(pre);
|
|
269
|
+
}
|
|
270
|
+
return row;
|
|
271
|
+
}
|
|
272
|
+
if (test.status === 'skipped' && test.name) {
|
|
273
|
+
return el('div', { className: 'rt-muted', textContent: `↓ ${test.name} # skipped` });
|
|
274
|
+
}
|
|
275
|
+
if (test.status === 'todo' && test.name) {
|
|
276
|
+
return el('div', { className: 'rt-todo', textContent: `… ${test.name} # todo` });
|
|
277
|
+
}
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
function buildStack(stack, baseDir) {
|
|
281
|
+
let frameLocRe = /([^():\s][^():]*\.[jt]sx?):(\d+):(\d+)/;
|
|
282
|
+
let frag = document.createDocumentFragment();
|
|
283
|
+
for (let raw of stack.split('\n')) {
|
|
284
|
+
let isTestModule = raw.includes('/@test/');
|
|
285
|
+
let line = normalizeLine(raw);
|
|
286
|
+
let match = isTestModule ? frameLocRe.exec(line) : null;
|
|
287
|
+
let div = document.createElement('div');
|
|
288
|
+
if (match) {
|
|
289
|
+
let [full, file, row, col] = match;
|
|
290
|
+
let abs = `${baseDir}/${file}`;
|
|
291
|
+
let href = `vscode://file/${abs}:${row}:${col}`;
|
|
292
|
+
div.append(line.slice(0, match.index));
|
|
293
|
+
let a = el('a', { className: 'rt-stack-link', textContent: full });
|
|
294
|
+
a.href = href;
|
|
295
|
+
div.appendChild(a);
|
|
296
|
+
div.append(line.slice(match.index + full.length));
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
div.textContent = line;
|
|
300
|
+
}
|
|
301
|
+
frag.appendChild(div);
|
|
302
|
+
}
|
|
303
|
+
return frag;
|
|
304
|
+
}
|
|
305
|
+
function summaryRow() {
|
|
306
|
+
let row = el('span', { className: 'rt-summary-row' });
|
|
307
|
+
let icon = el('span', { className: 'rt-info', textContent: 'ℹ' });
|
|
308
|
+
let textNode = document.createTextNode('');
|
|
309
|
+
row.append(icon, ' ', textNode);
|
|
310
|
+
return {
|
|
311
|
+
el: row,
|
|
312
|
+
text(s) {
|
|
313
|
+
textNode.data = ' ' + s;
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
function el(tag, props, ...children) {
|
|
318
|
+
let node = document.createElement(tag);
|
|
319
|
+
if (props?.id)
|
|
320
|
+
node.id = props.id;
|
|
321
|
+
if (props?.className)
|
|
322
|
+
node.className = props.className;
|
|
323
|
+
if (props?.textContent != null)
|
|
324
|
+
node.textContent = props.textContent;
|
|
325
|
+
if (children.length)
|
|
326
|
+
node.append(...children);
|
|
327
|
+
return node;
|
|
328
|
+
}
|
|
@@ -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
|
+
}
|