@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.
- package/LICENSE +21 -0
- package/README.md +325 -2
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +171 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/lib/config.d.ts +60 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +152 -0
- package/dist/lib/context.d.ts +69 -0
- package/dist/lib/context.d.ts.map +1 -0
- package/dist/lib/context.js +49 -0
- package/dist/lib/e2e-server.d.ts +11 -0
- package/dist/lib/e2e-server.d.ts.map +1 -0
- package/dist/lib/e2e-server.js +15 -0
- package/dist/lib/executor.d.ts +27 -0
- package/dist/lib/executor.d.ts.map +1 -0
- package/dist/lib/executor.js +123 -0
- package/dist/lib/framework.d.ts +107 -0
- package/dist/lib/framework.d.ts.map +1 -0
- package/dist/lib/framework.js +198 -0
- package/dist/lib/framework.test.d.ts +2 -0
- package/dist/lib/framework.test.d.ts.map +1 -0
- package/dist/lib/framework.test.e2e.d.ts +2 -0
- package/dist/lib/framework.test.e2e.d.ts.map +1 -0
- package/dist/lib/framework.test.e2e.js +29 -0
- package/dist/lib/framework.test.js +283 -0
- package/dist/lib/mock.d.ts +52 -0
- package/dist/lib/mock.d.ts.map +1 -0
- package/dist/lib/mock.js +61 -0
- package/dist/lib/playwright.d.ts +15 -0
- package/dist/lib/playwright.d.ts.map +1 -0
- package/dist/lib/playwright.js +84 -0
- package/dist/lib/reporters/dot.d.ts +10 -0
- package/dist/lib/reporters/dot.d.ts.map +1 -0
- package/dist/lib/reporters/dot.js +55 -0
- package/dist/lib/reporters/files.d.ts +10 -0
- package/dist/lib/reporters/files.d.ts.map +1 -0
- package/dist/lib/reporters/files.js +70 -0
- package/dist/lib/reporters/index.d.ts +14 -0
- package/dist/lib/reporters/index.d.ts.map +1 -0
- package/dist/lib/reporters/index.js +18 -0
- package/dist/lib/reporters/spec.d.ts +10 -0
- package/dist/lib/reporters/spec.d.ts.map +1 -0
- package/dist/lib/reporters/spec.js +152 -0
- package/dist/lib/reporters/tap.d.ts +10 -0
- package/dist/lib/reporters/tap.d.ts.map +1 -0
- package/dist/lib/reporters/tap.js +54 -0
- package/dist/lib/runner.d.ts +9 -0
- package/dist/lib/runner.d.ts.map +1 -0
- package/dist/lib/runner.js +89 -0
- package/dist/lib/utils.d.ts +16 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +27 -0
- package/dist/lib/watcher.d.ts +5 -0
- package/dist/lib/watcher.d.ts.map +1 -0
- package/dist/lib/watcher.js +39 -0
- package/dist/lib/worker-e2e.d.ts +2 -0
- package/dist/lib/worker-e2e.d.ts.map +1 -0
- package/dist/lib/worker-e2e.js +48 -0
- package/dist/lib/worker.d.ts +2 -0
- package/dist/lib/worker.d.ts.map +1 -0
- package/dist/lib/worker.js +29 -0
- package/package.json +58 -5
- package/src/cli.ts +210 -0
- package/src/index.ts +15 -0
- package/src/lib/config.ts +231 -0
- package/src/lib/context.ts +126 -0
- package/src/lib/e2e-server.ts +28 -0
- package/src/lib/executor.ts +162 -0
- package/src/lib/framework.ts +251 -0
- package/src/lib/mock.ts +89 -0
- package/src/lib/playwright.ts +102 -0
- package/src/lib/reporters/dot.ts +57 -0
- package/src/lib/reporters/files.ts +76 -0
- package/src/lib/reporters/index.ts +28 -0
- package/src/lib/reporters/spec.ts +173 -0
- package/src/lib/reporters/tap.ts +58 -0
- package/src/lib/runner.ts +137 -0
- package/src/lib/utils.ts +40 -0
- package/src/lib/watcher.ts +46 -0
- package/src/lib/worker-e2e.ts +52 -0
- package/src/lib/worker.ts +30 -0
- 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
|
+
}
|
package/src/lib/utils.ts
ADDED
|
@@ -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
|
+
}
|