@thegetty/quire-cli 1.0.0-rc.34 → 1.0.0-rc.36

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.
Files changed (159) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +9 -0
  3. package/bin/cli.js +19 -1
  4. package/package.json +21 -8
  5. package/schemas/config.schema.json +194 -0
  6. package/schemas/figures.schema.json +56 -0
  7. package/schemas/layout.schema.json +5 -0
  8. package/schemas/objects.schema.json +62 -0
  9. package/schemas/publication.schema.json +140 -0
  10. package/schemas/references.schema.json +29 -0
  11. package/src/Command.js +26 -6
  12. package/src/Command.spec.js +99 -0
  13. package/src/commands/README.md +213 -122
  14. package/src/commands/build.js +46 -37
  15. package/src/commands/build.spec.js +113 -0
  16. package/src/commands/build.test.js +402 -0
  17. package/src/commands/clean.js +25 -19
  18. package/src/commands/clean.spec.js +108 -0
  19. package/src/commands/clean.test.js +260 -0
  20. package/src/commands/config.js +251 -0
  21. package/src/commands/config.spec.js +107 -0
  22. package/src/commands/config.test.js +715 -0
  23. package/src/commands/create.js +42 -14
  24. package/src/commands/create.spec.js +112 -0
  25. package/src/commands/create.test.js +415 -0
  26. package/src/commands/epub.js +59 -30
  27. package/src/commands/epub.spec.js +114 -0
  28. package/src/commands/epub.test.js +503 -0
  29. package/src/commands/index.js +9 -2
  30. package/src/commands/info.js +18 -24
  31. package/src/commands/info.spec.js +64 -0
  32. package/src/commands/info.test.js +415 -0
  33. package/src/commands/pdf.js +58 -84
  34. package/src/commands/pdf.spec.js +114 -0
  35. package/src/commands/pdf.test.js +464 -0
  36. package/src/commands/preview.js +26 -31
  37. package/src/commands/preview.spec.js +97 -0
  38. package/src/commands/preview.test.js +250 -0
  39. package/src/commands/use.js +56 -0
  40. package/src/commands/use.spec.js +61 -0
  41. package/src/commands/use.test.js +280 -0
  42. package/src/commands/validate.js +31 -19
  43. package/src/commands/validate.spec.js +82 -0
  44. package/src/commands/validate.test.js +234 -0
  45. package/src/commands/workflows.js +70 -0
  46. package/src/errors/build/build-failed-error.js +19 -0
  47. package/src/errors/build/config-field-missing-error.js +20 -0
  48. package/src/errors/build/config-file-not-found-error.js +20 -0
  49. package/src/errors/build/index.js +11 -0
  50. package/src/errors/index.js +48 -0
  51. package/src/errors/install/dependency-install-error.js +19 -0
  52. package/src/errors/install/directory-not-empty-error.js +25 -0
  53. package/src/errors/install/index.js +12 -0
  54. package/src/errors/install/invalid-path-error.js +31 -0
  55. package/src/errors/install/invalid-starter-error.js +27 -0
  56. package/src/errors/install/version-not-found-error.js +35 -0
  57. package/src/errors/output/epub-generation-error.js +19 -0
  58. package/src/errors/output/index.js +14 -0
  59. package/src/errors/output/invalid-epub-library-error.js +20 -0
  60. package/src/errors/output/invalid-pdf-library-error.js +20 -0
  61. package/src/errors/output/missing-build-output-error.js +21 -0
  62. package/src/errors/output/pdf-generation-error.js +24 -0
  63. package/src/errors/output/tool-not-found-error.js +37 -0
  64. package/src/errors/project/index.js +10 -0
  65. package/src/errors/project/not-in-project-error.js +20 -0
  66. package/src/errors/project/project-create-error.js +21 -0
  67. package/src/errors/quire-error.js +27 -0
  68. package/src/errors/validation/validation-error.js +20 -11
  69. package/src/helpers/clean.js +1 -1
  70. package/src/helpers/docs-url.js +32 -0
  71. package/src/helpers/test-cwd.js +5 -6
  72. package/src/helpers/test-cwd.test.js +192 -0
  73. package/src/helpers/which.js +10 -4
  74. package/src/lib/11ty/README.md +135 -19
  75. package/src/lib/11ty/api.js +176 -93
  76. package/src/lib/11ty/cli.js +77 -35
  77. package/src/lib/11ty/index.js +64 -5
  78. package/src/lib/11ty/index.test.js +655 -0
  79. package/src/lib/README.md +275 -0
  80. package/src/lib/commander/index.js +100 -0
  81. package/src/lib/commander/index.test.js +86 -0
  82. package/src/lib/commander/options.js +195 -0
  83. package/src/lib/commander/options.test.js +109 -0
  84. package/src/lib/conf/README.md +84 -73
  85. package/src/lib/conf/config.js +5 -3
  86. package/src/lib/conf/config.test.js +281 -0
  87. package/src/lib/conf/defaults.js +44 -0
  88. package/src/lib/conf/format.js +60 -0
  89. package/src/lib/conf/format.test.js +106 -0
  90. package/src/lib/conf/helpers.js +91 -0
  91. package/src/lib/conf/helpers.test.js +136 -0
  92. package/src/lib/conf/index.js +22 -0
  93. package/src/lib/conf/schema.js +53 -8
  94. package/src/lib/epub/README.md +133 -2
  95. package/src/lib/epub/engines.js +46 -0
  96. package/src/lib/epub/epub.js +36 -11
  97. package/src/lib/epub/index.js +111 -21
  98. package/src/lib/epub/index.test.js +518 -0
  99. package/src/lib/epub/pandoc.js +33 -4
  100. package/src/lib/epub/pandoc.test.js +122 -0
  101. package/src/lib/epub/schema.js +21 -0
  102. package/src/lib/error/README.md +170 -0
  103. package/src/lib/error/handler.js +107 -0
  104. package/src/lib/git/README.md +151 -2
  105. package/src/lib/git/index.js +217 -13
  106. package/src/lib/git/index.spec.js +80 -0
  107. package/src/lib/git/index.test.js +453 -0
  108. package/src/lib/installer/index.js +309 -0
  109. package/src/lib/installer/index.spec.js +83 -0
  110. package/src/lib/installer/index.test.js +545 -0
  111. package/src/lib/logger/README.md +424 -0
  112. package/src/lib/logger/debug.js +90 -0
  113. package/src/lib/logger/debug.spec.js +130 -0
  114. package/src/lib/logger/index.js +228 -0
  115. package/src/lib/logger/index.spec.js +131 -0
  116. package/src/lib/logger/index.test.js +477 -0
  117. package/src/lib/npm/README.md +127 -0
  118. package/src/lib/npm/index.js +198 -0
  119. package/src/lib/npm/index.spec.js +60 -0
  120. package/src/lib/npm/index.test.js +355 -0
  121. package/src/lib/pdf/README.md +131 -0
  122. package/src/lib/pdf/engines.js +46 -0
  123. package/src/lib/pdf/index.js +124 -21
  124. package/src/lib/pdf/index.test.js +708 -0
  125. package/src/lib/pdf/paged.js +100 -52
  126. package/src/lib/pdf/paged.test.js +366 -0
  127. package/src/lib/pdf/prince.js +115 -37
  128. package/src/lib/pdf/prince.test.js +202 -0
  129. package/src/lib/pdf/schema.js +21 -0
  130. package/src/lib/pdf/split.js +61 -33
  131. package/src/lib/pdf/split.test.js +445 -0
  132. package/src/lib/process/manager.js +110 -0
  133. package/src/lib/process/manager.test.js +55 -0
  134. package/src/lib/project/build.js +143 -0
  135. package/src/lib/project/build.test.js +253 -0
  136. package/src/lib/project/config.js +48 -0
  137. package/src/lib/project/config.test.js +134 -0
  138. package/src/{helpers/is-quire.js → lib/project/detect.js} +5 -3
  139. package/src/lib/project/detect.test.js +157 -0
  140. package/src/lib/project/index.js +40 -0
  141. package/src/lib/project/paths.js +224 -0
  142. package/src/lib/project/version.js +110 -0
  143. package/src/lib/project/version.test.js +350 -0
  144. package/src/lib/reporter/README.md +211 -2
  145. package/src/lib/reporter/index.js +512 -0
  146. package/src/lib/reporter/index.test.js +593 -0
  147. package/src/main.js +134 -47
  148. package/src/main.spec.js +61 -0
  149. package/src/main.test.js +347 -0
  150. package/src/validators/utils.js +2 -1
  151. package/src/commands/conf.js +0 -43
  152. package/src/commands/version.js +0 -43
  153. package/src/lib/11ty/paths.js +0 -103
  154. package/src/lib/i18n/README.md +0 -3
  155. package/src/lib/i18n/config.js +0 -53
  156. package/src/lib/i18n/index.js +0 -43
  157. package/src/lib/i18n/localeService.js +0 -58
  158. package/src/lib/quire/README.md +0 -19
  159. package/src/lib/quire/index.js +0 -350
