@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.
Files changed (143) hide show
  1. package/README.md +140 -35
  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 +324 -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 +273 -139
  17. package/dist/index.d.ts +1 -0
  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 +32 -1
  23. package/dist/lib/config.d.ts.map +1 -1
  24. package/dist/lib/config.js +125 -22
  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 +6 -0
  38. package/dist/lib/fake-timers.d.ts.map +1 -0
  39. package/dist/lib/fake-timers.js +45 -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 +29 -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 +2 -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 +2 -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 +2 -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 +1 -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 +117 -0
  70. package/dist/lib/runner.d.ts +7 -2
  71. package/dist/lib/runner.d.ts.map +1 -1
  72. package/dist/lib/runner.js +33 -4
  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.js +5 -4
  80. package/dist/lib/worker.js +31 -3
  81. package/dist/test/coverage/fixture.d.ts +5 -0
  82. package/dist/test/coverage/fixture.d.ts.map +1 -0
  83. package/dist/test/coverage/fixture.js +32 -0
  84. package/dist/test/coverage/test-browser.d.ts +2 -0
  85. package/dist/test/coverage/test-browser.d.ts.map +1 -0
  86. package/dist/test/coverage/test-browser.js +24 -0
  87. package/dist/test/coverage/test-e2e.d.ts +2 -0
  88. package/dist/test/coverage/test-e2e.d.ts.map +1 -0
  89. package/dist/test/coverage/test-e2e.js +60 -0
  90. package/dist/test/coverage/test-unit.d.ts +2 -0
  91. package/dist/test/coverage/test-unit.d.ts.map +1 -0
  92. package/dist/test/coverage/test-unit.js +27 -0
  93. package/dist/test/framework.test.browser.d.ts +2 -0
  94. package/dist/test/framework.test.browser.d.ts.map +1 -0
  95. package/dist/test/framework.test.browser.js +107 -0
  96. package/dist/test/framework.test.e2e.d.ts.map +1 -0
  97. package/dist/test/framework.test.e2e.js +34 -0
  98. package/package.json +30 -9
  99. package/src/app/client/entry.ts +353 -0
  100. package/src/app/client/iframe.ts +18 -0
  101. package/src/app/server.ts +336 -0
  102. package/src/cli-entry.ts +15 -0
  103. package/src/cli.ts +322 -148
  104. package/src/index.ts +1 -0
  105. package/src/lib/colors.ts +3 -0
  106. package/src/lib/config.ts +169 -23
  107. package/src/lib/context.ts +59 -17
  108. package/src/lib/coverage-loader.ts +31 -0
  109. package/src/lib/coverage.ts +320 -0
  110. package/src/lib/executor.ts +18 -35
  111. package/src/lib/fake-timers.ts +64 -0
  112. package/src/lib/import-module.ts +29 -0
  113. package/src/lib/{utils.ts → normalize.ts} +0 -18
  114. package/src/lib/playwright.ts +5 -7
  115. package/src/lib/reporters/dot.ts +3 -2
  116. package/src/lib/reporters/files.ts +3 -2
  117. package/src/lib/reporters/index.ts +4 -5
  118. package/src/lib/reporters/results.ts +29 -0
  119. package/src/lib/reporters/spec.ts +3 -2
  120. package/src/lib/reporters/tap.ts +2 -2
  121. package/src/lib/runner-browser.ts +165 -0
  122. package/src/lib/runner.ts +62 -10
  123. package/src/lib/runtime.ts +2 -0
  124. package/src/lib/ts-transform.ts +36 -0
  125. package/src/lib/worker-e2e.ts +7 -5
  126. package/src/lib/worker.ts +24 -4
  127. package/src/test/coverage/fixture.ts +34 -0
  128. package/src/test/coverage/test-browser.ts +29 -0
  129. package/src/test/coverage/test-e2e.ts +70 -0
  130. package/src/test/coverage/test-unit.ts +32 -0
  131. package/tsconfig.json +3 -1
  132. package/dist/lib/e2e-server.d.ts +0 -11
  133. package/dist/lib/e2e-server.d.ts.map +0 -1
  134. package/dist/lib/e2e-server.js +0 -15
  135. package/dist/lib/framework.test.d.ts +0 -2
  136. package/dist/lib/framework.test.d.ts.map +0 -1
  137. package/dist/lib/framework.test.e2e.d.ts.map +0 -1
  138. package/dist/lib/framework.test.e2e.js +0 -29
  139. package/dist/lib/framework.test.js +0 -283
  140. package/dist/lib/utils.d.ts +0 -16
  141. package/dist/lib/utils.d.ts.map +0 -1
  142. package/src/lib/e2e-server.ts +0 -28
  143. /package/dist/{lib → test}/framework.test.e2e.d.ts +0 -0
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ import * as process from 'node:process'
3
+
4
+ import { runRemixTest } from './cli.ts'
5
+
6
+ try {
7
+ let exitCode = await runRemixTest({
8
+ argv: process.argv.slice(2),
9
+ cwd: process.cwd(),
10
+ })
11
+ process.exit(exitCode)
12
+ } catch (error) {
13
+ console.error('Error running tests:', error)
14
+ process.exit(1)
15
+ }
package/src/cli.ts CHANGED
@@ -1,210 +1,384 @@
1
- #!/usr/bin/env node
2
1
  import * as fsp from 'node:fs/promises'
