@crab-dev/wake 0.1.21 → 0.1.23
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/CHANGELOG.md +32 -0
- package/bin/terminal.mjs +20 -4
- package/bin/wake.mjs +418 -0
- package/index.cjs +197 -0
- package/index.d.ts +254 -0
- package/index.mjs +3 -0
- package/loader.cjs +19 -2
- package/package.json +32 -12
- package/test-context-internal.cjs +30 -0
- package/test-react.cjs +60 -0
- package/test-react.d.ts +162 -0
- package/test-react.mjs +29 -0
- package/test.cjs +98 -0
- package/test.d.ts +265 -0
- package/test.mjs +15 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.23
|
|
4
|
+
|
|
5
|
+
- Made Yarn 4.16 PnP authoritative for package visibility, alias dependencies, peer/virtual packages,
|
|
6
|
+
ignore patterns, nested roots, compressed zip archives, watch invalidation, and structured resolver
|
|
7
|
+
diagnostics; PnP rejection can no longer be overridden by Wake aliases, `node_modules`, or the
|
|
8
|
+
retired Components fallback.
|
|
9
|
+
- Migrated the source repository and VS Code editor to one strict Yarn PnP workspace/lock, with
|
|
10
|
+
PnP-aware embedded dependency and maintenance tooling plus differential `pnpapi` conformance gates.
|
|
11
|
+
- Kept npm `node_modules` projects as a first-class resolver contract, including workspace links and
|
|
12
|
+
package-lock watch invalidation, and added blocking Windows/Linux Node 22.14/26 `npm ci` consumer
|
|
13
|
+
gates built exclusively from the current local tarballs.
|
|
14
|
+
- Moved private Wake Docs generated imports to a non-package alias namespace so Yarn PnP remains
|
|
15
|
+
authoritative for valid bare package names without intercepting internal runtime modules.
|
|
16
|
+
- Kept absolute issuers outside a PnP root on classic npm resolution, and made Wake Test resolve
|
|
17
|
+
React package metadata through the same issuer-owned PnP/npm environment as executable modules.
|
|
18
|
+
- Made the pinned Corepack bootstrap overwrite preinstalled Windows shims consistently across CI,
|
|
19
|
+
release, and VS Code workflows.
|
|
20
|
+
- Made npm 11/12 consumer locks include every optional platform locator from local artifacts before
|
|
21
|
+
`npm ci --omit=optional`, while still loading only the matching native package at runtime, and
|
|
22
|
+
invoke the installed CLI without the removed `npx --no-install` behavior.
|
|
23
|
+
|
|
24
|
+
## 0.1.22
|
|
25
|
+
|
|
26
|
+
- Added the experimental Wake-native, React-first `wake test` system, `runTests()` and `TestContext`
|
|
27
|
+
Node APIs, explicit `@crab-dev/wake/test` ESM/CommonJS imports, isolated V8 suite realms, Wake
|
|
28
|
+
snapshots, function/network mocks, an async clock, projects, sharding, and a token-authenticated
|
|
29
|
+
crash-isolated test host. ADR 0020 defines the private fast-DOM and system-Chromium boundary;
|
|
30
|
+
external runner/config/plugin/result compatibility is intentionally outside the contract.
|
|
31
|
+
- Added typed Chromium input and browser screenshot snapshots. `toMatchScreenshot()` stores
|
|
32
|
+
rendering-profiled PNG baselines and emits received PNG plus self-contained visual-diff artifacts
|
|
33
|
+
without exposing raw CDP or adding a second browser event consumer.
|
|
34
|
+
|
|
3
35
|
## 0.1.21
|
|
4
36
|
|
|
5
37
|
- Added aggregate Wake Docs workspaces with isolated production bundles, deterministic manifests,
|
package/bin/terminal.mjs
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import clipboard from 'clipboardy'
|
|
2
|
-
import open from 'open'
|
|
3
1
|
import stringWidth from 'string-width'
|
|
4
2
|
|
|
5
3
|
import {
|
|
@@ -15,6 +13,24 @@ const BOLD = '\x1b[1m'
|
|
|
15
13
|
const DIM = '\x1b[2m'
|
|
16
14
|
const MAX_ACTIVITY = 200
|
|
17
15
|
const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧']
|
|
16
|
+
const OPTIONAL_CLIPBOARD_MODULE = 'clip' + 'boardy'
|
|
17
|
+
const OPTIONAL_OPEN_MODULE = 'op' + 'en'
|
|
18
|
+
|
|
19
|
+
const defaultClipboard = {
|
|
20
|
+
async read() {
|
|
21
|
+
const { default: clipboard } = await import(OPTIONAL_CLIPBOARD_MODULE)
|
|
22
|
+
return clipboard.read()
|
|
23
|
+
},
|
|
24
|
+
async write(value) {
|
|
25
|
+
const { default: clipboard } = await import(OPTIONAL_CLIPBOARD_MODULE)
|
|
26
|
+
return clipboard.write(value)
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function defaultOpenUrl(value) {
|
|
31
|
+
const { default: open } = await import(OPTIONAL_OPEN_MODULE)
|
|
32
|
+
return open(value)
|
|
33
|
+
}
|
|
18
34
|
|
|
19
35
|
export function supportsColor(stream = process.stderr, env = process.env) {
|
|
20
36
|
return stream.isTTY === true && !Object.hasOwn(env, 'NO_COLOR')
|
|
@@ -550,8 +566,8 @@ export function createDashboardSession(
|
|
|
550
566
|
input = process.stdin,
|
|
551
567
|
output = process.stderr,
|
|
552
568
|
ui = createUi(),
|
|
553
|
-
clipboardAdapter =
|
|
554
|
-
openUrl =
|
|
569
|
+
clipboardAdapter = defaultClipboard,
|
|
570
|
+
openUrl = defaultOpenUrl,
|
|
555
571
|
env = process.env,
|
|
556
572
|
} = {},
|
|
557
573
|
) {
|
package/bin/wake.mjs
CHANGED
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
3
4
|
import { readFile } from 'node:fs/promises'
|
|
4
5
|
import {
|
|
5
6
|
build,
|
|
6
7
|
buildLibrary,
|
|
7
8
|
buildDocs,
|
|
8
9
|
bundle,
|
|
10
|
+
createTestContext,
|
|
9
11
|
generateCssToken,
|
|
10
12
|
generateDocgen,
|
|
13
|
+
runTests,
|
|
11
14
|
startDevServer,
|
|
12
15
|
startDocsDevServer,
|
|
13
16
|
version,
|
|
14
17
|
WakeError,
|
|
15
18
|
} from '../index.mjs'
|
|
16
19
|
import { parse, tokenize } from '../experimental.mjs'
|
|
20
|
+
import testContextInternal from '../test-context-internal.cjs'
|
|
17
21
|
import {
|
|
18
22
|
applyDashboardEvent,
|
|
19
23
|
createDashboardSession,
|
|
@@ -47,6 +51,12 @@ Usage:
|
|
|
47
51
|
wake docs dev [root] [--mode site|components] [--host HOST] [--port PORT] [--open]
|
|
48
52
|
wake parse <file> [--format auto|human|json]
|
|
49
53
|
wake tokenize <file> [--format auto|human|json]
|
|
54
|
+
wake test [patterns...] [--root DIR] [--name-pattern TEXT] [--project NAME]
|
|
55
|
+
[--environment auto|dom|browser] [--watch] [--changed] [--related PATH...]
|
|
56
|
+
[--coverage] [--update-snapshots] [--serial] [--workers COUNT]
|
|
57
|
+
[--bail [COUNT]] [--shard INDEX/TOTAL] [--seed SEED] [--shuffle]
|
|
58
|
+
[--reporter pretty|json|junit] [--output FILE] [--allow-no-tests]
|
|
59
|
+
[--browser-path FILE] [--headful]
|
|
50
60
|
wake --version
|
|
51
61
|
|
|
52
62
|
Options:
|
|
@@ -89,6 +99,68 @@ function takeOptions(args, name, usage = false) {
|
|
|
89
99
|
}
|
|
90
100
|
}
|
|
91
101
|
|
|
102
|
+
function takeVariadicOptions(args, name) {
|
|
103
|
+
const values = []
|
|
104
|
+
for (;;) {
|
|
105
|
+
const index = args.indexOf(name)
|
|
106
|
+
if (index === -1) return values
|
|
107
|
+
let end = index + 1
|
|
108
|
+
while (end < args.length && !args[end].startsWith('-')) end += 1
|
|
109
|
+
if (end === index + 1) throw testUsageError(`${name} requires at least one value`)
|
|
110
|
+
values.push(...args.slice(index + 1, end))
|
|
111
|
+
args.splice(index, end - index)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function testUsageError(message) {
|
|
116
|
+
const error = new WakeError('WAKE_TEST_CONFIG', message)
|
|
117
|
+
error.exitCode = 2
|
|
118
|
+
return error
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function validateTestChoice(value, name, choices) {
|
|
122
|
+
if (value !== undefined && !choices.includes(value)) {
|
|
123
|
+
throw testUsageError(`${name} must be one of: ${choices.join(', ')}`)
|
|
124
|
+
}
|
|
125
|
+
return value
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function parseTestWorkers(value) {
|
|
129
|
+
if (value === undefined || value === 'auto') return value
|
|
130
|
+
if (/^[1-9][0-9]*$/.test(value)) {
|
|
131
|
+
const count = Number(value)
|
|
132
|
+
if (Number.isSafeInteger(count)) return count
|
|
133
|
+
}
|
|
134
|
+
const match = /^([1-9][0-9]?)%$|^(100)%$/.exec(value)
|
|
135
|
+
if (match) return `${Number(match[1] || match[2])}%`
|
|
136
|
+
throw testUsageError('--workers requires auto, a positive integer, or 1%-100%')
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function takeTestBail(args) {
|
|
140
|
+
const index = args.indexOf('--bail')
|
|
141
|
+
if (index === -1) return undefined
|
|
142
|
+
const candidate = args[index + 1]
|
|
143
|
+
const hasValue = candidate !== undefined && !candidate.startsWith('-')
|
|
144
|
+
const value = hasValue ? Number(candidate) : 1
|
|
145
|
+
args.splice(index, hasValue ? 2 : 1)
|
|
146
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
147
|
+
throw testUsageError('--bail requires a non-negative integer')
|
|
148
|
+
}
|
|
149
|
+
return value
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function parseTestShard(value) {
|
|
153
|
+
if (value === undefined) return undefined
|
|
154
|
+
const match = /^([1-9][0-9]*)\/([1-9][0-9]*)$/.exec(value)
|
|
155
|
+
if (!match) throw testUsageError('--shard requires the 1-based INDEX/TOTAL form')
|
|
156
|
+
const index = Number(match[1])
|
|
157
|
+
const total = Number(match[2])
|
|
158
|
+
if (!Number.isSafeInteger(index) || !Number.isSafeInteger(total) || index > total) {
|
|
159
|
+
throw testUsageError('--shard requires 1 <= INDEX <= TOTAL')
|
|
160
|
+
}
|
|
161
|
+
return `${index}/${total}`
|
|
162
|
+
}
|
|
163
|
+
|
|
92
164
|
function commonOptions(args) {
|
|
93
165
|
return {
|
|
94
166
|
configPath: takeOption(args, '--config'),
|
|
@@ -137,6 +209,114 @@ function printResult(ui, result, label, extra = '') {
|
|
|
137
209
|
printLines(formatBuildResult(ui, result, label, extra))
|
|
138
210
|
}
|
|
139
211
|
|
|
212
|
+
function formatTestFailure(failure) {
|
|
213
|
+
let rendered = failure.code ? `${failure.code}: ` : ''
|
|
214
|
+
rendered += failure.message
|
|
215
|
+
if (failure.location) {
|
|
216
|
+
rendered += `\n at ${failure.location.path}:${failure.location.line}:${failure.location.column}`
|
|
217
|
+
}
|
|
218
|
+
if (failure.diff?.unified) rendered += `\n${failure.diff.unified}`
|
|
219
|
+
if (failure.stack && !rendered.includes(failure.stack)) rendered += `\n${failure.stack}`
|
|
220
|
+
return rendered
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function xmlEscape(value) {
|
|
224
|
+
return String(value)
|
|
225
|
+
.replaceAll('&', '&')
|
|
226
|
+
.replaceAll('<', '<')
|
|
227
|
+
.replaceAll('>', '>')
|
|
228
|
+
.replaceAll('"', '"')
|
|
229
|
+
.replaceAll("'", ''')
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function junitTestReport(result) {
|
|
233
|
+
const suiteErrors = result.suites.filter((suite) => suite.failures.length > 0).length
|
|
234
|
+
const pending = result.counts.tests.skipped + result.counts.tests.todo
|
|
235
|
+
const lines = [
|
|
236
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
237
|
+
`<testsuites tests="${result.counts.tests.total + suiteErrors}" failures="${result.counts.tests.failed}" errors="${suiteErrors}" skipped="${pending}" time="${(result.durationMs / 1_000).toFixed(3)}">`,
|
|
238
|
+
]
|
|
239
|
+
for (const suite of result.suites) {
|
|
240
|
+
const failures = suite.tests.filter((testCase) => testCase.status === 'failed').length
|
|
241
|
+
const skipped = suite.tests.filter((testCase) => testCase.status === 'skipped' || testCase.status === 'todo').length
|
|
242
|
+
const suiteError = suite.failures.length > 0 ? 1 : 0
|
|
243
|
+
lines.push(` <testsuite name="${xmlEscape(suite.path)}" tests="${suite.tests.length + suiteError}" failures="${failures}" errors="${suiteError}" skipped="${skipped}" time="${(suite.durationMs / 1_000).toFixed(3)}">`)
|
|
244
|
+
for (const testCase of suite.tests) {
|
|
245
|
+
const prefix = ` <testcase name="${xmlEscape(testCase.name)}" classname="${xmlEscape(suite.path)}" time="${(testCase.durationMs / 1_000).toFixed(3)}">`
|
|
246
|
+
if (testCase.status === 'failed') {
|
|
247
|
+
lines.push(`${prefix}<failure>${xmlEscape(testCase.failures.map(formatTestFailure).join('\n'))}</failure></testcase>`)
|
|
248
|
+
} else if (testCase.status === 'skipped' || testCase.status === 'todo') {
|
|
249
|
+
lines.push(`${prefix}<skipped /></testcase>`)
|
|
250
|
+
} else {
|
|
251
|
+
lines.push(`${prefix}</testcase>`)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (suite.failures.length > 0) {
|
|
255
|
+
lines.push(` <testcase name="[suite setup]"><error>${xmlEscape(suite.failures.map(formatTestFailure).join('\n'))}</error></testcase>`)
|
|
256
|
+
}
|
|
257
|
+
lines.push(' </testsuite>')
|
|
258
|
+
}
|
|
259
|
+
lines.push('</testsuites>')
|
|
260
|
+
return lines.join('\n')
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function printTestRun(result, reporter = 'pretty', output, includeDiagnostics = true) {
|
|
264
|
+
if (reporter !== 'pretty') {
|
|
265
|
+
const report = reporter === 'json' ? JSON.stringify(result) : junitTestReport(result)
|
|
266
|
+
if (output) writeFileSync(output, report)
|
|
267
|
+
else console.log(report)
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
for (const suite of result.suites) {
|
|
271
|
+
const status = suite.status === 'failed'
|
|
272
|
+
? 'FAIL'
|
|
273
|
+
: suite.status === 'skipped'
|
|
274
|
+
? 'SKIP'
|
|
275
|
+
: 'PASS'
|
|
276
|
+
console.error(`${status} ${suite.path}`)
|
|
277
|
+
for (const testCase of suite.tests) {
|
|
278
|
+
const marker = testCase.status === 'passed'
|
|
279
|
+
? '✓'
|
|
280
|
+
: testCase.status === 'failed'
|
|
281
|
+
? '✕'
|
|
282
|
+
: testCase.status === 'todo'
|
|
283
|
+
? '✎'
|
|
284
|
+
: '○'
|
|
285
|
+
console.error(` ${marker} ${testCase.name}`)
|
|
286
|
+
for (const failure of testCase.failures) {
|
|
287
|
+
console.error(` ${formatTestFailure(failure).replaceAll('\n', '\n ')}`)
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
for (const failure of suite.failures) {
|
|
291
|
+
console.error(` ${formatTestFailure(failure).replaceAll('\n', '\n ')}`)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
console.error(`Test Suites: ${result.counts.suites.passed} passed, ${result.counts.suites.failed} failed, ${result.counts.suites.total} total`)
|
|
295
|
+
console.error(`Tests: ${result.counts.tests.passed} passed, ${result.counts.tests.failed} failed, ${result.counts.tests.skipped + result.counts.tests.todo} pending, ${result.counts.tests.total} total`)
|
|
296
|
+
const coverageText = result.artifacts.find((artifact) => artifact.kind === 'coverage-text')
|
|
297
|
+
if (coverageText) {
|
|
298
|
+
console.error(readFileSync(coverageText.path, 'utf8').trimEnd())
|
|
299
|
+
}
|
|
300
|
+
console.error(`Seed: ${result.seed}`)
|
|
301
|
+
console.error(`Time: ${result.durationMs} ms`)
|
|
302
|
+
if (result.terminationReason !== 'completed') {
|
|
303
|
+
console.error(`Termination: ${result.terminationReason}`)
|
|
304
|
+
}
|
|
305
|
+
if (includeDiagnostics) {
|
|
306
|
+
for (const diagnostic of result.diagnostics) {
|
|
307
|
+
console.error(`${diagnostic.code}: ${diagnostic.message}`)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function testResultExitCode(result) {
|
|
313
|
+
if (result.terminationReason === 'cancelled' || result.terminationReason === 'watch-restart') {
|
|
314
|
+
return 130
|
|
315
|
+
}
|
|
316
|
+
if (['host-crash', 'oom', 'internal-error'].includes(result.terminationReason)) return 2
|
|
317
|
+
return result.success ? 0 : 1
|
|
318
|
+
}
|
|
319
|
+
|
|
140
320
|
function metricsFromEvent(event) {
|
|
141
321
|
return {
|
|
142
322
|
modules: event.modules,
|
|
@@ -257,6 +437,236 @@ async function runServer(factory, options, command, root, ui, uiMode) {
|
|
|
257
437
|
}
|
|
258
438
|
}
|
|
259
439
|
|
|
440
|
+
async function runTestCommand(args) {
|
|
441
|
+
try {
|
|
442
|
+
const root = takeOption(args, '--root', true)
|
|
443
|
+
const namePattern = takeOption(args, '--name-pattern', true)
|
|
444
|
+
const projects = takeOptions(args, '--project', true)
|
|
445
|
+
const environment = validateTestChoice(
|
|
446
|
+
takeOption(args, '--environment', true),
|
|
447
|
+
'--environment',
|
|
448
|
+
['auto', 'dom', 'browser'],
|
|
449
|
+
)
|
|
450
|
+
const watch = takeFlag(args, '--watch')
|
|
451
|
+
const changed = takeFlag(args, '--changed')
|
|
452
|
+
const related = takeVariadicOptions(args, '--related')
|
|
453
|
+
const coverage = takeFlag(args, '--coverage')
|
|
454
|
+
const updateSnapshots = takeFlag(args, '--update-snapshots') ? 'all' : undefined
|
|
455
|
+
const serial = takeFlag(args, '--serial')
|
|
456
|
+
const workers = parseTestWorkers(takeOption(args, '--workers', true))
|
|
457
|
+
const bail = takeTestBail(args)
|
|
458
|
+
const shard = parseTestShard(takeOption(args, '--shard', true))
|
|
459
|
+
const seed = takeOption(args, '--seed', true)
|
|
460
|
+
const shuffle = takeFlag(args, '--shuffle')
|
|
461
|
+
const reporter = validateTestChoice(
|
|
462
|
+
takeOption(args, '--reporter', true),
|
|
463
|
+
'--reporter',
|
|
464
|
+
['pretty', 'json', 'junit'],
|
|
465
|
+
)
|
|
466
|
+
const output = takeOption(args, '--output', true)
|
|
467
|
+
const allowNoTests = takeFlag(args, '--allow-no-tests')
|
|
468
|
+
const browserPath = takeOption(args, '--browser-path', true)
|
|
469
|
+
const headful = takeFlag(args, '--headful')
|
|
470
|
+
|
|
471
|
+
if (changed && related.length > 0) {
|
|
472
|
+
throw testUsageError('--changed cannot be combined with --related')
|
|
473
|
+
}
|
|
474
|
+
if (serial && workers !== undefined) {
|
|
475
|
+
throw testUsageError('--serial cannot be combined with --workers')
|
|
476
|
+
}
|
|
477
|
+
if (output && reporter === undefined) {
|
|
478
|
+
throw testUsageError('--output requires --reporter json or --reporter junit')
|
|
479
|
+
}
|
|
480
|
+
if (output && reporter === 'pretty') {
|
|
481
|
+
throw testUsageError('--output requires --reporter json or --reporter junit')
|
|
482
|
+
}
|
|
483
|
+
if (args.some((argument) => argument.startsWith('-'))) {
|
|
484
|
+
throw testUsageError(`unknown test arguments: ${args.filter((argument) => argument.startsWith('-')).join(' ')}`)
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const options = {
|
|
488
|
+
root,
|
|
489
|
+
patterns: args.splice(0),
|
|
490
|
+
namePattern,
|
|
491
|
+
projects,
|
|
492
|
+
environment,
|
|
493
|
+
watch,
|
|
494
|
+
changed,
|
|
495
|
+
related,
|
|
496
|
+
coverage,
|
|
497
|
+
updateSnapshots,
|
|
498
|
+
serial,
|
|
499
|
+
workers,
|
|
500
|
+
bail,
|
|
501
|
+
shard,
|
|
502
|
+
seed,
|
|
503
|
+
shuffle,
|
|
504
|
+
reporter,
|
|
505
|
+
output,
|
|
506
|
+
allowNoTests,
|
|
507
|
+
browserPath,
|
|
508
|
+
headful,
|
|
509
|
+
}
|
|
510
|
+
const selectedReporter = reporter || 'pretty'
|
|
511
|
+
|
|
512
|
+
if (!watch) {
|
|
513
|
+
const result = await runTests(options)
|
|
514
|
+
printTestRun(result, selectedReporter, output)
|
|
515
|
+
return testResultExitCode(result)
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const context = await createTestContext(options)
|
|
519
|
+
let lastExitCode = 0
|
|
520
|
+
let outputError
|
|
521
|
+
let finish
|
|
522
|
+
const finished = new Promise((resolve) => { finish = resolve })
|
|
523
|
+
let closing
|
|
524
|
+
const requestClose = () => {
|
|
525
|
+
closing ||= context.close().catch((error) => {
|
|
526
|
+
outputError ||= error
|
|
527
|
+
})
|
|
528
|
+
return closing
|
|
529
|
+
}
|
|
530
|
+
const finishAndClose = (reason) => {
|
|
531
|
+
finish(reason)
|
|
532
|
+
void requestClose()
|
|
533
|
+
}
|
|
534
|
+
const onSigint = () => finishAndClose('signal')
|
|
535
|
+
const onSigterm = () => finishAndClose('signal')
|
|
536
|
+
const onClosed = () => {
|
|
537
|
+
const fatalError = testContextInternal.getTestContextFatalError(context)
|
|
538
|
+
if (fatalError) {
|
|
539
|
+
outputError = fatalError
|
|
540
|
+
finish('fatal')
|
|
541
|
+
} else {
|
|
542
|
+
finish('closed')
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
process.once('SIGINT', onSigint)
|
|
546
|
+
process.once('SIGTERM', onSigterm)
|
|
547
|
+
context.once('closed', onClosed)
|
|
548
|
+
context.on('runComplete', (result) => {
|
|
549
|
+
if (result.terminationReason === 'watch-restart' || result.terminationReason === 'cancelled') {
|
|
550
|
+
return
|
|
551
|
+
}
|
|
552
|
+
lastExitCode = testResultExitCode(result)
|
|
553
|
+
try {
|
|
554
|
+
printTestRun(result, selectedReporter, output, false)
|
|
555
|
+
} catch (error) {
|
|
556
|
+
outputError = error
|
|
557
|
+
finish('output-error')
|
|
558
|
+
}
|
|
559
|
+
})
|
|
560
|
+
context.on('diagnostic', (diagnostic) => {
|
|
561
|
+
console.error(`${diagnostic.code}: ${diagnostic.message}`)
|
|
562
|
+
})
|
|
563
|
+
const stdin = process.stdin
|
|
564
|
+
const interactive = Boolean(stdin.isTTY && stdin.setRawMode)
|
|
565
|
+
const previousRaw = interactive ? Boolean(stdin.isRaw) : false
|
|
566
|
+
let prompt
|
|
567
|
+
const sendControl = (control) => {
|
|
568
|
+
try {
|
|
569
|
+
testContextInternal.sendTestWatchControl(context, control)
|
|
570
|
+
} catch (error) {
|
|
571
|
+
outputError = error
|
|
572
|
+
finish('control-error')
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
const onWatchKey = (chunk) => {
|
|
576
|
+
for (const character of chunk.toString('utf8')) {
|
|
577
|
+
if (character === '\u0003') {
|
|
578
|
+
finishAndClose('signal')
|
|
579
|
+
continue
|
|
580
|
+
}
|
|
581
|
+
if (prompt) {
|
|
582
|
+
if (character === '\r' || character === '\n') {
|
|
583
|
+
process.stdout.write('\n')
|
|
584
|
+
const value = prompt.value.trim()
|
|
585
|
+
if (value) sendControl({ type: prompt.type, pattern: value })
|
|
586
|
+
prompt = undefined
|
|
587
|
+
} else if (character === '\u001b') {
|
|
588
|
+
process.stdout.write('\n')
|
|
589
|
+
prompt = undefined
|
|
590
|
+
} else if (character === '\b' || character === '\u007f') {
|
|
591
|
+
if (prompt.value) {
|
|
592
|
+
prompt.value = prompt.value.slice(0, -1)
|
|
593
|
+
process.stdout.write('\b \b')
|
|
594
|
+
}
|
|
595
|
+
} else if (character >= ' ') {
|
|
596
|
+
prompt.value += character
|
|
597
|
+
process.stdout.write(character)
|
|
598
|
+
}
|
|
599
|
+
continue
|
|
600
|
+
}
|
|
601
|
+
switch (character) {
|
|
602
|
+
case 'a':
|
|
603
|
+
sendControl({ type: 'all' })
|
|
604
|
+
break
|
|
605
|
+
case 'f':
|
|
606
|
+
sendControl({ type: 'failed' })
|
|
607
|
+
break
|
|
608
|
+
case 'p':
|
|
609
|
+
prompt = { type: 'path', value: '' }
|
|
610
|
+
process.stdout.write('\nPath pattern: ')
|
|
611
|
+
break
|
|
612
|
+
case 't':
|
|
613
|
+
prompt = { type: 'name', value: '' }
|
|
614
|
+
process.stdout.write('\nTest name pattern: ')
|
|
615
|
+
break
|
|
616
|
+
case 'u':
|
|
617
|
+
sendControl({ type: 'updateSnapshots' })
|
|
618
|
+
break
|
|
619
|
+
case 'r':
|
|
620
|
+
sendControl({ type: 'rerun' })
|
|
621
|
+
break
|
|
622
|
+
case 'q':
|
|
623
|
+
finishAndClose('quit')
|
|
624
|
+
break
|
|
625
|
+
default:
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
try {
|
|
630
|
+
context.startWatch()
|
|
631
|
+
if (interactive) {
|
|
632
|
+
stdin.setRawMode(true)
|
|
633
|
+
stdin.resume()
|
|
634
|
+
stdin.on('data', onWatchKey)
|
|
635
|
+
console.error('Watch keys: a all · f failed · p path · t name · u snapshots · r rerun · q quit')
|
|
636
|
+
}
|
|
637
|
+
// StartWatch schedules the first host-owned run. The JS thread remains free for watch
|
|
638
|
+
// controls and cancellation even when the suite itself is an infinite loop.
|
|
639
|
+
const reason = await finished
|
|
640
|
+
if (closing) await closing
|
|
641
|
+
if (outputError) throw outputError
|
|
642
|
+
return reason === 'closed' || reason === 'quit' ? lastExitCode : 130
|
|
643
|
+
} finally {
|
|
644
|
+
process.off('SIGINT', onSigint)
|
|
645
|
+
process.off('SIGTERM', onSigterm)
|
|
646
|
+
context.off('closed', onClosed)
|
|
647
|
+
if (interactive) {
|
|
648
|
+
stdin.off('data', onWatchKey)
|
|
649
|
+
stdin.setRawMode(previousRaw)
|
|
650
|
+
if (!previousRaw) stdin.pause()
|
|
651
|
+
}
|
|
652
|
+
await (closing || context.close())
|
|
653
|
+
}
|
|
654
|
+
} catch (error) {
|
|
655
|
+
if (error && typeof error === 'object') {
|
|
656
|
+
if (error instanceof WakeError && error.code === 'WAKE_CONFIG') {
|
|
657
|
+
const mapped = new WakeError('WAKE_TEST_CONFIG', error.message, { cause: error })
|
|
658
|
+
mapped.exitCode = 2
|
|
659
|
+
throw mapped
|
|
660
|
+
}
|
|
661
|
+
error.exitCode = error.code === 'WAKE_CANCELLED' ? 130 : 2
|
|
662
|
+
throw error
|
|
663
|
+
}
|
|
664
|
+
const wrapped = new WakeError('WAKE_TEST_RUNTIME', String(error))
|
|
665
|
+
wrapped.exitCode = 2
|
|
666
|
+
throw wrapped
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
260
670
|
export async function runCli(argv = process.argv.slice(2)) {
|
|
261
671
|
const args = [...argv]
|
|
262
672
|
const noColor = takeFlag(args, '--no-color')
|
|
@@ -273,6 +683,14 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
273
683
|
}
|
|
274
684
|
|
|
275
685
|
const command = args.shift()
|
|
686
|
+
if (command === 'test') {
|
|
687
|
+
if (takeFlag(args, '--help') || takeFlag(args, '-h')) {
|
|
688
|
+
console.log(HELP)
|
|
689
|
+
return 0
|
|
690
|
+
}
|
|
691
|
+
ensureStaticMode(uiMode)
|
|
692
|
+
return runTestCommand(args)
|
|
693
|
+
}
|
|
276
694
|
if (command === 'build') {
|
|
277
695
|
ensureStaticMode(uiMode)
|
|
278
696
|
const options = commonOptions(args)
|