@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.
@@ -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
  // ─────────────────────────────────────────────────────────────────────────────
@@ -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
  */
@@ -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',
@@ -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
- throw new HelpTopicNotFoundError(name)
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().resolves(false)
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', this.#quiet, this.#json, this.#verbose)
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
+ })