@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
|
@@ -1,15 +1,23 @@
|
|
|
1
|
-
import { colors
|
|
2
|
-
import
|
|
1
|
+
import { colors } from '../colors.ts'
|
|
2
|
+
import { normalizeLine } from '../normalize.ts'
|
|
3
3
|
import type { Reporter } from './index.ts'
|
|
4
|
+
import type { Counts, TestResult, TestResults } from './results.ts'
|
|
4
5
|
|
|
5
6
|
export class SpecReporter implements Reporter {
|
|
6
7
|
#failures: { suiteName: string; name: string; error: TestResult['error'] }[] = []
|
|
8
|
+
#files = new Set<string>()
|
|
9
|
+
#suites = new Set<string>()
|
|
7
10
|
|
|
8
11
|
onSectionStart(label: string) {
|
|
9
12
|
console.log(label)
|
|
10
13
|
}
|
|
11
14
|
|
|
12
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
|
+
|
|
13
21
|
let suiteMap = new Map<string, TestResult[]>()
|
|
14
22
|
for (let test of results.tests) {
|
|
15
23
|
let suite = test.suiteName || 'Global'
|
|
@@ -162,6 +170,8 @@ export class SpecReporter implements Reporter {
|
|
|
162
170
|
let { passed, failed, skipped, todo } = counts
|
|
163
171
|
let info = colors.cyan('ℹ')
|
|
164
172
|
console.log()
|
|
173
|
+
console.log(`${info} files ${this.#files.size}`)
|
|
174
|
+
console.log(`${info} suites ${this.#suites.size}`)
|
|
165
175
|
console.log(`${info} tests ${passed + failed + skipped + todo}`)
|
|
166
176
|
console.log(`${info} pass ${passed}`)
|
|
167
177
|
console.log(`${info} fail ${failed}`)
|
package/src/lib/reporters/tap.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { normalizeLine
|
|
2
|
-
import type { TestResults } from '../executor.ts'
|
|
1
|
+
import { normalizeLine } from '../normalize.ts'
|
|
3
2
|
import type { Reporter } from './index.ts'
|
|
3
|
+
import type { Counts, TestResults } from './results.ts'
|
|
4
4
|
|
|
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}`)
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import * as path from 'node:path'
|
|
2
|
+
import type { Browser, Page, Request } from 'playwright'
|
|
3
|
+
import { colors } from './colors.ts'
|
|
4
|
+
import { getBrowserTestRootDir } from './config.ts'
|
|
5
|
+
import {
|
|
6
|
+
collectCoverageMapFromPlaywright,
|
|
7
|
+
type CoverageMap,
|
|
8
|
+
type V8CoverageEntry,
|
|
9
|
+
} from './coverage.ts'
|
|
10
|
+
import {
|
|
11
|
+
getBrowserLauncher,
|
|
12
|
+
getPlaywrightLaunchOptions,
|
|
13
|
+
getPlaywrightPageOptions,
|
|
14
|
+
type PlaywrightUseOpts,
|
|
15
|
+
} from './playwright.ts'
|
|
16
|
+
import type { Reporter } from './reporters/index.ts'
|
|
17
|
+
import type { TestResults } from './reporters/results.ts'
|
|
18
|
+
|
|
19
|
+
// The harness reports each test result with `filePath` set to the
|
|
20
|
+
// `/scripts/<rel>` URL the iframe loaded. Reporters expect a real filesystem
|
|
21
|
+
// path so they can compute `path.relative(cwd, ...)` cleanly; otherwise they
|
|
22
|
+
// produce noisy `../../../scripts/...` strings.
|
|
23
|
+
function urlPathToFilePath(urlPath: string, rootDir: string): string {
|
|
24
|
+
if (!urlPath.startsWith('/scripts/')) return urlPath
|
|
25
|
+
return path.resolve(rootDir, urlPath.slice('/scripts/'.length))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface TestRunOptions {
|
|
29
|
+
baseUrl: string
|
|
30
|
+
console?: boolean
|
|
31
|
+
coverage?: boolean
|
|
32
|
+
open?: boolean
|
|
33
|
+
playwrightUseOpts?: PlaywrightUseOpts
|
|
34
|
+
projectName?: string
|
|
35
|
+
reporter: Reporter
|
|
36
|
+
// Test file paths so coverage collection can skip them when mapping V8
|
|
37
|
+
// entries back to filesystem files.
|
|
38
|
+
testFiles?: string[]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function runBrowserTests(options: TestRunOptions): Promise<{
|
|
42
|
+
results: TestResults
|
|
43
|
+
coverageMap: CoverageMap | null
|
|
44
|
+
close: () => Promise<void>
|
|
45
|
+
disconnected: Promise<void>
|
|
46
|
+
}> {
|
|
47
|
+
let envLabel = options.projectName ? `browser:${options.projectName}` : 'browser'
|
|
48
|
+
let browser: Browser | undefined
|
|
49
|
+
let page: Page | undefined
|
|
50
|
+
let close = async () => {
|
|
51
|
+
await page?.close()
|
|
52
|
+
await browser?.close()
|
|
53
|
+
browser = undefined
|
|
54
|
+
page = undefined
|
|
55
|
+
}
|
|
56
|
+
let results: TestResults
|
|
57
|
+
let coverageMap: CoverageMap | null = null
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
browser = await getBrowserLauncher(options.playwrightUseOpts).launch(
|
|
61
|
+
getPlaywrightLaunchOptions(options.playwrightUseOpts),
|
|
62
|
+
)
|
|
63
|
+
page = await browser.newPage(getPlaywrightPageOptions(options.playwrightUseOpts))
|
|
64
|
+
// Cap how long we'll wait for a browser-test file to signal completion.
|
|
65
|
+
// Playwright's default is 30s; bumping to 60s buys headroom for slower
|
|
66
|
+
// suites without letting a hung test hide forever. Plumb this through
|
|
67
|
+
// config later if anyone needs to tune it.
|
|
68
|
+
page.setDefaultTimeout(90_000)
|
|
69
|
+
page.setDefaultNavigationTimeout(90_000)
|
|
70
|
+
|
|
71
|
+
if (options.console) {
|
|
72
|
+
page.on('console', (msg) => console.log(`${colors.dim('[browser console]')} ${msg.text()}`))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Playwright's JS coverage is Chromium-only. Start before navigation so
|
|
76
|
+
// the harness scripts and test modules are instrumented from first parse.
|
|
77
|
+
let coverageEnabled = options.coverage && browser.browserType().name() === 'chromium'
|
|
78
|
+
if (coverageEnabled) {
|
|
79
|
+
await page.coverage.startJSCoverage({ resetOnNavigation: false })
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let totalPassed = 0
|
|
83
|
+
let totalFailed = 0
|
|
84
|
+
let totalSkipped = 0
|
|
85
|
+
let totalTodo = 0
|
|
86
|
+
let rootDir = getBrowserTestRootDir()
|
|
87
|
+
|
|
88
|
+
await page.route('**/file-results', async (route) => {
|
|
89
|
+
let results = route.request().postDataJSON() as TestResults
|
|
90
|
+
for (let test of results.tests) {
|
|
91
|
+
if (test.filePath) test.filePath = urlPathToFilePath(test.filePath, rootDir)
|
|
92
|
+
}
|
|
93
|
+
options.reporter.onResult(results, envLabel)
|
|
94
|
+
totalPassed += results.passed
|
|
95
|
+
totalFailed += results.failed
|
|
96
|
+
totalSkipped += results.skipped
|
|
97
|
+
totalTodo += results.todo
|
|
98
|
+
await route.fulfill({ status: 200 })
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// Fail the tests if any /scripts/ request fails (harness scripts, test
|
|
102
|
+
// modules, or their transitive imports — all served via the same prefix).
|
|
103
|
+
let errorPromise = new Promise((_, reject) => {
|
|
104
|
+
let isScriptRequest = (request: Request) =>
|
|
105
|
+
new URL(request.url()).pathname.startsWith('/scripts/')
|
|
106
|
+
page!.on('response', (response) => {
|
|
107
|
+
if (!response.ok() && isScriptRequest(response.request())) {
|
|
108
|
+
reject(new Error(`Failed to load script: ${response.request().url()}`))
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
page!.on('requestfailed', (request) => {
|
|
112
|
+
if (isScriptRequest(request)) {
|
|
113
|
+
reject(new Error(`Failed to load script: ${request.url()}`))
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
// Prevent unhandled rejection if we fail before setting up the listener
|
|
119
|
+
errorPromise.catch(() => {})
|
|
120
|
+
|
|
121
|
+
await page.goto(options.baseUrl)
|
|
122
|
+
await Promise.race([page.waitForFunction('window.__testsDone'), errorPromise])
|
|
123
|
+
|
|
124
|
+
if (coverageEnabled) {
|
|
125
|
+
let entries = (await page.coverage.stopJSCoverage()) as unknown as V8CoverageEntry[]
|
|
126
|
+
if (entries.length > 0) {
|
|
127
|
+
coverageMap = await collectCoverageMapFromPlaywright(
|
|
128
|
+
entries,
|
|
129
|
+
getBrowserTestRootDir(),
|
|
130
|
+
new Set(options.testFiles ?? []),
|
|
131
|
+
async (urlPath) =>
|
|
132
|
+
urlPath.startsWith('/scripts/') ? urlPath.slice('/scripts/'.length) : null,
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
results = {
|
|
138
|
+
passed: totalPassed,
|
|
139
|
+
failed: totalFailed,
|
|
140
|
+
skipped: totalSkipped,
|
|
141
|
+
todo: totalTodo,
|
|
142
|
+
tests: [],
|
|
143
|
+
}
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.error('Browser tests failed to run:', error)
|
|
146
|
+
results = {
|
|
147
|
+
passed: 0,
|
|
148
|
+
failed: 1,
|
|
149
|
+
skipped: 0,
|
|
150
|
+
todo: 0,
|
|
151
|
+
tests: [],
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (options.open) {
|
|
156
|
+
return {
|
|
157
|
+
results,
|
|
158
|
+
coverageMap,
|
|
159
|
+
close,
|
|
160
|
+
disconnected: new Promise((r) => browser!.on('disconnected', () => r())),
|
|
161
|
+
}
|
|
162
|
+
} else {
|
|
163
|
+
await close()
|
|
164
|
+
return {
|
|
165
|
+
results,
|
|
166
|
+
coverageMap,
|
|
167
|
+
close,
|
|
168
|
+
disconnected: Promise.resolve(),
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
package/src/lib/runner.ts
CHANGED
|
@@ -1,14 +1,41 @@
|
|
|
1
|
+
import { fork, type ChildProcess } from 'node:child_process'
|
|
2
|
+
import * as fsp from 'node:fs/promises'
|
|
1
3
|
import * as path from 'node:path'
|
|
2
|
-
import { pathToFileURL } from 'node:url'
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
3
5
|
import { Worker } from 'node:worker_threads'
|
|
4
|
-
import type
|
|
6
|
+
import { IS_RUNNING_FROM_SRC, type RemixTestPool } from './config.ts'
|
|
7
|
+
import {
|
|
8
|
+
collectCoverageMapFromPlaywright,
|
|
9
|
+
collectServerCoverageMap,
|
|
10
|
+
type CoverageConfig,
|
|
11
|
+
type CoverageMap,
|
|
12
|
+
type V8CoverageEntry,
|
|
13
|
+
} from './coverage.ts'
|
|
5
14
|
import { type PlaywrightUseOpts } from './playwright.ts'
|
|
6
15
|
import type { Reporter } from './reporters/index.ts'
|
|
7
|
-
import type { Counts } from './
|
|
16
|
+
import type { Counts, TestResults } from './reporters/results.ts'
|
|
8
17
|
|
|
9
|
-
|
|
18
|
+
// Ensure we load the right file whether we're running in the monorepo (TS) or
|
|
19
|
+
// from a published package (JS)
|
|
20
|
+
const ext = IS_RUNNING_FROM_SRC ? '.ts' : '.js'
|
|
10
21
|
const workerUrl = new URL(`./worker${ext}`, import.meta.url)
|
|
11
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
|
+
}
|
|
12
39
|
|
|
13
40
|
export async function runServerTests(
|
|
14
41
|
files: string[],
|
|
@@ -16,13 +43,20 @@ export async function runServerTests(
|
|
|
16
43
|
concurrency: number,
|
|
17
44
|
type: 'server' | 'e2e',
|
|
18
45
|
options: {
|
|
46
|
+
cwd?: string
|
|
19
47
|
open?: boolean
|
|
20
48
|
playwrightUseOpts?: PlaywrightUseOpts
|
|
21
49
|
projectName?: string
|
|
50
|
+
coverage?: CoverageConfig
|
|
51
|
+
workerShutdownTimeoutMs?: number
|
|
52
|
+
pool?: RemixTestPool
|
|
22
53
|
} = {},
|
|
23
|
-
): Promise<Counts> {
|
|
54
|
+
): Promise<Counts & { coverageMap: CoverageMap | null }> {
|
|
24
55
|
let counts: Counts = { passed: 0, failed: 0, skipped: 0, todo: 0 }
|
|
56
|
+
let coverageMap: CoverageMap | null = null
|
|
57
|
+
let cwd = options.cwd ?? process.cwd()
|
|
25
58
|
let envLabel = options.projectName ? `${type}:${options.projectName}` : type
|
|
59
|
+
let pool = options.pool ?? 'forks'
|
|
26
60
|
|
|
27
61
|
function accumulate(results: TestResults, file: string) {
|
|
28
62
|
reporter.onResult(
|
|
@@ -36,33 +70,78 @@ export async function runServerTests(
|
|
|
36
70
|
}
|
|
37
71
|
|
|
38
72
|
if (type === 'e2e') {
|
|
73
|
+
let allBrowserCoverageEntries: Array<{ entries: V8CoverageEntry[]; baseUrl: string }> = []
|
|
74
|
+
|
|
39
75
|
await runInConcurrentWorkers(
|
|
40
76
|
files,
|
|
41
77
|
concurrency,
|
|
42
78
|
(file) =>
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
79
|
+
runFileInPool(
|
|
80
|
+
file,
|
|
81
|
+
type,
|
|
82
|
+
(results) => {
|
|
83
|
+
accumulate(results, file)
|
|
84
|
+
if (results.e2eBrowserCoverageEntries) {
|
|
85
|
+
allBrowserCoverageEntries.push(...results.e2eBrowserCoverageEntries)
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
...options,
|
|
90
|
+
pool,
|
|
91
|
+
playwrightUseOpts: options.playwrightUseOpts,
|
|
92
|
+
},
|
|
93
|
+
),
|
|
47
94
|
() => counts.failed++,
|
|
95
|
+
!options.open,
|
|
96
|
+
options.workerShutdownTimeoutMs ?? DEFAULT_WORKER_SHUTDOWN_TIMEOUT_MS,
|
|
48
97
|
)
|
|
98
|
+
|
|
99
|
+
if (options.coverage && allBrowserCoverageEntries.length > 0) {
|
|
100
|
+
coverageMap = await collectCoverageMapFromPlaywright(
|
|
101
|
+
allBrowserCoverageEntries.flatMap((e) => e.entries),
|
|
102
|
+
cwd,
|
|
103
|
+
new Set(files),
|
|
104
|
+
async (urlPath) => (urlPath.startsWith('/') ? urlPath.slice(1) : urlPath),
|
|
105
|
+
)
|
|
106
|
+
}
|
|
49
107
|
} else {
|
|
108
|
+
let coverageDataDir: string | undefined
|
|
109
|
+
if (options.coverage) {
|
|
110
|
+
coverageDataDir = path.resolve(cwd, options.coverage.dir)
|
|
111
|
+
await fsp.mkdir(coverageDataDir, { recursive: true })
|
|
112
|
+
process.env.NODE_V8_COVERAGE = coverageDataDir
|
|
113
|
+
}
|
|
114
|
+
|
|
50
115
|
await runInConcurrentWorkers(
|
|
51
116
|
files,
|
|
52
117
|
concurrency,
|
|
53
|
-
(file) =>
|
|
118
|
+
(file) =>
|
|
119
|
+
runFileInPool(file, type, (results) => accumulate(results, file), {
|
|
120
|
+
...options,
|
|
121
|
+
pool,
|
|
122
|
+
}),
|
|
54
123
|
() => counts.failed++,
|
|
124
|
+
true,
|
|
125
|
+
options.workerShutdownTimeoutMs ?? DEFAULT_WORKER_SHUTDOWN_TIMEOUT_MS,
|
|
55
126
|
)
|
|
127
|
+
|
|
128
|
+
if (coverageDataDir) {
|
|
129
|
+
delete process.env.NODE_V8_COVERAGE
|
|
130
|
+
let serverMap = await collectServerCoverageMap(coverageDataDir, cwd, new Set(files))
|
|
131
|
+
coverageMap = serverMap
|
|
132
|
+
}
|
|
56
133
|
}
|
|
57
134
|
|
|
58
|
-
return { ...counts }
|
|
135
|
+
return { ...counts, coverageMap }
|
|
59
136
|
}
|
|
60
137
|
|
|
61
138
|
async function runInConcurrentWorkers(
|
|
62
139
|
files: string[],
|
|
63
140
|
concurrency: number,
|
|
64
|
-
runFile: (file: string) =>
|
|
141
|
+
runFile: (file: string) => WorkerRun,
|
|
65
142
|
onError: () => void,
|
|
143
|
+
terminateWhenFinished: boolean,
|
|
144
|
+
workerShutdownTimeoutMs: number,
|
|
66
145
|
): Promise<void> {
|
|
67
146
|
let index = 0
|
|
68
147
|
let active = 0
|
|
@@ -74,22 +153,42 @@ async function runInConcurrentWorkers(
|
|
|
74
153
|
index++
|
|
75
154
|
active++
|
|
76
155
|
|
|
77
|
-
runFile(file)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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()
|
|
84
181
|
}
|
|
85
182
|
},
|
|
86
|
-
(err) => {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
+
}
|
|
93
192
|
},
|
|
94
193
|
)
|
|
95
194
|
}
|
|
@@ -101,37 +200,193 @@ async function runInConcurrentWorkers(
|
|
|
101
200
|
})
|
|
102
201
|
}
|
|
103
202
|
|
|
104
|
-
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(
|
|
105
214
|
file: string,
|
|
106
215
|
type: 'server' | 'e2e',
|
|
107
216
|
onResults: (results: TestResults) => void,
|
|
108
|
-
options:
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
+
})
|
|
131
267
|
worker.once('error', reject)
|
|
132
|
-
|
|
133
|
-
if (code
|
|
134
|
-
|
|
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
|
+
}
|
|
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
|
+
}
|
|
135
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
|
+
)
|
|
136
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
|
|
137
392
|
}
|