@remix-run/test 0.2.0 → 0.4.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 +43 -44
- package/dist/app/client/entry.js +4 -0
- package/dist/app/server.d.ts.map +1 -1
- package/dist/app/server.js +10 -10
- package/dist/cli.d.ts +30 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +87 -23
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/lib/config.d.ts +55 -21
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +82 -33
- package/dist/lib/context.d.ts +5 -5
- package/dist/lib/coverage-loader.js +2 -2
- package/dist/lib/coverage.js +1 -1
- package/dist/lib/fake-timers.d.ts +39 -0
- package/dist/lib/fake-timers.d.ts.map +1 -1
- package/dist/lib/fake-timers.js +27 -8
- package/dist/lib/framework.d.ts +12 -6
- package/dist/lib/framework.d.ts.map +1 -1
- package/dist/lib/framework.js +24 -12
- package/dist/lib/import-module.d.ts.map +1 -1
- package/dist/lib/import-module.js +13 -3
- package/dist/lib/reporters/dot.d.ts.map +1 -1
- package/dist/lib/reporters/dot.js +10 -0
- package/dist/lib/reporters/files.d.ts.map +1 -1
- package/dist/lib/reporters/files.js +10 -0
- package/dist/lib/reporters/results.d.ts +1 -1
- package/dist/lib/reporters/results.d.ts.map +1 -1
- package/dist/lib/reporters/spec.d.ts.map +1 -1
- package/dist/lib/reporters/spec.js +10 -0
- package/dist/lib/reporters/tap.d.ts.map +1 -1
- package/dist/lib/reporters/tap.js +10 -0
- package/dist/lib/runner-browser.d.ts.map +1 -1
- package/dist/lib/runner-browser.js +40 -2
- package/dist/lib/runner.d.ts +18 -1
- package/dist/lib/runner.d.ts.map +1 -1
- package/dist/lib/runner.js +187 -38
- 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 -47
- 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 +112 -0
- package/dist/lib/worker.js +6 -55
- package/package.json +5 -5
- package/src/app/client/entry.ts +4 -0
- package/src/app/server.ts +11 -10
- package/src/cli.ts +121 -28
- package/src/index.ts +1 -1
- package/src/lib/config.ts +144 -58
- package/src/lib/context.ts +5 -5
- package/src/lib/coverage-loader.ts +2 -2
- package/src/lib/coverage.ts +1 -1
- package/src/lib/fake-timers.ts +65 -8
- package/src/lib/framework.ts +53 -36
- package/src/lib/import-module.ts +14 -3
- package/src/lib/reporters/dot.ts +9 -0
- package/src/lib/reporters/files.ts +9 -0
- package/src/lib/reporters/results.ts +1 -1
- package/src/lib/reporters/spec.ts +9 -0
- package/src/lib/reporters/tap.ts +9 -0
- package/src/lib/runner-browser.ts +46 -2
- package/src/lib/runner.ts +253 -50
- package/src/lib/ts-transform.ts +1 -1
- package/src/lib/worker-e2e-file.ts +98 -0
- package/src/lib/worker-e2e.ts +14 -51
- 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 +7 -47
- package/tsconfig.json +6 -3
|
@@ -6,10 +6,17 @@ import type { Counts, TestResult, TestResults } from './results.ts'
|
|
|
6
6
|
|
|
7
7
|
export class FilesReporter implements Reporter {
|
|
8
8
|
#failures: { suiteName: string; name: string; error: TestResult['error'] }[] = []
|
|
9
|
+
#files = new Set<string>()
|
|
10
|
+
#suites = new Set<string>()
|
|
9
11
|
|
|
10
12
|
onSectionStart(_label: string) {}
|
|
11
13
|
|
|
12
14
|
onResult(results: TestResults, env?: string) {
|
|
15
|
+
for (let test of results.tests) {
|
|
16
|
+
if (test.filePath) this.#files.add(test.filePath)
|
|
17
|
+
if (test.suiteName) this.#suites.add(test.suiteName)
|
|
18
|
+
}
|
|
19
|
+
|
|
13
20
|
let filePath = results.tests[0]?.filePath
|
|
14
21
|
let fileName = filePath ? path.relative(process.cwd(), filePath) : '(unknown)'
|
|
15
22
|
let envLabel = env ? ` ${colors.dim(`[${env}]`)}` : ''
|
|
@@ -66,6 +73,8 @@ export class FilesReporter implements Reporter {
|
|
|
66
73
|
let { passed, failed, skipped, todo } = counts
|
|
67
74
|
let info = colors.cyan('ℹ')
|
|
68
75
|
console.log()
|
|
76
|
+
console.log(`${info} files ${this.#files.size}`)
|
|
77
|
+
console.log(`${info} suites ${this.#suites.size}`)
|
|
69
78
|
console.log(`${info} tests ${passed + failed + skipped + todo}`)
|
|
70
79
|
console.log(`${info} pass ${passed}`)
|
|
71
80
|
console.log(`${info} fail ${failed}`)
|
|
@@ -5,12 +5,19 @@ import type { Counts, TestResult, TestResults } from './results.ts'
|
|
|
5
5
|
|
|
6
6
|
export class SpecReporter implements Reporter {
|
|
7
7
|
#failures: { suiteName: string; name: string; error: TestResult['error'] }[] = []
|
|
8
|
+
#files = new Set<string>()
|
|
9
|
+
#suites = new Set<string>()
|
|
8
10
|
|
|
9
11
|
onSectionStart(label: string) {
|
|
10
12
|
console.log(label)
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
onResult(results: TestResults, env?: string) {
|
|
16
|
+
for (let test of results.tests) {
|
|
17
|
+
if (test.filePath) this.#files.add(test.filePath)
|
|
18
|
+
if (test.suiteName) this.#suites.add(test.suiteName)
|
|
19
|
+
}
|
|
20
|
+
|
|
14
21
|
let suiteMap = new Map<string, TestResult[]>()
|
|
15
22
|
for (let test of results.tests) {
|
|
16
23
|
let suite = test.suiteName || 'Global'
|
|
@@ -163,6 +170,8 @@ export class SpecReporter implements Reporter {
|
|
|
163
170
|
let { passed, failed, skipped, todo } = counts
|
|
164
171
|
let info = colors.cyan('ℹ')
|
|
165
172
|
console.log()
|
|
173
|
+
console.log(`${info} files ${this.#files.size}`)
|
|
174
|
+
console.log(`${info} suites ${this.#suites.size}`)
|
|
166
175
|
console.log(`${info} tests ${passed + failed + skipped + todo}`)
|
|
167
176
|
console.log(`${info} pass ${passed}`)
|
|
168
177
|
console.log(`${info} fail ${failed}`)
|
package/src/lib/reporters/tap.ts
CHANGED
|
@@ -5,6 +5,8 @@ import type { Counts, TestResults } from './results.ts'
|
|
|
5
5
|
export class TapReporter implements Reporter {
|
|
6
6
|
#counter = 0
|
|
7
7
|
#total = 0
|
|
8
|
+
#files = new Set<string>()
|
|
9
|
+
#suites = new Set<string>()
|
|
8
10
|
|
|
9
11
|
onSectionStart(_label: string) {}
|
|
10
12
|
|
|
@@ -15,6 +17,11 @@ export class TapReporter implements Reporter {
|
|
|
15
17
|
|
|
16
18
|
let envComment = env ? ` # ${env}` : ''
|
|
17
19
|
|
|
20
|
+
for (let test of results.tests) {
|
|
21
|
+
if (test.filePath) this.#files.add(test.filePath)
|
|
22
|
+
if (test.suiteName) this.#suites.add(test.suiteName)
|
|
23
|
+
}
|
|
24
|
+
|
|
18
25
|
for (let test of results.tests) {
|
|
19
26
|
this.#counter++
|
|
20
27
|
this.#total++
|
|
@@ -48,6 +55,8 @@ export class TapReporter implements Reporter {
|
|
|
48
55
|
onSummary(counts: Counts, durationMs: number) {
|
|
49
56
|
let { passed, failed, skipped, todo } = counts
|
|
50
57
|
console.log(`1..${this.#total}`)
|
|
58
|
+
console.log(`# files ${this.#files.size}`)
|
|
59
|
+
console.log(`# suites ${this.#suites.size}`)
|
|
51
60
|
console.log(`# tests ${passed + failed + skipped + todo}`)
|
|
52
61
|
console.log(`# pass ${passed}`)
|
|
53
62
|
console.log(`# fail ${failed}`)
|
|
@@ -16,6 +16,8 @@ import {
|
|
|
16
16
|
import type { Reporter } from './reporters/index.ts'
|
|
17
17
|
import type { TestResults } from './reporters/results.ts'
|
|
18
18
|
|
|
19
|
+
const BROWSER_TEST_FILE_TIMEOUT_MS = 90_000
|
|
20
|
+
|
|
19
21
|
// The harness reports each test result with `filePath` set to the
|
|
20
22
|
// `/scripts/<rel>` URL the iframe loaded. Reporters expect a real filesystem
|
|
21
23
|
// path so they can compute `path.relative(cwd, ...)` cleanly; otherwise they
|
|
@@ -61,6 +63,10 @@ export async function runBrowserTests(options: TestRunOptions): Promise<{
|
|
|
61
63
|
getPlaywrightLaunchOptions(options.playwrightUseOpts),
|
|
62
64
|
)
|
|
63
65
|
page = await browser.newPage(getPlaywrightPageOptions(options.playwrightUseOpts))
|
|
66
|
+
// Cap individual browser operations, then separately watch for per-file
|
|
67
|
+
// progress so large suites can run longer than this without hiding hangs.
|
|
68
|
+
page.setDefaultTimeout(BROWSER_TEST_FILE_TIMEOUT_MS)
|
|
69
|
+
page.setDefaultNavigationTimeout(BROWSER_TEST_FILE_TIMEOUT_MS)
|
|
64
70
|
|
|
65
71
|
if (options.console) {
|
|
66
72
|
page.on('console', (msg) => console.log(`${colors.dim('[browser console]')} ${msg.text()}`))
|
|
@@ -78,6 +84,32 @@ export async function runBrowserTests(options: TestRunOptions): Promise<{
|
|
|
78
84
|
let totalSkipped = 0
|
|
79
85
|
let totalTodo = 0
|
|
80
86
|
let rootDir = getBrowserTestRootDir()
|
|
87
|
+
let completedFiles = 0
|
|
88
|
+
let totalFiles = options.testFiles?.length ?? 0
|
|
89
|
+
let progressTimeoutId: ReturnType<typeof setTimeout> | undefined
|
|
90
|
+
let rejectProgressTimeout: (error: Error) => void = () => {}
|
|
91
|
+
let progressTimeoutPromise = new Promise<never>((_, reject) => {
|
|
92
|
+
rejectProgressTimeout = reject
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
function clearProgressTimeout() {
|
|
96
|
+
if (progressTimeoutId !== undefined) {
|
|
97
|
+
clearTimeout(progressTimeoutId)
|
|
98
|
+
progressTimeoutId = undefined
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function resetProgressTimeout() {
|
|
103
|
+
clearProgressTimeout()
|
|
104
|
+
progressTimeoutId = setTimeout(() => {
|
|
105
|
+
let progress = totalFiles > 0 ? ` (${completedFiles}/${totalFiles} files completed)` : ''
|
|
106
|
+
rejectProgressTimeout(
|
|
107
|
+
new Error(
|
|
108
|
+
`Timed out waiting ${BROWSER_TEST_FILE_TIMEOUT_MS}ms for browser test progress${progress}`,
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
}, BROWSER_TEST_FILE_TIMEOUT_MS)
|
|
112
|
+
}
|
|
81
113
|
|
|
82
114
|
await page.route('**/file-results', async (route) => {
|
|
83
115
|
let results = route.request().postDataJSON() as TestResults
|
|
@@ -89,6 +121,8 @@ export async function runBrowserTests(options: TestRunOptions): Promise<{
|
|
|
89
121
|
totalFailed += results.failed
|
|
90
122
|
totalSkipped += results.skipped
|
|
91
123
|
totalTodo += results.todo
|
|
124
|
+
completedFiles++
|
|
125
|
+
resetProgressTimeout()
|
|
92
126
|
await route.fulfill({ status: 200 })
|
|
93
127
|
})
|
|
94
128
|
|
|
@@ -111,9 +145,19 @@ export async function runBrowserTests(options: TestRunOptions): Promise<{
|
|
|
111
145
|
|
|
112
146
|
// Prevent unhandled rejection if we fail before setting up the listener
|
|
113
147
|
errorPromise.catch(() => {})
|
|
148
|
+
progressTimeoutPromise.catch(() => {})
|
|
114
149
|
|
|
115
|
-
|
|
116
|
-
|
|
150
|
+
resetProgressTimeout()
|
|
151
|
+
try {
|
|
152
|
+
await page.goto(options.baseUrl)
|
|
153
|
+
await Promise.race([
|
|
154
|
+
page.waitForFunction('window.__testsDone', undefined, { timeout: 0 }),
|
|
155
|
+
errorPromise,
|
|
156
|
+
progressTimeoutPromise,
|
|
157
|
+
])
|
|
158
|
+
} finally {
|
|
159
|
+
clearProgressTimeout()
|
|
160
|
+
}
|
|
117
161
|
|
|
118
162
|
if (coverageEnabled) {
|
|
119
163
|
let entries = (await page.coverage.stopJSCoverage()) as unknown as V8CoverageEntry[]
|
package/src/lib/runner.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { fork, type ChildProcess } from 'node:child_process'
|
|
1
2
|
import * as fsp from 'node:fs/promises'
|
|
2
3
|
import * as path from 'node:path'
|
|
3
|
-
import { pathToFileURL } from 'node:url'
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
4
5
|
import { Worker } from 'node:worker_threads'
|
|
5
|
-
import { IS_RUNNING_FROM_SRC } from './config.ts'
|
|
6
|
+
import { IS_RUNNING_FROM_SRC, type RemixTestPool } from './config.ts'
|
|
6
7
|
import {
|
|
7
8
|
collectCoverageMapFromPlaywright,
|
|
8
9
|
collectServerCoverageMap,
|
|
@@ -19,6 +20,22 @@ import type { Counts, TestResults } from './reporters/results.ts'
|
|
|
19
20
|
const ext = IS_RUNNING_FROM_SRC ? '.ts' : '.js'
|
|
20
21
|
const workerUrl = new URL(`./worker${ext}`, import.meta.url)
|
|
21
22
|
const workerE2EUrl = new URL(`./worker-e2e${ext}`, import.meta.url)
|
|
23
|
+
const workerProcessUrl = new URL(`./worker-process${ext}`, import.meta.url)
|
|
24
|
+
const DEFAULT_WORKER_SHUTDOWN_TIMEOUT_MS = 10_000
|
|
25
|
+
|
|
26
|
+
interface WorkerRun {
|
|
27
|
+
finished: Promise<void>
|
|
28
|
+
exited: Promise<number | null>
|
|
29
|
+
terminate(): Promise<boolean>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface RunFileOptions {
|
|
33
|
+
cwd?: string
|
|
34
|
+
coverage?: CoverageConfig
|
|
35
|
+
open?: boolean
|
|
36
|
+
playwrightUseOpts?: PlaywrightUseOpts
|
|
37
|
+
pool?: RemixTestPool
|
|
38
|
+
}
|
|
22
39
|
|
|
23
40
|
export async function runServerTests(
|
|
24
41
|
files: string[],
|
|
@@ -31,12 +48,15 @@ export async function runServerTests(
|
|
|
31
48
|
playwrightUseOpts?: PlaywrightUseOpts
|
|
32
49
|
projectName?: string
|
|
33
50
|
coverage?: CoverageConfig
|
|
51
|
+
workerShutdownTimeoutMs?: number
|
|
52
|
+
pool?: RemixTestPool
|
|
34
53
|
} = {},
|
|
35
54
|
): Promise<Counts & { coverageMap: CoverageMap | null }> {
|
|
36
55
|
let counts: Counts = { passed: 0, failed: 0, skipped: 0, todo: 0 }
|
|
37
56
|
let coverageMap: CoverageMap | null = null
|
|
38
57
|
let cwd = options.cwd ?? process.cwd()
|
|
39
58
|
let envLabel = options.projectName ? `${type}:${options.projectName}` : type
|
|
59
|
+
let pool = options.pool ?? 'forks'
|
|
40
60
|
|
|
41
61
|
function accumulate(results: TestResults, file: string) {
|
|
42
62
|
reporter.onResult(
|
|
@@ -56,7 +76,7 @@ export async function runServerTests(
|
|
|
56
76
|
files,
|
|
57
77
|
concurrency,
|
|
58
78
|
(file) =>
|
|
59
|
-
|
|
79
|
+
runFileInPool(
|
|
60
80
|
file,
|
|
61
81
|
type,
|
|
62
82
|
(results) => {
|
|
@@ -67,10 +87,13 @@ export async function runServerTests(
|
|
|
67
87
|
},
|
|
68
88
|
{
|
|
69
89
|
...options,
|
|
90
|
+
pool,
|
|
70
91
|
playwrightUseOpts: options.playwrightUseOpts,
|
|
71
92
|
},
|
|
72
93
|
),
|
|
73
94
|
() => counts.failed++,
|
|
95
|
+
!options.open,
|
|
96
|
+
options.workerShutdownTimeoutMs ?? DEFAULT_WORKER_SHUTDOWN_TIMEOUT_MS,
|
|
74
97
|
)
|
|
75
98
|
|
|
76
99
|
if (options.coverage && allBrowserCoverageEntries.length > 0) {
|
|
@@ -92,8 +115,14 @@ export async function runServerTests(
|
|
|
92
115
|
await runInConcurrentWorkers(
|
|
93
116
|
files,
|
|
94
117
|
concurrency,
|
|
95
|
-
(file) =>
|
|
118
|
+
(file) =>
|
|
119
|
+
runFileInPool(file, type, (results) => accumulate(results, file), {
|
|
120
|
+
...options,
|
|
121
|
+
pool,
|
|
122
|
+
}),
|
|
96
123
|
() => counts.failed++,
|
|
124
|
+
true,
|
|
125
|
+
options.workerShutdownTimeoutMs ?? DEFAULT_WORKER_SHUTDOWN_TIMEOUT_MS,
|
|
97
126
|
)
|
|
98
127
|
|
|
99
128
|
if (coverageDataDir) {
|
|
@@ -109,8 +138,10 @@ export async function runServerTests(
|
|
|
109
138
|
async function runInConcurrentWorkers(
|
|
110
139
|
files: string[],
|
|
111
140
|
concurrency: number,
|
|
112
|
-
runFile: (file: string) =>
|
|
141
|
+
runFile: (file: string) => WorkerRun,
|
|
113
142
|
onError: () => void,
|
|
143
|
+
terminateWhenFinished: boolean,
|
|
144
|
+
workerShutdownTimeoutMs: number,
|
|
114
145
|
): Promise<void> {
|
|
115
146
|
let index = 0
|
|
116
147
|
let active = 0
|
|
@@ -122,22 +153,42 @@ async function runInConcurrentWorkers(
|
|
|
122
153
|
index++
|
|
123
154
|
active++
|
|
124
155
|
|
|
125
|
-
runFile(file)
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
156
|
+
let run = runFile(file)
|
|
157
|
+
|
|
158
|
+
function complete() {
|
|
159
|
+
active--
|
|
160
|
+
if (index < files.length) {
|
|
161
|
+
dispatch()
|
|
162
|
+
} else if (active === 0) {
|
|
163
|
+
resolve()
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
run.finished.then(
|
|
168
|
+
async () => {
|
|
169
|
+
try {
|
|
170
|
+
if (terminateWhenFinished) {
|
|
171
|
+
let exited = await waitForWorkerExit(run.exited, workerShutdownTimeoutMs)
|
|
172
|
+
if (!exited) {
|
|
173
|
+
let terminated = await run.terminate()
|
|
174
|
+
if (!terminated) {
|
|
175
|
+
onError()
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
} finally {
|
|
180
|
+
complete()
|
|
132
181
|
}
|
|
133
182
|
},
|
|
134
|
-
(err) => {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
183
|
+
async (err) => {
|
|
184
|
+
try {
|
|
185
|
+
console.error(`Error running ${file}:`, err instanceof Error ? err.message : err)
|
|
186
|
+
console.error(err)
|
|
187
|
+
onError()
|
|
188
|
+
await run.terminate()
|
|
189
|
+
} finally {
|
|
190
|
+
complete()
|
|
191
|
+
}
|
|
141
192
|
},
|
|
142
193
|
)
|
|
143
194
|
}
|
|
@@ -149,41 +200,193 @@ async function runInConcurrentWorkers(
|
|
|
149
200
|
})
|
|
150
201
|
}
|
|
151
202
|
|
|
152
|
-
function
|
|
203
|
+
function waitForWorkerExit(exited: Promise<number | null>, timeoutMs: number): Promise<boolean> {
|
|
204
|
+
return new Promise((resolve) => {
|
|
205
|
+
let timeout = setTimeout(() => resolve(false), timeoutMs)
|
|
206
|
+
exited.then(() => {
|
|
207
|
+
clearTimeout(timeout)
|
|
208
|
+
resolve(true)
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function runFileInPool(
|
|
153
214
|
file: string,
|
|
154
215
|
type: 'server' | 'e2e',
|
|
155
216
|
onResults: (results: TestResults) => void,
|
|
156
|
-
options:
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
217
|
+
options: RunFileOptions,
|
|
218
|
+
): WorkerRun {
|
|
219
|
+
return options.pool === 'threads'
|
|
220
|
+
? runFileInWorker(file, type, onResults, options)
|
|
221
|
+
: runFileInProcess(file, type, onResults, options)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function runFileInWorker(
|
|
225
|
+
file: string,
|
|
226
|
+
type: 'server' | 'e2e',
|
|
227
|
+
onResults: (results: TestResults) => void,
|
|
228
|
+
options: RunFileOptions = {},
|
|
229
|
+
): WorkerRun {
|
|
230
|
+
let receivedResults = false
|
|
231
|
+
let worker =
|
|
232
|
+
type === 'e2e'
|
|
233
|
+
? new Worker(workerE2EUrl, {
|
|
234
|
+
workerData: {
|
|
235
|
+
file: pathToFileURL(file).href,
|
|
236
|
+
type,
|
|
237
|
+
coverage: options.coverage,
|
|
238
|
+
open: options.open,
|
|
239
|
+
playwrightUseOpts: options.playwrightUseOpts,
|
|
240
|
+
},
|
|
241
|
+
})
|
|
242
|
+
: new Worker(workerUrl, {
|
|
243
|
+
workerData: {
|
|
244
|
+
file: pathToFileURL(file).href,
|
|
245
|
+
type,
|
|
246
|
+
coverage: options.coverage,
|
|
247
|
+
},
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
let exited = new Promise<number>((resolve) => {
|
|
251
|
+
worker.once('exit', (code) => resolve(code))
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
let finished = new Promise<void>((resolve, reject) => {
|
|
255
|
+
worker.once('message', (msg: TestResults) => {
|
|
256
|
+
receivedResults = true
|
|
257
|
+
try {
|
|
258
|
+
onResults(msg)
|
|
259
|
+
} catch (error) {
|
|
260
|
+
reject(error)
|
|
261
|
+
return
|
|
262
|
+
}
|
|
263
|
+
if (!options.open) {
|
|
264
|
+
resolve()
|
|
265
|
+
}
|
|
266
|
+
})
|
|
183
267
|
worker.once('error', reject)
|
|
184
|
-
|
|
185
|
-
if (code
|
|
186
|
-
|
|
268
|
+
exited.then((code) => {
|
|
269
|
+
if (receivedResults || code === 0) {
|
|
270
|
+
resolve()
|
|
271
|
+
} else {
|
|
272
|
+
reject(new Error(`Worker exited with code ${code}`))
|
|
273
|
+
}
|
|
274
|
+
})
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
finished,
|
|
279
|
+
exited,
|
|
280
|
+
async terminate() {
|
|
281
|
+
try {
|
|
282
|
+
await worker.terminate()
|
|
283
|
+
return true
|
|
284
|
+
} catch (err) {
|
|
285
|
+
console.error(
|
|
286
|
+
`Error terminating worker for ${file}:`,
|
|
287
|
+
err instanceof Error ? err.message : err,
|
|
288
|
+
)
|
|
289
|
+
console.error(err)
|
|
290
|
+
return false
|
|
291
|
+
}
|
|
292
|
+
},
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function runFileInProcess(
|
|
297
|
+
file: string,
|
|
298
|
+
type: 'server' | 'e2e',
|
|
299
|
+
onResults: (results: TestResults) => void,
|
|
300
|
+
options: RunFileOptions = {},
|
|
301
|
+
): WorkerRun {
|
|
302
|
+
let receivedResults = false
|
|
303
|
+
let child = fork(fileURLToPath(workerProcessUrl), [], {
|
|
304
|
+
serialization: 'advanced',
|
|
305
|
+
stdio: ['ignore', 'inherit', 'inherit', 'ipc'],
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
let exited = new Promise<number | null>((resolve) => {
|
|
309
|
+
child.once('exit', (code) => resolve(code))
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
let finished = new Promise<void>((resolve, reject) => {
|
|
313
|
+
child.once('message', (msg: unknown) => {
|
|
314
|
+
if (!isTestResults(msg)) {
|
|
315
|
+
reject(new Error('Test worker process sent invalid results'))
|
|
316
|
+
return
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
receivedResults = true
|
|
320
|
+
try {
|
|
321
|
+
onResults(msg)
|
|
322
|
+
} catch (error) {
|
|
323
|
+
reject(error)
|
|
324
|
+
return
|
|
325
|
+
}
|
|
326
|
+
if (!options.open) {
|
|
327
|
+
resolve()
|
|
328
|
+
}
|
|
187
329
|
})
|
|
330
|
+
child.once('error', reject)
|
|
331
|
+
exited.then((code) => {
|
|
332
|
+
if (receivedResults || code === 0) {
|
|
333
|
+
resolve()
|
|
334
|
+
} else {
|
|
335
|
+
reject(new Error(`Worker process exited with code ${code}`))
|
|
336
|
+
}
|
|
337
|
+
})
|
|
338
|
+
child.send(
|
|
339
|
+
{
|
|
340
|
+
file: pathToFileURL(file).href,
|
|
341
|
+
type,
|
|
342
|
+
coverage: options.coverage,
|
|
343
|
+
open: options.open,
|
|
344
|
+
playwrightUseOpts: options.playwrightUseOpts,
|
|
345
|
+
},
|
|
346
|
+
(error) => {
|
|
347
|
+
if (error) {
|
|
348
|
+
reject(error)
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
)
|
|
188
352
|
})
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
finished,
|
|
356
|
+
exited,
|
|
357
|
+
terminate: () => terminateChildProcess(child, exited),
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function terminateChildProcess(
|
|
362
|
+
child: ChildProcess,
|
|
363
|
+
exited: Promise<number | null>,
|
|
364
|
+
): Promise<boolean> {
|
|
365
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
366
|
+
return true
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (!child.kill()) {
|
|
370
|
+
return false
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return await waitForWorkerExit(exited, 5_000)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function isTestResults(value: unknown): value is TestResults {
|
|
377
|
+
if (!isRecord(value)) {
|
|
378
|
+
return false
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return (
|
|
382
|
+
typeof value.passed === 'number' &&
|
|
383
|
+
typeof value.failed === 'number' &&
|
|
384
|
+
typeof value.skipped === 'number' &&
|
|
385
|
+
typeof value.todo === 'number' &&
|
|
386
|
+
Array.isArray(value.tests)
|
|
387
|
+
)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
391
|
+
return typeof value === 'object' && value !== null
|
|
189
392
|
}
|
package/src/lib/ts-transform.ts
CHANGED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { runTests } from './executor.ts'
|
|
2
|
+
import { importModule } from './import-module.ts'
|
|
3
|
+
import {
|
|
4
|
+
getBrowserLauncher,
|
|
5
|
+
getPlaywrightLaunchOptions,
|
|
6
|
+
getPlaywrightPageOptions,
|
|
7
|
+
type PlaywrightUseOpts,
|
|
8
|
+
} from './playwright.ts'
|
|
9
|
+
import type { CoverageConfig } from './coverage.ts'
|
|
10
|
+
import type { TestResults } from './reporters/results.ts'
|
|
11
|
+
import { createFailedResults } from './worker-results.ts'
|
|
12
|
+
import { isRecord, parseCoverageConfig } from './worker-server.ts'
|
|
13
|
+
|
|
14
|
+
export interface E2ETestWorkerData {
|
|
15
|
+
file: string
|
|
16
|
+
coverage?: CoverageConfig
|
|
17
|
+
open?: boolean
|
|
18
|
+
playwrightUseOpts?: PlaywrightUseOpts
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function runE2ETestFile(
|
|
22
|
+
value: unknown,
|
|
23
|
+
onOpenResults?: (results: TestResults) => void | Promise<void>,
|
|
24
|
+
): Promise<TestResults | undefined> {
|
|
25
|
+
try {
|
|
26
|
+
let workerData = parseE2ETestWorkerData(value)
|
|
27
|
+
|
|
28
|
+
await importModule(workerData.file, import.meta)
|
|
29
|
+
|
|
30
|
+
let launcher = await getBrowserLauncher(workerData.playwrightUseOpts)
|
|
31
|
+
let opts = getPlaywrightLaunchOptions(workerData.playwrightUseOpts)
|
|
32
|
+
let browser = await launcher.launch(opts)
|
|
33
|
+
let browserClosed = false
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
let results = await runTests({
|
|
37
|
+
browser,
|
|
38
|
+
open: workerData.open ?? false,
|
|
39
|
+
playwrightPageOptions: getPlaywrightPageOptions(workerData.playwrightUseOpts),
|
|
40
|
+
coverage: !!workerData.coverage,
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
if (workerData.open) {
|
|
44
|
+
await onOpenResults?.(results)
|
|
45
|
+
console.log('\nBrowser is open. Press Ctrl+C to close.')
|
|
46
|
+
await new Promise<void>((resolve) => browser.on('disconnected', () => resolve()))
|
|
47
|
+
return undefined
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
await browser.close()
|
|
51
|
+
browserClosed = true
|
|
52
|
+
return results
|
|
53
|
+
} finally {
|
|
54
|
+
if (!browserClosed) {
|
|
55
|
+
await browser.close()
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} catch (error) {
|
|
59
|
+
return createFailedResults(error)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseE2ETestWorkerData(value: unknown): E2ETestWorkerData {
|
|
64
|
+
if (!isRecord(value) || typeof value.file !== 'string') {
|
|
65
|
+
throw new Error('Invalid E2E test worker data')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
file: value.file,
|
|
70
|
+
coverage: parseCoverageConfig(value.coverage),
|
|
71
|
+
open: parseBoolean(value.open, 'open'),
|
|
72
|
+
playwrightUseOpts: parsePlaywrightUseOpts(value.playwrightUseOpts),
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function parseBoolean(value: unknown, name: string): boolean | undefined {
|
|
77
|
+
if (value === undefined) {
|
|
78
|
+
return undefined
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (typeof value !== 'boolean') {
|
|
82
|
+
throw new Error(`Invalid E2E test worker ${name}`)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return value
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function parsePlaywrightUseOpts(value: unknown): PlaywrightUseOpts | undefined {
|
|
89
|
+
if (value === undefined) {
|
|
90
|
+
return undefined
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!isRecord(value)) {
|
|
94
|
+
throw new Error('Invalid E2E test worker playwright options')
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return value as PlaywrightUseOpts
|
|
98
|
+
}
|