@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/src/lib/config.ts
CHANGED
|
@@ -1,9 +1,34 @@
|
|
|
1
|
+
import * as fsp from 'node:fs/promises'
|
|
1
2
|
import * as os from 'node:os'
|
|
2
3
|
import * as path from 'node:path'
|
|
3
|
-
import
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
4
5
|
import * as util from 'node:util'
|
|
5
|
-
import { tsImport } from 'tsx/esm/api'
|
|
6
6
|
import type { PlaywrightTestConfig } from 'playwright/test'
|
|
7
|
+
import { importModule } from './import-module.ts'
|
|
8
|
+
|
|
9
|
+
export const IS_RUNNING_FROM_SRC = path.extname(new URL(import.meta.url).pathname) === '.ts'
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
* The root directory for the test code. Coverage URLs are emitted as
|
|
13
|
+
* `/scripts/<rel-from-rootDir>` and resolved back via the same anchor.
|
|
14
|
+
*
|
|
15
|
+
* - In a published install: `process.cwd()`, since deps and user source all
|
|
16
|
+
* live under it.
|
|
17
|
+
* - In monorepo src mode: the monorepo root, computed by walking back from
|
|
18
|
+
* the resolved `@remix-run/test` source path. `process.cwd()` doesn't work
|
|
19
|
+
* here because workspace deps and node_modules live above the per-package
|
|
20
|
+
* cwd.
|
|
21
|
+
*/
|
|
22
|
+
export function getBrowserTestRootDir(): string {
|
|
23
|
+
return IS_RUNNING_FROM_SRC
|
|
24
|
+
? // Resolve to packages/test/src/index.ts and the pop 3 directories off to the repo root
|
|
25
|
+
path
|
|
26
|
+
.dirname(fileURLToPath(import.meta.resolve('@remix-run/test')))
|
|
27
|
+
.split(path.sep)
|
|
28
|
+
.slice(0, -3)
|
|
29
|
+
.join(path.sep)
|
|
30
|
+
: process.cwd()
|
|
31
|
+
}
|
|
7
32
|
|
|
8
33
|
// prettier-ignore
|
|
9
34
|
// Note: `description` is not a field used by parseArgs(), it's an additional field
|
|
@@ -17,13 +42,25 @@ const cliOptions = {
|
|
|
17
42
|
type: 'boolean',
|
|
18
43
|
description: 'Open browser window and keep open after tests finish',
|
|
19
44
|
},
|
|
45
|
+
'glob.browser': {
|
|
46
|
+
type: 'string',
|
|
47
|
+
multiple: true,
|
|
48
|
+
description: 'Glob pattern(s) for browser test files',
|
|
49
|
+
},
|
|
20
50
|
'glob.e2e': {
|
|
21
51
|
type: 'string',
|
|
22
|
-
|
|
52
|
+
multiple: true,
|
|
53
|
+
description: 'Glob pattern(s) for E2E test files',
|
|
54
|
+
},
|
|
55
|
+
'glob.exclude': {
|
|
56
|
+
type: 'string',
|
|
57
|
+
multiple: true,
|
|
58
|
+
description: 'Glob pattern(s) for paths to exclude from discovery',
|
|
23
59
|
},
|
|
24
60
|
'glob.test': {
|
|
25
61
|
type: 'string',
|
|
26
|
-
|
|
62
|
+
multiple: true,
|
|
63
|
+
description: 'Glob pattern(s) for all test files',
|
|
27
64
|
},
|
|
28
65
|
concurrency: {
|
|
29
66
|
type: 'string',
|
|
@@ -34,6 +71,40 @@ const cliOptions = {
|
|
|
34
71
|
type: 'string',
|
|
35
72
|
description: 'Path to config file (default: remix-test.config.ts)',
|
|
36
73
|
},
|
|
74
|
+
coverage: {
|
|
75
|
+
type: 'boolean',
|
|
76
|
+
description: 'Enable or disable coverage collection (default: false)',
|
|
77
|
+
},
|
|
78
|
+
'coverage.dir': {
|
|
79
|
+
type: 'string',
|
|
80
|
+
description: 'Directory to output coverage reports (default: .coverage)',
|
|
81
|
+
},
|
|
82
|
+
'coverage.include': {
|
|
83
|
+
type: 'string',
|
|
84
|
+
multiple: true,
|
|
85
|
+
description: 'Glob pattern(s) for files to include in coverage',
|
|
86
|
+
},
|
|
87
|
+
'coverage.exclude': {
|
|
88
|
+
type: 'string',
|
|
89
|
+
multiple: true,
|
|
90
|
+
description: 'Glob pattern(s) for files to exclude from coverage',
|
|
91
|
+
},
|
|
92
|
+
'coverage.branches': {
|
|
93
|
+
type: 'string',
|
|
94
|
+
description: 'Branches coverage threshold percentage',
|
|
95
|
+
},
|
|
96
|
+
'coverage.functions': {
|
|
97
|
+
type: 'string',
|
|
98
|
+
description: 'Functions coverage threshold percentage',
|
|
99
|
+
},
|
|
100
|
+
'coverage.lines': {
|
|
101
|
+
type: 'string',
|
|
102
|
+
description: 'Lines coverage threshold percentage',
|
|
103
|
+
},
|
|
104
|
+
'coverage.statements': {
|
|
105
|
+
type: 'string',
|
|
106
|
+
description: 'Statements coverage threshold percentage',
|
|
107
|
+
},
|
|
37
108
|
setup: {
|
|
38
109
|
type: 'string',
|
|
39
110
|
short: 's',
|
|
@@ -46,7 +117,12 @@ const cliOptions = {
|
|
|
46
117
|
project: {
|
|
47
118
|
type: 'string',
|
|
48
119
|
short: 'p',
|
|
49
|
-
|
|
120
|
+
multiple: true,
|
|
121
|
+
description: 'Filter to specific Playwright project(s)',
|
|
122
|
+
},
|
|
123
|
+
pool: {
|
|
124
|
+
type: 'string',
|
|
125
|
+
description: 'Pool used to run server and E2E test files: forks, threads (default: forks)',
|
|
50
126
|
},
|
|
51
127
|
reporter: {
|
|
52
128
|
type: 'string',
|
|
@@ -56,7 +132,8 @@ const cliOptions = {
|
|
|
56
132
|
type: {
|
|
57
133
|
type: 'string',
|
|
58
134
|
short: 't',
|
|
59
|
-
|
|
135
|
+
multiple: true,
|
|
136
|
+
description: 'Test types to run (default: server, browser, e2e)',
|
|
60
137
|
},
|
|
61
138
|
watch: {
|
|
62
139
|
type: 'boolean',
|
|
@@ -71,17 +148,31 @@ const defaultValues: ResolvedRemixTestConfig = {
|
|
|
71
148
|
open: false,
|
|
72
149
|
},
|
|
73
150
|
concurrency: os.availableParallelism(),
|
|
151
|
+
coverage: {
|
|
152
|
+
dir: '.coverage',
|
|
153
|
+
include: undefined,
|
|
154
|
+
exclude: undefined,
|
|
155
|
+
statements: undefined,
|
|
156
|
+
lines: undefined,
|
|
157
|
+
branches: undefined,
|
|
158
|
+
functions: undefined,
|
|
159
|
+
},
|
|
74
160
|
glob: {
|
|
75
|
-
test: '**/*.test
|
|
76
|
-
|
|
161
|
+
test: ['**/*.test{,.e2e,.browser}.{ts,tsx}'],
|
|
162
|
+
browser: ['**/*.test.browser.{ts,tsx}'],
|
|
163
|
+
e2e: ['**/*.test.e2e.{ts,tsx}'],
|
|
164
|
+
exclude: ['node_modules/**'],
|
|
77
165
|
},
|
|
78
|
-
|
|
79
|
-
type: 'server,e2e',
|
|
80
|
-
setup: undefined,
|
|
166
|
+
pool: 'forks',
|
|
81
167
|
playwrightConfig: undefined,
|
|
82
168
|
project: undefined,
|
|
169
|
+
reporter: process.env.CI === 'true' ? 'files' : 'spec',
|
|
170
|
+
setup: undefined,
|
|
171
|
+
type: ['server', 'browser', 'e2e'],
|
|
83
172
|
watch: false,
|
|
84
|
-
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export type RemixTestPool = 'forks' | 'threads'
|
|
85
176
|
|
|
86
177
|
export interface RemixTestConfig {
|
|
87
178
|
/**
|
|
@@ -94,16 +185,36 @@ export interface RemixTestConfig {
|
|
|
94
185
|
open?: boolean
|
|
95
186
|
}
|
|
96
187
|
/**
|
|
97
|
-
* Glob patterns to identify test files
|
|
98
|
-
*
|
|
99
|
-
* - `glob.
|
|
188
|
+
* Glob patterns to identify test files. Each field accepts a single pattern
|
|
189
|
+
* or an array of patterns; arrays are unioned during discovery.
|
|
190
|
+
* - `glob.test`: Glob pattern(s) for all test files (--glob.test)
|
|
191
|
+
* - `glob.browser`: Glob pattern(s) for the subset of browser test files (--glob.browser)
|
|
192
|
+
* - `glob.e2e`: Glob pattern(s) for the subset of e2e test files (--glob.e2e)
|
|
193
|
+
* - `glob.exclude`: Glob pattern(s) for paths to exclude from discovery (--glob.exclude)
|
|
100
194
|
*/
|
|
101
195
|
glob?: {
|
|
102
|
-
test?: string
|
|
103
|
-
|
|
196
|
+
test?: string | string[]
|
|
197
|
+
browser?: string | string[]
|
|
198
|
+
e2e?: string | string[]
|
|
199
|
+
exclude?: string | string[]
|
|
104
200
|
}
|
|
105
201
|
/** Max number of concurrent test workers (--concurrency) */
|
|
106
202
|
concurrency?: number | string
|
|
203
|
+
/**
|
|
204
|
+
* Coverage configuration. `true` enables with defaults; an object enables with settings;
|
|
205
|
+
* `false` disables. CLI `--coverage` flag overrides the boolean aspect.
|
|
206
|
+
*/
|
|
207
|
+
coverage?:
|
|
208
|
+
| boolean
|
|
209
|
+
| {
|
|
210
|
+
dir?: string
|
|
211
|
+
include?: string | string[]
|
|
212
|
+
exclude?: string | string[]
|
|
213
|
+
statements?: number | string
|
|
214
|
+
lines?: number | string
|
|
215
|
+
branches?: number | string
|
|
216
|
+
functions?: number | string
|
|
217
|
+
}
|
|
107
218
|
/**
|
|
108
219
|
* Path to a module that exports `globalSetup` and/or `globalTeardown` functions,
|
|
109
220
|
* called once before and after the test run respectively. (--setup)
|
|
@@ -114,12 +225,23 @@ export interface RemixTestConfig {
|
|
|
114
225
|
* PlaywrightTestConfig object. CLI `--playwrightConfig` only accepts a file path.
|
|
115
226
|
*/
|
|
116
227
|
playwrightConfig?: string | PlaywrightTestConfig
|
|
117
|
-
/**
|
|
118
|
-
|
|
228
|
+
/**
|
|
229
|
+
* Pool used to run server and E2E test files. Forked child processes are the default,
|
|
230
|
+
* but worker threads are available for projects that prefer the previous behavior.
|
|
231
|
+
*/
|
|
232
|
+
pool?: RemixTestPool
|
|
233
|
+
/**
|
|
234
|
+
* Filter tests to specific playwright project(s) (--project). Accepts a single
|
|
235
|
+
* project name or an array of names; `--project` may be repeated on the CLI.
|
|
236
|
+
*/
|
|
237
|
+
project?: string | string[]
|
|
119
238
|
/** Test reporter (--reporter) */
|
|
120
239
|
reporter?: string
|
|
121
|
-
/**
|
|
122
|
-
|
|
240
|
+
/**
|
|
241
|
+
* Test type(s) to run (--type). Accepts a single type or an array of types;
|
|
242
|
+
* `--type` may be repeated on the CLI. Valid values: "server", "browser", "e2e".
|
|
243
|
+
*/
|
|
244
|
+
type?: string | string[]
|
|
123
245
|
/** Watch mode — re-run tests on file changes (--watch) */
|
|
124
246
|
watch?: boolean
|
|
125
247
|
}
|
|
@@ -130,36 +252,45 @@ export interface ResolvedRemixTestConfig {
|
|
|
130
252
|
open?: boolean
|
|
131
253
|
}
|
|
132
254
|
concurrency: number
|
|
255
|
+
coverage:
|
|
256
|
+
| {
|
|
257
|
+
dir: string
|
|
258
|
+
include?: string[]
|
|
259
|
+
exclude?: string[]
|
|
260
|
+
statements?: number
|
|
261
|
+
lines?: number
|
|
262
|
+
branches?: number
|
|
263
|
+
functions?: number
|
|
264
|
+
}
|
|
265
|
+
| undefined
|
|
133
266
|
glob: {
|
|
134
|
-
test: string
|
|
135
|
-
|
|
267
|
+
test: string[]
|
|
268
|
+
browser: string[]
|
|
269
|
+
e2e: string[]
|
|
270
|
+
exclude: string[]
|
|
136
271
|
}
|
|
137
272
|
playwrightConfig: string | PlaywrightTestConfig | undefined
|
|
138
|
-
project: string | undefined
|
|
273
|
+
project: string[] | undefined
|
|
139
274
|
reporter: string
|
|
275
|
+
pool: RemixTestPool
|
|
140
276
|
setup: string | undefined
|
|
141
|
-
type: string
|
|
277
|
+
type: string[]
|
|
142
278
|
watch: boolean
|
|
143
279
|
}
|
|
144
280
|
|
|
145
|
-
export async function loadConfig() {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
process.exit(0)
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
let parsed = parseCliArgs()
|
|
152
|
-
let fileConfig = await loadConfigFile(parsed.values.config)
|
|
281
|
+
export async function loadConfig(args: string[] = process.argv.slice(2), cwd = process.cwd()) {
|
|
282
|
+
let parsed = parseCliArgs(args)
|
|
283
|
+
let fileConfig = await loadConfigFile(parsed.values.config, cwd)
|
|
153
284
|
let config = resolveConfig(fileConfig, parsed)
|
|
154
285
|
return config
|
|
155
286
|
}
|
|
156
287
|
|
|
157
|
-
function
|
|
288
|
+
export function getRemixTestHelpText(_target: NodeJS.WriteStream = process.stdout): string {
|
|
158
289
|
let lines = [
|
|
159
|
-
'Usage: remix-test [glob] [options]',
|
|
290
|
+
'Usage: remix-test [glob...] [options]',
|
|
160
291
|
'',
|
|
161
292
|
'Arguments:',
|
|
162
|
-
` glob Glob pattern for test files (default: "${defaultValues.glob.test}")`,
|
|
293
|
+
` glob Glob pattern(s) for test files (default: "${defaultValues.glob.test.join(', ')}")`,
|
|
163
294
|
'',
|
|
164
295
|
'Options:',
|
|
165
296
|
]
|
|
@@ -175,22 +306,42 @@ function generateHelp(): string {
|
|
|
175
306
|
return lines.join('\n')
|
|
176
307
|
}
|
|
177
308
|
|
|
178
|
-
function parseCliArgs(args
|
|
309
|
+
function parseCliArgs(args: string[]) {
|
|
179
310
|
return util.parseArgs({ args, options: cliOptions, allowPositionals: true })
|
|
180
311
|
}
|
|
181
312
|
|
|
313
|
+
function toArray<T>(value: T | readonly T[]): T[] {
|
|
314
|
+
return Array.isArray(value) ? [...value] : [value as T]
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function toCommaSeparatedArray(value: string | readonly string[]): string[] {
|
|
318
|
+
return toArray(value).flatMap((item) =>
|
|
319
|
+
item
|
|
320
|
+
.split(',')
|
|
321
|
+
.map((part) => part.trim())
|
|
322
|
+
.filter(Boolean),
|
|
323
|
+
)
|
|
324
|
+
}
|
|
325
|
+
|
|
182
326
|
function resolveConfig(
|
|
183
327
|
fileConfig: RemixTestConfig,
|
|
184
328
|
{ values: cliValues, positionals }: ReturnType<typeof parseCliArgs>,
|
|
185
329
|
): ResolvedRemixTestConfig {
|
|
330
|
+
let fileCoverage = typeof fileConfig.coverage === 'boolean' ? {} : fileConfig.coverage || {}
|
|
186
331
|
return {
|
|
187
332
|
glob: {
|
|
188
|
-
test:
|
|
189
|
-
positionals
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
333
|
+
test: toArray(
|
|
334
|
+
positionals.length > 0
|
|
335
|
+
? positionals
|
|
336
|
+
: (cliValues['glob.test'] ?? fileConfig.glob?.test ?? defaultValues.glob.test),
|
|
337
|
+
),
|
|
338
|
+
browser: toArray(
|
|
339
|
+
cliValues['glob.browser'] ?? fileConfig.glob?.browser ?? defaultValues.glob.browser,
|
|
340
|
+
),
|
|
341
|
+
e2e: toArray(cliValues['glob.e2e'] ?? fileConfig.glob?.e2e ?? defaultValues.glob.e2e),
|
|
342
|
+
exclude: toArray(
|
|
343
|
+
cliValues['glob.exclude'] ?? fileConfig.glob?.exclude ?? defaultValues.glob.exclude,
|
|
344
|
+
),
|
|
194
345
|
},
|
|
195
346
|
browser: {
|
|
196
347
|
echo: cliValues['browser.echo'] ?? fileConfig.browser?.echo ?? defaultValues.browser.echo,
|
|
@@ -199,32 +350,93 @@ function resolveConfig(
|
|
|
199
350
|
concurrency: Number(
|
|
200
351
|
cliValues.concurrency ?? fileConfig.concurrency ?? defaultValues.concurrency,
|
|
201
352
|
),
|
|
353
|
+
coverage:
|
|
354
|
+
cliValues.coverage === true || !!fileConfig.coverage
|
|
355
|
+
? {
|
|
356
|
+
dir: cliValues['coverage.dir'] ?? fileCoverage.dir ?? defaultValues.coverage!.dir,
|
|
357
|
+
include: (() => {
|
|
358
|
+
let raw =
|
|
359
|
+
cliValues['coverage.include'] ??
|
|
360
|
+
fileCoverage.include ??
|
|
361
|
+
defaultValues.coverage!.include
|
|
362
|
+
return raw === undefined ? undefined : toArray(raw)
|
|
363
|
+
})(),
|
|
364
|
+
exclude: (() => {
|
|
365
|
+
let raw =
|
|
366
|
+
cliValues['coverage.exclude'] ??
|
|
367
|
+
fileCoverage.exclude ??
|
|
368
|
+
defaultValues.coverage!.exclude
|
|
369
|
+
return raw === undefined ? undefined : toArray(raw)
|
|
370
|
+
})(),
|
|
371
|
+
statements:
|
|
372
|
+
cliValues['coverage.statements'] !== undefined
|
|
373
|
+
? Number(cliValues['coverage.statements'])
|
|
374
|
+
: fileCoverage.statements !== undefined
|
|
375
|
+
? Number(fileCoverage.statements)
|
|
376
|
+
: undefined,
|
|
377
|
+
lines:
|
|
378
|
+
cliValues['coverage.lines'] !== undefined
|
|
379
|
+
? Number(cliValues['coverage.lines'])
|
|
380
|
+
: fileCoverage.lines !== undefined
|
|
381
|
+
? Number(fileCoverage.lines)
|
|
382
|
+
: undefined,
|
|
383
|
+
branches:
|
|
384
|
+
cliValues['coverage.branches'] !== undefined
|
|
385
|
+
? Number(cliValues['coverage.branches'])
|
|
386
|
+
: fileCoverage.branches !== undefined
|
|
387
|
+
? Number(fileCoverage.branches)
|
|
388
|
+
: undefined,
|
|
389
|
+
functions:
|
|
390
|
+
cliValues['coverage.functions'] !== undefined
|
|
391
|
+
? Number(cliValues['coverage.functions'])
|
|
392
|
+
: fileCoverage.functions !== undefined
|
|
393
|
+
? Number(fileCoverage.functions)
|
|
394
|
+
: undefined,
|
|
395
|
+
}
|
|
396
|
+
: undefined,
|
|
202
397
|
setup: cliValues.setup ?? fileConfig.setup ?? defaultValues.setup,
|
|
203
398
|
playwrightConfig:
|
|
204
399
|
cliValues.playwrightConfig ?? fileConfig.playwrightConfig ?? defaultValues.playwrightConfig,
|
|
205
|
-
|
|
400
|
+
pool: resolvePool(cliValues.pool ?? fileConfig.pool ?? defaultValues.pool),
|
|
401
|
+
project: (() => {
|
|
402
|
+
let raw = cliValues.project ?? fileConfig.project ?? defaultValues.project
|
|
403
|
+
return raw === undefined ? undefined : toCommaSeparatedArray(raw)
|
|
404
|
+
})(),
|
|
206
405
|
reporter: cliValues.reporter ?? fileConfig.reporter ?? defaultValues.reporter,
|
|
207
|
-
type: cliValues.type ?? fileConfig.type ?? defaultValues.type,
|
|
406
|
+
type: toCommaSeparatedArray(cliValues.type ?? fileConfig.type ?? defaultValues.type),
|
|
208
407
|
watch: cliValues.watch ?? fileConfig.watch ?? defaultValues.watch,
|
|
209
408
|
}
|
|
210
409
|
}
|
|
211
410
|
|
|
212
|
-
|
|
411
|
+
function resolvePool(value: string): RemixTestPool {
|
|
412
|
+
if (value === 'forks' || value === 'threads') {
|
|
413
|
+
return value
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
throw new Error(`Unsupported test pool "${value}". Supported pools are: forks, threads`)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async function loadConfigFile(
|
|
420
|
+
configPath: string | undefined,
|
|
421
|
+
cwd: string,
|
|
422
|
+
): Promise<RemixTestConfig> {
|
|
213
423
|
let candidates = configPath
|
|
214
|
-
? [path.resolve(
|
|
215
|
-
: [
|
|
216
|
-
path.join(process.cwd(), 'remix-test.config.ts'),
|
|
217
|
-
path.join(process.cwd(), 'remix-test.config.js'),
|
|
218
|
-
]
|
|
424
|
+
? [path.resolve(cwd, configPath)]
|
|
425
|
+
: [path.join(cwd, 'remix-test.config.ts'), path.join(cwd, 'remix-test.config.js')]
|
|
219
426
|
|
|
220
427
|
for (let candidate of candidates) {
|
|
221
428
|
try {
|
|
222
429
|
await fsp.access(candidate)
|
|
223
|
-
let mod = await tsImport(candidate, { parentURL: import.meta.url })
|
|
224
|
-
return mod.default ?? mod
|
|
225
430
|
} catch {
|
|
226
|
-
// not found
|
|
431
|
+
// not found — try the next candidate
|
|
432
|
+
continue
|
|
227
433
|
}
|
|
434
|
+
// The file exists; let import errors propagate rather than silently
|
|
435
|
+
// falling through to defaults — that masking is what hid "Windows
|
|
436
|
+
// absolute paths aren't valid ESM specifiers" by classifying every
|
|
437
|
+
// browser test as a server test.
|
|
438
|
+
let mod = await importModule(candidate, import.meta)
|
|
439
|
+
return mod.default ?? mod
|
|
228
440
|
}
|
|
229
441
|
|
|
230
442
|
return {}
|
package/src/lib/context.ts
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import type { Browser, Page } from 'playwright'
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
import type
|
|
2
|
+
import type { V8CoverageEntry } from './coverage.ts'
|
|
3
|
+
import { createFakeTimers, type FakeTimers } from './fake-timers.ts'
|
|
4
|
+
import { mock, type MockCall, type MockContext, type MockFunction } from './mock.ts'
|
|
5
5
|
import type { getPlaywrightPageOptions } from './playwright.ts'
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* The shape `t.serve()` consumes. Matches the result of `createTestServer`
|
|
9
|
+
* from `@remix-run/node-fetch-server/test`, but any object with a `baseUrl`
|
|
10
|
+
* and async `close()` works.
|
|
11
|
+
*/
|
|
12
|
+
export interface TestServer {
|
|
13
|
+
baseUrl: string
|
|
14
|
+
close(): Promise<void>
|
|
15
|
+
}
|
|
16
|
+
|
|
7
17
|
/**
|
|
8
18
|
* Test Context providing utilities for testing via remix-test. The context is
|
|
9
19
|
* passed as the first argument to the {@link test}/{@link it} functions.
|
|
@@ -58,20 +68,36 @@ export interface TestContext {
|
|
|
58
68
|
}
|
|
59
69
|
|
|
60
70
|
/**
|
|
61
|
-
*
|
|
71
|
+
* Activates fake timers for testing time-dependent code.
|
|
72
|
+
*
|
|
73
|
+
* @returns {FakeTimers} A fake timers instance for controlling time
|
|
74
|
+
*/
|
|
75
|
+
useFakeTimers(): FakeTimers
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Wires a running test server up to a Playwright page so the test can drive
|
|
79
|
+
* it. The server is closed automatically when the test ends. Pair with
|
|
80
|
+
* `createTestServer` from `@remix-run/node-fetch-server/test` (or any other
|
|
81
|
+
* source of a `{ baseUrl, close }` handle) to spin up the server first.
|
|
62
82
|
*
|
|
63
|
-
* @param
|
|
64
|
-
* @returns
|
|
83
|
+
* @param server - The running server the page should target
|
|
84
|
+
* @returns A `Page` whose `baseURL` is set to `server.baseUrl`.
|
|
65
85
|
*/
|
|
66
|
-
serve(
|
|
86
|
+
serve(server: TestServer): Promise<Page>
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface CreateTestContextOptions {
|
|
90
|
+
addE2ECoverageEntries: (value: { entries: V8CoverageEntry[]; baseUrl: string }) => void
|
|
91
|
+
browser: Browser
|
|
92
|
+
coverage: boolean
|
|
93
|
+
open: boolean
|
|
94
|
+
playwrightPageOptions: ReturnType<typeof getPlaywrightPageOptions>
|
|
67
95
|
}
|
|
68
96
|
|
|
69
|
-
export function createTestContext(options: {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
playwrightPageOptions?: ReturnType<typeof getPlaywrightPageOptions>
|
|
74
|
-
}): { testContext: TestContext; cleanup(): Promise<void> } {
|
|
97
|
+
export function createTestContext(options?: CreateTestContextOptions): {
|
|
98
|
+
testContext: TestContext
|
|
99
|
+
cleanup(): Promise<void>
|
|
100
|
+
} {
|
|
75
101
|
let cleanups: Array<() => void | Promise<void>> = []
|
|
76
102
|
|
|
77
103
|
let testContext: TestContext = {
|
|
@@ -86,12 +112,16 @@ export function createTestContext(options: {
|
|
|
86
112
|
after(fn) {
|
|
87
113
|
cleanups.push(fn)
|
|
88
114
|
},
|
|
89
|
-
|
|
90
|
-
|
|
115
|
+
useFakeTimers() {
|
|
116
|
+
let timers = createFakeTimers()
|
|
117
|
+
cleanups.push(timers.restore)
|
|
118
|
+
return timers
|
|
119
|
+
},
|
|
120
|
+
async serve(server) {
|
|
121
|
+
if (!options || !options.browser) {
|
|
91
122
|
throw new Error('t.serve() is only available in E2E test suites')
|
|
92
123
|
}
|
|
93
124
|
|
|
94
|
-
let server = await options.createServer(handler)
|
|
95
125
|
let page = await options.browser.newPage({
|
|
96
126
|
...options.playwrightPageOptions,
|
|
97
127
|
baseURL: server.baseUrl,
|
|
@@ -103,6 +133,18 @@ export function createTestContext(options: {
|
|
|
103
133
|
page.setDefaultTimeout(options.playwrightPageOptions.actionTimeout)
|
|
104
134
|
}
|
|
105
135
|
|
|
136
|
+
let coverageEnabled = options.coverage && options.browser.browserType().name() === 'chromium'
|
|
137
|
+
if (coverageEnabled) {
|
|
138
|
+
await page.coverage.startJSCoverage({ resetOnNavigation: false })
|
|
139
|
+
cleanups.push(async () => {
|
|
140
|
+
let entries = await page.coverage.stopJSCoverage()
|
|
141
|
+
options.addE2ECoverageEntries?.({
|
|
142
|
+
entries: entries as unknown as V8CoverageEntry[],
|
|
143
|
+
baseUrl: server.baseUrl,
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
106
148
|
cleanups.push(async () => {
|
|
107
149
|
if (!options.open) {
|
|
108
150
|
await page.close()
|
|
@@ -123,4 +165,4 @@ export function createTestContext(options: {
|
|
|
123
165
|
}
|
|
124
166
|
}
|
|
125
167
|
|
|
126
|
-
export type {
|
|
168
|
+
export type { MockCall, MockContext, MockFunction }
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import { transformTypeScript } from './ts-transform.ts'
|
|
4
|
+
|
|
5
|
+
// Custom ESM loader hook for TypeScript files.
|
|
6
|
+
//
|
|
7
|
+
// Replaces tsx's minified transformation with an un-minified esbuild transform
|
|
8
|
+
// that preserves line structure. This ensures V8 coverage byte offsets map
|
|
9
|
+
// cleanly to TypeScript source lines via the inline source map, giving
|
|
10
|
+
// accurate per-line coverage rather than collapsing multiple statements onto
|
|
11
|
+
// a single minified line.
|
|
12
|
+
|
|
13
|
+
export async function load(
|
|
14
|
+
url: string,
|
|
15
|
+
context: { format?: string },
|
|
16
|
+
nextLoad: (
|
|
17
|
+
url: string,
|
|
18
|
+
context: { format?: string },
|
|
19
|
+
) => Promise<{ format: string; source: string }>,
|
|
20
|
+
) {
|
|
21
|
+
let cleanUrl = url.includes('?') ? url.slice(0, url.indexOf('?')) : url
|
|
22
|
+
if (!cleanUrl.endsWith('.ts') && !cleanUrl.endsWith('.tsx')) {
|
|
23
|
+
return nextLoad(url, context)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let filePath = fileURLToPath(cleanUrl)
|
|
27
|
+
let source = await readFile(filePath, 'utf-8')
|
|
28
|
+
let { code } = await transformTypeScript(source, filePath)
|
|
29
|
+
|
|
30
|
+
return { format: 'module', source: code, shortCircuit: true }
|
|
31
|
+
}
|