@remix-run/test 0.0.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 (150) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +430 -2
  3. package/dist/app/client/entry.d.ts +2 -0
  4. package/dist/app/client/entry.d.ts.map +1 -0
  5. package/dist/app/client/entry.js +324 -0
  6. package/dist/app/client/iframe.d.ts +2 -0
  7. package/dist/app/client/iframe.d.ts.map +1 -0
  8. package/dist/app/client/iframe.js +22 -0
  9. package/dist/app/server.d.ts +6 -0
  10. package/dist/app/server.d.ts.map +1 -0
  11. package/dist/app/server.js +303 -0
  12. package/dist/cli-entry.d.ts +3 -0
  13. package/dist/cli-entry.d.ts.map +1 -0
  14. package/dist/cli-entry.js +14 -0
  15. package/dist/cli.d.ts +8 -0
  16. package/dist/cli.d.ts.map +1 -0
  17. package/dist/cli.js +305 -0
  18. package/dist/index.d.ts +6 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +2 -0
  21. package/dist/lib/colors.d.ts +2 -0
  22. package/dist/lib/colors.d.ts.map +1 -0
  23. package/dist/lib/colors.js +2 -0
  24. package/dist/lib/config.d.ts +91 -0
  25. package/dist/lib/config.d.ts.map +1 -0
  26. package/dist/lib/config.js +255 -0
  27. package/dist/lib/context.d.ts +93 -0
  28. package/dist/lib/context.d.ts.map +1 -0
  29. package/dist/lib/context.js +65 -0
  30. package/dist/lib/coverage-loader.d.ts +16 -0
  31. package/dist/lib/coverage-loader.d.ts.map +1 -0
  32. package/dist/lib/coverage-loader.js +20 -0
  33. package/dist/lib/coverage.d.ts +28 -0
  34. package/dist/lib/coverage.d.ts.map +1 -0
  35. package/dist/lib/coverage.js +212 -0
  36. package/dist/lib/executor.d.ts +4 -0
  37. package/dist/lib/executor.d.ts.map +1 -0
  38. package/dist/lib/executor.js +128 -0
  39. package/dist/lib/fake-timers.d.ts +6 -0
  40. package/dist/lib/fake-timers.d.ts.map +1 -0
  41. package/dist/lib/fake-timers.js +45 -0
  42. package/dist/lib/framework.d.ts +107 -0
  43. package/dist/lib/framework.d.ts.map +1 -0
  44. package/dist/lib/framework.js +198 -0
  45. package/dist/lib/import-module.d.ts +2 -0
  46. package/dist/lib/import-module.d.ts.map +1 -0
  47. package/dist/lib/import-module.js +29 -0
  48. package/dist/lib/mock.d.ts +52 -0
  49. package/dist/lib/mock.d.ts.map +1 -0
  50. package/dist/lib/mock.js +61 -0
  51. package/dist/lib/normalize.d.ts +2 -0
  52. package/dist/lib/normalize.d.ts.map +1 -0
  53. package/dist/lib/normalize.js +18 -0
  54. package/dist/lib/playwright.d.ts +15 -0
  55. package/dist/lib/playwright.d.ts.map +1 -0
  56. package/dist/lib/playwright.js +81 -0
  57. package/dist/lib/reporters/dot.d.ts +9 -0
  58. package/dist/lib/reporters/dot.d.ts.map +1 -0
  59. package/dist/lib/reporters/dot.js +56 -0
  60. package/dist/lib/reporters/files.d.ts +9 -0
  61. package/dist/lib/reporters/files.d.ts.map +1 -0
  62. package/dist/lib/reporters/files.js +71 -0
  63. package/dist/lib/reporters/index.d.ts +13 -0
  64. package/dist/lib/reporters/index.d.ts.map +1 -0
  65. package/dist/lib/reporters/index.js +18 -0
  66. package/dist/lib/reporters/results.d.ts +30 -0
  67. package/dist/lib/reporters/results.d.ts.map +1 -0
  68. package/dist/lib/reporters/results.js +1 -0
  69. package/dist/lib/reporters/spec.d.ts +9 -0
  70. package/dist/lib/reporters/spec.d.ts.map +1 -0
  71. package/dist/lib/reporters/spec.js +153 -0
  72. package/dist/lib/reporters/tap.d.ts +9 -0
  73. package/dist/lib/reporters/tap.d.ts.map +1 -0
  74. package/dist/lib/reporters/tap.js +54 -0
  75. package/dist/lib/runner-browser.d.ts +21 -0
  76. package/dist/lib/runner-browser.d.ts.map +1 -0
  77. package/dist/lib/runner-browser.js +117 -0
  78. package/dist/lib/runner.d.ts +14 -0
  79. package/dist/lib/runner.d.ts.map +1 -0
  80. package/dist/lib/runner.js +118 -0
  81. package/dist/lib/runtime.d.ts +2 -0
  82. package/dist/lib/runtime.d.ts.map +1 -0
  83. package/dist/lib/runtime.js +2 -0
  84. package/dist/lib/ts-transform.d.ts +4 -0
  85. package/dist/lib/ts-transform.d.ts.map +1 -0
  86. package/dist/lib/ts-transform.js +29 -0
  87. package/dist/lib/watcher.d.ts +5 -0
  88. package/dist/lib/watcher.d.ts.map +1 -0
  89. package/dist/lib/watcher.js +39 -0
  90. package/dist/lib/worker-e2e.d.ts +2 -0
  91. package/dist/lib/worker-e2e.d.ts.map +1 -0
  92. package/dist/lib/worker-e2e.js +49 -0
  93. package/dist/lib/worker.d.ts +2 -0
  94. package/dist/lib/worker.d.ts.map +1 -0
  95. package/dist/lib/worker.js +57 -0
  96. package/dist/test/coverage/fixture.d.ts +5 -0
  97. package/dist/test/coverage/fixture.d.ts.map +1 -0
  98. package/dist/test/coverage/fixture.js +32 -0
  99. package/dist/test/coverage/test-browser.d.ts +2 -0
  100. package/dist/test/coverage/test-browser.d.ts.map +1 -0
  101. package/dist/test/coverage/test-browser.js +24 -0
  102. package/dist/test/coverage/test-e2e.d.ts +2 -0
  103. package/dist/test/coverage/test-e2e.d.ts.map +1 -0
  104. package/dist/test/coverage/test-e2e.js +60 -0
  105. package/dist/test/coverage/test-unit.d.ts +2 -0
  106. package/dist/test/coverage/test-unit.d.ts.map +1 -0
  107. package/dist/test/coverage/test-unit.js +27 -0
  108. package/dist/test/framework.test.browser.d.ts +2 -0
  109. package/dist/test/framework.test.browser.d.ts.map +1 -0
  110. package/dist/test/framework.test.browser.js +107 -0
  111. package/dist/test/framework.test.e2e.d.ts +2 -0
  112. package/dist/test/framework.test.e2e.d.ts.map +1 -0
  113. package/dist/test/framework.test.e2e.js +34 -0
  114. package/package.json +79 -5
  115. package/src/app/client/entry.ts +353 -0
  116. package/src/app/client/iframe.ts +18 -0
  117. package/src/app/server.ts +336 -0
  118. package/src/cli-entry.ts +15 -0
  119. package/src/cli.ts +384 -0
  120. package/src/index.ts +16 -0
  121. package/src/lib/colors.ts +3 -0
  122. package/src/lib/config.ts +377 -0
  123. package/src/lib/context.ts +168 -0
  124. package/src/lib/coverage-loader.ts +31 -0
  125. package/src/lib/coverage.ts +320 -0
  126. package/src/lib/executor.ts +145 -0
  127. package/src/lib/fake-timers.ts +64 -0
  128. package/src/lib/framework.ts +251 -0
  129. package/src/lib/import-module.ts +29 -0
  130. package/src/lib/mock.ts +89 -0
  131. package/src/lib/normalize.ts +22 -0
  132. package/src/lib/playwright.ts +100 -0
  133. package/src/lib/reporters/dot.ts +58 -0
  134. package/src/lib/reporters/files.ts +77 -0
  135. package/src/lib/reporters/index.ts +27 -0
  136. package/src/lib/reporters/results.ts +29 -0
  137. package/src/lib/reporters/spec.ts +174 -0
  138. package/src/lib/reporters/tap.ts +58 -0
  139. package/src/lib/runner-browser.ts +165 -0
  140. package/src/lib/runner.ts +189 -0
  141. package/src/lib/runtime.ts +2 -0
  142. package/src/lib/ts-transform.ts +36 -0
  143. package/src/lib/watcher.ts +46 -0
  144. package/src/lib/worker-e2e.ts +54 -0
  145. package/src/lib/worker.ts +50 -0
  146. package/src/test/coverage/fixture.ts +34 -0
  147. package/src/test/coverage/test-browser.ts +29 -0
  148. package/src/test/coverage/test-e2e.ts +70 -0
  149. package/src/test/coverage/test-unit.ts +32 -0
  150. package/tsconfig.json +16 -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 ADDED
