@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.
Files changed (159) hide show
  1. package/README.md +161 -50
  2. package/dist/app/client/entry.d.ts +2 -0
  3. package/dist/app/client/entry.d.ts.map +1 -0
  4. package/dist/app/client/entry.js +328 -0
  5. package/dist/app/client/iframe.d.ts +2 -0
  6. package/dist/app/client/iframe.d.ts.map +1 -0
  7. package/dist/app/client/iframe.js +22 -0
  8. package/dist/app/server.d.ts +6 -0
  9. package/dist/app/server.d.ts.map +1 -0
  10. package/dist/app/server.js +303 -0
  11. package/dist/cli-entry.d.ts +3 -0
  12. package/dist/cli-entry.d.ts.map +1 -0
  13. package/dist/cli-entry.js +14 -0
  14. package/dist/cli.d.ts +7 -2
  15. package/dist/cli.d.ts.map +1 -1
  16. package/dist/cli.js +319 -140
  17. package/dist/index.d.ts +2 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/lib/colors.d.ts +2 -0
  20. package/dist/lib/colors.d.ts.map +1 -0
  21. package/dist/lib/colors.js +2 -0
  22. package/dist/lib/config.d.ts +59 -14
  23. package/dist/lib/config.d.ts.map +1 -1
  24. package/dist/lib/config.js +181 -38
  25. package/dist/lib/context.d.ts +37 -13
  26. package/dist/lib/context.d.ts.map +1 -1
  27. package/dist/lib/context.js +19 -3
  28. package/dist/lib/coverage-loader.d.ts +16 -0
  29. package/dist/lib/coverage-loader.d.ts.map +1 -0
  30. package/dist/lib/coverage-loader.js +20 -0
  31. package/dist/lib/coverage.d.ts +28 -0
  32. package/dist/lib/coverage.d.ts.map +1 -0
  33. package/dist/lib/coverage.js +212 -0
  34. package/dist/lib/executor.d.ts +3 -26
  35. package/dist/lib/executor.d.ts.map +1 -1
  36. package/dist/lib/executor.js +11 -6
  37. package/dist/lib/fake-timers.d.ts +13 -0
  38. package/dist/lib/fake-timers.d.ts.map +1 -0
  39. package/dist/lib/fake-timers.js +64 -0
  40. package/dist/lib/import-module.d.ts +2 -0
  41. package/dist/lib/import-module.d.ts.map +1 -0
  42. package/dist/lib/import-module.js +38 -0
  43. package/dist/lib/normalize.d.ts +2 -0
  44. package/dist/lib/normalize.d.ts.map +1 -0
  45. package/dist/lib/{utils.js → normalize.js} +0 -9
  46. package/dist/lib/playwright.d.ts +1 -1
  47. package/dist/lib/playwright.d.ts.map +1 -1
  48. package/dist/lib/playwright.js +5 -8
  49. package/dist/lib/reporters/dot.d.ts +1 -2
  50. package/dist/lib/reporters/dot.d.ts.map +1 -1
  51. package/dist/lib/reporters/dot.js +12 -1
  52. package/dist/lib/reporters/files.d.ts +1 -2
  53. package/dist/lib/reporters/files.d.ts.map +1 -1
  54. package/dist/lib/reporters/files.js +12 -1
  55. package/dist/lib/reporters/index.d.ts +4 -5
  56. package/dist/lib/reporters/index.d.ts.map +1 -1
  57. package/dist/lib/reporters/index.js +3 -3
  58. package/dist/lib/reporters/results.d.ts +30 -0
  59. package/dist/lib/reporters/results.d.ts.map +1 -0
  60. package/dist/lib/reporters/results.js +1 -0
  61. package/dist/lib/reporters/spec.d.ts +1 -2
  62. package/dist/lib/reporters/spec.d.ts.map +1 -1
  63. package/dist/lib/reporters/spec.js +12 -1
  64. package/dist/lib/reporters/tap.d.ts +1 -2
  65. package/dist/lib/reporters/tap.d.ts.map +1 -1
  66. package/dist/lib/reporters/tap.js +11 -1
  67. package/dist/lib/runner-browser.d.ts +21 -0
  68. package/dist/lib/runner-browser.d.ts.map +1 -0
  69. package/dist/lib/runner-browser.js +123 -0
  70. package/dist/lib/runner.d.ts +24 -2
  71. package/dist/lib/runner.d.ts.map +1 -1
  72. package/dist/lib/runner.js +216 -38
  73. package/dist/lib/runtime.d.ts +2 -0
  74. package/dist/lib/runtime.d.ts.map +1 -0
  75. package/dist/lib/runtime.js +2 -0
  76. package/dist/lib/ts-transform.d.ts +4 -0
  77. package/dist/lib/ts-transform.d.ts.map +1 -0
  78. package/dist/lib/ts-transform.js +29 -0
  79. package/dist/lib/worker-e2e-file.d.ts +11 -0
  80. package/dist/lib/worker-e2e-file.d.ts.map +1 -0
  81. package/dist/lib/worker-e2e-file.js +69 -0
  82. package/dist/lib/worker-e2e.js +11 -46
  83. package/dist/lib/worker-process.d.ts +2 -0
  84. package/dist/lib/worker-process.d.ts.map +1 -0
  85. package/dist/lib/worker-process.js +55 -0
  86. package/dist/lib/worker-results.d.ts +3 -0
  87. package/dist/lib/worker-results.d.ts.map +1 -0
  88. package/dist/lib/worker-results.js +20 -0
  89. package/dist/lib/worker-server.d.ts +10 -0
  90. package/dist/lib/worker-server.d.ts.map +1 -0
  91. package/dist/lib/worker-server.js +113 -0
  92. package/dist/lib/worker.js +7 -28
  93. package/dist/test/coverage/fixture.d.ts +5 -0
  94. package/dist/test/coverage/fixture.d.ts.map +1 -0
  95. package/dist/test/coverage/fixture.js +32 -0
  96. package/dist/test/coverage/test-browser.d.ts +2 -0
  97. package/dist/test/coverage/test-browser.d.ts.map +1 -0
  98. package/dist/test/coverage/test-browser.js +24 -0
  99. package/dist/test/coverage/test-e2e.d.ts +2 -0
  100. package/dist/test/coverage/test-e2e.d.ts.map +1 -0
  101. package/dist/test/coverage/test-e2e.js +60 -0
  102. package/dist/test/coverage/test-unit.d.ts +2 -0
  103. package/dist/test/coverage/test-unit.d.ts.map +1 -0
  104. package/dist/test/coverage/test-unit.js +27 -0
  105. package/dist/test/framework.test.browser.d.ts +2 -0
  106. package/dist/test/framework.test.browser.d.ts.map +1 -0
  107. package/dist/test/framework.test.browser.js +107 -0
  108. package/dist/test/framework.test.e2e.d.ts.map +1 -0
  109. package/dist/test/framework.test.e2e.js +34 -0
  110. package/package.json +30 -9
  111. package/src/app/client/entry.ts +357 -0
  112. package/src/app/client/iframe.ts +18 -0
  113. package/src/app/server.ts +336 -0
  114. package/src/cli-entry.ts +15 -0
  115. package/src/cli.ts +382 -145
  116. package/src/index.ts +2 -1
  117. package/src/lib/colors.ts +3 -0
  118. package/src/lib/config.ts +266 -54
  119. package/src/lib/context.ts +59 -17
  120. package/src/lib/coverage-loader.ts +31 -0
  121. package/src/lib/coverage.ts +320 -0
  122. package/src/lib/executor.ts +18 -35
  123. package/src/lib/fake-timers.ts +89 -0
  124. package/src/lib/import-module.ts +39 -0
  125. package/src/lib/{utils.ts → normalize.ts} +0 -18
  126. package/src/lib/playwright.ts +5 -7
  127. package/src/lib/reporters/dot.ts +12 -2
  128. package/src/lib/reporters/files.ts +12 -2
  129. package/src/lib/reporters/index.ts +4 -5
  130. package/src/lib/reporters/results.ts +29 -0
  131. package/src/lib/reporters/spec.ts +12 -2
  132. package/src/lib/reporters/tap.ts +11 -2
  133. package/src/lib/runner-browser.ts +171 -0
  134. package/src/lib/runner.ts +308 -53
  135. package/src/lib/runtime.ts +2 -0
  136. package/src/lib/ts-transform.ts +36 -0
  137. package/src/lib/worker-e2e-file.ts +98 -0
  138. package/src/lib/worker-e2e.ts +14 -49
  139. package/src/lib/worker-process.ts +69 -0
  140. package/src/lib/worker-results.ts +22 -0
  141. package/src/lib/worker-server.ts +123 -0
  142. package/src/lib/worker.ts +8 -28
  143. package/src/test/coverage/fixture.ts +34 -0
  144. package/src/test/coverage/test-browser.ts +29 -0
  145. package/src/test/coverage/test-e2e.ts +70 -0
  146. package/src/test/coverage/test-unit.ts +32 -0
  147. package/tsconfig.json +3 -1
  148. package/dist/lib/e2e-server.d.ts +0 -11
  149. package/dist/lib/e2e-server.d.ts.map +0 -1
  150. package/dist/lib/e2e-server.js +0 -15
  151. package/dist/lib/framework.test.d.ts +0 -2
  152. package/dist/lib/framework.test.d.ts.map +0 -1
  153. package/dist/lib/framework.test.e2e.d.ts.map +0 -1
  154. package/dist/lib/framework.test.e2e.js +0 -29
  155. package/dist/lib/framework.test.js +0 -283
  156. package/dist/lib/utils.d.ts +0 -16
  157. package/dist/lib/utils.d.ts.map +0 -1
  158. package/src/lib/e2e-server.ts +0 -28
  159. /package/dist/{lib → test}/framework.test.e2e.d.ts +0 -0
