@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.
- package/README.md +140 -35
- package/dist/app/client/entry.d.ts +2 -0
- package/dist/app/client/entry.d.ts.map +1 -0
- package/dist/app/client/entry.js +324 -0
- package/dist/app/client/iframe.d.ts +2 -0
- package/dist/app/client/iframe.d.ts.map +1 -0
- package/dist/app/client/iframe.js +22 -0
- package/dist/app/server.d.ts +6 -0
- package/dist/app/server.d.ts.map +1 -0
- package/dist/app/server.js +303 -0
- package/dist/cli-entry.d.ts +3 -0
- package/dist/cli-entry.d.ts.map +1 -0
- package/dist/cli-entry.js +14 -0
- package/dist/cli.d.ts +7 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +273 -139
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/lib/colors.d.ts +2 -0
- package/dist/lib/colors.d.ts.map +1 -0
- package/dist/lib/colors.js +2 -0
- package/dist/lib/config.d.ts +32 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +125 -22
- package/dist/lib/context.d.ts +37 -13
- package/dist/lib/context.d.ts.map +1 -1
- package/dist/lib/context.js +19 -3
- package/dist/lib/coverage-loader.d.ts +16 -0
- package/dist/lib/coverage-loader.d.ts.map +1 -0
- package/dist/lib/coverage-loader.js +20 -0
- package/dist/lib/coverage.d.ts +28 -0
- package/dist/lib/coverage.d.ts.map +1 -0
- package/dist/lib/coverage.js +212 -0
- package/dist/lib/executor.d.ts +3 -26
- package/dist/lib/executor.d.ts.map +1 -1
- package/dist/lib/executor.js +11 -6
- package/dist/lib/fake-timers.d.ts +6 -0
- package/dist/lib/fake-timers.d.ts.map +1 -0
- package/dist/lib/fake-timers.js +45 -0
- package/dist/lib/import-module.d.ts +2 -0
- package/dist/lib/import-module.d.ts.map +1 -0
- package/dist/lib/import-module.js +29 -0
- package/dist/lib/normalize.d.ts +2 -0
- package/dist/lib/normalize.d.ts.map +1 -0
- package/dist/lib/{utils.js → normalize.js} +0 -9
- package/dist/lib/playwright.d.ts +1 -1
- package/dist/lib/playwright.d.ts.map +1 -1
- package/dist/lib/playwright.js +5 -8
- package/dist/lib/reporters/dot.d.ts +1 -2
- package/dist/lib/reporters/dot.d.ts.map +1 -1
- package/dist/lib/reporters/dot.js +2 -1
- package/dist/lib/reporters/files.d.ts +1 -2
- package/dist/lib/reporters/files.d.ts.map +1 -1
- package/dist/lib/reporters/files.js +2 -1
- package/dist/lib/reporters/index.d.ts +4 -5
- package/dist/lib/reporters/index.d.ts.map +1 -1
- package/dist/lib/reporters/index.js +3 -3
- package/dist/lib/reporters/results.d.ts +30 -0
- package/dist/lib/reporters/results.d.ts.map +1 -0
- package/dist/lib/reporters/results.js +1 -0
- package/dist/lib/reporters/spec.d.ts +1 -2
- package/dist/lib/reporters/spec.d.ts.map +1 -1
- package/dist/lib/reporters/spec.js +2 -1
- package/dist/lib/reporters/tap.d.ts +1 -2
- package/dist/lib/reporters/tap.d.ts.map +1 -1
- package/dist/lib/reporters/tap.js +1 -1
- package/dist/lib/runner-browser.d.ts +21 -0
- package/dist/lib/runner-browser.d.ts.map +1 -0
- package/dist/lib/runner-browser.js +117 -0
- package/dist/lib/runner.d.ts +7 -2
- package/dist/lib/runner.d.ts.map +1 -1
- package/dist/lib/runner.js +33 -4
- package/dist/lib/runtime.d.ts +2 -0
- package/dist/lib/runtime.d.ts.map +1 -0
- package/dist/lib/runtime.js +2 -0
- package/dist/lib/ts-transform.d.ts +4 -0
- package/dist/lib/ts-transform.d.ts.map +1 -0
- package/dist/lib/ts-transform.js +29 -0
- package/dist/lib/worker-e2e.js +5 -4
- package/dist/lib/worker.js +31 -3
- package/dist/test/coverage/fixture.d.ts +5 -0
- package/dist/test/coverage/fixture.d.ts.map +1 -0
- package/dist/test/coverage/fixture.js +32 -0
- package/dist/test/coverage/test-browser.d.ts +2 -0
- package/dist/test/coverage/test-browser.d.ts.map +1 -0
- package/dist/test/coverage/test-browser.js +24 -0
- package/dist/test/coverage/test-e2e.d.ts +2 -0
- package/dist/test/coverage/test-e2e.d.ts.map +1 -0
- package/dist/test/coverage/test-e2e.js +60 -0
- package/dist/test/coverage/test-unit.d.ts +2 -0
- package/dist/test/coverage/test-unit.d.ts.map +1 -0
- package/dist/test/coverage/test-unit.js +27 -0
- package/dist/test/framework.test.browser.d.ts +2 -0
- package/dist/test/framework.test.browser.d.ts.map +1 -0
- package/dist/test/framework.test.browser.js +107 -0
- package/dist/test/framework.test.e2e.d.ts.map +1 -0
- package/dist/test/framework.test.e2e.js +34 -0
- package/package.json +30 -9
- package/src/app/client/entry.ts +353 -0
- package/src/app/client/iframe.ts +18 -0
- package/src/app/server.ts +336 -0
- package/src/cli-entry.ts +15 -0
- package/src/cli.ts +322 -148
- package/src/index.ts +1 -0
- package/src/lib/colors.ts +3 -0
- package/src/lib/config.ts +169 -23
- package/src/lib/context.ts +59 -17
- package/src/lib/coverage-loader.ts +31 -0
- package/src/lib/coverage.ts +320 -0
- package/src/lib/executor.ts +18 -35
- package/src/lib/fake-timers.ts +64 -0
- package/src/lib/import-module.ts +29 -0
- package/src/lib/{utils.ts → normalize.ts} +0 -18
- package/src/lib/playwright.ts +5 -7
- package/src/lib/reporters/dot.ts +3 -2
- package/src/lib/reporters/files.ts +3 -2
- package/src/lib/reporters/index.ts +4 -5
- package/src/lib/reporters/results.ts +29 -0
- package/src/lib/reporters/spec.ts +3 -2
- package/src/lib/reporters/tap.ts +2 -2
- package/src/lib/runner-browser.ts +165 -0
- package/src/lib/runner.ts +62 -10
- package/src/lib/runtime.ts +2 -0
- package/src/lib/ts-transform.ts +36 -0
- package/src/lib/worker-e2e.ts +7 -5
- package/src/lib/worker.ts +24 -4
- package/src/test/coverage/fixture.ts +34 -0
- package/src/test/coverage/test-browser.ts +29 -0
- package/src/test/coverage/test-e2e.ts +70 -0
- package/src/test/coverage/test-unit.ts +32 -0
- package/tsconfig.json +3 -1
- package/dist/lib/e2e-server.d.ts +0 -11
- package/dist/lib/e2e-server.d.ts.map +0 -1
- package/dist/lib/e2e-server.js +0 -15
- package/dist/lib/framework.test.d.ts +0 -2
- package/dist/lib/framework.test.d.ts.map +0 -1
- package/dist/lib/framework.test.e2e.d.ts.map +0 -1
- package/dist/lib/framework.test.e2e.js +0 -29
- package/dist/lib/framework.test.js +0 -283
- package/dist/lib/utils.d.ts +0 -16
- package/dist/lib/utils.d.ts.map +0 -1
- package/src/lib/e2e-server.ts +0 -28
- /package/dist/{lib → test}/framework.test.e2e.d.ts +0 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import type { createCoverageMap as CreateCoverageMap } from 'istanbul-lib-coverage'
|
|
2
|
+
import type { createContext as CreateContext } from 'istanbul-lib-report'
|
|
3
|
+
import type IstanbulReports from 'istanbul-reports'
|
|
4
|
+
import * as fsp from 'node:fs/promises'
|
|
5
|
+
import { createRequire } from 'node:module'
|
|
6
|
+
import * as path from 'node:path'
|
|
7
|
+
import { fileURLToPath } from 'node:url'
|
|
8
|
+
import { colors } from './colors.ts'
|
|
9
|
+
import { transformTypeScript } from './ts-transform.ts'
|
|
10
|
+
|
|
11
|
+
// Istanbul packages are loaded lazily so that FORCE_COLOR can be set based on
|
|
12
|
+
// the actual TTY state before supports-color caches its detection result.
|
|
13
|
+
let _istanbul:
|
|
14
|
+
| {
|
|
15
|
+
V8ToIstanbul: any
|
|
16
|
+
createCoverageMap: typeof CreateCoverageMap
|
|
17
|
+
createContext: typeof CreateContext
|
|
18
|
+
reports: typeof IstanbulReports
|
|
19
|
+
}
|
|
20
|
+
| undefined
|
|
21
|
+
|
|
22
|
+
function getIstanbul() {
|
|
23
|
+
if (!_istanbul) {
|
|
24
|
+
process.env.FORCE_COLOR ??= process.stdout.isTTY ? '1' : '0'
|
|
25
|
+
let require = createRequire(import.meta.url)
|
|
26
|
+
_istanbul = {
|
|
27
|
+
V8ToIstanbul: require('v8-to-istanbul'),
|
|
28
|
+
createCoverageMap: (
|
|
29
|
+
require('istanbul-lib-coverage') as { createCoverageMap: typeof CreateCoverageMap }
|
|
30
|
+
).createCoverageMap,
|
|
31
|
+
createContext: (require('istanbul-lib-report') as { createContext: typeof CreateContext })
|
|
32
|
+
.createContext,
|
|
33
|
+
reports: require('istanbul-reports') as typeof IstanbulReports,
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return _istanbul
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface CoverageConfig {
|
|
40
|
+
dir: string
|
|
41
|
+
include?: string[]
|
|
42
|
+
exclude?: string[]
|
|
43
|
+
statements?: number
|
|
44
|
+
lines?: number
|
|
45
|
+
branches?: number
|
|
46
|
+
functions?: number
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface V8CoverageEntry {
|
|
50
|
+
url: string
|
|
51
|
+
source?: string
|
|
52
|
+
functions: Array<{
|
|
53
|
+
functionName: string
|
|
54
|
+
isBlockCoverage: boolean
|
|
55
|
+
ranges: Array<{ startOffset: number; endOffset: number; count: number }>
|
|
56
|
+
}>
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type CoverageMap = ReturnType<typeof CreateCoverageMap>
|
|
60
|
+
|
|
61
|
+
function matchesGlobs(filePath: string, globs: string[]): boolean {
|
|
62
|
+
return globs.some((glob) => path.matchesGlob(filePath, glob))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function filterCoverageMap(
|
|
66
|
+
coverageMap: CoverageMap,
|
|
67
|
+
cwd: string,
|
|
68
|
+
config: CoverageConfig,
|
|
69
|
+
): CoverageMap {
|
|
70
|
+
let filtered = getIstanbul().createCoverageMap({})
|
|
71
|
+
for (let filePath of coverageMap.files()) {
|
|
72
|
+
let relative = path.relative(cwd, filePath)
|
|
73
|
+
|
|
74
|
+
if (config.include && config.include.length > 0) {
|
|
75
|
+
if (!matchesGlobs(relative, config.include)) continue
|
|
76
|
+
}
|
|
77
|
+
if (config.exclude && config.exclude.length > 0) {
|
|
78
|
+
if (matchesGlobs(relative, config.exclude)) continue
|
|
79
|
+
}
|
|
80
|
+
let fc = coverageMap.fileCoverageFor(filePath) as any
|
|
81
|
+
filtered.addFileCoverage({ ...fc.toJSON(), path: relative })
|
|
82
|
+
}
|
|
83
|
+
return filtered
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function checkThresholds(coverageMap: CoverageMap, config: CoverageConfig): boolean {
|
|
87
|
+
let { statements, lines, branches, functions } = config
|
|
88
|
+
if (
|
|
89
|
+
statements === undefined &&
|
|
90
|
+
lines === undefined &&
|
|
91
|
+
branches === undefined &&
|
|
92
|
+
functions === undefined
|
|
93
|
+
)
|
|
94
|
+
return true
|
|
95
|
+
|
|
96
|
+
let summary = coverageMap.getCoverageSummary()
|
|
97
|
+
let passed = true
|
|
98
|
+
|
|
99
|
+
if (statements !== undefined) {
|
|
100
|
+
let pct = summary.statements.pct
|
|
101
|
+
if (pct < statements) {
|
|
102
|
+
console.error(
|
|
103
|
+
colors.red(
|
|
104
|
+
`\nError: Coverage threshold not met (statements ${pct.toFixed(2)}% < ${statements}%)`,
|
|
105
|
+
),
|
|
106
|
+
)
|
|
107
|
+
passed = false
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (lines !== undefined) {
|
|
111
|
+
let pct = summary.lines.pct
|
|
112
|
+
if (pct < lines) {
|
|
113
|
+
console.error(
|
|
114
|
+
colors.red(`\nError: Coverage threshold not met (lines ${pct.toFixed(2)}% < ${lines}%)`),
|
|
115
|
+
)
|
|
116
|
+
passed = false
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (branches !== undefined) {
|
|
120
|
+
let pct = summary.branches.pct
|
|
121
|
+
if (pct < branches) {
|
|
122
|
+
console.error(
|
|
123
|
+
colors.red(
|
|
124
|
+
`\nError: Coverage threshold not met (branches ${pct.toFixed(2)}% < ${branches}%)`,
|
|
125
|
+
),
|
|
126
|
+
)
|
|
127
|
+
passed = false
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (functions !== undefined) {
|
|
131
|
+
let pct = summary.functions.pct
|
|
132
|
+
if (pct < functions) {
|
|
133
|
+
console.error(
|
|
134
|
+
colors.red(
|
|
135
|
+
`\nError: Coverage threshold not met (functions ${pct.toFixed(2)}% < ${functions}%)`,
|
|
136
|
+
),
|
|
137
|
+
)
|
|
138
|
+
passed = false
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return passed
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function writeIstanbulReports(coverageMap: CoverageMap, cwd: string, outDir: string) {
|
|
146
|
+
await fsp.mkdir(outDir, { recursive: true })
|
|
147
|
+
let { createContext, reports } = getIstanbul()
|
|
148
|
+
let ctx = createContext({ coverageMap, dir: outDir } as any)
|
|
149
|
+
console.log('\nCoverage report:')
|
|
150
|
+
reports.create('text').execute(ctx)
|
|
151
|
+
reports.create('lcovonly').execute(ctx)
|
|
152
|
+
console.log(`\nLCOV coverage written to ${path.relative(cwd, path.join(outDir, 'lcov.info'))}`)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Convert a single V8 coverage entry to Istanbul format and merge it into the
|
|
156
|
+
// coverage map.
|
|
157
|
+
//
|
|
158
|
+
// V8 reports byte offsets against the JS bytes it actually instrumented. When
|
|
159
|
+
// the entry already carries that source (Playwright's `coverage.stopJSCoverage`
|
|
160
|
+
// returns it on each entry, including the inline source map), we hand it
|
|
161
|
+
// straight to v8-to-istanbul so the offsets line up exactly. The server path
|
|
162
|
+
// uses Node's `NODE_V8_COVERAGE` JSON, which doesn't include source — there we
|
|
163
|
+
// re-derive by re-running our esbuild transform on the original TS file.
|
|
164
|
+
async function addV8EntryToCoverageMap(
|
|
165
|
+
coverageMap: CoverageMap,
|
|
166
|
+
filePath: string,
|
|
167
|
+
functions: V8CoverageEntry['functions'],
|
|
168
|
+
source: string,
|
|
169
|
+
): Promise<boolean> {
|
|
170
|
+
let { V8ToIstanbul } = getIstanbul()
|
|
171
|
+
let converter = new V8ToIstanbul(filePath, undefined, { source })
|
|
172
|
+
await converter.load()
|
|
173
|
+
converter.applyCoverage(functions)
|
|
174
|
+
coverageMap.merge(converter.toIstanbul())
|
|
175
|
+
return true
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function shouldExcludeFromCoverage(
|
|
179
|
+
filePath: string,
|
|
180
|
+
rootDir: string,
|
|
181
|
+
testFiles: Set<string>,
|
|
182
|
+
): boolean {
|
|
183
|
+
return (
|
|
184
|
+
!filePath.startsWith(rootDir + path.sep) ||
|
|
185
|
+
filePath.includes(`${path.sep}node_modules${path.sep}`) ||
|
|
186
|
+
testFiles.has(filePath)
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export async function collectServerCoverageMap(
|
|
191
|
+
coverageDataDir: string,
|
|
192
|
+
cwd: string,
|
|
193
|
+
testFiles: Set<string>,
|
|
194
|
+
): Promise<CoverageMap | null> {
|
|
195
|
+
let { createCoverageMap } = getIstanbul()
|
|
196
|
+
let coverageMap = createCoverageMap({})
|
|
197
|
+
let converted = 0
|
|
198
|
+
|
|
199
|
+
let files: string[]
|
|
200
|
+
try {
|
|
201
|
+
files = (await fsp.readdir(coverageDataDir)).filter(
|
|
202
|
+
(f) => f.startsWith('coverage-') && f.endsWith('.json'),
|
|
203
|
+
)
|
|
204
|
+
} catch {
|
|
205
|
+
return null
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
for (let file of files) {
|
|
209
|
+
let data = JSON.parse(await fsp.readFile(path.join(coverageDataDir, file), 'utf-8'))
|
|
210
|
+
let scriptCoverages: Array<{ url: string; functions: any[] }> = data.result ?? []
|
|
211
|
+
|
|
212
|
+
for (let entry of scriptCoverages) {
|
|
213
|
+
if (!entry.url.startsWith('file://')) continue
|
|
214
|
+
|
|
215
|
+
let filePath: string
|
|
216
|
+
try {
|
|
217
|
+
filePath = fileURLToPath(entry.url)
|
|
218
|
+
} catch {
|
|
219
|
+
continue
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (
|
|
223
|
+
!filePath ||
|
|
224
|
+
!['.ts', '.tsx'].includes(path.extname(filePath)) ||
|
|
225
|
+
shouldExcludeFromCoverage(filePath, cwd, testFiles)
|
|
226
|
+
) {
|
|
227
|
+
continue
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
// For server unit tests, we transform the TS with a module loader and V8 tracks
|
|
232
|
+
// coverage using byte offsets from the transformed JS. Re-transform with the
|
|
233
|
+
// same `esbuild` call here so offsets align, then pass the result with its
|
|
234
|
+
// inline source map to v8-to-istanbul.
|
|
235
|
+
let tsSource = await fsp.readFile(filePath, 'utf-8')
|
|
236
|
+
let { code } = await transformTypeScript(tsSource, filePath)
|
|
237
|
+
let success = await addV8EntryToCoverageMap(coverageMap, filePath, entry.functions, code)
|
|
238
|
+
if (success) converted++
|
|
239
|
+
} catch (e) {
|
|
240
|
+
// Skip files that can't be converted
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Clean up raw V8 coverage JSON files now that we've processed them
|
|
246
|
+
//await Promise.all(files.map((f) => fsp.rm(path.join(coverageDataDir, f), { force: true })))
|
|
247
|
+
|
|
248
|
+
return converted > 0 ? coverageMap : null
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export async function collectCoverageMapFromPlaywright(
|
|
252
|
+
entries: V8CoverageEntry[],
|
|
253
|
+
rootDir: string,
|
|
254
|
+
testFiles: Set<string>,
|
|
255
|
+
resolveRelativePath: (url: string) => Promise<string | null>,
|
|
256
|
+
): Promise<CoverageMap | null> {
|
|
257
|
+
let { createCoverageMap } = getIstanbul()
|
|
258
|
+
let coverageMap = createCoverageMap({})
|
|
259
|
+
let converted = 0
|
|
260
|
+
|
|
261
|
+
for (let entry of entries) {
|
|
262
|
+
let filePath: string
|
|
263
|
+
try {
|
|
264
|
+
let relativePath = await resolveRelativePath(new URL(entry.url).pathname)
|
|
265
|
+
if (!relativePath) continue
|
|
266
|
+
|
|
267
|
+
// Ignore entries outside the root dir, entries in node_modules, and test files
|
|
268
|
+
filePath = path.resolve(rootDir, relativePath)
|
|
269
|
+
if (shouldExcludeFromCoverage(filePath, rootDir, testFiles)) {
|
|
270
|
+
continue
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Ensure file exists
|
|
274
|
+
await fsp.access(filePath)
|
|
275
|
+
} catch {
|
|
276
|
+
continue
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (!entry.source) {
|
|
280
|
+
throw new Error(
|
|
281
|
+
`Entry for ${entry.url} is missing source, cannot convert coverage. Ensure the browser launched with Playwright's JS coverage enabled.`,
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
let success = await addV8EntryToCoverageMap(
|
|
287
|
+
coverageMap,
|
|
288
|
+
filePath,
|
|
289
|
+
entry.functions,
|
|
290
|
+
entry.source,
|
|
291
|
+
)
|
|
292
|
+
if (success) converted++
|
|
293
|
+
} catch {
|
|
294
|
+
// Skip files that can't be converted
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return converted > 0 ? coverageMap : null
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export async function generateCombinedCoverageReport(
|
|
302
|
+
maps: (CoverageMap | null | undefined)[],
|
|
303
|
+
cwd: string,
|
|
304
|
+
config: CoverageConfig,
|
|
305
|
+
): Promise<boolean> {
|
|
306
|
+
let { createCoverageMap } = getIstanbul()
|
|
307
|
+
let combined = createCoverageMap({})
|
|
308
|
+
for (let map of maps) {
|
|
309
|
+
if (map) combined.merge(map)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (combined.files().length === 0) {
|
|
313
|
+
console.log('No coverage data collected.')
|
|
314
|
+
return true
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
let filtered = filterCoverageMap(combined, cwd, config)
|
|
318
|
+
await writeIstanbulReports(filtered, cwd, config.dir)
|
|
319
|
+
return checkThresholds(filtered, config)
|
|
320
|
+
}
|
package/src/lib/executor.ts
CHANGED
|
@@ -1,34 +1,12 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import type {
|
|
4
|
-
|
|
5
|
-
export interface TestResult {
|
|
6
|
-
name: string
|
|
7
|
-
suiteName: string
|
|
8
|
-
filePath?: string
|
|
9
|
-
status: 'passed' | 'failed' | 'skipped' | 'todo'
|
|
10
|
-
error?: {
|
|
11
|
-
message: string
|
|
12
|
-
stack?: string
|
|
13
|
-
}
|
|
14
|
-
duration: number
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface TestResults {
|
|
18
|
-
passed: number
|
|
19
|
-
failed: number
|
|
20
|
-
skipped: number
|
|
21
|
-
todo: number
|
|
22
|
-
tests: TestResult[]
|
|
23
|
-
}
|
|
1
|
+
import { createTestContext, type CreateTestContextOptions } from './context.ts'
|
|
2
|
+
import type { V8CoverageEntry } from './coverage.ts'
|
|
3
|
+
import type { TestResult, TestResults } from './reporters/results.ts'
|
|
24
4
|
|
|
25
|
-
export async function runTests(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
open?: boolean
|
|
29
|
-
playwrightPageOptions?: BrowserContextOptions
|
|
30
|
-
}): Promise<TestResults> {
|
|
5
|
+
export async function runTests(
|
|
6
|
+
options?: Omit<CreateTestContextOptions, 'addE2ECoverageEntries'>,
|
|
7
|
+
): Promise<TestResults> {
|
|
31
8
|
let suites = (globalThis as any).__testSuites || []
|
|
9
|
+
let e2eCoverageEntries: Array<{ entries: V8CoverageEntry[]; baseUrl: string }> = []
|
|
32
10
|
let results: TestResults = {
|
|
33
11
|
passed: 0,
|
|
34
12
|
failed: 0,
|
|
@@ -107,12 +85,13 @@ export async function runTests(options?: {
|
|
|
107
85
|
duration: 0,
|
|
108
86
|
}
|
|
109
87
|
|
|
110
|
-
let
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
88
|
+
let contextOpts: CreateTestContextOptions | undefined = options
|
|
89
|
+
? {
|
|
90
|
+
...options,
|
|
91
|
+
addE2ECoverageEntries: (e) => e2eCoverageEntries.push(e),
|
|
92
|
+
}
|
|
93
|
+
: undefined
|
|
94
|
+
let { testContext, cleanup } = createTestContext(contextOpts)
|
|
116
95
|
|
|
117
96
|
try {
|
|
118
97
|
if (suite.beforeEach) {
|
|
@@ -158,5 +137,9 @@ export async function runTests(options?: {
|
|
|
158
137
|
// for the next test file (which reuses the same cached module instance)
|
|
159
138
|
suites.length = 0
|
|
160
139
|
|
|
140
|
+
if (e2eCoverageEntries.length > 0) {
|
|
141
|
+
results.e2eBrowserCoverageEntries = e2eCoverageEntries
|
|
142
|
+
}
|
|
143
|
+
|
|
161
144
|
return results
|
|
162
145
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { mock } from './mock.ts'
|
|
2
|
+
|
|
3
|
+
export interface FakeTimers {
|
|
4
|
+
advance(ms: number): void
|
|
5
|
+
restore(): void
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function createFakeTimers(): FakeTimers {
|
|
9
|
+
let currentTime = 0
|
|
10
|
+
let nextId = 1
|
|
11
|
+
let pending: Array<{ id: number; fn: () => void; time: number; repeatMs?: number }> = []
|
|
12
|
+
|
|
13
|
+
function schedule(fn: () => void, delay: number, repeatMs?: number): number {
|
|
14
|
+
let id = nextId++
|
|
15
|
+
pending.push({ id, fn, time: currentTime + Math.max(0, delay), repeatMs })
|
|
16
|
+
return id
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function cancel(id: number) {
|
|
20
|
+
pending = pending.filter((t) => t.id !== id)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let setTimeoutMock = mock.method(globalThis, 'setTimeout', ((fn: () => void, delay = 0) =>
|
|
24
|
+
schedule(fn, delay)) as unknown as typeof setTimeout)
|
|
25
|
+
let clearTimeoutMock = mock.method(
|
|
26
|
+
globalThis,
|
|
27
|
+
'clearTimeout',
|
|
28
|
+
cancel as unknown as typeof clearTimeout,
|
|
29
|
+
)
|
|
30
|
+
let setIntervalMock = mock.method(globalThis, 'setInterval', ((fn: () => void, delay = 0) =>
|
|
31
|
+
schedule(fn, delay, Math.max(0, delay))) as unknown as typeof setInterval)
|
|
32
|
+
let clearIntervalMock = mock.method(
|
|
33
|
+
globalThis,
|
|
34
|
+
'clearInterval',
|
|
35
|
+
cancel as unknown as typeof clearInterval,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
advance(ms: number) {
|
|
40
|
+
let targetTime = currentTime + ms
|
|
41
|
+
while (true) {
|
|
42
|
+
let next = pending.filter((t) => t.time <= targetTime).sort((a, b) => a.time - b.time)[0]
|
|
43
|
+
if (!next) break
|
|
44
|
+
currentTime = next.time
|
|
45
|
+
pending = pending.filter((t) => t.id !== next.id)
|
|
46
|
+
// Requeue intervals before running the callback so that calling
|
|
47
|
+
// clearInterval(id) from inside the callback can cancel the next firing.
|
|
48
|
+
if (next.repeatMs !== undefined) {
|
|
49
|
+
pending.push({ ...next, time: next.time + Math.max(1, next.repeatMs) })
|
|
50
|
+
}
|
|
51
|
+
next.fn()
|
|
52
|
+
}
|
|
53
|
+
currentTime = targetTime
|
|
54
|
+
},
|
|
55
|
+
restore() {
|
|
56
|
+
setTimeoutMock.mock.restore?.()
|
|
57
|
+
clearTimeoutMock.mock.restore?.()
|
|
58
|
+
setIntervalMock.mock.restore?.()
|
|
59
|
+
clearIntervalMock.mock.restore?.()
|
|
60
|
+
pending = []
|
|
61
|
+
currentTime = 0
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { tsImport } from 'tsx/esm/api'
|
|
2
|
+
import { IS_BUN } from './runtime.ts'
|
|
3
|
+
|
|
4
|
+
interface ImportMetaWithResolve extends ImportMeta {
|
|
5
|
+
resolve(specifier: string, parent?: string | URL): string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function hasImportMetaResolve(meta: ImportMeta): meta is ImportMetaWithResolve {
|
|
9
|
+
return 'resolve' in meta && typeof meta.resolve === 'function'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
* Loads a module specifier relative to the caller's module context.
|
|
14
|
+
*
|
|
15
|
+
* @param specifier The module specifier or file path to load.
|
|
16
|
+
* @param meta The caller's `import.meta`, used as the context for resolution.
|
|
17
|
+
* @returns The imported module namespace.
|
|
18
|
+
*/
|
|
19
|
+
export async function importModule(specifier: string, meta: ImportMeta): Promise<any> {
|
|
20
|
+
if (IS_BUN) {
|
|
21
|
+
if (!hasImportMetaResolve(meta)) {
|
|
22
|
+
throw new Error('importModule() requires import.meta.resolve() in Bun')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return import(meta.resolve(specifier, meta.url))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return tsImport(specifier, meta.url)
|
|
29
|
+
}
|
|
@@ -1,21 +1,3 @@
|
|
|
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
1
|
function normalizeFilePath(path: string): string {
|
|
20
2
|
let locSuffix = path.match(/(:\d+:\d+)$/)?.[0] || ''
|
|
21
3
|
let normalized =
|
package/src/lib/playwright.ts
CHANGED
|
@@ -3,24 +3,22 @@ import * as fs from 'node:fs/promises'
|
|
|
3
3
|
import { chromium, firefox, webkit } from 'playwright'
|
|
4
4
|
import type { BrowserContextOptions, LaunchOptions } from 'playwright'
|
|
5
5
|
import type { PlaywrightTestConfig } from 'playwright/test'
|
|
6
|
-
import {
|
|
6
|
+
import { importModule } from './import-module.ts'
|
|
7
7
|
|
|
8
8
|
export type PlaywrightUseOpts = PlaywrightTestConfig['use']
|
|
9
9
|
|
|
10
10
|
export async function loadPlaywrightConfig(
|
|
11
11
|
input: string | undefined,
|
|
12
|
+
cwd = process.cwd(),
|
|
12
13
|
): Promise<PlaywrightTestConfig | undefined> {
|
|
13
14
|
let candidates = input
|
|
14
|
-
? [path.resolve(
|
|
15
|
-
: [
|
|
16
|
-
path.join(process.cwd(), 'playwright.config.ts'),
|
|
17
|
-
path.join(process.cwd(), 'playwright.config.js'),
|
|
18
|
-
]
|
|
15
|
+
? [path.resolve(cwd, input)]
|
|
16
|
+
: [path.join(cwd, 'playwright.config.ts'), path.join(cwd, 'playwright.config.js')]
|
|
19
17
|
|
|
20
18
|
for (let configPath of candidates) {
|
|
21
19
|
try {
|
|
22
20
|
await fs.access(configPath)
|
|
23
|
-
let mod = await
|
|
21
|
+
let mod = await importModule(configPath, import.meta)
|
|
24
22
|
return mod.default ?? mod
|
|
25
23
|
} catch {
|
|
26
24
|
// not found or failed to load — try next
|
package/src/lib/reporters/dot.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { colors
|
|
2
|
-
import
|
|
1
|
+
import { colors } from '../colors.ts'
|
|
2
|
+
import { normalizeLine } from '../normalize.ts'
|
|
3
3
|
import type { Reporter } from './index.ts'
|
|
4
|
+
import type { Counts, TestResult, TestResults } from './results.ts'
|
|
4
5
|
|
|
5
6
|
export class DotReporter implements Reporter {
|
|
6
7
|
#failures: { name: string; error: TestResult['error'] }[] = []
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as path from 'node:path'
|
|
2
|
-
import { colors
|
|
3
|
-
import
|
|
2
|
+
import { colors } from '../colors.ts'
|
|
3
|
+
import { normalizeLine } from '../normalize.ts'
|
|
4
4
|
import type { Reporter } from './index.ts'
|
|
5
|
+
import type { Counts, TestResult, TestResults } from './results.ts'
|
|
5
6
|
|
|
6
7
|
export class FilesReporter implements Reporter {
|
|
7
8
|
#failures: { suiteName: string; name: string; error: TestResult['error'] }[] = []
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import type { Counts } from '../utils.ts'
|
|
2
|
-
import type { TestResults } from '../executor.ts'
|
|
3
|
-
import { SpecReporter } from './spec.ts'
|
|
4
|
-
import { TapReporter } from './tap.ts'
|
|
5
1
|
import { DotReporter } from './dot.ts'
|
|
6
2
|
import { FilesReporter } from './files.ts'
|
|
3
|
+
import type { Counts, TestResults } from './results.ts'
|
|
4
|
+
import { SpecReporter } from './spec.ts'
|
|
5
|
+
import { TapReporter } from './tap.ts'
|
|
7
6
|
|
|
8
7
|
export interface Reporter {
|
|
9
8
|
onResult(results: TestResults, env?: string): void
|
|
@@ -11,7 +10,7 @@ export interface Reporter {
|
|
|
11
10
|
onSectionStart(label: string): void
|
|
12
11
|
}
|
|
13
12
|
|
|
14
|
-
export {
|
|
13
|
+
export { DotReporter, FilesReporter, SpecReporter, TapReporter }
|
|
15
14
|
|
|
16
15
|
export function createReporter(type: string): Reporter {
|
|
17
16
|
switch (type) {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { V8CoverageEntry } from '../coverage'
|
|
2
|
+
|
|
3
|
+
export interface TestResult {
|
|
4
|
+
name: string
|
|
5
|
+
suiteName: string
|
|
6
|
+
filePath?: string
|
|
7
|
+
status: 'passed' | 'failed' | 'skipped' | 'todo'
|
|
8
|
+
error?: {
|
|
9
|
+
message: string
|
|
10
|
+
stack?: string
|
|
11
|
+
}
|
|
12
|
+
duration: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface TestResults {
|
|
16
|
+
passed: number
|
|
17
|
+
failed: number
|
|
18
|
+
skipped: number
|
|
19
|
+
todo: number
|
|
20
|
+
tests: TestResult[]
|
|
21
|
+
e2eBrowserCoverageEntries?: Array<{ entries: V8CoverageEntry[]; baseUrl: string }>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type Counts = {
|
|
25
|
+
passed: number
|
|
26
|
+
failed: number
|
|
27
|
+
skipped: number
|
|
28
|
+
todo: number
|
|
29
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { colors
|
|
2
|
-
import
|
|
1
|
+
import { colors } from '../colors.ts'
|
|
2
|
+
import { normalizeLine } from '../normalize.ts'
|
|
3
3
|
import type { Reporter } from './index.ts'
|
|
4
|
+
import type { Counts, TestResult, TestResults } from './results.ts'
|
|
4
5
|
|
|
5
6
|
export class SpecReporter implements Reporter {
|
|
6
7
|
#failures: { suiteName: string; name: string; error: TestResult['error'] }[] = []
|
package/src/lib/reporters/tap.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { normalizeLine
|
|
2
|
-
import type { TestResults } from '../executor.ts'
|
|
1
|
+
import { normalizeLine } from '../normalize.ts'
|
|
3
2
|
import type { Reporter } from './index.ts'
|
|
3
|
+
import type { Counts, TestResults } from './results.ts'
|
|
4
4
|
|
|
5
5
|
export class TapReporter implements Reporter {
|
|
6
6
|
#counter = 0
|