@remix-run/test 0.0.0 → 0.1.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 (86) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +325 -2
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +171 -0
  6. package/dist/index.d.ts +5 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +2 -0
  9. package/dist/lib/config.d.ts +60 -0
  10. package/dist/lib/config.d.ts.map +1 -0
  11. package/dist/lib/config.js +152 -0
  12. package/dist/lib/context.d.ts +69 -0
  13. package/dist/lib/context.d.ts.map +1 -0
  14. package/dist/lib/context.js +49 -0
  15. package/dist/lib/e2e-server.d.ts +11 -0
  16. package/dist/lib/e2e-server.d.ts.map +1 -0
  17. package/dist/lib/e2e-server.js +15 -0
  18. package/dist/lib/executor.d.ts +27 -0
  19. package/dist/lib/executor.d.ts.map +1 -0
  20. package/dist/lib/executor.js +123 -0
  21. package/dist/lib/framework.d.ts +107 -0
  22. package/dist/lib/framework.d.ts.map +1 -0
  23. package/dist/lib/framework.js +198 -0
  24. package/dist/lib/framework.test.d.ts +2 -0
  25. package/dist/lib/framework.test.d.ts.map +1 -0
  26. package/dist/lib/framework.test.e2e.d.ts +2 -0
  27. package/dist/lib/framework.test.e2e.d.ts.map +1 -0
  28. package/dist/lib/framework.test.e2e.js +29 -0
  29. package/dist/lib/framework.test.js +283 -0
  30. package/dist/lib/mock.d.ts +52 -0
  31. package/dist/lib/mock.d.ts.map +1 -0
  32. package/dist/lib/mock.js +61 -0
  33. package/dist/lib/playwright.d.ts +15 -0
  34. package/dist/lib/playwright.d.ts.map +1 -0
  35. package/dist/lib/playwright.js +84 -0
  36. package/dist/lib/reporters/dot.d.ts +10 -0
  37. package/dist/lib/reporters/dot.d.ts.map +1 -0
  38. package/dist/lib/reporters/dot.js +55 -0
  39. package/dist/lib/reporters/files.d.ts +10 -0
  40. package/dist/lib/reporters/files.d.ts.map +1 -0
  41. package/dist/lib/reporters/files.js +70 -0
  42. package/dist/lib/reporters/index.d.ts +14 -0
  43. package/dist/lib/reporters/index.d.ts.map +1 -0
  44. package/dist/lib/reporters/index.js +18 -0
  45. package/dist/lib/reporters/spec.d.ts +10 -0
  46. package/dist/lib/reporters/spec.d.ts.map +1 -0
  47. package/dist/lib/reporters/spec.js +152 -0
  48. package/dist/lib/reporters/tap.d.ts +10 -0
  49. package/dist/lib/reporters/tap.d.ts.map +1 -0
  50. package/dist/lib/reporters/tap.js +54 -0
  51. package/dist/lib/runner.d.ts +9 -0
  52. package/dist/lib/runner.d.ts.map +1 -0
  53. package/dist/lib/runner.js +89 -0
  54. package/dist/lib/utils.d.ts +16 -0
  55. package/dist/lib/utils.d.ts.map +1 -0
  56. package/dist/lib/utils.js +27 -0
  57. package/dist/lib/watcher.d.ts +5 -0
  58. package/dist/lib/watcher.d.ts.map +1 -0
  59. package/dist/lib/watcher.js +39 -0
  60. package/dist/lib/worker-e2e.d.ts +2 -0
  61. package/dist/lib/worker-e2e.d.ts.map +1 -0
  62. package/dist/lib/worker-e2e.js +48 -0
  63. package/dist/lib/worker.d.ts +2 -0
  64. package/dist/lib/worker.d.ts.map +1 -0
  65. package/dist/lib/worker.js +29 -0
  66. package/package.json +58 -5
  67. package/src/cli.ts +210 -0
  68. package/src/index.ts +15 -0
  69. package/src/lib/config.ts +231 -0
  70. package/src/lib/context.ts +126 -0
  71. package/src/lib/e2e-server.ts +28 -0
  72. package/src/lib/executor.ts +162 -0
  73. package/src/lib/framework.ts +251 -0
  74. package/src/lib/mock.ts +89 -0
  75. package/src/lib/playwright.ts +102 -0
  76. package/src/lib/reporters/dot.ts +57 -0
  77. package/src/lib/reporters/files.ts +76 -0
  78. package/src/lib/reporters/index.ts +28 -0
  79. package/src/lib/reporters/spec.ts +173 -0
  80. package/src/lib/reporters/tap.ts +58 -0
  81. package/src/lib/runner.ts +137 -0
  82. package/src/lib/utils.ts +40 -0
  83. package/src/lib/watcher.ts +46 -0
  84. package/src/lib/worker-e2e.ts +52 -0
  85. package/src/lib/worker.ts +30 -0
  86. package/tsconfig.json +14 -0