@@ -0,0 +1,593 @@
1
+ import esmock from 'esmock'
2
+ import sinon from 'sinon'
3
+ import test from 'ava'
4
+ import reporter, { Reporter } from './index.js'
5
+
6
+ /**
7
+ * Reporter Module Tests
8
+ *
9
+ * Tests the Reporter class and singleton behavior for CLI progress feedback.
10
+ * Uses esmock to mock ora for controlled testing.
11
+ */
12
+
13
+ test.beforeEach((t) => {
14
+ t.context.sandbox = sinon.createSandbox()
15
+ // Reset reporter state before each test
16
+ reporter.reset()
17
+ })
18
+
19
+ test.afterEach.always((t) => {
20
+ t.context.sandbox.restore()
21
+ reporter.reset()
22
+ })
23
+
24
+ // ─────────────────────────────────────────────────────────────────────────────
25
+ // Singleton behavior tests
26
+ // ─────────────────────────────────────────────────────────────────────────────
27
+
28
+ test('default export is a Reporter instance', (t) => {
29
+ t.true(reporter instanceof Reporter)
30
+ })
31
+
32
+ test('Reporter class is exported for testing', (t) => {
33
+ t.is(typeof Reporter, 'function')
34
+ const instance = new Reporter()
35
+ t.true(instance instanceof Reporter)
36
+ })
37
+
38
+ // ─────────────────────────────────────────────────────────────────────────────
39
+ // Configuration tests
40
+ // ─────────────────────────────────────────────────────────────────────────────
41
+
42
+ test('configure() sets quiet mode', (t) => {
43
+ reporter.configure({ quiet: true })
44
+ // Spinner should be suppressed - verify by checking isSpinning after start
45
+ reporter.start('Test')
46
+ t.false(reporter.isSpinning())
47
+ })
48
+
49
+ test('configure() sets json mode', (t) => {
50
+ reporter.configure({ json: true })
51
+ reporter.start('Test')
52
+ t.false(reporter.isSpinning())
53
+ })
54
+
55
+ test('configure() returns this for chaining', (t) => {
56
+ const result = reporter.configure({ quiet: false })
57
+ t.is(result, reporter)
58
+ })
59
+
60
+ test('configure() resets previous configuration', (t) => {
61
+ reporter.configure({ quiet: true })
62
+ reporter.configure({ quiet: false })
63
+ // After reset, spinner should work (if TTY)
64
+ // Can't test TTY directly, but verify configure doesn't throw
65
+ t.notThrows(() => reporter.configure({}))
66
+ })
67
+
68
+ // ─────────────────────────────────────────────────────────────────────────────
69
+ // Start/stop tests
70
+ // ─────────────────────────────────────────────────────────────────────────────
71
+
72
+ test('start() returns this for chaining', (t) => {
73
+ reporter.configure({ quiet: true })
74
+ const result = reporter.start('Test')
75
+ t.is(result, reporter)
76
+ })
77
+
78
+ test('stop() returns this for chaining', (t) => {
79
+ reporter.configure({ quiet: true })
80
+ reporter.start('Test')
81
+ const result = reporter.stop()
82
+ t.is(result, reporter)
83
+ })
84
+
85
+ test('stop() clears spinner state', (t) => {
86
+ reporter.configure({ quiet: true })
87
+ reporter.start('Test')
88
+ reporter.stop()
89
+ t.false(reporter.isSpinning())
90
+ })
91
+
92
+ test('start() stops previous spinner before starting new one', (t) => {
93
+ reporter.configure({ quiet: true })
94
+ reporter.start('First')
95
+ reporter.start('Second')
96
+ // Should not throw, previous spinner should be stopped
97
+ t.pass()
98
+ })
99
+
100
+ // ─────────────────────────────────────────────────────────────────────────────
101
+ // Update tests
102
+ // ─────────────────────────────────────────────────────────────────────────────
103
+
104
+ test('update() returns this for chaining', (t) => {
105
+ reporter.configure({ quiet: true })
106
+ reporter.start('Initial')
107
+ const result = reporter.update('Updated')
108
+ t.is(result, reporter)
109
+ })
110
+
111
+ test('update() starts spinner if none running', (t) => {
112
+ reporter.configure({ quiet: true })
113
+ reporter.update('Auto-start')
114
+ // Should not throw
115
+ t.pass()
116
+ })
117
+
118
+ // ─────────────────────────────────────────────────────────────────────────────
119
+ // Status completion tests
120
+ // ─────────────────────────────────────────────────────────────────────────────
121
+
122
+ test('succeed() returns this for chaining', (t) => {
123
+ reporter.configure({ quiet: true })
124
+ reporter.start('Test')
125
+ const result = reporter.succeed('Done')
126
+ t.is(result, reporter)
127
+ })
128
+
129
+ test('succeed() clears spinner', (t) => {
130
+ reporter.configure({ quiet: true })
131
+ reporter.start('Test')
132
+ reporter.succeed('Done')
133
+ t.false(reporter.isSpinning())
134
+ })
135
+
136
+ test('succeed() without text uses base text', (t) => {
137
+ reporter.configure({ quiet: true })
138
+ reporter.start('Base message')
139
+ reporter.succeed()
140
+ // Should not throw
141
+ t.pass()
142
+ })
143
+
144
+ test('fail() returns this for chaining', (t) => {
145
+ reporter.configure({ quiet: true })
146
+ reporter.start('Test')
147
+ const result = reporter.fail('Failed')
148
+ t.is(result, reporter)
149
+ })
150
+
151
+ test('fail() clears spinner', (t) => {
152
+ reporter.configure({ quiet: true })
153
+ reporter.start('Test')
154
+ reporter.fail('Failed')
155
+ t.false(reporter.isSpinning())
156
+ })
157
+
158
+ test('warn() returns this for chaining', (t) => {
159
+ reporter.configure({ quiet: true })
160
+ reporter.start('Test')
161
+ const result = reporter.warn('Warning')
162
+ t.is(result, reporter)
163
+ })
164
+
165
+ test('warn() clears spinner', (t) => {
166
+ reporter.configure({ quiet: true })
167
+ reporter.start('Test')
168
+ reporter.warn('Warning')
169
+ t.false(reporter.isSpinning())
170
+ })
171
+
172
+ test('info() returns this for chaining', (t) => {
173
+ reporter.configure({ quiet: true })
174
+ const result = reporter.info('Info message')
175
+ t.is(result, reporter)
176
+ })
177
+
178
+ // ─────────────────────────────────────────────────────────────────────────────
179
+ // Elapsed time tests
180
+ // ─────────────────────────────────────────────────────────────────────────────
181
+
182
+ test('getElapsed() returns null before start', (t) => {
183
+ reporter.reset()
184
+ t.is(reporter.getElapsed(), null)
185
+ })
186
+
187
+ test('getElapsed() returns elapsed time after start', async (t) => {
188
+ // Create a fresh Reporter instance for this test to avoid singleton issues
189
+ const rep = new Reporter()
190
+ rep.configure({ quiet: true })
191
+ rep.start('Test')
192
+
193
+ // Wait a small amount
194
+ await new Promise((resolve) => setTimeout(resolve, 50))
195
+
196
+ const elapsed = rep.getElapsed()
197
+ t.true(typeof elapsed === 'number', `elapsed should be a number, got: ${elapsed} (${typeof elapsed})`)
198
+ t.true(elapsed >= 40, `elapsed (${elapsed}ms) should be >= 40ms`)
199
+
200
+ // Clean up
201
+ rep.stop()
202
+ })
203
+
204
+ test('elapsed timer is cleared on succeed', async (t) => {
205
+ reporter.configure({ quiet: true })
206
+ reporter.start('Test', { showElapsed: true })
207
+ await new Promise((resolve) => setTimeout(resolve, 10))
208
+ reporter.succeed()
209
+ // Timer should be stopped - no way to directly test, but ensure no errors
210
+ t.pass()
211
+ })
212
+
213
+ test('elapsed timer is cleared on fail', async (t) => {
214
+ reporter.configure({ quiet: true })
215
+ reporter.start('Test', { showElapsed: true })
216
+ await new Promise((resolve) => setTimeout(resolve, 10))
217
+ reporter.fail()
218
+ t.pass()
219
+ })
220
+
221
+ test('elapsed timer is cleared on stop', async (t) => {
222
+ reporter.configure({ quiet: true })
223
+ reporter.start('Test', { showElapsed: true })
224
+ await new Promise((resolve) => setTimeout(resolve, 10))
225
+ reporter.stop()
226
+ t.pass()
227
+ })
228
+
229
+ // ─────────────────────────────────────────────────────────────────────────────
230
+ // Reset tests
231
+ // ─────────────────────────────────────────────────────────────────────────────
232
+
233
+ test('reset() returns this for chaining', (t) => {
234
+ const result = reporter.reset()
235
+ t.is(result, reporter)
236
+ })
237
+
238
+ test('reset() clears all state', (t) => {
239
+ reporter.configure({ quiet: true, json: true })
240
+ reporter.start('Test')
241
+ reporter.reset()
242
+
243
+ t.false(reporter.isSpinning())
244
+ t.is(reporter.getElapsed(), null)
245
+ })
246
+
247
+ // ─────────────────────────────────────────────────────────────────────────────
248
+ // isSpinning tests
249
+ // ─────────────────────────────────────────────────────────────────────────────
250
+
251
+ test('isSpinning() returns false initially', (t) => {
252
+ reporter.reset()
253
+ t.false(reporter.isSpinning())
254
+ })
255
+
256
+ test('isSpinning() returns false in quiet mode', (t) => {
257
+ reporter.configure({ quiet: true })
258
+ reporter.start('Test')
259
+ t.false(reporter.isSpinning())
260
+ })
261
+
262
+ // ─────────────────────────────────────────────────────────────────────────────
263
+ // Integration tests with mocked ora
264
+ // ─────────────────────────────────────────────────────────────────────────────
265
+
266
+ test.serial('start() calls ora with correct text', async (t) => {
267
+ const { sandbox } = t.context
268
+ const mockOraInstance = {
269
+ start: sandbox.stub().returnsThis(),
270
+ stop: sandbox.stub().returnsThis(),
271
+ succeed: sandbox.stub().returnsThis(),
272
+ fail: sandbox.stub().returnsThis(),
273
+ warn: sandbox.stub().returnsThis(),
274
+ info: sandbox.stub().returnsThis(),
275
+ isSpinning: true,
276
+ text: '',
277
+ }
278
+ const mockOra = sandbox.stub().returns(mockOraInstance)
279
+
280
+ const { Reporter: MockedReporter } = await esmock('./index.js', {
281
+ ora: { default: mockOra },
282
+ })
283
+
284
+ // Mock isTTY to enable spinner
285
+ const originalIsTTY = process.stdout.isTTY
286
+ process.stdout.isTTY = true
287
+
288
+ try {
289
+ const rep = new MockedReporter()
290
+ rep.start('Building...')
291
+
292
+ t.true(mockOra.calledOnce)
293
+ t.deepEqual(mockOra.firstCall.args[0], {
294
+ text: 'Building...',
295
+ spinner: 'dots',
296
+ })
297
+ } finally {
298
+ process.stdout.isTTY = originalIsTTY
299
+ }
300
+ })
301
+
302
+ test.serial('succeed() calls ora.succeed with text', async (t) => {
303
+ const { sandbox } = t.context
304
+ const mockOraInstance = {
305
+ start: sandbox.stub().returnsThis(),
306
+ stop: sandbox.stub().returnsThis(),
307
+ succeed: sandbox.stub().returnsThis(),
308
+ fail: sandbox.stub().returnsThis(),
309
+ warn: sandbox.stub().returnsThis(),
310
+ info: sandbox.stub().returnsThis(),
311
+ isSpinning: true,
312
+ text: '',
313
+ }
314
+ const mockOra = sandbox.stub().returns(mockOraInstance)
315
+
316
+ const { Reporter: MockedReporter } = await esmock('./index.js', {
317
+ ora: { default: mockOra },
318
+ })
319
+
320
+ const originalIsTTY = process.stdout.isTTY
321
+ process.stdout.isTTY = true
322
+
323
+ try {
324
+ const rep = new MockedReporter()
325
+ rep.start('Building...')
326
+ rep.succeed('Build complete')
327
+
328
+ t.true(mockOraInstance.succeed.calledOnce)
329
+ t.is(mockOraInstance.succeed.firstCall.args[0], 'Build complete')
330
+ } finally {
331
+ process.stdout.isTTY = originalIsTTY
332
+ }
333
+ })
334
+
335
+ test.serial('fail() calls ora.fail with text', async (t) => {
336
+ const { sandbox } = t.context
337
+ const mockOraInstance = {
338
+ start: sandbox.stub().returnsThis(),
339
+ stop: sandbox.stub().returnsThis(),
340
+ succeed: sandbox.stub().returnsThis(),
341
+ fail: sandbox.stub().returnsThis(),
342
+ warn: sandbox.stub().returnsThis(),
343
+ info: sandbox.stub().returnsThis(),
344
+ isSpinning: true,
345
+ text: '',
346
+ }
347
+ const mockOra = sandbox.stub().returns(mockOraInstance)
348
+
349
+ const { Reporter: MockedReporter } = await esmock('./index.js', {
350
+ ora: { default: mockOra },
351
+ })
352
+
353
+ const originalIsTTY = process.stdout.isTTY
354
+ process.stdout.isTTY = true
355
+
356
+ try {
357
+ const rep = new MockedReporter()
358
+ rep.start('Generating PDF...')
359
+ rep.fail('PDF generation failed')
360
+
361
+ t.true(mockOraInstance.fail.calledOnce)
362
+ t.is(mockOraInstance.fail.firstCall.args[0], 'PDF generation failed')
363
+ } finally {
364
+ process.stdout.isTTY = originalIsTTY
365
+ }
366
+ })
367
+
368
+ test.serial('warn() calls ora.warn with text', async (t) => {
369
+ const { sandbox } = t.context
370
+ const mockOraInstance = {
371
+ start: sandbox.stub().returnsThis(),
372
+ stop: sandbox.stub().returnsThis(),
373
+ succeed: sandbox.stub().returnsThis(),
374
+ fail: sandbox.stub().returnsThis(),
375
+ warn: sandbox.stub().returnsThis(),
376
+ info: sandbox.stub().returnsThis(),
377
+ isSpinning: true,
378
+ text: '',
379
+ }
380
+ const mockOra = sandbox.stub().returns(mockOraInstance)
381
+
382
+ const { Reporter: MockedReporter } = await esmock('./index.js', {
383
+ ora: { default: mockOra },
384
+ })
385
+
386
+ const originalIsTTY = process.stdout.isTTY
387
+ process.stdout.isTTY = true
388
+
389
+ try {
390
+ const rep = new MockedReporter()
391
+ rep.start('Checking...')
392
+ rep.warn('Dependencies outdated')
393
+
394
+ t.true(mockOraInstance.warn.calledOnce)
395
+ t.is(mockOraInstance.warn.firstCall.args[0], 'Dependencies outdated')
396
+ } finally {
397
+ process.stdout.isTTY = originalIsTTY
398
+ }
399
+ })
400
+
401
+ test.serial('update() changes spinner text', async (t) => {
402
+ const { sandbox } = t.context
403
+ const mockOraInstance = {
404
+ start: sandbox.stub().returnsThis(),
405
+ stop: sandbox.stub().returnsThis(),
406
+ succeed: sandbox.stub().returnsThis(),
407
+ fail: sandbox.stub().returnsThis(),
408
+ warn: sandbox.stub().returnsThis(),
409
+ info: sandbox.stub().returnsThis(),
410
+ isSpinning: true,
411
+ text: '',
412
+ }
413
+ const mockOra = sandbox.stub().returns(mockOraInstance)
414
+
415
+ const { Reporter: MockedReporter } = await esmock('./index.js', {
416
+ ora: { default: mockOra },
417
+ })
418
+
419
+ const originalIsTTY = process.stdout.isTTY
420
+ process.stdout.isTTY = true
421
+
422
+ try {
423
+ const rep = new MockedReporter()
424
+ rep.start('Step 1...')
425
+ rep.update('Step 2...')
426
+
427
+ t.is(mockOraInstance.text, 'Step 2...')
428
+ } finally {
429
+ process.stdout.isTTY = originalIsTTY
430
+ }
431
+ })
432
+
433
+ test.serial('spinner is not created when quiet=true', async (t) => {
434
+ const { sandbox } = t.context
435
+ const mockOra = sandbox.stub()
436
+
437
+ const { Reporter: MockedReporter } = await esmock('./index.js', {
438
+ ora: { default: mockOra },
439
+ })
440
+
441
+ const rep = new MockedReporter()
442
+ rep.configure({ quiet: true })
443
+ rep.start('Should not create spinner')
444
+
445
+ t.false(mockOra.called)
446
+ })
447
+
448
+ test.serial('spinner is not created when json=true', async (t) => {
449
+ const { sandbox } = t.context
450
+ const mockOra = sandbox.stub()
451
+
452
+ const { Reporter: MockedReporter } = await esmock('./index.js', {
453
+ ora: { default: mockOra },
454
+ })
455
+
456
+ const rep = new MockedReporter()
457
+ rep.configure({ json: true })
458
+ rep.start('Should not create spinner')
459
+
460
+ t.false(mockOra.called)
461
+ })
462
+
463
+ test.serial('spinner is not created when not TTY', async (t) => {
464
+ const { sandbox } = t.context
465
+ const mockOra = sandbox.stub()
466
+
467
+ const { Reporter: MockedReporter } = await esmock('./index.js', {
468
+ ora: { default: mockOra },
469
+ })
470
+
471
+ const originalIsTTY = process.stdout.isTTY
472
+ process.stdout.isTTY = false
473
+
474
+ try {
475
+ const rep = new MockedReporter()
476
+ rep.start('Should not create spinner')
477
+
478
+ t.false(mockOra.called)
479
+ } finally {
480
+ process.stdout.isTTY = originalIsTTY
481
+ }
482
+ })
483
+
484
+ // ─────────────────────────────────────────────────────────────────────────────
485
+ // Elapsed time display tests
486
+ // ─────────────────────────────────────────────────────────────────────────────
487
+
488
+ test.serial('showElapsed option updates spinner text with time', async (t) => {
489
+ const { sandbox } = t.context
490
+ const mockOraInstance = {
491
+ start: sandbox.stub().returnsThis(),
492
+ stop: sandbox.stub().returnsThis(),
493
+ succeed: sandbox.stub().returnsThis(),
494
+ fail: sandbox.stub().returnsThis(),
495
+ warn: sandbox.stub().returnsThis(),
496
+ info: sandbox.stub().returnsThis(),
497
+ isSpinning: true,
498
+ text: '',
499
+ }
500
+ const mockOra = sandbox.stub().returns(mockOraInstance)
501
+
502
+ const { Reporter: MockedReporter } = await esmock('./index.js', {
503
+ ora: { default: mockOra },
504
+ })
505
+
506
+ const originalIsTTY = process.stdout.isTTY
507
+ process.stdout.isTTY = true
508
+
509
+ let rep
510
+ try {
511
+ rep = new MockedReporter()
512
+ rep.start('Building...', { showElapsed: true })
513
+
514
+ // Wait for timer to tick (use 1200ms to account for CI timing variations)
515
+ await new Promise((resolve) => setTimeout(resolve, 1200))
516
+
517
+ // Nota bene: text should include elapsed time in format "(Ns)" where N is a number.
518
+ // We use a regex pattern to avoid flaky tests on slower CI systems.
519
+ t.regex(mockOraInstance.text, /\(\d+s\)/, 'spinner text should include elapsed time')
520
+ t.true(mockOraInstance.text.startsWith('Building...'), 'spinner text should include base text')
521
+ } finally {
522
+ // Ensure timer is stopped
523
+ if (rep) rep.stop()
524
+ process.stdout.isTTY = originalIsTTY
525
+ }
526
+ })
527
+
528
+ // ─────────────────────────────────────────────────────────────────────────────
529
+ // Method call safety tests
530
+ // ─────────────────────────────────────────────────────────────────────────────
531
+
532
+ test('calling succeed without start does not throw', (t) => {
533
+ reporter.reset()
534
+ t.notThrows(() => reporter.succeed('Done'))
535
+ })
536
+
537
+ test('calling fail without start does not throw', (t) => {
538
+ reporter.reset()
539
+ t.notThrows(() => reporter.fail('Failed'))
540
+ })
541
+
542
+ test('calling warn without start does not throw', (t) => {
543
+ reporter.reset()
544
+ t.notThrows(() => reporter.warn('Warning'))
545
+ })
546
+
547
+ test('calling stop without start does not throw', (t) => {
548
+ reporter.reset()
549
+ t.notThrows(() => reporter.stop())
550
+ })
551
+
552
+ test('calling update without start does not throw', (t) => {
553
+ reporter.reset()
554
+ t.notThrows(() => reporter.update('Update'))
555
+ })
556
+
557
+ // ─────────────────────────────────────────────────────────────────────────────
558
+ // Workflow tests
559
+ // ─────────────────────────────────────────────────────────────────────────────
560
+
561
+ test('typical success workflow', (t) => {
562
+ reporter.configure({ quiet: true })
563
+ reporter.start('Step 1...')
564
+ reporter.update('Step 2...')
565
+ reporter.update('Step 3...')
566
+ reporter.succeed('All steps complete')
567
+ t.pass()
568
+ })
569
+
570
+ test('typical failure workflow', (t) => {
571
+ reporter.configure({ quiet: true })
572
+ reporter.start('Processing...')
573
+ reporter.fail('Processing failed')
574
+ t.pass()
575
+ })
576
+
577
+ test('multi-phase workflow with different outcomes', (t) => {
578
+ reporter.configure({ quiet: true })
579
+
580
+ // Phase 1: success
581
+ reporter.start('Phase 1...')
582
+ reporter.succeed('Phase 1 complete')
583
+
584
+ // Phase 2: warning
585
+ reporter.start('Phase 2...')
586
+ reporter.warn('Phase 2 completed with warnings')
587
+
588
+ // Phase 3: failure
589
+ reporter.start('Phase 3...')
590
+ reporter.fail('Phase 3 failed')
591
+
592
+ t.pass()
593
+ })