@crab-dev/wake 0.1.20 → 0.1.22
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 +27 -0
- package/bin/terminal.mjs +42 -4
- package/bin/wake.mjs +424 -0
- package/index.cjs +199 -0
- package/index.d.ts +278 -0
- package/index.mjs +3 -0
- package/loader.cjs +19 -2
- package/package.json +25 -9
- 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,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.22
|
|
4
|
+
|
|
5
|
+
- Added the experimental Wake-native, React-first `wake test` system, `runTests()` and `TestContext`
|
|
6
|
+
Node APIs, explicit `@crab-dev/wake/test` ESM/CommonJS imports, isolated V8 suite realms, Wake
|
|
7
|
+
snapshots, function/network mocks, an async clock, projects, sharding, and a token-authenticated
|
|
8
|
+
crash-isolated test host. ADR 0020 defines the private fast-DOM and system-Chromium boundary;
|
|
9
|
+
external runner/config/plugin/result compatibility is intentionally outside the contract.
|
|
10
|
+
- Added typed Chromium input and browser screenshot snapshots. `toMatchScreenshot()` stores
|
|
11
|
+
rendering-profiled PNG baselines and emits received PNG plus self-contained visual-diff artifacts
|
|
12
|
+
without exposing raw CDP or adding a second browser event consumer.
|
|
13
|
+
|
|
14
|
+
## 0.1.21
|
|
15
|
+
|
|
16
|
+
- Added aggregate Wake Docs workspaces with isolated production bundles, deterministic manifests,
|
|
17
|
+
transactional output commits, one-port lazy/eager development mounts, scoped HMR events, and
|
|
18
|
+
embedded or standalone workbench presentation.
|
|
19
|
+
- Fixed MDX JSX structure generation by lifting block JSX and splitting mixed paragraphs at AST
|
|
20
|
+
boundaries, preventing invalid paragraph/block nesting without changing inline JSX or component
|
|
21
|
+
Markdown children.
|
|
22
|
+
- Fixed constrained, defaulted, and comma-disambiguated generic arrow functions in TSX so Wake
|
|
23
|
+
parses them as TypeScript instead of emitting cascading JSX diagnostics.
|
|
24
|
+
- Added a TypeScript 7 compatibility gate covering strict type checking, TS/TSX erasure, module
|
|
25
|
+
extensions, value transforms, and production/development JSX runtime execution.
|
|
26
|
+
- Fixed top-level TypeScript overload signatures so only their implementation is emitted, and
|
|
27
|
+
removed false runtime dependencies from imports and exports containing only inline `type`
|
|
28
|
+
specifiers.
|
|
29
|
+
|
|
3
30
|
## 0.1.20
|
|
4
31
|
|
|
5
32
|
- Added a modern interactive terminal console with editable commands, history, Unicode-aware selection, clipboard copy and paste, and safe development-server opening across Rust and npm CLIs.
|
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')
|
|
@@ -267,6 +283,7 @@ export function createDashboardState({
|
|
|
267
283
|
rebuilds: 0,
|
|
268
284
|
startedAt: Date.now(),
|
|
269
285
|
activity: [],
|
|
286
|
+
workspaceState: undefined,
|
|
270
287
|
scrollFromBottom: 0,
|
|
271
288
|
}
|
|
272
289
|
pushActivity(state, 'info', 'Starting Wake…')
|
|
@@ -307,6 +324,17 @@ export function applyDashboardEvent(state, event) {
|
|
|
307
324
|
} else if (event.type === 'diagnostic') {
|
|
308
325
|
state.status = 'error'
|
|
309
326
|
pushActivity(state, 'error', formatDiagnostic(createUi(false), event.diagnostic).join('\n'))
|
|
327
|
+
} else if (event.type === 'workspaceState') {
|
|
328
|
+
state.workspaceState = {
|
|
329
|
+
total: event.total,
|
|
330
|
+
loaded: event.loaded,
|
|
331
|
+
failed: event.failed,
|
|
332
|
+
current: event.current,
|
|
333
|
+
failedNames: event.failedNames || [],
|
|
334
|
+
}
|
|
335
|
+
if (event.current) {
|
|
336
|
+
pushActivity(state, 'info', `Loading workspace ${event.current}…`)
|
|
337
|
+
}
|
|
310
338
|
} else if (event.type === 'closed') {
|
|
311
339
|
state.status = 'stopped'
|
|
312
340
|
pushActivity(state, 'info', 'Wake stopped')
|
|
@@ -412,6 +440,14 @@ function activityRows(state, available) {
|
|
|
412
440
|
return rows.slice(start, end)
|
|
413
441
|
}
|
|
414
442
|
|
|
443
|
+
function workspaceText(state) {
|
|
444
|
+
const workspaces = state.workspaceState
|
|
445
|
+
if (!workspaces) return undefined
|
|
446
|
+
const current = workspaces.current ? ` · loading ${workspaces.current}` : ''
|
|
447
|
+
const failed = workspaces.failed ? ` · ${workspaces.failed} failed` : ''
|
|
448
|
+
return `WORKSPACES ${workspaces.loaded}/${workspaces.total} loaded${failed}${current}`
|
|
449
|
+
}
|
|
450
|
+
|
|
415
451
|
function activityRowCount(state) {
|
|
416
452
|
return state.activity.reduce((count, item) => count + String(item.message).split('\n').length, 0)
|
|
417
453
|
}
|
|
@@ -442,6 +478,7 @@ function plainFrame(state, width, height, editor = new InputEditor(), notice) {
|
|
|
442
478
|
boxLine(metricsText(state), width),
|
|
443
479
|
separator(width, 'ACTIVITY'),
|
|
444
480
|
]
|
|
481
|
+
if (workspaceText(state)) fixed.splice(-1, 0, boxLine(workspaceText(state), width))
|
|
445
482
|
activityHeight = Math.max(1, height - fixed.length - 2)
|
|
446
483
|
} else {
|
|
447
484
|
fixed = [
|
|
@@ -455,6 +492,7 @@ function plainFrame(state, width, height, editor = new InputEditor(), notice) {
|
|
|
455
492
|
boxLine(metricsText(state), width),
|
|
456
493
|
separator(width, 'ACTIVITY'),
|
|
457
494
|
]
|
|
495
|
+
if (workspaceText(state)) fixed.splice(-1, 0, boxLine(workspaceText(state), width))
|
|
458
496
|
activityHeight = Math.max(1, height - fixed.length - 2)
|
|
459
497
|
}
|
|
460
498
|
|
|
@@ -528,8 +566,8 @@ export function createDashboardSession(
|
|
|
528
566
|
input = process.stdin,
|
|
529
567
|
output = process.stderr,
|
|
530
568
|
ui = createUi(),
|
|
531
|
-
clipboardAdapter =
|
|
532
|
-
openUrl =
|
|
569
|
+
clipboardAdapter = defaultClipboard,
|
|
570
|
+
openUrl = defaultOpenUrl,
|
|
533
571
|
env = process.env,
|
|
534
572
|
} = {},
|
|
535
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,
|
|
@@ -187,6 +367,10 @@ async function runServer(factory, options, command, root, ui, uiMode) {
|
|
|
187
367
|
applyDashboardEvent(state, { type: 'diagnostic', diagnostic })
|
|
188
368
|
dashboard?.draw()
|
|
189
369
|
}
|
|
370
|
+
const onWorkspaceState = (event) => {
|
|
371
|
+
applyDashboardEvent(state, event)
|
|
372
|
+
dashboard?.draw()
|
|
373
|
+
}
|
|
190
374
|
const onClosed = () => {
|
|
191
375
|
applyDashboardEvent(state, { type: 'closed' })
|
|
192
376
|
dashboard?.draw()
|
|
@@ -194,6 +378,7 @@ async function runServer(factory, options, command, root, ui, uiMode) {
|
|
|
194
378
|
server.on('rebuildStart', onRebuildStart)
|
|
195
379
|
server.on('rebuilt', onRebuilt)
|
|
196
380
|
server.on('diagnostic', onDiagnostic)
|
|
381
|
+
server.on('workspaceState', onWorkspaceState)
|
|
197
382
|
server.on('closed', onClosed)
|
|
198
383
|
|
|
199
384
|
if (!useTui) stopObserving = observeServer(server, ui)
|
|
@@ -235,6 +420,7 @@ async function runServer(factory, options, command, root, ui, uiMode) {
|
|
|
235
420
|
server.off('rebuildStart', onRebuildStart)
|
|
236
421
|
server.off('rebuilt', onRebuilt)
|
|
237
422
|
server.off('diagnostic', onDiagnostic)
|
|
423
|
+
server.off('workspaceState', onWorkspaceState)
|
|
238
424
|
server.off('closed', onClosed)
|
|
239
425
|
}
|
|
240
426
|
} catch (error) {
|
|
@@ -251,6 +437,236 @@ async function runServer(factory, options, command, root, ui, uiMode) {
|
|
|
251
437
|
}
|
|
252
438
|
}
|
|
253
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
|
+
|
|
254
670
|
export async function runCli(argv = process.argv.slice(2)) {
|
|
255
671
|
const args = [...argv]
|
|
256
672
|
const noColor = takeFlag(args, '--no-color')
|
|
@@ -267,6 +683,14 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
267
683
|
}
|
|
268
684
|
|
|
269
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
|
+
}
|
|
270
694
|
if (command === 'build') {
|
|
271
695
|
ensureStaticMode(uiMode)
|
|
272
696
|
const options = commonOptions(args)
|