2
+ import type * as http from 'node:http'
3
3
  import * as path from 'node:path'
4
- import { tsImport } from 'tsx/esm/api'
5
- import { runServerTests } from './lib/runner.ts'
4
+ import {
5
+ getRemixTestHelpText,
6
+ IS_RUNNING_FROM_SRC,
7
+ loadConfig,
8
+ type ResolvedRemixTestConfig,
9
+ } from './lib/config.ts'
10
+ import { generateCombinedCoverageReport } from './lib/coverage.ts'
11
+ import { loadPlaywrightConfig, resolveProjects } from './lib/playwright.ts'
6
12
  import { createReporter } from './lib/reporters/index.ts'
13
+ import { runBrowserTests } from './lib/runner-browser.ts'
14
+ import { runServerTests } from './lib/runner.ts'
7
15
  import { createWatcher } from './lib/watcher.ts'
8
- import { loadPlaywrightConfig, resolveProjects } from './lib/playwright.ts'
9
- import { loadConfig, type ResolvedRemixTestConfig } from './lib/config.ts'
10
- import type { Counts } from './lib/utils.ts'
16
+ import { importModule } from './lib/import-module.ts'
17
+ import type { Counts } from './lib/reporters/results.ts'
18
+ import { IS_BUN } from './lib/runtime.ts'
19
+ import { isMainThread } from 'node:worker_threads'
11
20
 
12
- const config = await loadConfig()
21
+ export { getRemixTestHelpText }
13
22
 
14
- let hasExited = false
15
- let latestExitCode = 0
16
- let watcher: ReturnType<typeof createWatcher> | undefined
17
- let running = false
18
- let queued = false
19
- let rerunTimer: NodeJS.Timeout | undefined
23
+ export interface RunRemixTestOptions {
24
+ argv?: string[]
25
+ cwd?: string
26
+ }
20
27
 
21
- process.on('SIGINT', () => cleanupAndExit(latestExitCode))
22
- process.on('SIGTERM', () => cleanupAndExit(latestExitCode))
28
+ interface DiscoveredTests {
29
+ files: string[]
30
+ serverFiles: string[]
31
+ browserFiles: string[]
32
+ e2eFiles: string[]
33
+ }
23
34
 