@@ -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
+ }
@@ -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
+ }
@@ -1,52 +1,17 @@
1
- import { workerData, parentPort } from 'node:worker_threads'
2
- import { tsImport } from 'tsx/esm/api'
3
- import { createServer } from './e2e-server.ts'
4
- import { runTests, type TestResults } from './executor.ts'
5
- import {
6
- getBrowserLauncher,
7
- getPlaywrightLaunchOptions,
8
- getPlaywrightPageOptions,
9
- } from './playwright.ts'
1
+ import { parentPort, workerData } from 'node:worker_threads'
2
+ import { runE2ETestFile } from './worker-e2e-file.ts'
10
3
 
11
- try {
12
- await tsImport(workerData.file, import.meta.url)
4
+ if (!parentPort) {
5
+ throw new Error('E2E test worker is missing a parent port')
6
+ }
7
+
8
+ const port = parentPort
9
+ const results = await runE2ETestFile(workerData, (openResults) => {
10
+ port.postMessage(openResults)
11
+ })
13
12
 
14
- let launcher = await getBrowserLauncher(workerData.playwrightUseOpts)
15
- let opts = getPlaywrightLaunchOptions(workerData.playwrightUseOpts)
16
- let browser = await launcher.launch(opts)
17
- try {
18
- let results = await runTests({
19
- browser,
20
- createServer,
21
- open: workerData.open,
22
- playwrightPageOptions: getPlaywrightPageOptions(workerData.playwrightUseOpts),
23
- })
24
- parentPort!.postMessage(results)
25
- if (workerData.open) {
26
- console.log('\nBrowser is open. Press Ctrl+C to close.')
27
- await new Promise<void>((resolve) => browser.on('disconnected', () => resolve()))
28
- }
29
- } finally {
30
- await browser.close()
31
- }
32
- } catch (e) {
33
- let results: TestResults = {
34
- passed: 0,
35
- failed: 1,
36
- skipped: 0,
37
- todo: 0,
38
- tests: [
39
- {
40
- name: '',
41
- suiteName: '',
42
- status: 'failed',
43
- duration: 0,
44
- error: {
45
- message: e instanceof Error ? e.message : String(e),
46
- stack: e instanceof Error ? e.stack : undefined,
47
- },
48
- },
49
- ],
50
- }
51
- parentPort!.postMessage(results)
13
+ if (results) {
14
+ port.postMessage(results)
52
15
  }
16
+
17
+ process.exit(0)
@@ -0,0 +1,69 @@
1
+ import { runE2ETestFile } from './worker-e2e-file.ts'
2
+ import type { TestResults } from './reporters/results.ts'
3
+ import { runServerTestFile } from './worker-server.ts'
4
+ import { createFailedResults } from './worker-results.ts'
5
+
6
+ const workerData = await readWorkerData()
7
+
8
+ const results = await runWorkerProcessFile(workerData)
9
+
10
+ if (results) {
11
+ await sendResults(results)
12
+ }
13
+
14
+ if (process.connected) {
15
+ process.disconnect()
16
+ }
17
+
18
+ process.exitCode = 0
19
+
20
+ function readWorkerData(): Promise<unknown> {
21
+ return new Promise((resolve, reject) => {
22
+ function cleanup() {
23
+ process.off('message', onMessage)
24
+ process.off('disconnect', onDisconnect)
25
+ }
26
+
27
+ function onMessage(value: unknown) {
28
+ cleanup()
29
+ resolve(value)
30
+ }
31
+
32
+ function onDisconnect() {
33
+ cleanup()
34
+ reject(new Error('Test worker process disconnected'))
35
+ }
36
+
37
+ process.once('message', onMessage)
38
+ process.once('disconnect', onDisconnect)
39
+ })
40
+ }
41
+
42
+ async function runWorkerProcessFile(value: unknown): Promise<TestResults | undefined> {
43
+ try {
44
+ if (!isRecord(value) || (value.type !== 'server' && value.type !== 'e2e')) {
45
+ throw new Error('Invalid test worker process data')
46
+ }
47
+
48
+ return value.type === 'e2e'
49
+ ? await runE2ETestFile(value, sendResults)
50
+ : await runServerTestFile(value)
51
+ } catch (error) {
52
+ return createFailedResults(error)
53
+ }
54
+ }
55
+
56
+ async function sendResults(results: TestResults): Promise<void> {
57
+ if (!process.send) {
58
+ throw new Error('Test worker process is missing an IPC channel')
59
+ }
60
+
61
+ let send = process.send.bind(process)
62
+ await new Promise<void>((resolve, reject) => {
63
+ send(results, undefined, undefined, (error) => (error ? reject(error) : resolve()))
64
+ })
65
+ }
66
+
67
+ function isRecord(value: unknown): value is Record<string, unknown> {
68
+ return typeof value === 'object' && value !== null
69
+ }
@@ -0,0 +1,22 @@
1
+ import type { TestResults } from './reporters/results.ts'
2
+
3
+ export function createFailedResults(error: unknown): TestResults {
4
+ return {
5
+ passed: 0,
6
+ failed: 1,
7
+ skipped: 0,
8
+ todo: 0,
9
+ tests: [
10
+ {
11
+ name: '',
12
+ suiteName: '',
13
+ status: 'failed',
14
+ duration: 0,
15
+ error: {
16
+ message: error instanceof Error ? error.message : String(error),
17
+ stack: error instanceof Error ? error.stack : undefined,
18
+ },
19
+ },
20
+ ],
21
+ }
22
+ }
@@ -0,0 +1,123 @@
1
+ import * as mod from 'node:module'
2
+ import { IS_RUNNING_FROM_SRC } from './config.ts'
3
+ import { importModule } from './import-module.ts'
4
+ import type { CoverageConfig } from './coverage.ts'
5
+ import type { TestResults } from './reporters/results.ts'
6
+ import { runTests } from './executor.ts'
7
+ import { IS_BUN } from './runtime.ts'
8
+ import { createFailedResults } from './worker-results.ts'
9
+
10
+ export interface ServerTestWorkerData {
11
+ file: string
12
+ coverage?: CoverageConfig
13
+ }
14
+
15
+ export async function runServerTestFile(value: unknown): Promise<TestResults> {
16
+ let workerData: ServerTestWorkerData | undefined
17
+
18
+ try {
19
+ workerData = parseServerTestWorkerData(value)
20
+
21
+ // When coverage is enabled in Node, we use a coverage-friendly TypeScript loader which
22
+ // replaces tsx's minified transformation with a non-minified esbuild transform
23
+ // so V8 coverage byte offsets align with readable source lines. This hook runs
24
+ // before the inherited tsx hook (hooks are LIFO), so it intercepts .ts imports and
25
+ // short-circuits before tsx transforms them.
26
+ if (workerData.coverage && !IS_BUN) {
27
+ // Ensure we load the right file whether we're running in the monorepo (TS) or
28
+ // from a published package (JS)
29
+ let ext = IS_RUNNING_FROM_SRC ? '.ts' : '.js'
30
+ mod.register(new URL(`./coverage-loader${ext}`, import.meta.url), import.meta.url)
31
+ await import(workerData.file)
32
+ } else {
33
+ await importModule(workerData.file, import.meta)
34
+ }
35
+
36
+ let results = await runTests()
37
+ await takeCoverage(workerData.coverage)
38
+ return results
39
+ } catch (e) {
40
+ try {
41
+ await takeCoverage(workerData?.coverage)
42
+ } catch (coverageError) {
43
+ e = coverageError
44
+ }
45
+
46
+ return createFailedResults(e)
47
+ }
48
+ }
49
+
50
+ function parseServerTestWorkerData(value: unknown): ServerTestWorkerData {
51
+ if (!isRecord(value) || typeof value.file !== 'string') {
52
+ throw new Error('Invalid server test worker data')
53
+ }
54
+
55
+ return {
56
+ file: value.file,
57
+ coverage: parseCoverageConfig(value.coverage),
58
+ }
59
+ }
60
+
61
+ export function isRecord(value: unknown): value is Record<string, unknown> {
62
+ return typeof value === 'object' && value !== null
63
+ }
64
+
65
+ export function parseCoverageConfig(value: unknown): CoverageConfig | undefined {
66
+ if (value === undefined) {
67
+ return undefined
68
+ }
69
+
70
+ if (!isRecord(value) || typeof value.dir !== 'string') {
71
+ throw new Error('Invalid server test worker coverage config')
72
+ }
73
+
74
+ let coverage: CoverageConfig = {
75
+ dir: value.dir,
76
+ }
77
+ let include = parseStringArray(value.include, 'include')
78
+ let exclude = parseStringArray(value.exclude, 'exclude')
79
+ let statements = parseNumber(value.statements, 'statements')
80
+ let lines = parseNumber(value.lines, 'lines')
81
+ let branches = parseNumber(value.branches, 'branches')
82
+ let functions = parseNumber(value.functions, 'functions')
83
+
84
+ if (include) coverage.include = include
85
+ if (exclude) coverage.exclude = exclude
86
+ if (statements !== undefined) coverage.statements = statements
87
+ if (lines !== undefined) coverage.lines = lines
88
+ if (branches !== undefined) coverage.branches = branches
89
+ if (functions !== undefined) coverage.functions = functions
90
+
91
+ return coverage
92
+ }
93
+
94
+ function parseStringArray(value: unknown, name: string): string[] | undefined {
95
+ if (value === undefined) {
96
+ return undefined
97
+ }
98
+
99
+ if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) {
100
+ throw new Error(`Invalid server test worker coverage ${name}`)
101
+ }
102
+
103
+ return value
104
+ }
105
+
106
+ function parseNumber(value: unknown, name: string): number | undefined {
107
+ if (value === undefined) {
108
+ return undefined
109
+ }
110
+
111
+ if (typeof value !== 'number') {
112
+ throw new Error(`Invalid server test worker coverage ${name}`)
113
+ }
114
+
115
+ return value
116
+ }
117
+
118
+ async function takeCoverage(coverage: CoverageConfig | undefined): Promise<void> {
119
+ if (coverage && !IS_BUN) {
120
+ let v8 = await import('node:v8')
121
+ v8.takeCoverage()
122
+ }
123
+ }
package/src/lib/worker.ts CHANGED
@@ -1,30 +1,10 @@
1
- import { workerData, parentPort } from 'node:worker_threads'
2
- import { tsImport } from 'tsx/esm/api'
3
- import { runTests, type TestResults } from './executor.ts'
1
+ import { parentPort, workerData } from 'node:worker_threads'
2
+ import { runServerTestFile } from './worker-server.ts'
4
3
 
5
- try {
6
- await tsImport(workerData.file, import.meta.url)
7
-
8
- let results = await runTests()
9
- parentPort!.postMessage(results)
10
- } catch (e) {
11
- let results: TestResults = {
12
- passed: 0,
13
- failed: 1,
14
- skipped: 0,
15
- todo: 0,
16
- tests: [
17
- {
18
- name: '',
19
- suiteName: '',
20
- status: 'failed',
21
- duration: 0,
22
- error: {
23
- message: e instanceof Error ? e.message : String(e),
24
- stack: e instanceof Error ? e.stack : undefined,
25
- },
26
- },
27
- ],
28
- }
29
- parentPort!.postMessage(results)
4
+ if (!parentPort) {
5
+ throw new Error('Server test worker is missing a parent port')
30
6
  }
7
+
8
+ const results = await runServerTestFile(workerData)
9
+ parentPort.postMessage(results)
10
+ process.exit(0)
@@ -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/component"
14
+ "jsxImportSource": "@remix-run/ui"
13
15
  }
14
16
  }
@@ -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"}
@@ -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,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=framework.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"framework.test.d.ts","sourceRoot":"","sources":["../../src/lib/framework.test.tsx"],"names":[],"mappings":""}