@@ -0,0 +1,173 @@
1
+ import { colors, normalizeLine, type Counts } from '../utils.ts'
2
+ import type { TestResult, TestResults } from '../executor.ts'
3
+ import type { Reporter } from './index.ts'
4
+
5
+ export class SpecReporter implements Reporter {
6
+ #failures: { suiteName: string; name: string; error: TestResult['error'] }[] = []
7
+
8
+ onSectionStart(label: string) {
9
+ console.log(label)
10
+ }
11
+
12
+ onResult(results: TestResults, env?: string) {
13
+ let suiteMap = new Map<string, TestResult[]>()
14
+ for (let test of results.tests) {
15
+ let suite = test.suiteName || 'Global'
16
+ if (!suiteMap.has(suite)) suiteMap.set(suite, [])
17
+ suiteMap.get(suite)!.push(test)
18
+ }
19
+
20
+ let envLabel = env ? ` ${colors.dim(`[${env}]`)}` : ''
21
+ let lastParts: string[] = []
22
+
23
+ // Pre-compute aggregate test results for each path prefix so non-leaf
24
+ // suite headings can be colored the same way as leaf headings.
25
+ let prefixTests = new Map<string, TestResult[]>()
26
+ for (let [suiteName, tests] of suiteMap) {
27
+ let parts = suiteName.split(' > ')
28
+ for (let i = 0; i < parts.length; i++) {
29
+ let prefix = parts.slice(0, i + 1).join(' > ')
30
+ if (!prefixTests.has(prefix)) prefixTests.set(prefix, [])
31
+ prefixTests.get(prefix)!.push(...tests)
32
+ }
33
+ }
34
+
35
+ for (let [suiteName, suiteTests] of suiteMap) {
36
+ let parts = suiteName.split(' > ')
37
+
38
+ // Find where this path diverges from the last rendered path
39
+ let commonLen = 0
40
+ while (
41
+ commonLen < lastParts.length &&
42
+ commonLen < parts.length &&
43
+ lastParts[commonLen] === parts[commonLen]
44
+ ) {
45
+ commonLen++
46
+ }
47
+
48
+ // Print each new path component
49
+ for (let i = commonLen; i < parts.length; i++) {
50
+ let indent = ' '.repeat(i)
51
+ let isLeaf = i === parts.length - 1
52
+
53
+ if (isLeaf) {
54
+ let totalDuration = suiteTests.reduce((sum, t) => sum + t.duration, 0)
55
+ let suiteHasFailed = suiteTests.some((t) => t.status === 'failed')
56
+ let suiteAllSkipped = suiteTests.every((t) => t.status === 'skipped')
57
+ let suiteAllTodo = suiteTests.every((t) => t.status === 'todo')
58
+ let label = suiteHasFailed
59
+ ? colors.red(parts[i])
60
+ : suiteAllSkipped
61
+ ? colors.dim(parts[i])
62
+ : suiteAllTodo
63
+ ? colors.yellow(parts[i])
64
+ : colors.green(parts[i])
65
+ let suiteComment = suiteAllSkipped
66
+ ? colors.dim(' # skipped')
67
+ : suiteAllTodo
68
+ ? colors.yellow(' # todo')
69
+ : ''
70
+ let duration = suiteComment ? '' : ` (${totalDuration.toFixed(2)}ms)`
71
+ let label2 = envLabel
72
+ console.log(`${indent}${colors.dim('▶')} ${label}${duration}${suiteComment}${label2}`)
73
+ } else {
74
+ let prefix = parts.slice(0, i + 1).join(' > ')
75
+ let prefixTestList = prefixTests.get(prefix) ?? []
76
+ let prefixHasFailed = prefixTestList.some((t) => t.status === 'failed')
77
+ let prefixAllSkipped =
78
+ prefixTestList.length > 0 && prefixTestList.every((t) => t.status === 'skipped')
79
+ let prefixAllTodo =
80
+ prefixTestList.length > 0 && prefixTestList.every((t) => t.status === 'todo')
81
+ let nameColor = prefixHasFailed
82
+ ? colors.red
83
+ : prefixAllSkipped
84
+ ? colors.dim
85
+ : prefixAllTodo
86
+ ? colors.yellow
87
+ : colors.green
88
+ let prefixDuration = prefixTestList.reduce((sum, t) => sum + t.duration, 0)
89
+ let prefixComment = prefixAllSkipped
90
+ ? colors.dim(' # skipped')
91
+ : prefixAllTodo
92
+ ? colors.yellow(' # todo')
93
+ : ''
94
+ let prefixDurationStr = prefixComment ? '' : ` (${prefixDuration.toFixed(2)}ms)`
95
+ console.log(
96
+ `${indent}${colors.dim('▶')} ${nameColor(parts[i])}${prefixDurationStr}${prefixComment}${envLabel}`,
97
+ )
98
+ }
99
+ }
100
+
101
+ lastParts = parts
102
+
103
+ // Print tests indented to the suite's depth
104
+ let testIndent = ' '.repeat(parts.length)
105
+ for (let test of suiteTests) {
106
+ if (test.status === 'passed') {
107
+ console.log(
108
+ `${testIndent}${colors.green('✓')} ${test.name} (${test.duration.toFixed(2)}ms)`,
109
+ )
110
+ } else if (test.status === 'failed') {
111
+ console.log(
112
+ `${testIndent}${colors.red('✗')} ${test.name} (${test.duration.toFixed(2)}ms)`,
113
+ )
114
+ if (test.error) {
115
+ console.log(`${testIndent} ${colors.red(`Error: ${test.error.message}`)}`)
116
+ if (test.error.stack) {
117
+ let stack = test.error.stack
118
+ .split('\n')
119
+ .map((line) => normalizeLine(line))
120
+ .join('\n')
121
+ console.log(
122
+ `${testIndent} ${stack.split('\n').slice(1, 5).join(`\n${testIndent} `)}`,
123
+ )
124
+ }
125
+ }
126
+ this.#failures.push({ suiteName: test.suiteName, name: test.name, error: test.error })
127
+ } else if (test.status === 'skipped') {
128
+ if (test.name)
129
+ console.log(`${testIndent}${colors.dim('↓')} ${colors.dim(`${test.name} # skipped`)}`)
130
+ } else if (test.status === 'todo') {
131
+ if (test.name)
132
+ console.log(
133
+ `${testIndent}${colors.yellow('…')} ${colors.yellow(`${test.name} # todo`)}`,
134
+ )
135
+ }
136
+ }
137
+ }
138
+ }
139
+
140
+ onSummary(counts: Counts, durationMs: number) {
141
+ if (this.#failures.length > 0) {
142
+ console.log()
143
+ console.log(colors.red('Failed tests:'))
144
+ for (let i = 0; i < this.#failures.length; i++) {
145
+ let { suiteName, name, error } = this.#failures[i]
146
+ let fullName = name ? `${suiteName} > ${name}` : suiteName
147
+ console.log(`\n ${colors.red(`${i + 1})`)} ${fullName}`)
148
+ if (error) {
149
+ console.log(` ${colors.red(error.message)}`)
150
+ if (error.stack) {
151
+ let frames = error.stack
152
+ .split('\n')
153
+ .slice(1, 4)
154
+ .map((l) => ` ${normalizeLine(l).trim()}`)
155
+ .join('\n')
156
+ console.log(frames)
157
+ }
158
+ }
159
+ }
160
+ }
161
+
162
+ let { passed, failed, skipped, todo } = counts
163
+ let info = colors.cyan('ℹ')
164
+ console.log()
165
+ console.log(`${info} tests ${passed + failed + skipped + todo}`)
166
+ console.log(`${info} pass ${passed}`)
167
+ console.log(`${info} fail ${failed}`)
168
+ if (skipped > 0) console.log(`${info} skipped ${skipped}`)
169
+ if (todo > 0) console.log(`${info} todo ${todo}`)
170
+ console.log(`${info} duration_ms ${durationMs.toFixed(5)}`)
171
+ console.log()
172
+ }
173
+ }
@@ -0,0 +1,58 @@
1
+ import { normalizeLine, type Counts } from '../utils.ts'
2
+ import type { TestResults } from '../executor.ts'
3
+ import type { Reporter } from './index.ts'
4
+
5
+ export class TapReporter implements Reporter {
6
+ #counter = 0
7
+ #total = 0
8
+
9
+ onSectionStart(_label: string) {}
10
+
11
+ onResult(results: TestResults, env?: string) {
12
+ if (this.#counter === 0) {
13
+ console.log('TAP version 14')
14
+ }
15
+
16
+ let envComment = env ? ` # ${env}` : ''
17
+
18
+ for (let test of results.tests) {
19
+ this.#counter++
20
+ this.#total++
21
+ let fullName = test.name
22
+ ? `${test.suiteName} > ${test.name}${envComment}`
23
+ : `${test.suiteName}${envComment}`
24
+
25
+ if (test.status === 'passed') {
26
+ console.log(`ok ${this.#counter} - ${fullName}`)
27
+ } else if (test.status === 'skipped') {
28
+ console.log(`ok ${this.#counter} - ${fullName} # SKIP`)
29
+ } else if (test.status === 'todo') {
30
+ console.log(`ok ${this.#counter} - ${fullName} # TODO`)
31
+ } else {
32
+ console.log(`not ok ${this.#counter} - ${fullName}`)
33
+ console.log(' ---')
34
+ console.log(` message: ${test.error?.message ?? 'unknown error'}`)
35
+ if (test.error?.stack) {
36
+ let frames = test.error.stack
37
+ .split('\n')
38
+ .slice(1, 4)
39
+ .map((l) => normalizeLine(l).trim())
40
+ .join('\n ')
41
+ console.log(` stack: |\n ${frames}`)
42
+ }
43
+ console.log(' ...')
44
+ }
45
+ }
46
+ }
47
+
48
+ onSummary(counts: Counts, durationMs: number) {
49
+ let { passed, failed, skipped, todo } = counts
50
+ console.log(`1..${this.#total}`)
51
+ console.log(`# tests ${passed + failed + skipped + todo}`)
52
+ console.log(`# pass ${passed}`)
53
+ console.log(`# fail ${failed}`)
54
+ if (skipped > 0) console.log(`# skipped ${skipped}`)
55
+ if (todo > 0) console.log(`# todo ${todo}`)
56
+ console.log(`# duration_ms ${durationMs.toFixed(5)}`)
57
+ }
58
+ }
@@ -0,0 +1,137 @@
1
+ import * as path from 'node:path'
2
+ import { pathToFileURL } from 'node:url'
3
+ import { Worker } from 'node:worker_threads'
4
+ import type { TestResults } from './executor.ts'
5
+ import { type PlaywrightUseOpts } from './playwright.ts'
6
+ import type { Reporter } from './reporters/index.ts'
7
+ import type { Counts } from './utils.ts'
8
+
9
+ const ext = path.extname(import.meta.url)
10
+ const workerUrl = new URL(`./worker${ext}`, import.meta.url)
11
+ const workerE2EUrl = new URL(`./worker-e2e${ext}`, import.meta.url)
12
+
13
+ export async function runServerTests(
14
+ files: string[],
15
+ reporter: Reporter,
16
+ concurrency: number,
17
+ type: 'server' | 'e2e',
18
+ options: {
19
+ open?: boolean
20
+ playwrightUseOpts?: PlaywrightUseOpts
21
+ projectName?: string
22
+ } = {},
23
+ ): Promise<Counts> {
24
+ let counts: Counts = { passed: 0, failed: 0, skipped: 0, todo: 0 }
25
+ let envLabel = options.projectName ? `${type}:${options.projectName}` : type
26
+
27
+ function accumulate(results: TestResults, file: string) {
28
+ reporter.onResult(
29
+ { ...results, tests: results.tests.map((t) => ({ ...t, filePath: file })) },
30
+ envLabel,
31
+ )
32
+ counts.passed += results.passed
33
+ counts.failed += results.failed
34
+ counts.skipped += results.skipped
35
+ counts.todo += results.todo
36
+ }
37
+
38
+ if (type === 'e2e') {
39
+ await runInConcurrentWorkers(
40
+ files,
41
+ concurrency,
42
+ (file) =>
43
+ runFileInWorker(file, type, (results) => accumulate(results, file), {
44
+ ...options,
45
+ playwrightUseOpts: options.playwrightUseOpts,
46
+ }),
47
+ () => counts.failed++,
48
+ )
49
+ } else {
50
+ await runInConcurrentWorkers(
51
+ files,
52
+ concurrency,
53
+ (file) => runFileInWorker(file, type, (results) => accumulate(results, file)),
54
+ () => counts.failed++,
55
+ )
56
+ }
57
+
58
+ return { ...counts }
59
+ }
60
+
61
+ async function runInConcurrentWorkers(
62
+ files: string[],
63
+ concurrency: number,
64
+ runFile: (file: string) => Promise<void>,
65
+ onError: () => void,
66
+ ): Promise<void> {
67
+ let index = 0
68
+ let active = 0
69
+
70
+ await new Promise<void>((resolve) => {
71
+ function dispatch() {
72
+ while (active < concurrency && index < files.length) {
73
+ let file = files[index]
74
+ index++
75
+ active++
76
+
77
+ runFile(file).then(
78
+ () => {
79
+ active--
80
+ if (index < files.length) {
81
+ dispatch()
82
+ } else if (active === 0) {
83
+ resolve()
84
+ }
85
+ },
86
+ (err) => {
87
+ console.error(`Error running ${file}:`, err.message)
88
+ console.error(err)
89
+ onError()
90
+ active--
91
+ if (active === 0 && index >= files.length) resolve()
92
+ else dispatch()
93
+ },
94
+ )
95
+ }
96
+
97
+ if (index >= files.length && active === 0) resolve()
98
+ }
99
+
100
+ dispatch()
101
+ })
102
+ }
103
+
104
+ function runFileInWorker(
105
+ file: string,
106
+ type: 'server' | 'e2e',
107
+ onResults: (results: TestResults) => void,
108
+ options: {
109
+ open?: boolean
110
+ playwrightUseOpts?: PlaywrightUseOpts
111
+ } = {},
112
+ ): Promise<void> {
113
+ return new Promise((resolve, reject) => {
114
+ let worker =
115
+ type === 'e2e'
116
+ ? new Worker(workerE2EUrl, {
117
+ workerData: {
118
+ file: pathToFileURL(file).href,
119
+ type,
120
+ open: options.open,
121
+ playwrightUseOpts: options.playwrightUseOpts,
122
+ },
123
+ })
124
+ : new Worker(workerUrl, {
125
+ workerData: {
126
+ file: pathToFileURL(file).href,
127
+ type,
128
+ },
129
+ })
130
+ worker.once('message', (msg: TestResults) => onResults(msg))
131
+ worker.once('error', reject)
132
+ worker.once('exit', (code) => {
133
+ if (code !== 0) reject(new Error(`Worker exited with code ${code}`))
134
+ else resolve()
135
+ })
136
+ })
137
+ }
@@ -0,0 +1,40 @@
1
+ export type Counts = {
2
+ passed: number
3
+ failed: number
4
+ skipped: number
5
+ todo: number
6
+ }
7
+
8
+ const noColor = process.env.CI === 'true' || !!process.env.NO_COLOR
9
+
10
+ export const colors = {
11
+ reset: noColor ? '' : '\x1b[0m',
12
+ dim: noColor ? (s: string) => s : (s: string) => `\x1b[2m${s}\x1b[0m`,
13
+ green: noColor ? (s: string) => s : (s: string) => `\x1b[32m${s}\x1b[0m`,
14
+ red: noColor ? (s: string) => s : (s: string) => `\x1b[31m${s}\x1b[0m`,
15
+ cyan: noColor ? (s: string) => s : (s: string) => `\x1b[36m${s}\x1b[0m`,
16
+ yellow: noColor ? (s: string) => s : (s: string) => `\x1b[2m\x1b[33m${s}\x1b[0m`,
17
+ }
18
+
19
+ function normalizeFilePath(path: string): string {
20
+ let locSuffix = path.match(/(:\d+:\d+)$/)?.[0] || ''
21
+ let normalized =
22
+ path
23
+ .replace(/^\/scripts\/@pkg\/([^):]+)/g, (...args) => args[1])
24
+ .replace(/^\/scripts\/@test\/([^):]+)/g, (...args) => args[1])
25
+ .replace(/^\/scripts\/([^):]+)/g, (...args) => args[1])
26
+ .replace(/^\s+/, ' ') + locSuffix
27
+
28
+ return path.includes('/@test/') ? `./${normalized}` : normalized
29
+ }
30
+
31
+ export function normalizeLine(line: string): string {
32
+ let match = line.match(/ \(.*\)$/)
33
+ if (match) {
34
+ let filepath = match[0].slice(2, -1)
35
+ filepath = filepath.replace(/https?:\/\/localhost:\d+\//g, '/')
36
+ return line.slice(0, match.index) + ' (' + normalizeFilePath(filepath) + ')'
37
+ }
38
+
39
+ return line
40
+ }
@@ -0,0 +1,46 @@
1
+ import * as fs from 'node:fs'
2
+
3
+ function getFileModTime(file: string): number {
4
+ try {
5
+ return fs.statSync(file).mtimeMs
6
+ } catch {
7
+ return 0
8
+ }
9
+ }
10
+
11
+ export function createWatcher(onChange: (file: string) => void) {
12
+ let watchers = new Set<fs.FSWatcher>()
13
+ let fileModTimes = new Map<string, number>()
14
+
15
+ function update(files: string[]) {
16
+ for (let watcher of watchers) {
17
+ watcher.close()
18
+ }
19
+ watchers.clear()
20
+
21
+ for (let file of files) {
22
+ fileModTimes.set(file, getFileModTime(file))
23
+ watchers.add(
24
+ fs.watch(file, () => {
25
+ // macOS FSEvents can fire multiple callbacks per save (e.g. write +
26
+ // metadata flush). Guard with mtime so only a real content change
27
+ // triggers a rerun instead of every duplicate event.
28
+ let mtime = getFileModTime(file)
29
+ if (mtime !== fileModTimes.get(file)) {
30
+ fileModTimes.set(file, mtime)
31
+ onChange(file)
32
+ }
33
+ }),
34
+ )
35
+ }
36
+ }
37
+
38
+ function close() {
39
+ for (let watcher of watchers) {
40
+ watcher.close()
41
+ }
42
+ watchers.clear()
43
+ }
44
+
45
+ return { update, close }
46
+ }
@@ -0,0 +1,52 @@
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'
10
+
11
+ try {
12
+ await tsImport(workerData.file, import.meta.url)
13
+
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)
52
+ }
@@ -0,0 +1,30 @@
1
+ import { workerData, parentPort } from 'node:worker_threads'
2
+ import { tsImport } from 'tsx/esm/api'
3
+ import { runTests, type TestResults } from './executor.ts'
4
+
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)
30
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "strict": true,
4
+ "lib": ["ES2024", "DOM", "DOM.Iterable"],
5
+ "module": "ES2022",
6
+ "moduleResolution": "Bundler",
7
+ "target": "ESNext",
8
+ "allowImportingTsExtensions": true,
9
+ "rewriteRelativeImportExtensions": true,
10
+ "verbatimModuleSyntax": true,
11
+ "jsx": "react-jsx",
12
+ "jsxImportSource": "@remix-run/component"
13
+ }
14
+ }