@remix-run/test 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -35
- package/dist/app/client/entry.d.ts +2 -0
- package/dist/app/client/entry.d.ts.map +1 -0
- package/dist/app/client/entry.js +324 -0
- package/dist/app/client/iframe.d.ts +2 -0
- package/dist/app/client/iframe.d.ts.map +1 -0
- package/dist/app/client/iframe.js +22 -0
- package/dist/app/server.d.ts +6 -0
- package/dist/app/server.d.ts.map +1 -0
- package/dist/app/server.js +303 -0
- package/dist/cli-entry.d.ts +3 -0
- package/dist/cli-entry.d.ts.map +1 -0
- package/dist/cli-entry.js +14 -0
- package/dist/cli.d.ts +7 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +273 -139
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/lib/colors.d.ts +2 -0
- package/dist/lib/colors.d.ts.map +1 -0
- package/dist/lib/colors.js +2 -0
- package/dist/lib/config.d.ts +32 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +125 -22
- package/dist/lib/context.d.ts +37 -13
- package/dist/lib/context.d.ts.map +1 -1
- package/dist/lib/context.js +19 -3
- package/dist/lib/coverage-loader.d.ts +16 -0
- package/dist/lib/coverage-loader.d.ts.map +1 -0
- package/dist/lib/coverage-loader.js +20 -0
- package/dist/lib/coverage.d.ts +28 -0
- package/dist/lib/coverage.d.ts.map +1 -0
- package/dist/lib/coverage.js +212 -0
- package/dist/lib/executor.d.ts +3 -26
- package/dist/lib/executor.d.ts.map +1 -1
- package/dist/lib/executor.js +11 -6
- package/dist/lib/fake-timers.d.ts +6 -0
- package/dist/lib/fake-timers.d.ts.map +1 -0
- package/dist/lib/fake-timers.js +45 -0
- package/dist/lib/import-module.d.ts +2 -0
- package/dist/lib/import-module.d.ts.map +1 -0
- package/dist/lib/import-module.js +29 -0
- package/dist/lib/normalize.d.ts +2 -0
- package/dist/lib/normalize.d.ts.map +1 -0
- package/dist/lib/{utils.js → normalize.js} +0 -9
- package/dist/lib/playwright.d.ts +1 -1
- package/dist/lib/playwright.d.ts.map +1 -1
- package/dist/lib/playwright.js +5 -8
- package/dist/lib/reporters/dot.d.ts +1 -2
- package/dist/lib/reporters/dot.d.ts.map +1 -1
- package/dist/lib/reporters/dot.js +2 -1
- package/dist/lib/reporters/files.d.ts +1 -2
- package/dist/lib/reporters/files.d.ts.map +1 -1
- package/dist/lib/reporters/files.js +2 -1
- package/dist/lib/reporters/index.d.ts +4 -5
- package/dist/lib/reporters/index.d.ts.map +1 -1
- package/dist/lib/reporters/index.js +3 -3
- package/dist/lib/reporters/results.d.ts +30 -0
- package/dist/lib/reporters/results.d.ts.map +1 -0
- package/dist/lib/reporters/results.js +1 -0
- package/dist/lib/reporters/spec.d.ts +1 -2
- package/dist/lib/reporters/spec.d.ts.map +1 -1
- package/dist/lib/reporters/spec.js +2 -1
- package/dist/lib/reporters/tap.d.ts +1 -2
- package/dist/lib/reporters/tap.d.ts.map +1 -1
- package/dist/lib/reporters/tap.js +1 -1
- package/dist/lib/runner-browser.d.ts +21 -0
- package/dist/lib/runner-browser.d.ts.map +1 -0
- package/dist/lib/runner-browser.js +117 -0
- package/dist/lib/runner.d.ts +7 -2
- package/dist/lib/runner.d.ts.map +1 -1
- package/dist/lib/runner.js +33 -4
- package/dist/lib/runtime.d.ts +2 -0
- package/dist/lib/runtime.d.ts.map +1 -0
- package/dist/lib/runtime.js +2 -0
- package/dist/lib/ts-transform.d.ts +4 -0
- package/dist/lib/ts-transform.d.ts.map +1 -0
- package/dist/lib/ts-transform.js +29 -0
- package/dist/lib/worker-e2e.js +5 -4
- package/dist/lib/worker.js +31 -3
- package/dist/test/coverage/fixture.d.ts +5 -0
- package/dist/test/coverage/fixture.d.ts.map +1 -0
- package/dist/test/coverage/fixture.js +32 -0
- package/dist/test/coverage/test-browser.d.ts +2 -0
- package/dist/test/coverage/test-browser.d.ts.map +1 -0
- package/dist/test/coverage/test-browser.js +24 -0
- package/dist/test/coverage/test-e2e.d.ts +2 -0
- package/dist/test/coverage/test-e2e.d.ts.map +1 -0
- package/dist/test/coverage/test-e2e.js +60 -0
- package/dist/test/coverage/test-unit.d.ts +2 -0
- package/dist/test/coverage/test-unit.d.ts.map +1 -0
- package/dist/test/coverage/test-unit.js +27 -0
- package/dist/test/framework.test.browser.d.ts +2 -0
- package/dist/test/framework.test.browser.d.ts.map +1 -0
- package/dist/test/framework.test.browser.js +107 -0
- package/dist/test/framework.test.e2e.d.ts.map +1 -0
- package/dist/test/framework.test.e2e.js +34 -0
- package/package.json +30 -9
- package/src/app/client/entry.ts +353 -0
- package/src/app/client/iframe.ts +18 -0
- package/src/app/server.ts +336 -0
- package/src/cli-entry.ts +15 -0
- package/src/cli.ts +322 -148
- package/src/index.ts +1 -0
- package/src/lib/colors.ts +3 -0
- package/src/lib/config.ts +169 -23
- package/src/lib/context.ts +59 -17
- package/src/lib/coverage-loader.ts +31 -0
- package/src/lib/coverage.ts +320 -0
- package/src/lib/executor.ts +18 -35
- package/src/lib/fake-timers.ts +64 -0
- package/src/lib/import-module.ts +29 -0
- package/src/lib/{utils.ts → normalize.ts} +0 -18
- package/src/lib/playwright.ts +5 -7
- package/src/lib/reporters/dot.ts +3 -2
- package/src/lib/reporters/files.ts +3 -2
- package/src/lib/reporters/index.ts +4 -5
- package/src/lib/reporters/results.ts +29 -0
- package/src/lib/reporters/spec.ts +3 -2
- package/src/lib/reporters/tap.ts +2 -2
- package/src/lib/runner-browser.ts +165 -0
- package/src/lib/runner.ts +62 -10
- package/src/lib/runtime.ts +2 -0
- package/src/lib/ts-transform.ts +36 -0
- package/src/lib/worker-e2e.ts +7 -5
- package/src/lib/worker.ts +24 -4
- package/src/test/coverage/fixture.ts +34 -0
- package/src/test/coverage/test-browser.ts +29 -0
- package/src/test/coverage/test-e2e.ts +70 -0
- package/src/test/coverage/test-unit.ts +32 -0
- package/tsconfig.json +3 -1
- package/dist/lib/e2e-server.d.ts +0 -11
- package/dist/lib/e2e-server.d.ts.map +0 -1
- package/dist/lib/e2e-server.js +0 -15
- package/dist/lib/framework.test.d.ts +0 -2
- package/dist/lib/framework.test.d.ts.map +0 -1
- package/dist/lib/framework.test.e2e.d.ts.map +0 -1
- package/dist/lib/framework.test.e2e.js +0 -29
- package/dist/lib/framework.test.js +0 -283
- package/dist/lib/utils.d.ts +0 -16
- package/dist/lib/utils.d.ts.map +0 -1
- package/src/lib/e2e-server.ts +0 -28
- /package/dist/{lib → test}/framework.test.e2e.d.ts +0 -0
|
@@ -0,0 +1,165 @@
|
|
|
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
|
+
|
|
65
|
+
if (options.console) {
|
|
66
|
+
page.on('console', (msg) => console.log(`${colors.dim('[browser console]')} ${msg.text()}`))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Playwright's JS coverage is Chromium-only. Start before navigation so
|
|
70
|
+
// the harness scripts and test modules are instrumented from first parse.
|
|
71
|
+
let coverageEnabled = options.coverage && browser.browserType().name() === 'chromium'
|
|
72
|
+
if (coverageEnabled) {
|
|
73
|
+
await page.coverage.startJSCoverage({ resetOnNavigation: false })
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let totalPassed = 0
|
|
77
|
+
let totalFailed = 0
|
|
78
|
+
let totalSkipped = 0
|
|
79
|
+
let totalTodo = 0
|
|
80
|
+
let rootDir = getBrowserTestRootDir()
|
|
81
|
+
|
|
82
|
+
await page.route('**/file-results', async (route) => {
|
|
83
|
+
let results = route.request().postDataJSON() as TestResults
|
|
84
|
+
for (let test of results.tests) {
|
|
85
|
+
if (test.filePath) test.filePath = urlPathToFilePath(test.filePath, rootDir)
|
|
86
|
+
}
|
|
87
|
+
options.reporter.onResult(results, envLabel)
|
|
88
|
+
totalPassed += results.passed
|
|
89
|
+
totalFailed += results.failed
|
|
90
|
+
totalSkipped += results.skipped
|
|
91
|
+
totalTodo += results.todo
|
|
92
|
+
await route.fulfill({ status: 200 })
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
// Fail the tests if any /scripts/ request fails (harness scripts, test
|
|
96
|
+
// modules, or their transitive imports — all served via the same prefix).
|
|
97
|
+
let errorPromise = new Promise((_, reject) => {
|
|
98
|
+
let isScriptRequest = (request: Request) =>
|
|
99
|
+
new URL(request.url()).pathname.startsWith('/scripts/')
|
|
100
|
+
page!.on('response', (response) => {
|
|
101
|
+
if (!response.ok() && isScriptRequest(response.request())) {
|
|
102
|
+
reject(new Error(`Failed to load script: ${response.request().url()}`))
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
page!.on('requestfailed', (request) => {
|
|
106
|
+
if (isScriptRequest(request)) {
|
|
107
|
+
reject(new Error(`Failed to load script: ${request.url()}`))
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
// Prevent unhandled rejection if we fail before setting up the listener
|
|
113
|
+
errorPromise.catch(() => {})
|
|
114
|
+
|
|
115
|
+
await page.goto(options.baseUrl)
|
|
116
|
+
await Promise.race([page.waitForFunction('window.__testsDone'), errorPromise])
|
|
117
|
+
|
|
118
|
+
if (coverageEnabled) {
|
|
119
|
+
let entries = (await page.coverage.stopJSCoverage()) as unknown as V8CoverageEntry[]
|
|
120
|
+
if (entries.length > 0) {
|
|
121
|
+
coverageMap = await collectCoverageMapFromPlaywright(
|
|
122
|
+
entries,
|
|
123
|
+
getBrowserTestRootDir(),
|
|
124
|
+
new Set(options.testFiles ?? []),
|
|
125
|
+
async (urlPath) =>
|
|
126
|
+
urlPath.startsWith('/scripts/') ? urlPath.slice('/scripts/'.length) : null,
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
results = {
|
|
132
|
+
passed: totalPassed,
|
|
133
|
+
failed: totalFailed,
|
|
134
|
+
skipped: totalSkipped,
|
|
135
|
+
todo: totalTodo,
|
|
136
|
+
tests: [],
|
|
137
|
+
}
|
|
138
|
+
} catch (error) {
|
|
139
|
+
console.error('Browser tests failed to run:', error)
|
|
140
|
+
results = {
|
|
141
|
+
passed: 0,
|
|
142
|
+
failed: 1,
|
|
143
|
+
skipped: 0,
|
|
144
|
+
todo: 0,
|
|
145
|
+
tests: [],
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (options.open) {
|
|
150
|
+
return {
|
|
151
|
+
results,
|
|
152
|
+
coverageMap,
|
|
153
|
+
close,
|
|
154
|
+
disconnected: new Promise((r) => browser!.on('disconnected', () => r())),
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
await close()
|
|
158
|
+
return {
|
|
159
|
+
results,
|
|
160
|
+
coverageMap,
|
|
161
|
+
close,
|
|
162
|
+
disconnected: Promise.resolve(),
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
package/src/lib/runner.ts
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
|
+
import * as fsp from 'node:fs/promises'
|
|
1
2
|
import * as path from 'node:path'
|
|
2
3
|
import { pathToFileURL } from 'node:url'
|
|
3
4
|
import { Worker } from 'node:worker_threads'
|
|
4
|
-
import
|
|
5
|
+
import { IS_RUNNING_FROM_SRC } from './config.ts'
|
|
6
|
+
import {
|
|
7
|
+
collectCoverageMapFromPlaywright,
|
|
8
|
+
collectServerCoverageMap,
|
|
9
|
+
type CoverageConfig,
|
|
10
|
+
type CoverageMap,
|
|
11
|
+
type V8CoverageEntry,
|
|
12
|
+
} from './coverage.ts'
|
|
5
13
|
import { type PlaywrightUseOpts } from './playwright.ts'
|
|
6
14
|
import type { Reporter } from './reporters/index.ts'
|
|
7
|
-
import type { Counts } from './
|
|
15
|
+
import type { Counts, TestResults } from './reporters/results.ts'
|
|
8
16
|
|
|
9
|
-
|
|
17
|
+
// Ensure we load the right file whether we're running in the monorepo (TS) or
|
|
18
|
+
// from a published package (JS)
|
|
19
|
+
const ext = IS_RUNNING_FROM_SRC ? '.ts' : '.js'
|
|
10
20
|
const workerUrl = new URL(`./worker${ext}`, import.meta.url)
|
|
11
21
|
const workerE2EUrl = new URL(`./worker-e2e${ext}`, import.meta.url)
|
|
12
22
|
|
|
@@ -16,12 +26,16 @@ export async function runServerTests(
|
|
|
16
26
|
concurrency: number,
|
|
17
27
|
type: 'server' | 'e2e',
|
|
18
28
|
options: {
|
|
29
|
+
cwd?: string
|
|
19
30
|
open?: boolean
|
|
20
31
|
playwrightUseOpts?: PlaywrightUseOpts
|
|
21
32
|
projectName?: string
|
|
33
|
+
coverage?: CoverageConfig
|
|
22
34
|
} = {},
|
|
23
|
-
): Promise<Counts> {
|
|
35
|
+
): Promise<Counts & { coverageMap: CoverageMap | null }> {
|
|
24
36
|
let counts: Counts = { passed: 0, failed: 0, skipped: 0, todo: 0 }
|
|
37
|
+
let coverageMap: CoverageMap | null = null
|
|
38
|
+
let cwd = options.cwd ?? process.cwd()
|
|
25
39
|
let envLabel = options.projectName ? `${type}:${options.projectName}` : type
|
|
26
40
|
|
|
27
41
|
function accumulate(results: TestResults, file: string) {
|
|
@@ -36,26 +50,60 @@ export async function runServerTests(
|
|
|
36
50
|
}
|
|
37
51
|
|
|
38
52
|
if (type === 'e2e') {
|
|
53
|
+
let allBrowserCoverageEntries: Array<{ entries: V8CoverageEntry[]; baseUrl: string }> = []
|
|
54
|
+
|
|
39
55
|
await runInConcurrentWorkers(
|
|
40
56
|
files,
|
|
41
57
|
concurrency,
|
|
42
58
|
(file) =>
|
|
43
|
-
runFileInWorker(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
59
|
+
runFileInWorker(
|
|
60
|
+
file,
|
|
61
|
+
type,
|
|
62
|
+
(results) => {
|
|
63
|
+
accumulate(results, file)
|
|
64
|
+
if (results.e2eBrowserCoverageEntries) {
|
|
65
|
+
allBrowserCoverageEntries.push(...results.e2eBrowserCoverageEntries)
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
...options,
|
|
70
|
+
playwrightUseOpts: options.playwrightUseOpts,
|
|
71
|
+
},
|
|
72
|
+
),
|
|
47
73
|
() => counts.failed++,
|
|
48
74
|
)
|
|
75
|
+
|
|
76
|
+
if (options.coverage && allBrowserCoverageEntries.length > 0) {
|
|
77
|
+
coverageMap = await collectCoverageMapFromPlaywright(
|
|
78
|
+
allBrowserCoverageEntries.flatMap((e) => e.entries),
|
|
79
|
+
cwd,
|
|
80
|
+
new Set(files),
|
|
81
|
+
async (urlPath) => (urlPath.startsWith('/') ? urlPath.slice(1) : urlPath),
|
|
82
|
+
)
|
|
83
|
+
}
|
|
49
84
|
} else {
|
|
85
|
+
let coverageDataDir: string | undefined
|
|
86
|
+
if (options.coverage) {
|
|
87
|
+
coverageDataDir = path.resolve(cwd, options.coverage.dir)
|
|
88
|
+
await fsp.mkdir(coverageDataDir, { recursive: true })
|
|
89
|
+
process.env.NODE_V8_COVERAGE = coverageDataDir
|
|
90
|
+
}
|
|
91
|
+
|
|
50
92
|
await runInConcurrentWorkers(
|
|
51
93
|
files,
|
|
52
94
|
concurrency,
|
|
53
|
-
(file) => runFileInWorker(file, type, (results) => accumulate(results, file)),
|
|
95
|
+
(file) => runFileInWorker(file, type, (results) => accumulate(results, file), options),
|
|
54
96
|
() => counts.failed++,
|
|
55
97
|
)
|
|
98
|
+
|
|
99
|
+
if (coverageDataDir) {
|
|
100
|
+
delete process.env.NODE_V8_COVERAGE
|
|
101
|
+
let serverMap = await collectServerCoverageMap(coverageDataDir, cwd, new Set(files))
|
|
102
|
+
coverageMap = serverMap
|
|
103
|
+
}
|
|
56
104
|
}
|
|
57
105
|
|
|
58
|
-
return { ...counts }
|
|
106
|
+
return { ...counts, coverageMap }
|
|
59
107
|
}
|
|
60
108
|
|
|
61
109
|
async function runInConcurrentWorkers(
|
|
@@ -106,6 +154,8 @@ function runFileInWorker(
|
|
|
106
154
|
type: 'server' | 'e2e',
|
|
107
155
|
onResults: (results: TestResults) => void,
|
|
108
156
|
options: {
|
|
157
|
+
cwd?: string
|
|
158
|
+
coverage?: CoverageConfig
|
|
109
159
|
open?: boolean
|
|
110
160
|
playwrightUseOpts?: PlaywrightUseOpts
|
|
111
161
|
} = {},
|
|
@@ -117,6 +167,7 @@ function runFileInWorker(
|
|
|
117
167
|
workerData: {
|
|
118
168
|
file: pathToFileURL(file).href,
|
|
119
169
|
type,
|
|
170
|
+
coverage: options.coverage,
|
|
120
171
|
open: options.open,
|
|
121
172
|
playwrightUseOpts: options.playwrightUseOpts,
|
|
122
173
|
},
|
|
@@ -125,6 +176,7 @@ function runFileInWorker(
|
|
|
125
176
|
workerData: {
|
|
126
177
|
file: pathToFileURL(file).href,
|
|
127
178
|
type,
|
|
179
|
+
coverage: options.coverage,
|
|
128
180
|
},
|
|
129
181
|
})
|
|
130
182
|
worker.once('message', (msg: TestResults) => onResults(msg))
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { transform, type TsconfigRaw } from 'esbuild'
|
|
2
|
+
import { getTsconfig, type TsConfigResult } from 'get-tsconfig'
|
|
3
|
+
import * as path from 'node:path'
|
|
4
|
+
|
|
5
|
+
const tsconfigCache = new Map<string, TsConfigResult | null>()
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* Transform a TypeScript file to JavaScript using esbuild with an inline
|
|
9
|
+
* source map and no minification. Used by the coverage ESM loader hook (so V8
|
|
10
|
+
* instruments readable JS), the coverage collector (so byte offsets can be
|
|
11
|
+
* re-derived and mapped back to TypeScript lines), and the browser harness
|
|
12
|
+
* server (so the bytes V8 sees in the browser match what the collector
|
|
13
|
+
* re-derives). Identical inputs must produce identical outputs across all
|
|
14
|
+
* call sites or coverage offsets won't line up.
|
|
15
|
+
*
|
|
16
|
+
* Compiler options (notably JSX) are taken from the nearest `tsconfig.json`
|
|
17
|
+
* walking up from the file's directory, so each project picks up its own
|
|
18
|
+
* `jsxImportSource` etc. Discovery results are cached by directory.
|
|
19
|
+
*/
|
|
20
|
+
export async function transformTypeScript(
|
|
21
|
+
source: string,
|
|
22
|
+
filePath: string,
|
|
23
|
+
): Promise<{ code: string }> {
|
|
24
|
+
let loader: 'ts' | 'tsx' = filePath.endsWith('.tsx') ? 'tsx' : 'ts'
|
|
25
|
+
|
|
26
|
+
let tsConfig = getTsconfig(path.dirname(filePath), 'tsconfig.json', tsconfigCache)
|
|
27
|
+
|
|
28
|
+
let result = await transform(source, {
|
|
29
|
+
loader,
|
|
30
|
+
sourcemap: 'inline',
|
|
31
|
+
sourcesContent: true,
|
|
32
|
+
sourcefile: filePath,
|
|
33
|
+
tsconfigRaw: { compilerOptions: tsConfig?.config.compilerOptions ?? {} },
|
|
34
|
+
})
|
|
35
|
+
return { code: result.code }
|
|
36
|
+
}
|
package/src/lib/worker-e2e.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { workerData, parentPort } from 'node:worker_threads'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { runTests, type TestResults } from './executor.ts'
|
|
2
|
+
import { runTests } from './executor.ts'
|
|
3
|
+
import { importModule } from './import-module.ts'
|
|
5
4
|
import {
|
|
6
5
|
getBrowserLauncher,
|
|
7
6
|
getPlaywrightLaunchOptions,
|
|
8
7
|
getPlaywrightPageOptions,
|
|
9
8
|
} from './playwright.ts'
|
|
9
|
+
import type { TestResults } from './reporters/results.ts'
|
|
10
10
|
|
|
11
11
|
try {
|
|
12
|
-
await
|
|
12
|
+
await importModule(workerData.file, import.meta)
|
|
13
13
|
|
|
14
14
|
let launcher = await getBrowserLauncher(workerData.playwrightUseOpts)
|
|
15
15
|
let opts = getPlaywrightLaunchOptions(workerData.playwrightUseOpts)
|
|
@@ -17,9 +17,9 @@ try {
|
|
|
17
17
|
try {
|
|
18
18
|
let results = await runTests({
|
|
19
19
|
browser,
|
|
20
|
-
createServer,
|
|
21
20
|
open: workerData.open,
|
|
22
21
|
playwrightPageOptions: getPlaywrightPageOptions(workerData.playwrightUseOpts),
|
|
22
|
+
coverage: workerData.coverage,
|
|
23
23
|
})
|
|
24
24
|
parentPort!.postMessage(results)
|
|
25
25
|
if (workerData.open) {
|
|
@@ -29,6 +29,7 @@ try {
|
|
|
29
29
|
} finally {
|
|
30
30
|
await browser.close()
|
|
31
31
|
}
|
|
32
|
+
process.exit(0)
|
|
32
33
|
} catch (e) {
|
|
33
34
|
let results: TestResults = {
|
|
34
35
|
passed: 0,
|
|
@@ -49,4 +50,5 @@ try {
|
|
|
49
50
|
],
|
|
50
51
|
}
|
|
51
52
|
parentPort!.postMessage(results)
|
|
53
|
+
process.exit(0)
|
|
52
54
|
}
|
package/src/lib/worker.ts
CHANGED
|
@@ -1,12 +1,31 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
1
|
+
import * as mod from 'node:module'
|
|
2
|
+
import * as path from 'node:path'
|
|
3
|
+
import { parentPort, workerData } from 'node:worker_threads'
|
|
4
|
+
import { runTests } from './executor.ts'
|
|
5
|
+
import { importModule } from './import-module.ts'
|
|
6
|
+
import type { TestResults } from './reporters/results.ts'
|
|
7
|
+
import { IS_BUN } from './runtime.ts'
|
|
8
|
+
import { IS_RUNNING_FROM_SRC } from './config.ts'
|
|
4
9
|
|
|
5
10
|
try {
|
|
6
|
-
|
|
11
|
+
// When coverage is enabled in Node, we use a coverage-friendly TypeScript loader which
|
|
12
|
+
// replaces tsx's minified transformation with a non-minified esbuild transform
|
|
13
|
+
// so V8 coverage byte offsets align with readable source lines. This hook runs
|
|
14
|
+
// before the inherited tsx hook (hooks are LIFO), so it intercepts .ts imports and
|
|
15
|
+
// short-circuits before tsx transforms them.
|
|
16
|
+
if (workerData.coverage && !IS_BUN) {
|
|
17
|
+
// Ensure we load the right file whether we're running in the monorepo (TS) or
|
|
18
|
+
// from a published package (JS)
|
|
19
|
+
let ext = IS_RUNNING_FROM_SRC ? '.ts' : '.js'
|
|
20
|
+
mod.register(new URL(`./coverage-loader${ext}`, import.meta.url), import.meta.url)
|
|
21
|
+
await import(workerData.file)
|
|
22
|
+
} else {
|
|
23
|
+
await importModule(workerData.file, import.meta)
|
|
24
|
+
}
|
|
7
25
|
|
|
8
26
|
let results = await runTests()
|
|
9
27
|
parentPort!.postMessage(results)
|
|
28
|
+
process.exit(0)
|
|
10
29
|
} catch (e) {
|
|
11
30
|
let results: TestResults = {
|
|
12
31
|
passed: 0,
|
|
@@ -27,4 +46,5 @@ try {
|
|
|
27
46
|
],
|
|
28
47
|
}
|
|
29
48
|
parentPort!.postMessage(results)
|
|
49
|
+
process.exit(0)
|
|
30
50
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// This file exists solely to validate coverage accuracy.
|
|
2
|
+
// Each function has a known expected coverage profile based on
|
|
3
|
+
// which paths the associated tests exercise.
|
|
4
|
+
|
|
5
|
+
// Fully covered — both statements and the single branch
|
|
6
|
+
export function add(a: number, b: number): number {
|
|
7
|
+
return a + b
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Partially covered — only the `n > 0` branch is tested
|
|
11
|
+
export function classify(n: number): string {
|
|
12
|
+
if (n > 0) {
|
|
13
|
+
return 'positive'
|
|
14
|
+
} else if (n < 0) {
|
|
15
|
+
return 'negative'
|
|
16
|
+
} else {
|
|
17
|
+
return 'zero'
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Never called — 0% across the board
|
|
22
|
+
export function uncalledFunction(): string {
|
|
23
|
+
let result = 'never'
|
|
24
|
+
result += ' reached'
|
|
25
|
+
return result
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Partially covered — only the truthy `name` branch is tested
|
|
29
|
+
export function greet(name?: string): string {
|
|
30
|
+
if (name) {
|
|
31
|
+
return `Hello, ${name}!`
|
|
32
|
+
}
|
|
33
|
+
return 'Hello, stranger!'
|
|
34
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as assert from '@remix-run/assert'
|
|
2
|
+
import { describe, it } from '../../lib/framework.ts'
|
|
3
|
+
import { add, classify, greet } from './fixture.ts'
|
|
4
|
+
|
|
5
|
+
// Expected coverage for coverage-fixture.ts (same as the server/e2e fixture
|
|
6
|
+
// tests):
|
|
7
|
+
//
|
|
8
|
+
// add — 100% functions, statements, lines, branches
|
|
9
|
+
// classify — function covered, but only the `n > 0` branch is hit
|
|
10
|
+
// (the `n < 0` and `else` branches are uncovered)
|
|
11
|
+
// uncalledFunction — 0% across the board (never imported)
|
|
12
|
+
// greet — function covered, but only the truthy `name` branch is hit
|
|
13
|
+
// (the fallback `Hello, stranger!` line is uncovered)
|
|
14
|
+
|
|
15
|
+
describe('browser coverage fixture', () => {
|
|
16
|
+
it('exercises some but not all code paths in the browser', () => {
|
|
17
|
+
assert.equal(add(2, 3), 5)
|
|
18
|
+
assert.equal(add(-1, 1), 0)
|
|
19
|
+
|
|
20
|
+
assert.equal(classify(42), 'positive')
|
|
21
|
+
assert.equal(classify(1), 'positive')
|
|
22
|
+
// deliberately NOT testing classify(-1) or classify(0)
|
|
23
|
+
|
|
24
|
+
assert.equal(greet('World'), 'Hello, World!')
|
|
25
|
+
// deliberately NOT testing greet() without an argument
|
|
26
|
+
|
|
27
|
+
// deliberately NOT importing or calling uncalledFunction
|
|
28
|
+
})
|
|
29
|
+
})
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import * as assert from '@remix-run/assert'
|
|
2
|
+
import { createTestServer } from '@remix-run/node-fetch-server/test'
|
|
3
|
+
import * as fsp from 'node:fs/promises'
|
|
4
|
+
import * as path from 'node:path'
|
|
5
|
+
import { describe, it } from '../../lib/framework.ts'
|
|
6
|
+
import { transformTypeScript } from '../../lib/ts-transform.ts'
|
|
7
|
+
|
|
8
|
+
// Expected coverage for coverage/fixture.ts (same as the server fixture test):
|
|
9
|
+
//
|
|
10
|
+
// add — 100% functions, statements, lines, branches
|
|
11
|
+
// classify — function covered, but only the `n > 0` branch is hit
|
|
12
|
+
// uncalledFunction — 0% across the board (never called)
|
|
13
|
+
// greet — function covered, but only the truthy `name` branch is hit
|
|
14
|
+
|
|
15
|
+
describe('e2e coverage fixture', () => {
|
|
16
|
+
it('exercises some but not all code paths in the browser', async (t) => {
|
|
17
|
+
// Compile the fixture TypeScript to browser-ready JS
|
|
18
|
+
let fixturePath = path.resolve(import.meta.dirname, './fixture.ts')
|
|
19
|
+
let fixtureSource = await fsp.readFile(fixturePath, 'utf-8')
|
|
20
|
+
let { code: fixtureJs } = await transformTypeScript(fixtureSource, fixturePath)
|
|
21
|
+
|
|
22
|
+
let handler: (request: Request) => Response = (req) => {
|
|
23
|
+
let url = new URL(req.url)
|
|
24
|
+
|
|
25
|
+
if (url.pathname === '/') {
|
|
26
|
+
return new Response(
|
|
27
|
+
[
|
|
28
|
+
`<!doctype html>`,
|
|
29
|
+
`<html>`,
|
|
30
|
+
`<body>`,
|
|
31
|
+
` <div id="result"></div>`,
|
|
32
|
+
` <script type="module">`,
|
|
33
|
+
` import { add, classify, greet } from '/src/test/coverage/fixture.ts'`,
|
|
34
|
+
` // Exercise the same paths as the server fixture test:`,
|
|
35
|
+
` // - add: fully covered`,
|
|
36
|
+
` // - classify: only positive branch`,
|
|
37
|
+
` // - greet: only with a name`,
|
|
38
|
+
` // - uncalledFunction: never imported`,
|
|
39
|
+
` let results = [`,
|
|
40
|
+
` add(2, 3),`,
|
|
41
|
+
` add(-1, 1),`,
|
|
42
|
+
` classify(42),`,
|
|
43
|
+
` classify(1),`,
|
|
44
|
+
` greet('World'),`,
|
|
45
|
+
` ]`,
|
|
46
|
+
` document.getElementById('result').textContent = results.join(',')`,
|
|
47
|
+
` </script>`,
|
|
48
|
+
`</body>`,
|
|
49
|
+
`</html>`,
|
|
50
|
+
].join('\n'),
|
|
51
|
+
{ headers: { 'Content-Type': 'text/html' } },
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Serve the compiled fixture at the path the import expects
|
|
56
|
+
if (url.pathname === '/src/test/coverage/fixture.ts') {
|
|
57
|
+
return new Response(fixtureJs, {
|
|
58
|
+
headers: { 'Content-Type': 'application/javascript' },
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return new Response('Not found', { status: 404 })
|
|
63
|
+
}
|
|
64
|
+
let page = await t.serve(await createTestServer(handler))
|
|
65
|
+
|
|
66
|
+
await page.goto('/')
|
|
67
|
+
let result = await page.locator('#result').textContent()
|
|
68
|
+
assert.equal(result, '5,0,positive,positive,Hello, World!')
|
|
69
|
+
})
|
|
70
|
+
})
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as assert from '@remix-run/assert'
|
|
2
|
+
import { describe, it } from '../../lib/framework.ts'
|
|
3
|
+
import { add, classify, greet } from './fixture.ts'
|
|
4
|
+
|
|
5
|
+
// Expected coverage for coverage-fixture.ts:
|
|
6
|
+
//
|
|
7
|
+
// add — 100% functions, statements, lines, branches
|
|
8
|
+
// classify — function covered, but only the `n > 0` branch is hit
|
|
9
|
+
// (the `n < 0` and `else` branches are uncovered)
|
|
10
|
+
// uncalledFunction — 0% across the board (never imported)
|
|
11
|
+
// greet — function covered, but only the truthy `name` branch is hit
|
|
12
|
+
// (the fallback `Hello, stranger!` line is uncovered)
|
|
13
|
+
|
|
14
|
+
describe('coverage/fixture.ts', () => {
|
|
15
|
+
it('add returns the sum', () => {
|
|
16
|
+
assert.equal(add(2, 3), 5)
|
|
17
|
+
assert.equal(add(-1, 1), 0)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('classify identifies positive numbers only', () => {
|
|
21
|
+
assert.equal(classify(42), 'positive')
|
|
22
|
+
assert.equal(classify(1), 'positive')
|
|
23
|
+
// deliberately NOT testing classify(-1) or classify(0)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
// deliberately NOT importing or calling uncalledFunction
|
|
27
|
+
|
|
28
|
+
it('greet with a name only', () => {
|
|
29
|
+
assert.equal(greet('World'), 'Hello, World!')
|
|
30
|
+
// deliberately NOT testing greet() without an argument
|
|
31
|
+
})
|
|
32
|
+
})
|
package/tsconfig.json
CHANGED
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
"compilerOptions": {
|
|
3
3
|
"strict": true,
|
|
4
4
|
"lib": ["ES2024", "DOM", "DOM.Iterable"],
|
|
5
|
+
"types": ["node", "dom-navigation"],
|
|
5
6
|
"module": "ES2022",
|
|
6
7
|
"moduleResolution": "Bundler",
|
|
7
8
|
"target": "ESNext",
|
|
8
9
|
"allowImportingTsExtensions": true,
|
|
9
10
|
"rewriteRelativeImportExtensions": true,
|
|
10
11
|
"verbatimModuleSyntax": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
11
13
|
"jsx": "react-jsx",
|
|
12
|
-
"jsxImportSource": "@remix-run/
|
|
14
|
+
"jsxImportSource": "@remix-run/ui"
|
|
13
15
|
}
|
|
14
16
|
}
|
package/dist/lib/e2e-server.d.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export interface CreateServerFunction {
|
|
2
|
-
(handler: (req: Request) => Promise<Response>): Promise<{
|
|
3
|
-
baseUrl: string;
|
|
4
|
-
close(): Promise<void>;
|
|
5
|
-
}>;
|
|
6
|
-
}
|
|
7
|
-
export declare function createServer(handler: (req: Request) => Promise<Response>): Promise<{
|
|
8
|
-
baseUrl: string;
|
|
9
|
-
close(): Promise<void>;
|
|
10
|
-
}>;
|
|
11
|
-
//# sourceMappingURL=e2e-server.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"e2e-server.d.ts","sourceRoot":"","sources":["../../src/lib/e2e-server.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,oBAAoB;IACnC,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC;QACtD,OAAO,EAAE,MAAM,CAAA;QACf,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;KACvB,CAAC,CAAA;CACH;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC;IAClF,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACvB,CAAC,CAcD"}
|
package/dist/lib/e2e-server.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import * as http from 'node:http';
|
|
2
|
-
import { createRequestListener } from '@remix-run/node-fetch-server';
|
|
3
|
-
export function createServer(handler) {
|
|
4
|
-
return new Promise((resolve, reject) => {
|
|
5
|
-
let server = http.createServer(createRequestListener(handler));
|
|
6
|
-
server.listen(0, '127.0.0.1', () => {
|
|
7
|
-
let addr = server.address();
|
|
8
|
-
resolve({
|
|
9
|
-
baseUrl: `http://127.0.0.1:${addr.port}`,
|
|
10
|
-
close: () => new Promise((r, rj) => server.close((e) => (e ? rj(e) : r()))),
|
|
11
|
-
});
|
|
12
|
-
});
|
|
13
|
-
server.on('error', reject);
|
|
14
|
-
});
|
|
15
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"framework.test.d.ts","sourceRoot":"","sources":["../../src/lib/framework.test.tsx"],"names":[],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"framework.test.e2e.d.ts","sourceRoot":"","sources":["../../src/lib/framework.test.e2e.tsx"],"names":[],"mappings":""}
|