24
- try {
25
- await executeRun()
35
+ export async function runRemixTest(options: RunRemixTestOptions = {}): Promise<number> {
36
+ let argv = options.argv ?? process.argv.slice(2)
37
+ let cwd = await resolveCwd(options.cwd ?? process.cwd())
38
+ let previousCwd = process.cwd()
26
39
 
27
- if (config.watch) {
28
- console.log('Watching for changes. Press Ctrl+C to stop.')
40
+ if (!isMainThread) {
41
+ return await runRemixTestInCwd(argv, cwd)
42
+ }
43
+
44
+ try {
45
+ process.chdir(cwd)
46
+ return await runRemixTestInCwd(argv, cwd)
47
+ } finally {
48
+ process.chdir(previousCwd)
29
49
  }
30
- } catch {
31
- cleanupAndExit(1)
32
50
  }
33
51
 
34
- async function executeRun() {
35
- if (hasExited) return
52
+ async function runRemixTestInCwd(argv: string[], cwd: string): Promise<number> {
53
+ if (argv.includes('--help') || argv.includes('-h')) {
54
+ console.log(getRemixTestHelpText())
55
+ return 0
56
+ }
36
57
 
37
- running = true
58
+ let config = await loadConfig(argv, cwd)
59
+ let hasExited = false
60
+ let latestExitCode = 0
61
+ let watcher: ReturnType<typeof createWatcher> | undefined
62
+ let running = false
63
+ let queued = false
64
+ let rerunTimer: NodeJS.Timeout | undefined
65
+ let browserServer: http.Server | undefined
66
+ let browserServerFilesKey: string | undefined
67
+ let browserPort: number | undefined
68
+ let resolveRun: ((exitCode: number) => void) | undefined
69
+
70
+ let runPromise = new Promise<number>((resolve) => {
71
+ resolveRun = resolve
72
+ })
73
+
74
+ let cleanupAndExit = (code: number) => {
75
+ if (hasExited) return
76
+ hasExited = true
77
+ watcher?.close()
78
+ browserServer?.close()
79
+ clearTimeout(rerunTimer)
80
+ process.off('SIGINT', handleInterrupt)
81
+ process.off('SIGTERM', handleInterrupt)
82
+ resolveRun?.(code)
83
+ }
38
84
 
39
- let globalTeardown: (() => Promise<void> | void) | undefined
85
+ let handleInterrupt = () => cleanupAndExit(latestExitCode)
86
+
87
+ let closeBrowserServer = async () => {
88
+ if (!browserServer) return
89
+ let server = browserServer
90
+ await new Promise<void>((resolve, reject) =>
91
+ server.close((error) => (error ? reject(error) : resolve())),
92
+ )
93
+ browserServer = undefined
94
+ browserServerFilesKey = undefined
95
+ browserPort = undefined
96
+ }
40
97
 
41
- try {
42
- if (config.setup) {
43
- let mod = await tsImport(path.resolve(process.cwd(), config.setup), {
44
- parentURL: import.meta.url,
45
- })
46
- let globalSetup: (() => Promise<void> | void) | undefined = mod.globalSetup
47
- globalTeardown = mod.globalTeardown
48
- await globalSetup?.()
49
- }
98
+ let queueRerun = (reason: string) => {
99
+ if (!config.watch || hasExited) return
50
100
 
51
- let { files, serverFiles, e2eFiles } = await discoverTests(config)
101
+ clearTimeout(rerunTimer)
52
102
 
53
- if (config.watch) {
54
- watcher ??= createWatcher((file) => queueRerun(file))
55
- watcher.update(files)
56
- }
103
+ rerunTimer = setTimeout(() => {
104
+ rerunTimer = undefined
105
+ if (running) {
106
+ queued = true
107
+ } else {
108
+ console.log(`\n↻ Change detected (${reason}), re-running tests...\n`)
109
+ void executeRun()
110
+ }
111
+ }, 100)
112
+ }
57
113
 
58
- let playwrightConfig =
59
- config.playwrightConfig == null || typeof config.playwrightConfig === 'string'
60
- ? await loadPlaywrightConfig(config.playwrightConfig)
61
- : config.playwrightConfig
114
+ let executeRun = async () => {
115
+ if (hasExited) return
62
116
 
63
- let reporter = createReporter(config.reporter)
64
- let startTime = performance.now()
117
+ running = true
65
118
 
66
- let counts: Counts = {
67
- passed: 0,
68
- failed: 0,
69
- skipped: 0,
70
- todo: 0,
71
- }
119
+ let globalTeardown: (() => Promise<void> | void) | undefined
72
120
 
73
- // Run server tests
74
- if (serverFiles.length > 0) {
75
- reporter.onSectionStart('\nRunning server tests:')
76
- let serverResult = await runServerTests(serverFiles, reporter, config.concurrency, 'server')
77
- counts.failed += serverResult.failed
78
- counts.passed += serverResult.passed
79
- counts.skipped += serverResult.skipped
80
- counts.todo += serverResult.todo
81
- }
121
+ try {
122
+ if (config.setup) {
123
+ let mod = await importModule(path.resolve(cwd, config.setup), import.meta)
124
+ let globalSetup: (() => Promise<void> | void) | undefined = mod.globalSetup
125
+ globalTeardown = mod.globalTeardown
126
+ await globalSetup?.()
127
+ }
82
128
 
83
- // Run e2e tests for all browsers configured by the user
84
- if (e2eFiles.length > 0) {
85
- let projects = resolveProjects(playwrightConfig)
86
- if (config.project) {
87
- let projectNames = config.project.split(',').map((p) => p.trim())
88
- projects = projects.filter((p) => p.name && projectNames.includes(p.name))
89
- if (projects.length === 0) {
90
- throw new Error(`No playwright projects found with name(s) "${config.project}"`)
91
- }
129
+ let discoveredTests = await discoverTests(config, cwd)
130
+ if (discoveredTests == null) {
131
+ latestExitCode = 1
132
+ cleanupAndExit(latestExitCode)
133
+ return
134
+ }
135
+
136
+ let { files, serverFiles, browserFiles, e2eFiles } = discoveredTests
137
+
138
+ if (config.watch) {
139
+ watcher ??= createWatcher((file) => queueRerun(file))
140
+ watcher.update(files)
141
+ }
142
+
143
+ let browserFilesKey = browserFiles.join('\0')
144
+ if (browserServer && browserFiles.length === 0) {
145
+ await closeBrowserServer()
146
+ } else if (
147
+ browserFiles.length > 0 &&
148
+ (!browserServer || browserServerFilesKey !== browserFilesKey)
149
+ ) {
150
+ await closeBrowserServer()
151
+ let { startServer } = IS_RUNNING_FROM_SRC
152
+ ? await importModule('./app/server.ts', import.meta)
153
+ : await import(`./app/server.js`)
154
+ let result = await startServer(browserFiles)
155
+ browserServer = result.server
156
+ browserServerFilesKey = browserFilesKey
157
+ browserPort = result.port
158
+ }
159
+
160
+ let playwrightConfig =
161
+ config.playwrightConfig == null || typeof config.playwrightConfig === 'string'
162
+ ? await loadPlaywrightConfig(config.playwrightConfig, cwd)
163
+ : config.playwrightConfig
164
+
165
+ let reporter = createReporter(config.reporter)
166
+ let startTime = performance.now()
167
+
168
+ let counts: Counts = {
169
+ passed: 0,
170
+ failed: 0,
171
+ skipped: 0,
172
+ todo: 0,
173
+ }
174
+ let allCoverageMaps: Array<ReturnType<typeof Object.values>[number] | null | undefined> = []
175
+
176
+ if (serverFiles.length > 0) {
177
+ reporter.onSectionStart('\nRunning server tests:')
178
+ let serverResult = await runServerTests(
179
+ serverFiles,
180
+ reporter,
181
+ config.concurrency,
182
+ 'server',
183
+ {
184
+ coverage: config.coverage,
185
+ cwd,
186
+ },
187
+ )
188
+ counts.failed += serverResult.failed
189
+ counts.passed += serverResult.passed
190
+ counts.skipped += serverResult.skipped
191
+ counts.todo += serverResult.todo
192
+ allCoverageMaps.push(serverResult.coverageMap)
92
193
  }
93
194
 
94
- for (let project of projects) {
95
- reporter.onSectionStart(`\nRunning tests for project \`${project.name}\`:`)
96
-
97
- if (config.browser?.open) {
98
- if (project.playwrightUseOpts?.headless === true) {
99
- let label = project.name ? ` (project "${project.name}")` : ''
100
- console.warn(
101
- `Warning: browser.open is set but playwright headless is explicitly true${label} — ignoring browser.open`,
102
- )
103
- } else {
104
- project.playwrightUseOpts = { ...project.playwrightUseOpts, headless: false }
195
+ // Run browser/e2e tests for all browsers configured by the user
196
+ if (browserFiles.length > 0 || e2eFiles.length > 0) {
197
+ let projects = resolveProjects(playwrightConfig)
198
+ if (config.project) {
199
+ let projectNames = config.project.split(',').map((project) => project.trim())
200
+ projects = projects.filter(
201
+ (project) => project.name && projectNames.includes(project.name),
202
+ )
203
+ if (projects.length === 0) {
204
+ throw new Error(`No playwright projects found with name(s) "${config.project}"`)
105
205
  }
106
206
  }
107
207
 
108
- let e2eResult =
109
- e2eFiles.length > 0
110
- ? await runServerTests(e2eFiles, reporter, config.concurrency, 'e2e', {
111
- open: config.browser?.open,
112
- playwrightUseOpts: project.playwrightUseOpts,
113
- projectName: project.name,
114
- })
115
- : null
116
-
117
- counts.passed += e2eResult?.passed ?? 0
118
- counts.failed += e2eResult?.failed ?? 0
119
- counts.skipped += e2eResult?.skipped ?? 0
120
- counts.todo += e2eResult?.todo ?? 0
208
+ let lastBrowserResult: Awaited<ReturnType<typeof runBrowserTests>> | null = null
209
+
210
+ for (let project of projects) {
211
+ reporter.onSectionStart(`\nRunning tests for project \`${project.name}\`:`)
212
+
213
+ if (config.browser?.open) {
214
+ if (project.playwrightUseOpts?.headless === true) {
215
+ let label = project.name ? ` (project "${project.name}")` : ''
216
+ console.warn(
217
+ `Warning: browser.open is set but playwright headless is explicitly true${label} — ignoring browser.open`,
218
+ )
219
+ } else {
220
+ project.playwrightUseOpts = { ...project.playwrightUseOpts, headless: false }
221
+ }
222
+ }
223
+
224
+ let [browserResult, e2eResult] = await Promise.all([
225
+ browserFiles.length > 0
226
+ ? runBrowserTests({
227
+ baseUrl: `http://localhost:${browserPort}`,
228
+ console: config.browser?.echo,
229
+ coverage: !!config.coverage,
230
+ open: config.browser?.open,
231
+ playwrightUseOpts: project.playwrightUseOpts,
232
+ projectName: project.name,
233
+ reporter,
234
+ testFiles: browserFiles,
235
+ })
236
+ : null,
237
+ e2eFiles.length > 0
238
+ ? runServerTests(e2eFiles, reporter, config.concurrency, 'e2e', {
239
+ open: config.browser?.open,
240
+ playwrightUseOpts: project.playwrightUseOpts,
241
+ projectName: project.name,
242
+ coverage: config.coverage,
243
+ cwd,
244
+ })
245
+ : null,
246
+ ])
247
+
248
+ counts.passed += (browserResult?.results.passed ?? 0) + (e2eResult?.passed ?? 0)
249
+ counts.failed += (browserResult?.results.failed ?? 0) + (e2eResult?.failed ?? 0)
250
+ counts.skipped += (browserResult?.results.skipped ?? 0) + (e2eResult?.skipped ?? 0)
251
+ counts.todo += (browserResult?.results.todo ?? 0) + (e2eResult?.todo ?? 0)
252
+ allCoverageMaps.push(browserResult?.coverageMap)
253
+ allCoverageMaps.push(e2eResult?.coverageMap)
254
+
255
+ if (browserResult) {
256
+ lastBrowserResult = browserResult
257
+ }
258
+ }
259
+
260
+ if (config.browser?.open && lastBrowserResult) {
261
+ console.log('\nBrowser is open. Press Ctrl+C to close.')
262
+ await Promise.race([
263
+ lastBrowserResult.disconnected,
264
+ new Promise<void>((resolve) => {
265
+ process.once('SIGINT', resolve)
266
+ process.once('SIGTERM', resolve)
267
+ }),
268
+ ])
269
+ await lastBrowserResult.close()
270
+ }
121
271
  }
122
- }
123
272
 
124
- reporter.onSummary(counts, performance.now() - startTime)
273
+ reporter.onSummary(counts, performance.now() - startTime)
125
274
 
126
- latestExitCode = counts.failed > 0 ? 1 : 0
127
- } catch (error) {
128
- console.error('Error running tests:', error)
129
- latestExitCode = 1
130
- } finally {
131
- await globalTeardown?.()
132
- running = false
133
- if (queued) {
134
- queued = false
135
- queueRerun('queued change')
136
- } else if (!config.watch) {
137
- cleanupAndExit(latestExitCode)
275
+ let thresholdsPassed = true
276
+ if (config.coverage) {
277
+ thresholdsPassed = await generateCombinedCoverageReport(
278
+ allCoverageMaps,
279
+ cwd,
280
+ config.coverage,
281
+ )
282
+ }
283
+ latestExitCode = counts.failed > 0 || !thresholdsPassed ? 1 : 0
284
+ } catch (error) {
285
+ console.error('Error running tests:', error)
286
+ latestExitCode = 1
287
+ } finally {
288
+ await globalTeardown?.()
289
+ running = false
290
+ if (queued) {
291
+ queued = false
292
+ queueRerun('queued change')
293
+ } else if (!config.watch) {
294
+ cleanupAndExit(latestExitCode)
295
+ }
138
296
  }
139
297
  }
140
- }
141
298
 
142
- async function discoverTests(config: ResolvedRemixTestConfig): Promise<{
143
- files: string[]
144
- serverFiles: string[]
145
- e2eFiles: string[]
146
- }> {
147
- async function findFiles(pattern: string) {
148
- let files: string[] = []
149
- let exclude = ['node_modules/**', '.git/**']
299
+ process.on('SIGINT', handleInterrupt)
300
+ process.on('SIGTERM', handleInterrupt)
150
301
 
151
- for await (let file of fsp.glob(pattern, { cwd: process.cwd(), exclude })) {
152
- files.push(path.resolve(process.cwd(), file))
302
+ try {
303
+ await executeRun()
304
+
305
+ if (config.watch && !hasExited) {
306
+ console.log('Watching for changes. Press Ctrl+C to stop.')
153
307
  }
308
+ } catch {
309
+ cleanupAndExit(1)
310
+ }
154
311
 
155
- return files
312
+ return await runPromise
313
+ }
314
+
315
+ async function resolveCwd(cwd: string): Promise<string> {
316
+ try {
317
+ return await fsp.realpath(cwd)
318
+ } catch {
319
+ return path.resolve(cwd)
156
320
  }
321
+ }
157
322
 
158
- let files = await findFiles(config.glob.test)
323
+ async function discoverTests(
324
+ config: ResolvedRemixTestConfig,
325
+ cwd: string,
326
+ ): Promise<DiscoveredTests | null> {
327
+ let files = await findFiles(config.glob.test, config.glob.exclude, cwd)
159
328
 
160
329
  if (files.length === 0) {
161
330
  console.log(`No test files found matching pattern: ${config.glob.test}`)
162
- process.exit(1)
331
+ return null
163
332
  }
164
333
 
165
- let e2eSet = new Set(await findFiles(config.glob.e2e))
166
-
334
+ let browserSet = new Set(await findFiles(config.glob.browser, config.glob.exclude, cwd))
335
+ let e2eSet = new Set(await findFiles(config.glob.e2e, config.glob.exclude, cwd))
167
336
  let types = new Set(config.type.split(','))
168
- let e2eFiles = types.has('e2e') ? files.filter((f) => e2eSet.has(f)) : []
169
- let serverFiles = types.has('server') ? files.filter((f) => !e2eSet.has(f)) : []
170
-
171
- let totalFiles = serverFiles.length + e2eFiles.length
337
+ let browserFiles = types.has('browser') ? files.filter((f) => browserSet.has(f)) : []
338
+ let e2eFiles = types.has('e2e') ? files.filter((file) => e2eSet.has(file)) : []
339
+ let serverFiles = types.has('server')
340
+ ? files.filter((file) => !browserSet.has(file) && !e2eSet.has(file))
341
+ : []
342
+ let totalFiles = browserFiles.length + serverFiles.length + e2eFiles.length
172
343
 
173
344
  if (totalFiles === 0) {
174
345
  console.log(`No test files remain after filtering for type ${config.type}`)
175
- process.exit(1)
346
+ return null
176
347
  }
177
348
 
178
349
  console.log(
179
- `Found ${totalFiles} test file(s) (${serverFiles.length} server, ${e2eFiles.length} e2e)`,
350
+ `Found ${totalFiles} test file(s) (${serverFiles.length} server, ${browserFiles.length} browser, ${e2eFiles.length} e2e)`,
180
351
  )
181
352
 
182
353
  return {
183
354
  files,
184
355
  serverFiles,
356
+ browserFiles,
185
357
  e2eFiles,
186
358
  }
187
359
  }
188
360
 
189
- function queueRerun(reason: string) {
190
- if (!config.watch || hasExited) return
191
-
192
- clearTimeout(rerunTimer)
193
-
194
- rerunTimer = setTimeout(() => {
195
- rerunTimer = undefined
196
- if (running) {
197
- queued = true
198
- } else {
199
- console.log(`\n↻ Change detected (${reason}), re-running tests...\n`)
200
- void executeRun()
361
+ async function findFiles(pattern: string, excludePattern: string, cwd: string): Promise<string[]> {
362
+ let files: string[] = []
363
+
364
+ if (IS_BUN) {
365
+ // Bun's `fs.promises.glob` follows symlinks and doesn't prune traversal
366
+ // via `exclude`, so it enters pnpm symlink cycles in `node_modules`.
367
+ // Use Bun's native Glob, which defaults to `followSymlinks: false`.
368
+ // @ts-expect-error — bun module is only resolvable under the Bun runtime
369
+ let { Glob } = await import('bun')
370
+ let glob = new Glob(pattern)
371
+ let excludeGlob = new Glob(excludePattern)
372
+ for await (let file of glob.scan({ cwd, absolute: true })) {
373
+ if (!excludeGlob.match(path.relative(cwd, file))) {
374
+ files.push(file)
375
+ }
201
376
  }
202
- }, 100)
203
- }
377
+ return files
378
+ }
204
379
 
205
- function cleanupAndExit(code: number) {
206
- if (hasExited) return
207
- hasExited = true
208
- watcher?.close()
209
- process.exit(code)
380
+ for await (let file of fsp.glob(pattern, { cwd, exclude: [excludePattern] })) {
381
+ files.push(path.resolve(cwd, file))
382
+ }
383
+ return files
210
384
  }
package/src/index.ts CHANGED
@@ -13,3 +13,4 @@ export {
13
13
  } from './lib/framework.ts'
14
14
  export { mock } from './lib/mock.ts'
15
15
  export type { TestContext } from './lib/context.ts'
16
+ export type { FakeTimers } from './lib/fake-timers.ts'
@@ -0,0 +1,3 @@
1
+ import { createStyles } from '@remix-run/terminal'
2
+
3
+ export const colors = createStyles()