@thegetty/quire-cli 1.0.0-rc.40 → 1.0.0-rc.42
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/bin/cli.js +14 -0
- package/package.json +1 -1
- package/src/commands/config.js +6 -21
- package/src/commands/config.test.js +109 -20
- package/src/errors/help/help-topic-not-found-error.js +12 -4
- package/src/errors/index.js +5 -1
- package/src/errors/validation/index.js +3 -0
- package/src/errors/validation/unknown-config-key-error.js +27 -0
- package/src/errors/validation/unknown-config-operation-error.js +24 -0
- package/src/helpers/suggest-similar.js +124 -0
- package/src/helpers/suggest-similar.test.js +108 -0
- package/src/lib/commander/index.js +1 -0
- package/src/lib/commander/options.js +18 -0
- package/src/lib/commander/options.test.js +52 -1
- package/src/lib/conf/defaults.js +7 -0
- package/src/lib/conf/schema.js +4 -0
- package/src/lib/help/index.js +12 -1
- package/src/lib/help/index.test.js +29 -1
- package/src/lib/reporter/index.js +96 -1
- package/src/lib/reporter/index.test.js +269 -0
- package/src/main.js +24 -4
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import test from 'ava'
|
|
2
2
|
import { Command } from 'commander'
|
|
3
3
|
import { arrayToOption } from './index.js'
|
|
4
|
-
import { colorOption, noColorOption, quietOption, verboseOption, debugOption } from './options.js'
|
|
4
|
+
import { colorOption, noColorOption, quietOption, verboseOption, debugOption, reducedMotionOption } from './options.js'
|
|
5
5
|
|
|
6
6
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
7
7
|
// Integration tests for option conflicts
|
|
@@ -21,6 +21,7 @@ function createTestProgram() {
|
|
|
21
21
|
.addOption(arrayToOption(quietOption))
|
|
22
22
|
.addOption(arrayToOption(verboseOption))
|
|
23
23
|
.addOption(arrayToOption(debugOption))
|
|
24
|
+
.addOption(arrayToOption(reducedMotionOption))
|
|
24
25
|
.action(() => {})
|
|
25
26
|
.exitOverride() // Throw instead of process.exit
|
|
26
27
|
.configureOutput({
|
|
@@ -110,6 +111,56 @@ test('options order does not affect conflict detection', (t) => {
|
|
|
110
111
|
t.throws(() => program2.parse(['node', 'test', '--verbose', '--quiet']))
|
|
111
112
|
})
|
|
112
113
|
|
|
114
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
115
|
+
// Reduced motion option tests
|
|
116
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
test('--reduced-motion alone is valid', (t) => {
|
|
119
|
+
const program = createTestProgram()
|
|
120
|
+
|
|
121
|
+
t.notThrows(() => {
|
|
122
|
+
program.parse(['node', 'test', '--reduced-motion'])
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('--reduced-motion sets opts.reducedMotion to true', (t) => {
|
|
127
|
+
const program = createTestProgram()
|
|
128
|
+
program.parse(['node', 'test', '--reduced-motion'])
|
|
129
|
+
|
|
130
|
+
t.is(program.opts().reducedMotion, true)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test('no flag leaves opts.reducedMotion as undefined', (t) => {
|
|
134
|
+
const program = createTestProgram()
|
|
135
|
+
program.parse(['node', 'test'])
|
|
136
|
+
|
|
137
|
+
t.is(program.opts().reducedMotion, undefined)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('--reduced-motion combines with --verbose without conflict', (t) => {
|
|
141
|
+
const program = createTestProgram()
|
|
142
|
+
|
|
143
|
+
t.notThrows(() => {
|
|
144
|
+
program.parse(['node', 'test', '--reduced-motion', '--verbose'])
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test('--reduced-motion combines with --quiet without conflict', (t) => {
|
|
149
|
+
const program = createTestProgram()
|
|
150
|
+
|
|
151
|
+
t.notThrows(() => {
|
|
152
|
+
program.parse(['node', 'test', '--reduced-motion', '--quiet'])
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test('--reduced-motion combines with --debug without conflict', (t) => {
|
|
157
|
+
const program = createTestProgram()
|
|
158
|
+
|
|
159
|
+
t.notThrows(() => {
|
|
160
|
+
program.parse(['node', 'test', '--reduced-motion', '--debug'])
|
|
161
|
+
})
|
|
162
|
+
})
|
|
163
|
+
|
|
113
164
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
114
165
|
// Color option tests
|
|
115
166
|
// ─────────────────────────────────────────────────────────────────────────────
|
package/src/lib/conf/defaults.js
CHANGED
|
@@ -48,6 +48,13 @@ export default {
|
|
|
48
48
|
* Default PDF engine for quire pdf command.
|
|
49
49
|
*/
|
|
50
50
|
pdfEngine: 'pagedjs',
|
|
51
|
+
/**
|
|
52
|
+
* Disable spinner animation and line overwriting.
|
|
53
|
+
* When enabled, progress output uses static text instead of animated spinners,
|
|
54
|
+
* and each stage prints on a new line rather than overwriting the current line.
|
|
55
|
+
* Can be overridden per-command with --reduced-motion.
|
|
56
|
+
*/
|
|
57
|
+
reducedMotion: false,
|
|
51
58
|
/**
|
|
52
59
|
* Project starter template to use when creating new projects.
|
|
53
60
|
*/
|
package/src/lib/conf/schema.js
CHANGED
|
@@ -41,6 +41,10 @@ export default {
|
|
|
41
41
|
type: 'boolean',
|
|
42
42
|
description: 'Color message text by log level (e.g., red for errors). Requires logUseColor'
|
|
43
43
|
},
|
|
44
|
+
reducedMotion: {
|
|
45
|
+
type: 'boolean',
|
|
46
|
+
description: 'Disable spinner animation and line overwriting (for screen readers and reduced-motion preferences)'
|
|
47
|
+
},
|
|
44
48
|
projectTemplate: {
|
|
45
49
|
type: 'string',
|
|
46
50
|
format: 'uri',
|
package/src/lib/help/index.js
CHANGED
|
@@ -59,6 +59,16 @@ async function listTopics() {
|
|
|
59
59
|
return topics.sort((a, b) => a.name.localeCompare(b.name))
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Get available topic names from the topics directory
|
|
64
|
+
* @returns {Promise<string[]>} Topic names (filenames without .md)
|
|
65
|
+
*/
|
|
66
|
+
async function getTopicNames() {
|
|
67
|
+
if (!await fs.pathExists(TOPICS_DIR)) return []
|
|
68
|
+
const files = await fs.readdir(TOPICS_DIR)
|
|
69
|
+
return files.filter((f) => f.endsWith('.md')).map((f) => path.basename(f, '.md'))
|
|
70
|
+
}
|
|
71
|
+
|
|
62
72
|
/**
|
|
63
73
|
* Load a help topic by name
|
|
64
74
|
* @param {string} name - Topic name (without .md extension)
|
|
@@ -69,7 +79,8 @@ async function loadTopic(name) {
|
|
|
69
79
|
const filePath = path.join(TOPICS_DIR, `${name}.md`)
|
|
70
80
|
|
|
71
81
|
if (!await fs.pathExists(filePath)) {
|
|
72
|
-
|
|
82
|
+
const validTopics = await getTopicNames()
|
|
83
|
+
throw new HelpTopicNotFoundError(name, validTopics)
|
|
73
84
|
}
|
|
74
85
|
|
|
75
86
|
const content = await fs.readFile(filePath, 'utf-8')
|
|
@@ -134,7 +134,10 @@ test('getTopicContent() throws HelpTopicNotFoundError for missing topic', async
|
|
|
134
134
|
const { sandbox } = t.context
|
|
135
135
|
|
|
136
136
|
const mockFs = {
|
|
137
|
-
pathExists: sandbox.stub()
|
|
137
|
+
pathExists: sandbox.stub()
|
|
138
|
+
.onFirstCall().resolves(false) // topic file does not exist
|
|
139
|
+
.onSecondCall().resolves(true), // topics directory exists
|
|
140
|
+
readdir: sandbox.stub().resolves(['epub.md', 'pdf.md', 'debugging.md'])
|
|
138
141
|
}
|
|
139
142
|
|
|
140
143
|
const { getTopicContent } = await esmock('./index.js', {
|
|
@@ -150,6 +153,31 @@ test('getTopicContent() throws HelpTopicNotFoundError for missing topic', async
|
|
|
150
153
|
t.is(error.topic, 'nonexistent')
|
|
151
154
|
})
|
|
152
155
|
|
|
156
|
+
test('getTopicContent() suggests similar topic for typo', async (t) => {
|
|
157
|
+
const { sandbox } = t.context
|
|
158
|
+
|
|
159
|
+
const mockFs = {
|
|
160
|
+
pathExists: sandbox.stub()
|
|
161
|
+
.onFirstCall().resolves(false)
|
|
162
|
+
.onSecondCall().resolves(true),
|
|
163
|
+
readdir: sandbox.stub().resolves([
|
|
164
|
+
'configuration.md', 'debugging.md', 'epub.md',
|
|
165
|
+
'pdf.md', 'publishing.md', 'workflows.md'
|
|
166
|
+
])
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const { getTopicContent } = await esmock('./index.js', {
|
|
170
|
+
'fs-extra': mockFs
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
const error = await t.throwsAsync(
|
|
174
|
+
() => getTopicContent('edub'),
|
|
175
|
+
{ name: 'HelpTopicNotFoundError' }
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
t.is(error.suggestion, 'Did you mean: epub?')
|
|
179
|
+
})
|
|
180
|
+
|
|
153
181
|
test('getTopicsDir() returns the topics directory path', async (t) => {
|
|
154
182
|
const { getTopicsDir } = await import('./index.js')
|
|
155
183
|
|
|
@@ -14,14 +14,27 @@
|
|
|
14
14
|
* | Default | (none) | Show spinner with basic status |
|
|
15
15
|
* | Quiet | `-q, --quiet` | Suppress all output (for CI/scripts) |
|
|
16
16
|
* | Verbose | `-v, --verbose` | Show detailed progress with paths, timing |
|
|
17
|
+
* | Reduced Motion | `--reduced-motion` | Static text, no animation, no line overwriting |
|
|
17
18
|
*
|
|
18
19
|
* Note: `--debug` is handled separately and enables DEBUG namespace logging,
|
|
19
20
|
* not reporter output. Use verbose mode for detailed user-facing output.
|
|
20
21
|
*
|
|
22
|
+
* ## Reduced Motion
|
|
23
|
+
*
|
|
24
|
+
* When reduced motion is enabled (via `--reduced-motion` flag, `REDUCED_MOTION`
|
|
25
|
+
* environment variable, or `reducedMotion` config setting), the reporter:
|
|
26
|
+
* - Prints static text instead of animated spinners
|
|
27
|
+
* - Outputs each stage on a new line (no line overwriting)
|
|
28
|
+
* - Still shows status symbols (✔ ✖ ⚠ ℹ) for completion states
|
|
29
|
+
*
|
|
30
|
+
* This mode is designed for screen reader users and environments where
|
|
31
|
+
* terminal animation is problematic.
|
|
32
|
+
*
|
|
21
33
|
* ## Config Defaults
|
|
22
34
|
*
|
|
23
35
|
* Users can set default values via `quire settings`:
|
|
24
36
|
* - `quire settings set verbose true` - Always run in verbose mode
|
|
37
|
+
* - `quire settings set reducedMotion true` - Disable spinner animation
|
|
25
38
|
*
|
|
26
39
|
* CLI flags override config settings. Use `--no-verbose` to disable verbose
|
|
27
40
|
* mode even when enabled in config.
|
|
@@ -81,11 +94,27 @@ import config from '#lib/conf/config.js'
|
|
|
81
94
|
|
|
82
95
|
const debug = createDebug('lib:reporter')
|
|
83
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Status symbols for reduced motion output
|
|
99
|
+
*
|
|
100
|
+
* These match the symbols used by ora for visual consistency,
|
|
101
|
+
* ensuring the same information is conveyed with or without animation.
|
|
102
|
+
* @private
|
|
103
|
+
*/
|
|
104
|
+
const STATUS_SYMBOLS = {
|
|
105
|
+
start: '–',
|
|
106
|
+
succeed: '✔',
|
|
107
|
+
fail: '✖',
|
|
108
|
+
warn: '⚠',
|
|
109
|
+
info: 'ℹ',
|
|
110
|
+
}
|
|
111
|
+
|
|
84
112
|
/**
|
|
85
113
|
* Reporter class for CLI progress feedback
|
|
86
114
|
*
|
|
87
115
|
* Wraps ora spinner with additional features:
|
|
88
116
|
* - Respects --quiet and --json flags
|
|
117
|
+
* - Supports reduced motion (static text, no line overwriting)
|
|
89
118
|
* - Supports elapsed time display
|
|
90
119
|
* - Provides consistent API across commands
|
|
91
120
|
*/
|
|
@@ -111,6 +140,22 @@ class Reporter {
|
|
|
111
140
|
/** @type {string} */
|
|
112
141
|
#baseText = ''
|
|
113
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Check if reduced motion mode is active
|
|
145
|
+
*
|
|
146
|
+
* Reduced motion disables spinner animation and line overwriting.
|
|
147
|
+
* Reads from the REDUCED_MOTION environment variable, which is set by:
|
|
148
|
+
* - `--reduced-motion` CLI flag (via preAction hook)
|
|
149
|
+
* - `REDUCED_MOTION=1` shell environment variable
|
|
150
|
+
* - `reducedMotion: true` config setting (via bin/cli.js)
|
|
151
|
+
*
|
|
152
|
+
* @private
|
|
153
|
+
* @returns {boolean}
|
|
154
|
+
*/
|
|
155
|
+
#isReducedMotion() {
|
|
156
|
+
return Boolean(process.env.REDUCED_MOTION)
|
|
157
|
+
}
|
|
158
|
+
|
|
114
159
|
/**
|
|
115
160
|
* Configure reporter for current command context
|
|
116
161
|
*
|
|
@@ -134,7 +179,8 @@ class Reporter {
|
|
|
134
179
|
this.#json = options.json || false
|
|
135
180
|
// CLI flag takes precedence, then config setting, then default false
|
|
136
181
|
this.#verbose = options.verbose ?? config.get('verbose') ?? false
|
|
137
|
-
debug('configured: quiet=%s, json=%s, verbose=%s
|
|
182
|
+
debug('configured: quiet=%s, json=%s, verbose=%s, reducedMotion=%s',
|
|
183
|
+
this.#quiet, this.#json, this.#verbose, this.#isReducedMotion())
|
|
138
184
|
return this
|
|
139
185
|
}
|
|
140
186
|
|
|
@@ -175,6 +221,13 @@ class Reporter {
|
|
|
175
221
|
return this
|
|
176
222
|
}
|
|
177
223
|
|
|
224
|
+
// Reduced motion: print static text on a new line, no spinner
|
|
225
|
+
if (this.#isReducedMotion()) {
|
|
226
|
+
debug('spinner start (reduced motion): %s', text)
|
|
227
|
+
console.log(`${STATUS_SYMBOLS.start} ${text}`)
|
|
228
|
+
return this
|
|
229
|
+
}
|
|
230
|
+
|
|
178
231
|
debug('spinner start: %s', text)
|
|
179
232
|
this.#spinner = ora({
|
|
180
233
|
text,
|
|
@@ -235,6 +288,13 @@ class Reporter {
|
|
|
235
288
|
return this
|
|
236
289
|
}
|
|
237
290
|
|
|
291
|
+
// Reduced motion: print new line instead of overwriting
|
|
292
|
+
if (this.#isReducedMotion()) {
|
|
293
|
+
debug('spinner update (reduced motion): %s', text)
|
|
294
|
+
console.log(`${STATUS_SYMBOLS.start} ${text}`)
|
|
295
|
+
return this
|
|
296
|
+
}
|
|
297
|
+
|
|
238
298
|
if (this.#spinner) {
|
|
239
299
|
debug('spinner update: %s', text)
|
|
240
300
|
this.#spinner.text = text
|
|
@@ -273,6 +333,13 @@ class Reporter {
|
|
|
273
333
|
return this
|
|
274
334
|
}
|
|
275
335
|
|
|
336
|
+
// Reduced motion: print static success line
|
|
337
|
+
if (this.#isReducedMotion()) {
|
|
338
|
+
debug('spinner succeed (reduced motion): %s', message)
|
|
339
|
+
console.log(`${STATUS_SYMBOLS.succeed} ${message}`)
|
|
340
|
+
return this
|
|
341
|
+
}
|
|
342
|
+
|
|
276
343
|
if (this.#spinner) {
|
|
277
344
|
debug('spinner succeed: %s', message)
|
|
278
345
|
this.#spinner.succeed(message)
|
|
@@ -309,6 +376,13 @@ class Reporter {
|
|
|
309
376
|
return this
|
|
310
377
|
}
|
|
311
378
|
|
|
379
|
+
// Reduced motion: print static failure line
|
|
380
|
+
if (this.#isReducedMotion()) {
|
|
381
|
+
debug('spinner fail (reduced motion): %s', message)
|
|
382
|
+
console.log(`${STATUS_SYMBOLS.fail} ${message}`)
|
|
383
|
+
return this
|
|
384
|
+
}
|
|
385
|
+
|
|
312
386
|
if (this.#spinner) {
|
|
313
387
|
debug('spinner fail: %s', message)
|
|
314
388
|
this.#spinner.fail(message)
|
|
@@ -343,6 +417,13 @@ class Reporter {
|
|
|
343
417
|
return this
|
|
344
418
|
}
|
|
345
419
|
|
|
420
|
+
// Reduced motion: print static warning line
|
|
421
|
+
if (this.#isReducedMotion()) {
|
|
422
|
+
debug('spinner warn (reduced motion): %s', message)
|
|
423
|
+
console.log(`${STATUS_SYMBOLS.warn} ${message}`)
|
|
424
|
+
return this
|
|
425
|
+
}
|
|
426
|
+
|
|
346
427
|
if (this.#spinner) {
|
|
347
428
|
debug('spinner warn: %s', message)
|
|
348
429
|
this.#spinner.warn(message)
|
|
@@ -370,6 +451,13 @@ class Reporter {
|
|
|
370
451
|
return this
|
|
371
452
|
}
|
|
372
453
|
|
|
454
|
+
// Reduced motion: print static info line
|
|
455
|
+
if (this.#isReducedMotion()) {
|
|
456
|
+
debug('spinner info (reduced motion): %s', text)
|
|
457
|
+
console.log(`${STATUS_SYMBOLS.info} ${text}`)
|
|
458
|
+
return this
|
|
459
|
+
}
|
|
460
|
+
|
|
373
461
|
if (this.#spinner) {
|
|
374
462
|
debug('spinner info: %s', text)
|
|
375
463
|
this.#spinner.info(text)
|
|
@@ -405,6 +493,13 @@ class Reporter {
|
|
|
405
493
|
return this
|
|
406
494
|
}
|
|
407
495
|
|
|
496
|
+
// Reduced motion: print detail directly (no spinner to stop/restart)
|
|
497
|
+
if (this.#isReducedMotion()) {
|
|
498
|
+
console.log(` ${text}`)
|
|
499
|
+
debug('detail (reduced motion): %s', text)
|
|
500
|
+
return this
|
|
501
|
+
}
|
|
502
|
+
|
|
408
503
|
// Print detail with indentation below spinner
|
|
409
504
|
if (this.#spinner) {
|
|
410
505
|
// Temporarily stop spinner to print detail
|
|
@@ -591,3 +591,272 @@ test('multi-phase workflow with different outcomes', (t) => {
|
|
|
591
591
|
|
|
592
592
|
t.pass()
|
|
593
593
|
})
|
|
594
|
+
|
|
595
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
596
|
+
// Reduced motion tests
|
|
597
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
598
|
+
|
|
599
|
+
test.serial('reduced motion: start() does not create ora spinner', async (t) => {
|
|
600
|
+
const { sandbox } = t.context
|
|
601
|
+
const mockOra = sandbox.stub()
|
|
602
|
+
|
|
603
|
+
const { Reporter: MockedReporter } = await esmock('./index.js', {
|
|
604
|
+
ora: { default: mockOra },
|
|
605
|
+
})
|
|
606
|
+
|
|
607
|
+
const originalIsTTY = process.stdout.isTTY
|
|
608
|
+
const originalReduceMotion = process.env.REDUCED_MOTION
|
|
609
|
+
const consoleLog = sandbox.stub(console, 'log')
|
|
610
|
+
|
|
611
|
+
try {
|
|
612
|
+
Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true })
|
|
613
|
+
process.env.REDUCED_MOTION = '1'
|
|
614
|
+
|
|
615
|
+
const rep = new MockedReporter()
|
|
616
|
+
rep.start('Building...')
|
|
617
|
+
|
|
618
|
+
t.false(mockOra.called, 'ora should not be called in reduced motion mode')
|
|
619
|
+
t.true(consoleLog.calledOnce, 'console.log should be called once')
|
|
620
|
+
t.true(consoleLog.firstCall.args[0].includes('Building...'),
|
|
621
|
+
'output should include the text')
|
|
622
|
+
} finally {
|
|
623
|
+
Object.defineProperty(process.stdout, 'isTTY', { value: originalIsTTY, configurable: true })
|
|
624
|
+
if (originalReduceMotion === undefined) {
|
|
625
|
+
delete process.env.REDUCED_MOTION
|
|
626
|
+
} else {
|
|
627
|
+
process.env.REDUCED_MOTION = originalReduceMotion
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
})
|
|
631
|
+
|
|
632
|
+
test.serial('reduced motion: update() prints new line instead of overwriting', async (t) => {
|
|
633
|
+
const { sandbox } = t.context
|
|
634
|
+
const mockOra = sandbox.stub()
|
|
635
|
+
|
|
636
|
+
const { Reporter: MockedReporter } = await esmock('./index.js', {
|
|
637
|
+
ora: { default: mockOra },
|
|
638
|
+
})
|
|
639
|
+
|
|
640
|
+
const originalIsTTY = process.stdout.isTTY
|
|
641
|
+
const originalReduceMotion = process.env.REDUCED_MOTION
|
|
642
|
+
const consoleLog = sandbox.stub(console, 'log')
|
|
643
|
+
|
|
644
|
+
try {
|
|
645
|
+
Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true })
|
|
646
|
+
process.env.REDUCED_MOTION = '1'
|
|
647
|
+
|
|
648
|
+
const rep = new MockedReporter()
|
|
649
|
+
rep.start('Step 1...')
|
|
650
|
+
rep.update('Step 2...')
|
|
651
|
+
|
|
652
|
+
t.is(consoleLog.callCount, 2, 'should print two separate lines')
|
|
653
|
+
t.true(consoleLog.firstCall.args[0].includes('Step 1...'))
|
|
654
|
+
t.true(consoleLog.secondCall.args[0].includes('Step 2...'))
|
|
655
|
+
} finally {
|
|
656
|
+
Object.defineProperty(process.stdout, 'isTTY', { value: originalIsTTY, configurable: true })
|
|
657
|
+
if (originalReduceMotion === undefined) {
|
|
658
|
+
delete process.env.REDUCED_MOTION
|
|
659
|
+
} else {
|
|
660
|
+
process.env.REDUCED_MOTION = originalReduceMotion
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
})
|
|
664
|
+
|
|
665
|
+
test.serial('reduced motion: succeed() prints static success line', async (t) => {
|
|
666
|
+
const { sandbox } = t.context
|
|
667
|
+
const mockOra = sandbox.stub()
|
|
668
|
+
|
|
669
|
+
const { Reporter: MockedReporter } = await esmock('./index.js', {
|
|
670
|
+
ora: { default: mockOra },
|
|
671
|
+
})
|
|
672
|
+
|
|
673
|
+
const originalIsTTY = process.stdout.isTTY
|
|
674
|
+
const originalReduceMotion = process.env.REDUCED_MOTION
|
|
675
|
+
const consoleLog = sandbox.stub(console, 'log')
|
|
676
|
+
|
|
677
|
+
try {
|
|
678
|
+
Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true })
|
|
679
|
+
process.env.REDUCED_MOTION = '1'
|
|
680
|
+
|
|
681
|
+
const rep = new MockedReporter()
|
|
682
|
+
rep.start('Building...')
|
|
683
|
+
rep.succeed('Build complete')
|
|
684
|
+
|
|
685
|
+
t.is(consoleLog.callCount, 2)
|
|
686
|
+
// Second call is succeed
|
|
687
|
+
t.true(consoleLog.secondCall.args[0].includes('✔'))
|
|
688
|
+
t.true(consoleLog.secondCall.args[0].includes('Build complete'))
|
|
689
|
+
} finally {
|
|
690
|
+
Object.defineProperty(process.stdout, 'isTTY', { value: originalIsTTY, configurable: true })
|
|
691
|
+
if (originalReduceMotion === undefined) {
|
|
692
|
+
delete process.env.REDUCED_MOTION
|
|
693
|
+
} else {
|
|
694
|
+
process.env.REDUCED_MOTION = originalReduceMotion
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
})
|
|
698
|
+
|
|
699
|
+
test.serial('reduced motion: fail() prints static failure line', async (t) => {
|
|
700
|
+
const { sandbox } = t.context
|
|
701
|
+
const mockOra = sandbox.stub()
|
|
702
|
+
|
|
703
|
+
const { Reporter: MockedReporter } = await esmock('./index.js', {
|
|
704
|
+
ora: { default: mockOra },
|
|
705
|
+
})
|
|
706
|
+
|
|
707
|
+
const originalIsTTY = process.stdout.isTTY
|
|
708
|
+
const originalReduceMotion = process.env.REDUCED_MOTION
|
|
709
|
+
const consoleLog = sandbox.stub(console, 'log')
|
|
710
|
+
|
|
711
|
+
try {
|
|
712
|
+
Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true })
|
|
713
|
+
process.env.REDUCED_MOTION = '1'
|
|
714
|
+
|
|
715
|
+
const rep = new MockedReporter()
|
|
716
|
+
rep.start('Generating PDF...')
|
|
717
|
+
rep.fail('PDF generation failed')
|
|
718
|
+
|
|
719
|
+
t.is(consoleLog.callCount, 2)
|
|
720
|
+
t.true(consoleLog.secondCall.args[0].includes('✖'))
|
|
721
|
+
t.true(consoleLog.secondCall.args[0].includes('PDF generation failed'))
|
|
722
|
+
} finally {
|
|
723
|
+
Object.defineProperty(process.stdout, 'isTTY', { value: originalIsTTY, configurable: true })
|
|
724
|
+
if (originalReduceMotion === undefined) {
|
|
725
|
+
delete process.env.REDUCED_MOTION
|
|
726
|
+
} else {
|
|
727
|
+
process.env.REDUCED_MOTION = originalReduceMotion
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
})
|
|
731
|
+
|
|
732
|
+
test.serial('reduced motion: warn() prints static warning line', async (t) => {
|
|
733
|
+
const { sandbox } = t.context
|
|
734
|
+
const mockOra = sandbox.stub()
|
|
735
|
+
|
|
736
|
+
const { Reporter: MockedReporter } = await esmock('./index.js', {
|
|
737
|
+
ora: { default: mockOra },
|
|
738
|
+
})
|
|
739
|
+
|
|
740
|
+
const originalIsTTY = process.stdout.isTTY
|
|
741
|
+
const originalReduceMotion = process.env.REDUCED_MOTION
|
|
742
|
+
const consoleLog = sandbox.stub(console, 'log')
|
|
743
|
+
|
|
744
|
+
try {
|
|
745
|
+
Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true })
|
|
746
|
+
process.env.REDUCED_MOTION = '1'
|
|
747
|
+
|
|
748
|
+
const rep = new MockedReporter()
|
|
749
|
+
rep.start('Checking...')
|
|
750
|
+
rep.warn('Dependencies outdated')
|
|
751
|
+
|
|
752
|
+
t.is(consoleLog.callCount, 2)
|
|
753
|
+
t.true(consoleLog.secondCall.args[0].includes('⚠'))
|
|
754
|
+
t.true(consoleLog.secondCall.args[0].includes('Dependencies outdated'))
|
|
755
|
+
} finally {
|
|
756
|
+
Object.defineProperty(process.stdout, 'isTTY', { value: originalIsTTY, configurable: true })
|
|
757
|
+
if (originalReduceMotion === undefined) {
|
|
758
|
+
delete process.env.REDUCED_MOTION
|
|
759
|
+
} else {
|
|
760
|
+
process.env.REDUCED_MOTION = originalReduceMotion
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
})
|
|
764
|
+
|
|
765
|
+
test.serial('reduced motion: info() prints static info line', async (t) => {
|
|
766
|
+
const { sandbox } = t.context
|
|
767
|
+
const mockOra = sandbox.stub()
|
|
768
|
+
|
|
769
|
+
const { Reporter: MockedReporter } = await esmock('./index.js', {
|
|
770
|
+
ora: { default: mockOra },
|
|
771
|
+
})
|
|
772
|
+
|
|
773
|
+
const originalIsTTY = process.stdout.isTTY
|
|
774
|
+
const originalReduceMotion = process.env.REDUCED_MOTION
|
|
775
|
+
const consoleLog = sandbox.stub(console, 'log')
|
|
776
|
+
|
|
777
|
+
try {
|
|
778
|
+
Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true })
|
|
779
|
+
process.env.REDUCED_MOTION = '1'
|
|
780
|
+
|
|
781
|
+
const rep = new MockedReporter()
|
|
782
|
+
rep.info('Using cached dependencies')
|
|
783
|
+
|
|
784
|
+
t.true(consoleLog.calledOnce)
|
|
785
|
+
t.true(consoleLog.firstCall.args[0].includes('ℹ'))
|
|
786
|
+
t.true(consoleLog.firstCall.args[0].includes('Using cached dependencies'))
|
|
787
|
+
} finally {
|
|
788
|
+
Object.defineProperty(process.stdout, 'isTTY', { value: originalIsTTY, configurable: true })
|
|
789
|
+
if (originalReduceMotion === undefined) {
|
|
790
|
+
delete process.env.REDUCED_MOTION
|
|
791
|
+
} else {
|
|
792
|
+
process.env.REDUCED_MOTION = originalReduceMotion
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
})
|
|
796
|
+
|
|
797
|
+
test.serial('reduced motion: multi-phase workflow outputs each phase on new line', async (t) => {
|
|
798
|
+
const { sandbox } = t.context
|
|
799
|
+
const mockOra = sandbox.stub()
|
|
800
|
+
|
|
801
|
+
const { Reporter: MockedReporter } = await esmock('./index.js', {
|
|
802
|
+
ora: { default: mockOra },
|
|
803
|
+
})
|
|
804
|
+
|
|
805
|
+
const originalIsTTY = process.stdout.isTTY
|
|
806
|
+
const originalReduceMotion = process.env.REDUCED_MOTION
|
|
807
|
+
const consoleLog = sandbox.stub(console, 'log')
|
|
808
|
+
|
|
809
|
+
try {
|
|
810
|
+
Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true })
|
|
811
|
+
process.env.REDUCED_MOTION = '1'
|
|
812
|
+
|
|
813
|
+
const rep = new MockedReporter()
|
|
814
|
+
rep.start('Cloning starter...')
|
|
815
|
+
rep.update('Installing dependencies...')
|
|
816
|
+
rep.succeed('Project created')
|
|
817
|
+
|
|
818
|
+
t.is(consoleLog.callCount, 3, 'each stage should print on a new line')
|
|
819
|
+
t.true(consoleLog.getCall(0).args[0].includes('Cloning starter...'))
|
|
820
|
+
t.true(consoleLog.getCall(1).args[0].includes('Installing dependencies...'))
|
|
821
|
+
t.true(consoleLog.getCall(2).args[0].includes('✔'))
|
|
822
|
+
t.true(consoleLog.getCall(2).args[0].includes('Project created'))
|
|
823
|
+
t.false(mockOra.called, 'ora should never be called')
|
|
824
|
+
} finally {
|
|
825
|
+
Object.defineProperty(process.stdout, 'isTTY', { value: originalIsTTY, configurable: true })
|
|
826
|
+
if (originalReduceMotion === undefined) {
|
|
827
|
+
delete process.env.REDUCED_MOTION
|
|
828
|
+
} else {
|
|
829
|
+
process.env.REDUCED_MOTION = originalReduceMotion
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
})
|
|
833
|
+
|
|
834
|
+
test.serial('reduced motion: quiet mode takes precedence over reduced motion', async (t) => {
|
|
835
|
+
const { sandbox } = t.context
|
|
836
|
+
const mockOra = sandbox.stub()
|
|
837
|
+
|
|
838
|
+
const { Reporter: MockedReporter } = await esmock('./index.js', {
|
|
839
|
+
ora: { default: mockOra },
|
|
840
|
+
})
|
|
841
|
+
|
|
842
|
+
const originalReduceMotion = process.env.REDUCED_MOTION
|
|
843
|
+
const consoleLog = sandbox.stub(console, 'log')
|
|
844
|
+
|
|
845
|
+
try {
|
|
846
|
+
process.env.REDUCED_MOTION = '1'
|
|
847
|
+
|
|
848
|
+
const rep = new MockedReporter()
|
|
849
|
+
rep.configure({ quiet: true })
|
|
850
|
+
rep.start('Building...')
|
|
851
|
+
rep.succeed('Build complete')
|
|
852
|
+
|
|
853
|
+
t.false(consoleLog.called, 'quiet mode should suppress all output')
|
|
854
|
+
t.false(mockOra.called, 'ora should not be called')
|
|
855
|
+
} finally {
|
|
856
|
+
if (originalReduceMotion === undefined) {
|
|
857
|
+
delete process.env.REDUCED_MOTION
|
|
858
|
+
} else {
|
|
859
|
+
process.env.REDUCED_MOTION = originalReduceMotion
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
})
|