@@ -0,0 +1,384 @@
1
+ import * as fsp from 'node:fs/promises'
2
+ import type * as http from 'node:http'
3
+ import * as path from 'node:path'
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'
12
+ import { createReporter } from './lib/reporters/index.ts'
13
+ import { runBrowserTests } from './lib/runner-browser.ts'
14
+ import { runServerTests } from './lib/runner.ts'
15
+ import { createWatcher } from './lib/watcher.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'
20
+
21
+ export { getRemixTestHelpText }
22
+
23
+ export interface RunRemixTestOptions {
24
+ argv?: string[]
25
+ cwd?: string
26
+ }
27
+
28
+ interface DiscoveredTests {
29
+ files: string[]
30
+ serverFiles: string[]
31
+ browserFiles: string[]
32
+ e2eFiles: string[]
33
+ }
34
+
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()
39
+
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)
49
+ }
50
+ }
51
+
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
+ }
57
+
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
+ }
84
+
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
+ }
97
+
98
+ let queueRerun = (reason: string) => {
99
+ if (!config.watch || hasExited) return
100
+
101
+ clearTimeout(rerunTimer)
102
+
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
+ }
113
+
114
+ let executeRun = async () => {
115
+ if (hasExited) return
116
+
117
+ running = true
118
+
119
+ let globalTeardown: (() => Promise<void> | void) | undefined
120
+
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
+ }
128
+
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)
193
+ }
194
+
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}"`)
205
+ }
206
+ }
207
+
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
+ }
271
+ }
272
+
273
+ reporter.onSummary(counts, performance.now() - startTime)
274
+
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
+ }
296
+ }
297
+ }
298
+
299
+ process.on('SIGINT', handleInterrupt)
300
+ process.on('SIGTERM', handleInterrupt)
301
+
302
+ try {
303
+ await executeRun()
304
+
305
+ if (config.watch && !hasExited) {
306
+ console.log('Watching for changes. Press Ctrl+C to stop.')
307
+ }
308
+ } catch {
309
+ cleanupAndExit(1)
310
+ }
311
+
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)
320
+ }
321
+ }
322
+
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)
328
+
329
+ if (files.length === 0) {
330
+ console.log(`No test files found matching pattern: ${config.glob.test}`)
331
+ return null
332
+ }
333
+
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))
336
+ let types = new Set(config.type.split(','))
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
343
+
344
+ if (totalFiles === 0) {
345
+ console.log(`No test files remain after filtering for type ${config.type}`)
346
+ return null
347
+ }
348
+
349
+ console.log(
350
+ `Found ${totalFiles} test file(s) (${serverFiles.length} server, ${browserFiles.length} browser, ${e2eFiles.length} e2e)`,
351
+ )
352
+
353
+ return {
354
+ files,
355
+ serverFiles,
356
+ browserFiles,
357
+ e2eFiles,
358
+ }
359
+ }
360
+
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
+ }
376
+ }
377
+ return files
378
+ }
379
+
380
+ for await (let file of fsp.glob(pattern, { cwd, exclude: [excludePattern] })) {
381
+ files.push(path.resolve(cwd, file))
382
+ }
383
+ return files
384
+ }
package/src/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ export type { RemixTestConfig } from './lib/config.ts'
2
+ export {
3
+ describe,
4
+ it,
5
+ suite,
6
+ test,
7
+ before,
8
+ after,
9
+ beforeEach,
10
+ afterEach,
11
+ beforeAll,
12
+ afterAll,
13
+ } from './lib/framework.ts'
14
+ export { mock } from './lib/mock.ts'
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()