@mdxui/terminal 2.0.0

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 (191) hide show
  1. package/README.md +571 -0
  2. package/dist/ansi-css-Sk5mWtdK.d.ts +119 -0
  3. package/dist/ansi-css-V6JIHGsM.d.ts +119 -0
  4. package/dist/ansi-css-_3eSEU9d.d.ts +119 -0
  5. package/dist/chunk-3EFDH7PK.js +5235 -0
  6. package/dist/chunk-3RG5ZIWI.js +10 -0
  7. package/dist/chunk-3X5IR6WE.js +884 -0
  8. package/dist/chunk-4FV5ZDCE.js +5236 -0
  9. package/dist/chunk-4OVMSF2J.js +243 -0
  10. package/dist/chunk-63FEETIS.js +4048 -0
  11. package/dist/chunk-B43KP7XJ.js +884 -0
  12. package/dist/chunk-BMTJXWUV.js +655 -0
  13. package/dist/chunk-C3SVH4N7.js +882 -0
  14. package/dist/chunk-EVWR7Y47.js +874 -0
  15. package/dist/chunk-F6A5VWUC.js +1285 -0
  16. package/dist/chunk-FD7KW7GE.js +882 -0
  17. package/dist/chunk-GBQ6UD6I.js +655 -0
  18. package/dist/chunk-GMDD3M6U.js +5227 -0
  19. package/dist/chunk-JBHRXOXM.js +1058 -0
  20. package/dist/chunk-JFOO3EYO.js +1182 -0
  21. package/dist/chunk-JQ5H3WXL.js +1291 -0
  22. package/dist/chunk-JQD5NASE.js +234 -0
  23. package/dist/chunk-KRHJP5R7.js +592 -0
  24. package/dist/chunk-KWF6WVJE.js +962 -0
  25. package/dist/chunk-LHYQVN3H.js +1038 -0
  26. package/dist/chunk-M3TLQLGC.js +1032 -0
  27. package/dist/chunk-MVW4Q5OP.js +240 -0
  28. package/dist/chunk-NXCZSWLU.js +1294 -0
  29. package/dist/chunk-O25TNRO6.js +607 -0
  30. package/dist/chunk-PNECDA2I.js +884 -0
  31. package/dist/chunk-QIHWRLJR.js +962 -0
  32. package/dist/chunk-QW5YMQ7K.js +882 -0
  33. package/dist/chunk-R5U7XKVJ.js +16 -0
  34. package/dist/chunk-RP2MVQLR.js +962 -0
  35. package/dist/chunk-TP6RXGXA.js +1087 -0
  36. package/dist/chunk-TQQSTITZ.js +655 -0
  37. package/dist/chunk-X24GWXQV.js +1281 -0
  38. package/dist/components/index.d.ts +802 -0
  39. package/dist/components/index.js +149 -0
  40. package/dist/data/index.d.ts +2554 -0
  41. package/dist/data/index.js +51 -0
  42. package/dist/forms/index.d.ts +1596 -0
  43. package/dist/forms/index.js +464 -0
  44. package/dist/index-CQRFZntR.d.ts +867 -0
  45. package/dist/index.d.ts +579 -0
  46. package/dist/index.js +786 -0
  47. package/dist/interactive-D0JkWosD.d.ts +217 -0
  48. package/dist/keyboard/index.d.ts +2 -0
  49. package/dist/keyboard/index.js +43 -0
  50. package/dist/renderers/index.d.ts +546 -0
  51. package/dist/renderers/index.js +2157 -0
  52. package/dist/storybook/index.d.ts +396 -0
  53. package/dist/storybook/index.js +641 -0
  54. package/dist/theme/index.d.ts +1339 -0
  55. package/dist/theme/index.js +123 -0
  56. package/dist/types-Bxu5PAgA.d.ts +710 -0
  57. package/dist/types-CIlop5Ji.d.ts +701 -0
  58. package/dist/types-Ca8p_p5X.d.ts +710 -0
  59. package/package.json +90 -0
  60. package/src/__tests__/components/data/card.test.ts +458 -0
  61. package/src/__tests__/components/data/list.test.ts +473 -0
  62. package/src/__tests__/components/data/metrics.test.ts +541 -0
  63. package/src/__tests__/components/data/table.test.ts +448 -0
  64. package/src/__tests__/components/input/field.test.ts +555 -0
  65. package/src/__tests__/components/input/form.test.ts +870 -0
  66. package/src/__tests__/components/input/search.test.ts +1238 -0
  67. package/src/__tests__/components/input/select.test.ts +658 -0
  68. package/src/__tests__/components/navigation/breadcrumb.test.ts +923 -0
  69. package/src/__tests__/components/navigation/command-palette.test.ts +1095 -0
  70. package/src/__tests__/components/navigation/sidebar.test.ts +1018 -0
  71. package/src/__tests__/components/navigation/tabs.test.ts +995 -0
  72. package/src/__tests__/components.test.tsx +1197 -0
  73. package/src/__tests__/core/compiler.test.ts +986 -0
  74. package/src/__tests__/core/parser.test.ts +785 -0
  75. package/src/__tests__/core/tier-switcher.test.ts +1103 -0
  76. package/src/__tests__/core/types.test.ts +1398 -0
  77. package/src/__tests__/data/collections.test.ts +1337 -0
  78. package/src/__tests__/data/db.test.ts +1265 -0
  79. package/src/__tests__/data/reactive.test.ts +1010 -0
  80. package/src/__tests__/data/sync.test.ts +1614 -0
  81. package/src/__tests__/errors.test.ts +660 -0
  82. package/src/__tests__/forms/integration.test.ts +444 -0
  83. package/src/__tests__/integration.test.ts +905 -0
  84. package/src/__tests__/keyboard.test.ts +1791 -0
  85. package/src/__tests__/renderer.test.ts +489 -0
  86. package/src/__tests__/renderers/ansi-css.test.ts +948 -0
  87. package/src/__tests__/renderers/ansi.test.ts +1366 -0
  88. package/src/__tests__/renderers/ascii.test.ts +1360 -0
  89. package/src/__tests__/renderers/interactive.test.ts +2353 -0
  90. package/src/__tests__/renderers/markdown.test.ts +1483 -0
  91. package/src/__tests__/renderers/text.test.ts +1369 -0
  92. package/src/__tests__/renderers/unicode.test.ts +1307 -0
  93. package/src/__tests__/theme.test.ts +639 -0
  94. package/src/__tests__/utils/assertions.ts +685 -0
  95. package/src/__tests__/utils/index.ts +115 -0
  96. package/src/__tests__/utils/test-renderer.ts +381 -0
  97. package/src/__tests__/utils/utils.test.ts +560 -0
  98. package/src/components/containers/card.ts +56 -0
  99. package/src/components/containers/dialog.ts +53 -0
  100. package/src/components/containers/index.ts +9 -0
  101. package/src/components/containers/panel.ts +59 -0
  102. package/src/components/feedback/badge.ts +40 -0
  103. package/src/components/feedback/index.ts +8 -0
  104. package/src/components/feedback/spinner.ts +23 -0
  105. package/src/components/helpers.ts +81 -0
  106. package/src/components/index.ts +153 -0
  107. package/src/components/layout/breadcrumb.ts +31 -0
  108. package/src/components/layout/index.ts +10 -0
  109. package/src/components/layout/list.ts +29 -0
  110. package/src/components/layout/sidebar.ts +79 -0
  111. package/src/components/layout/table.ts +62 -0
  112. package/src/components/primitives/box.ts +95 -0
  113. package/src/components/primitives/button.ts +54 -0
  114. package/src/components/primitives/index.ts +11 -0
  115. package/src/components/primitives/input.ts +88 -0
  116. package/src/components/primitives/select.ts +97 -0
  117. package/src/components/primitives/text.ts +60 -0
  118. package/src/components/render.ts +155 -0
  119. package/src/components/templates/app.ts +43 -0
  120. package/src/components/templates/index.ts +8 -0
  121. package/src/components/templates/site.ts +54 -0
  122. package/src/components/types.ts +777 -0
  123. package/src/core/compiler.ts +718 -0
  124. package/src/core/parser.ts +127 -0
  125. package/src/core/tier-switcher.ts +607 -0
  126. package/src/core/types.ts +672 -0
  127. package/src/data/collection.ts +316 -0
  128. package/src/data/collections.ts +50 -0
  129. package/src/data/context.tsx +174 -0
  130. package/src/data/db.ts +127 -0
  131. package/src/data/hooks.ts +532 -0
  132. package/src/data/index.ts +138 -0
  133. package/src/data/reactive.ts +1225 -0
  134. package/src/data/saas-collections.ts +375 -0
  135. package/src/data/sync.ts +1213 -0
  136. package/src/data/types.ts +660 -0
  137. package/src/forms/converters.ts +512 -0
  138. package/src/forms/index.ts +133 -0
  139. package/src/forms/schemas.ts +403 -0
  140. package/src/forms/types.ts +476 -0
  141. package/src/index.ts +542 -0
  142. package/src/keyboard/focus.ts +748 -0
  143. package/src/keyboard/index.ts +96 -0
  144. package/src/keyboard/integration.ts +371 -0
  145. package/src/keyboard/manager.ts +377 -0
  146. package/src/keyboard/presets.ts +90 -0
  147. package/src/renderers/ansi-css.ts +576 -0
  148. package/src/renderers/ansi.ts +802 -0
  149. package/src/renderers/ascii.ts +680 -0
  150. package/src/renderers/breadcrumb.ts +480 -0
  151. package/src/renderers/command-palette.ts +802 -0
  152. package/src/renderers/components/field.ts +210 -0
  153. package/src/renderers/components/form.ts +327 -0
  154. package/src/renderers/components/index.ts +21 -0
  155. package/src/renderers/components/search.ts +449 -0
  156. package/src/renderers/components/select.ts +222 -0
  157. package/src/renderers/index.ts +101 -0
  158. package/src/renderers/interactive/component-handlers.ts +622 -0
  159. package/src/renderers/interactive/cursor-manager.ts +147 -0
  160. package/src/renderers/interactive/focus-manager.ts +279 -0
  161. package/src/renderers/interactive/index.ts +661 -0
  162. package/src/renderers/interactive/input-handler.ts +164 -0
  163. package/src/renderers/interactive/keyboard-handler.ts +212 -0
  164. package/src/renderers/interactive/mouse-handler.ts +167 -0
  165. package/src/renderers/interactive/state-manager.ts +109 -0
  166. package/src/renderers/interactive/types.ts +338 -0
  167. package/src/renderers/interactive-string.ts +299 -0
  168. package/src/renderers/interactive.ts +59 -0
  169. package/src/renderers/markdown.ts +950 -0
  170. package/src/renderers/sidebar.ts +549 -0
  171. package/src/renderers/tabs.ts +682 -0
  172. package/src/renderers/text.ts +791 -0
  173. package/src/renderers/unicode.ts +917 -0
  174. package/src/renderers/utils.ts +942 -0
  175. package/src/router/adapters.ts +383 -0
  176. package/src/router/types.ts +140 -0
  177. package/src/router/utils.ts +452 -0
  178. package/src/schemas.ts +205 -0
  179. package/src/storybook/index.ts +91 -0
  180. package/src/storybook/interactive-decorator.tsx +659 -0
  181. package/src/storybook/keyboard-simulator.ts +501 -0
  182. package/src/theme/ansi-codes.ts +80 -0
  183. package/src/theme/box-drawing.ts +132 -0
  184. package/src/theme/color-convert.ts +254 -0
  185. package/src/theme/color-support.ts +321 -0
  186. package/src/theme/index.ts +134 -0
  187. package/src/theme/strip-ansi.ts +50 -0
  188. package/src/theme/tailwind-map.ts +469 -0
  189. package/src/theme/text-styles.ts +206 -0
  190. package/src/theme/theme-system.ts +568 -0
  191. package/src/types.ts +103 -0
@@ -0,0 +1,1103 @@
1
+ /**
2
+ * @mdxui/terminal Tier Switcher Control Tests (RED phase)
3
+ *
4
+ * TDD RED Phase: These tests define the contract for runtime tier switching
5
+ * in the Universal Terminal UI. The Tier Switcher allows users to dynamically
6
+ * switch between render tiers at runtime.
7
+ *
8
+ * Key behaviors:
9
+ * - Tier detection based on terminal capabilities
10
+ * - Runtime tier switching (TEXT → ANSI → INTERACTIVE)
11
+ * - Graceful fallback when a tier is unavailable
12
+ * - User preference override
13
+ * - Re-render on tier change
14
+ *
15
+ * NOTE: These tests are expected to FAIL until implementation is complete.
16
+ * Run: pnpm --filter @mdxui/terminal test
17
+ */
18
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
19
+ import type { RenderTier } from '../../core/types'
20
+
21
+ // ============================================================================
22
+ // These imports WILL FAIL until src/core/tier-switcher.ts is implemented
23
+ // ============================================================================
24
+
25
+ import {
26
+ TierSwitcher,
27
+ createTierSwitcher,
28
+ detectTierCapabilities,
29
+ getTierOrder,
30
+ canSwitchToTier,
31
+ type TierCapabilities,
32
+ type TierSwitcherOptions,
33
+ type TierChangeEvent,
34
+ } from '../../core/tier-switcher'
35
+
36
+ // ============================================================================
37
+ // Test Fixtures
38
+ // ============================================================================
39
+
40
+ const ALL_TIERS: RenderTier[] = ['text', 'markdown', 'ascii', 'unicode', 'ansi', 'interactive']
41
+
42
+ const mockFullCapabilities: TierCapabilities = {
43
+ text: true,
44
+ markdown: true,
45
+ ascii: true,
46
+ unicode: true,
47
+ ansi: true,
48
+ interactive: true,
49
+ }
50
+
51
+ const mockBasicCapabilities: TierCapabilities = {
52
+ text: true,
53
+ markdown: true,
54
+ ascii: true,
55
+ unicode: false,
56
+ ansi: false,
57
+ interactive: false,
58
+ }
59
+
60
+ const mockAnsiCapabilities: TierCapabilities = {
61
+ text: true,
62
+ markdown: true,
63
+ ascii: true,
64
+ unicode: true,
65
+ ansi: true,
66
+ interactive: false, // No interactive support (e.g., piped output)
67
+ }
68
+
69
+ // ============================================================================
70
+ // Tier Detection Tests
71
+ // ============================================================================
72
+
73
+ describe('Tier Detection', () => {
74
+ describe('detectTierCapabilities', () => {
75
+ it('returns TierCapabilities object with all tier flags', () => {
76
+ const capabilities = detectTierCapabilities()
77
+
78
+ expect(capabilities).toBeDefined()
79
+ expect(typeof capabilities.text).toBe('boolean')
80
+ expect(typeof capabilities.markdown).toBe('boolean')
81
+ expect(typeof capabilities.ascii).toBe('boolean')
82
+ expect(typeof capabilities.unicode).toBe('boolean')
83
+ expect(typeof capabilities.ansi).toBe('boolean')
84
+ expect(typeof capabilities.interactive).toBe('boolean')
85
+ })
86
+
87
+ it('always supports text tier', () => {
88
+ const capabilities = detectTierCapabilities()
89
+
90
+ // Text tier should always be available as the fallback
91
+ expect(capabilities.text).toBe(true)
92
+ })
93
+
94
+ it('always supports markdown tier', () => {
95
+ const capabilities = detectTierCapabilities()
96
+
97
+ // Markdown is just text with formatting markers, always available
98
+ expect(capabilities.markdown).toBe(true)
99
+ })
100
+
101
+ it('always supports ascii tier', () => {
102
+ const capabilities = detectTierCapabilities()
103
+
104
+ // ASCII characters are universally supported
105
+ expect(capabilities.ascii).toBe(true)
106
+ })
107
+
108
+ it('detects unicode support based on terminal encoding', () => {
109
+ const capabilities = detectTierCapabilities()
110
+
111
+ // Unicode support depends on terminal UTF-8 support
112
+ expect(typeof capabilities.unicode).toBe('boolean')
113
+ })
114
+
115
+ it('detects ANSI color support based on terminal capabilities', () => {
116
+ const capabilities = detectTierCapabilities()
117
+
118
+ // ANSI support depends on terminal color support
119
+ expect(typeof capabilities.ansi).toBe('boolean')
120
+ })
121
+
122
+ it('detects interactive support based on TTY and input availability', () => {
123
+ const capabilities = detectTierCapabilities()
124
+
125
+ // Interactive requires TTY with stdin/stdout
126
+ expect(typeof capabilities.interactive).toBe('boolean')
127
+ })
128
+
129
+ it('accepts optional environment override', () => {
130
+ const customEnv = {
131
+ TERM: 'xterm-256color',
132
+ COLORTERM: 'truecolor',
133
+ FORCE_COLOR: '1',
134
+ }
135
+
136
+ const capabilities = detectTierCapabilities({ env: customEnv })
137
+
138
+ expect(capabilities).toBeDefined()
139
+ })
140
+
141
+ it('accepts optional TTY override', () => {
142
+ const capabilities = detectTierCapabilities({
143
+ isTTY: true,
144
+ isStdinTTY: true,
145
+ isStdoutTTY: true,
146
+ })
147
+
148
+ expect(capabilities).toBeDefined()
149
+ })
150
+
151
+ it('disables interactive when stdin is not a TTY', () => {
152
+ const capabilities = detectTierCapabilities({
153
+ isTTY: true,
154
+ isStdinTTY: false, // Piped input
155
+ isStdoutTTY: true,
156
+ })
157
+
158
+ expect(capabilities.interactive).toBe(false)
159
+ })
160
+
161
+ it('disables interactive when stdout is not a TTY', () => {
162
+ const capabilities = detectTierCapabilities({
163
+ isTTY: true,
164
+ isStdinTTY: true,
165
+ isStdoutTTY: false, // Piped output
166
+ })
167
+
168
+ expect(capabilities.interactive).toBe(false)
169
+ })
170
+
171
+ it('respects NO_COLOR environment variable', () => {
172
+ const capabilities = detectTierCapabilities({
173
+ env: { NO_COLOR: '1' },
174
+ })
175
+
176
+ expect(capabilities.ansi).toBe(false)
177
+ })
178
+
179
+ it('respects FORCE_COLOR environment variable', () => {
180
+ const capabilities = detectTierCapabilities({
181
+ env: { FORCE_COLOR: '1' },
182
+ isTTY: false, // Even without TTY
183
+ })
184
+
185
+ expect(capabilities.ansi).toBe(true)
186
+ })
187
+ })
188
+
189
+ describe('getTierOrder', () => {
190
+ it('returns tiers in capability order', () => {
191
+ const order = getTierOrder()
192
+
193
+ expect(order).toEqual(['text', 'markdown', 'ascii', 'unicode', 'ansi', 'interactive'])
194
+ })
195
+
196
+ it('returns tier ordering as a map', () => {
197
+ const orderMap = getTierOrder('map')
198
+
199
+ expect(orderMap).toEqual({
200
+ text: 0,
201
+ markdown: 1,
202
+ ascii: 2,
203
+ unicode: 3,
204
+ ansi: 4,
205
+ interactive: 5,
206
+ })
207
+ })
208
+
209
+ it('text has lowest order (0)', () => {
210
+ const orderMap = getTierOrder('map')
211
+ expect(orderMap.text).toBe(0)
212
+ })
213
+
214
+ it('interactive has highest order (5)', () => {
215
+ const orderMap = getTierOrder('map')
216
+ expect(orderMap.interactive).toBe(5)
217
+ })
218
+ })
219
+
220
+ describe('canSwitchToTier', () => {
221
+ it('returns true when tier is supported', () => {
222
+ const result = canSwitchToTier('ansi', mockFullCapabilities)
223
+ expect(result).toBe(true)
224
+ })
225
+
226
+ it('returns false when tier is not supported', () => {
227
+ const result = canSwitchToTier('interactive', mockAnsiCapabilities)
228
+ expect(result).toBe(false)
229
+ })
230
+
231
+ it('always returns true for text tier', () => {
232
+ const result = canSwitchToTier('text', mockBasicCapabilities)
233
+ expect(result).toBe(true)
234
+ })
235
+
236
+ it('returns false for unicode when not supported', () => {
237
+ const result = canSwitchToTier('unicode', mockBasicCapabilities)
238
+ expect(result).toBe(false)
239
+ })
240
+
241
+ it('returns false for ansi when not supported', () => {
242
+ const result = canSwitchToTier('ansi', mockBasicCapabilities)
243
+ expect(result).toBe(false)
244
+ })
245
+ })
246
+ })
247
+
248
+ // ============================================================================
249
+ // TierSwitcher Factory Tests
250
+ // ============================================================================
251
+
252
+ describe('createTierSwitcher', () => {
253
+ it('creates a TierSwitcher instance', () => {
254
+ const switcher = createTierSwitcher()
255
+
256
+ expect(switcher).toBeDefined()
257
+ expect(typeof switcher.getCurrentTier).toBe('function')
258
+ expect(typeof switcher.setTier).toBe('function')
259
+ })
260
+
261
+ it('accepts initial tier option', () => {
262
+ const switcher = createTierSwitcher({ initialTier: 'unicode' })
263
+
264
+ expect(switcher.getCurrentTier()).toBe('unicode')
265
+ })
266
+
267
+ it('accepts capabilities override', () => {
268
+ const switcher = createTierSwitcher({
269
+ capabilities: mockBasicCapabilities,
270
+ })
271
+
272
+ expect(switcher.getCapabilities()).toEqual(mockBasicCapabilities)
273
+ })
274
+
275
+ it('accepts auto-detect option', () => {
276
+ const switcher = createTierSwitcher({ autoDetect: true })
277
+
278
+ expect(switcher).toBeDefined()
279
+ })
280
+
281
+ it('defaults to highest available tier when autoDetect is true', () => {
282
+ const switcher = createTierSwitcher({
283
+ autoDetect: true,
284
+ capabilities: mockAnsiCapabilities,
285
+ })
286
+
287
+ // Should auto-select 'ansi' as highest available
288
+ expect(switcher.getCurrentTier()).toBe('ansi')
289
+ })
290
+
291
+ it('defaults to text tier when autoDetect is false', () => {
292
+ const switcher = createTierSwitcher({ autoDetect: false })
293
+
294
+ expect(switcher.getCurrentTier()).toBe('text')
295
+ })
296
+
297
+ it('accepts onChange callback', () => {
298
+ const onChange = vi.fn()
299
+ const switcher = createTierSwitcher({ onChange })
300
+
301
+ switcher.setTier('ansi')
302
+
303
+ expect(onChange).toHaveBeenCalled()
304
+ })
305
+ })
306
+
307
+ // ============================================================================
308
+ // TierSwitcher Instance Tests
309
+ // ============================================================================
310
+
311
+ describe('TierSwitcher', () => {
312
+ let switcher: TierSwitcher
313
+
314
+ beforeEach(() => {
315
+ switcher = createTierSwitcher({
316
+ capabilities: mockFullCapabilities,
317
+ initialTier: 'text',
318
+ })
319
+ })
320
+
321
+ afterEach(() => {
322
+ switcher.destroy()
323
+ })
324
+
325
+ describe('getCurrentTier', () => {
326
+ it('returns the current render tier', () => {
327
+ expect(switcher.getCurrentTier()).toBe('text')
328
+ })
329
+
330
+ it('reflects tier changes', () => {
331
+ switcher.setTier('ansi')
332
+ expect(switcher.getCurrentTier()).toBe('ansi')
333
+ })
334
+ })
335
+
336
+ describe('setTier', () => {
337
+ it('changes the current tier', () => {
338
+ switcher.setTier('unicode')
339
+ expect(switcher.getCurrentTier()).toBe('unicode')
340
+ })
341
+
342
+ it('returns true on successful change', () => {
343
+ const result = switcher.setTier('ansi')
344
+ expect(result).toBe(true)
345
+ })
346
+
347
+ it('returns false when tier is not available', () => {
348
+ const limitedSwitcher = createTierSwitcher({
349
+ capabilities: mockBasicCapabilities,
350
+ initialTier: 'text',
351
+ })
352
+
353
+ const result = limitedSwitcher.setTier('ansi')
354
+ expect(result).toBe(false)
355
+ expect(limitedSwitcher.getCurrentTier()).toBe('text')
356
+
357
+ limitedSwitcher.destroy()
358
+ })
359
+
360
+ it('emits change event on successful switch', () => {
361
+ const listener = vi.fn()
362
+ switcher.on('change', listener)
363
+
364
+ switcher.setTier('ansi')
365
+
366
+ expect(listener).toHaveBeenCalledWith(
367
+ expect.objectContaining({
368
+ previousTier: 'text',
369
+ newTier: 'ansi',
370
+ })
371
+ )
372
+ })
373
+
374
+ it('does not emit change event when setting same tier', () => {
375
+ const listener = vi.fn()
376
+ switcher.on('change', listener)
377
+
378
+ switcher.setTier('text') // Already text
379
+
380
+ expect(listener).not.toHaveBeenCalled()
381
+ })
382
+
383
+ it('validates tier value', () => {
384
+ // @ts-expect-error - Testing runtime validation
385
+ const result = switcher.setTier('invalid')
386
+ expect(result).toBe(false)
387
+ })
388
+ })
389
+
390
+ describe('getCapabilities', () => {
391
+ it('returns current capabilities', () => {
392
+ const caps = switcher.getCapabilities()
393
+ expect(caps).toEqual(mockFullCapabilities)
394
+ })
395
+
396
+ it('returns frozen object', () => {
397
+ const caps = switcher.getCapabilities()
398
+ expect(Object.isFrozen(caps)).toBe(true)
399
+ })
400
+ })
401
+
402
+ describe('getAvailableTiers', () => {
403
+ it('returns array of available tiers', () => {
404
+ const tiers = switcher.getAvailableTiers()
405
+ expect(tiers).toEqual(ALL_TIERS)
406
+ })
407
+
408
+ it('only returns supported tiers', () => {
409
+ const limitedSwitcher = createTierSwitcher({
410
+ capabilities: mockBasicCapabilities,
411
+ })
412
+
413
+ const tiers = limitedSwitcher.getAvailableTiers()
414
+ expect(tiers).toEqual(['text', 'markdown', 'ascii'])
415
+
416
+ limitedSwitcher.destroy()
417
+ })
418
+
419
+ it('returns tiers in capability order', () => {
420
+ const tiers = switcher.getAvailableTiers()
421
+ const orderMap = getTierOrder('map')
422
+
423
+ for (let i = 1; i < tiers.length; i++) {
424
+ expect(orderMap[tiers[i - 1]]).toBeLessThan(orderMap[tiers[i]])
425
+ }
426
+ })
427
+ })
428
+
429
+ describe('canSwitchTo', () => {
430
+ it('returns true for available tiers', () => {
431
+ expect(switcher.canSwitchTo('interactive')).toBe(true)
432
+ })
433
+
434
+ it('returns false for unavailable tiers', () => {
435
+ const limitedSwitcher = createTierSwitcher({
436
+ capabilities: mockBasicCapabilities,
437
+ })
438
+
439
+ expect(limitedSwitcher.canSwitchTo('ansi')).toBe(false)
440
+
441
+ limitedSwitcher.destroy()
442
+ })
443
+
444
+ it('always returns true for text tier', () => {
445
+ expect(switcher.canSwitchTo('text')).toBe(true)
446
+ })
447
+ })
448
+
449
+ describe('upgradeToHighest', () => {
450
+ it('switches to highest available tier', () => {
451
+ switcher.upgradeToHighest()
452
+ expect(switcher.getCurrentTier()).toBe('interactive')
453
+ })
454
+
455
+ it('respects capabilities', () => {
456
+ const limitedSwitcher = createTierSwitcher({
457
+ capabilities: mockAnsiCapabilities,
458
+ initialTier: 'text',
459
+ })
460
+
461
+ limitedSwitcher.upgradeToHighest()
462
+ expect(limitedSwitcher.getCurrentTier()).toBe('ansi')
463
+
464
+ limitedSwitcher.destroy()
465
+ })
466
+
467
+ it('returns new tier', () => {
468
+ const result = switcher.upgradeToHighest()
469
+ expect(result).toBe('interactive')
470
+ })
471
+ })
472
+
473
+ describe('downgradeToLowest', () => {
474
+ it('switches to text tier', () => {
475
+ switcher.setTier('ansi')
476
+ switcher.downgradeToLowest()
477
+ expect(switcher.getCurrentTier()).toBe('text')
478
+ })
479
+
480
+ it('returns text tier', () => {
481
+ const result = switcher.downgradeToLowest()
482
+ expect(result).toBe('text')
483
+ })
484
+ })
485
+
486
+ describe('upgradeOneLevel', () => {
487
+ it('switches to next higher tier', () => {
488
+ switcher.setTier('ascii')
489
+ const result = switcher.upgradeOneLevel()
490
+
491
+ expect(result).toBe('unicode')
492
+ expect(switcher.getCurrentTier()).toBe('unicode')
493
+ })
494
+
495
+ it('skips unavailable tiers', () => {
496
+ const limitedSwitcher = createTierSwitcher({
497
+ capabilities: {
498
+ ...mockBasicCapabilities,
499
+ ansi: true, // Skip unicode, go to ansi
500
+ },
501
+ initialTier: 'ascii',
502
+ })
503
+
504
+ limitedSwitcher.upgradeOneLevel()
505
+ expect(limitedSwitcher.getCurrentTier()).toBe('ansi')
506
+
507
+ limitedSwitcher.destroy()
508
+ })
509
+
510
+ it('returns same tier when already at highest', () => {
511
+ switcher.setTier('interactive')
512
+ const result = switcher.upgradeOneLevel()
513
+
514
+ expect(result).toBe('interactive')
515
+ })
516
+ })
517
+
518
+ describe('downgradeOneLevel', () => {
519
+ it('switches to next lower tier', () => {
520
+ switcher.setTier('ansi')
521
+ const result = switcher.downgradeOneLevel()
522
+
523
+ expect(result).toBe('unicode')
524
+ expect(switcher.getCurrentTier()).toBe('unicode')
525
+ })
526
+
527
+ it('returns text when already at lowest', () => {
528
+ switcher.setTier('text')
529
+ const result = switcher.downgradeOneLevel()
530
+
531
+ expect(result).toBe('text')
532
+ })
533
+ })
534
+ })
535
+
536
+ // ============================================================================
537
+ // Graceful Fallback Tests
538
+ // ============================================================================
539
+
540
+ describe('Graceful Fallback', () => {
541
+ it('falls back to lower tier when requested tier unavailable', () => {
542
+ const switcher = createTierSwitcher({
543
+ capabilities: mockBasicCapabilities,
544
+ initialTier: 'text',
545
+ fallbackBehavior: 'downgrade',
546
+ })
547
+
548
+ const result = switcher.setTier('ansi', { fallback: true })
549
+
550
+ // Should fall back to ascii (highest available)
551
+ expect(result).toBe(true)
552
+ expect(switcher.getCurrentTier()).toBe('ascii')
553
+
554
+ switcher.destroy()
555
+ })
556
+
557
+ it('does not fallback when fallback is disabled', () => {
558
+ const switcher = createTierSwitcher({
559
+ capabilities: mockBasicCapabilities,
560
+ initialTier: 'text',
561
+ })
562
+
563
+ const result = switcher.setTier('ansi', { fallback: false })
564
+
565
+ expect(result).toBe(false)
566
+ expect(switcher.getCurrentTier()).toBe('text')
567
+
568
+ switcher.destroy()
569
+ })
570
+
571
+ it('emits fallback event when fallback occurs', () => {
572
+ const listener = vi.fn()
573
+ const switcher = createTierSwitcher({
574
+ capabilities: mockBasicCapabilities,
575
+ initialTier: 'text',
576
+ fallbackBehavior: 'downgrade',
577
+ })
578
+
579
+ switcher.on('fallback', listener)
580
+ switcher.setTier('interactive', { fallback: true })
581
+
582
+ expect(listener).toHaveBeenCalledWith(
583
+ expect.objectContaining({
584
+ requestedTier: 'interactive',
585
+ actualTier: 'ascii',
586
+ reason: 'unavailable',
587
+ })
588
+ )
589
+
590
+ switcher.destroy()
591
+ })
592
+
593
+ it('finds nearest available tier when falling back', () => {
594
+ const customCapabilities: TierCapabilities = {
595
+ text: true,
596
+ markdown: true,
597
+ ascii: true,
598
+ unicode: false,
599
+ ansi: true, // Skip unicode, support ansi
600
+ interactive: false,
601
+ }
602
+
603
+ const switcher = createTierSwitcher({
604
+ capabilities: customCapabilities,
605
+ initialTier: 'text',
606
+ fallbackBehavior: 'downgrade',
607
+ })
608
+
609
+ // Request interactive, should fall back to ansi
610
+ switcher.setTier('interactive', { fallback: true })
611
+ expect(switcher.getCurrentTier()).toBe('ansi')
612
+
613
+ switcher.destroy()
614
+ })
615
+ })
616
+
617
+ // ============================================================================
618
+ // User Preference Override Tests
619
+ // ============================================================================
620
+
621
+ describe('User Preference Override', () => {
622
+ it('respects user preference even when higher tier available', () => {
623
+ const switcher = createTierSwitcher({
624
+ capabilities: mockFullCapabilities,
625
+ userPreference: 'ansi', // User prefers ansi over interactive
626
+ autoDetect: true,
627
+ })
628
+
629
+ expect(switcher.getCurrentTier()).toBe('ansi')
630
+
631
+ switcher.destroy()
632
+ })
633
+
634
+ it('allows setting user preference', () => {
635
+ const switcher = createTierSwitcher({
636
+ capabilities: mockFullCapabilities,
637
+ })
638
+
639
+ switcher.setUserPreference('unicode')
640
+ expect(switcher.getUserPreference()).toBe('unicode')
641
+
642
+ switcher.destroy()
643
+ })
644
+
645
+ it('falls back when user preference is unavailable', () => {
646
+ const switcher = createTierSwitcher({
647
+ capabilities: mockBasicCapabilities,
648
+ userPreference: 'ansi', // Not available
649
+ autoDetect: true,
650
+ })
651
+
652
+ // Should fall back to highest available (ascii)
653
+ expect(switcher.getCurrentTier()).toBe('ascii')
654
+
655
+ switcher.destroy()
656
+ })
657
+
658
+ it('clearing user preference returns to auto-detect behavior', () => {
659
+ const switcher = createTierSwitcher({
660
+ capabilities: mockFullCapabilities,
661
+ userPreference: 'text',
662
+ autoDetect: true,
663
+ })
664
+
665
+ expect(switcher.getCurrentTier()).toBe('text')
666
+
667
+ switcher.clearUserPreference()
668
+ expect(switcher.getUserPreference()).toBeNull()
669
+ // After clearing, should use highest available
670
+ expect(switcher.getCurrentTier()).toBe('interactive')
671
+
672
+ switcher.destroy()
673
+ })
674
+
675
+ it('persists user preference', () => {
676
+ const storage = new Map<string, string>()
677
+ const switcher = createTierSwitcher({
678
+ capabilities: mockFullCapabilities,
679
+ storage: {
680
+ get: (key) => storage.get(key) ?? null,
681
+ set: (key, value) => storage.set(key, value),
682
+ remove: (key) => storage.delete(key),
683
+ },
684
+ })
685
+
686
+ switcher.setUserPreference('unicode')
687
+
688
+ expect(storage.get('tier-preference')).toBe('unicode')
689
+
690
+ switcher.destroy()
691
+ })
692
+
693
+ it('loads user preference from storage', () => {
694
+ const storage = new Map<string, string>([['tier-preference', 'ansi']])
695
+ const switcher = createTierSwitcher({
696
+ capabilities: mockFullCapabilities,
697
+ storage: {
698
+ get: (key) => storage.get(key) ?? null,
699
+ set: (key, value) => storage.set(key, value),
700
+ remove: (key) => storage.delete(key),
701
+ },
702
+ autoDetect: true,
703
+ })
704
+
705
+ expect(switcher.getUserPreference()).toBe('ansi')
706
+ expect(switcher.getCurrentTier()).toBe('ansi')
707
+
708
+ switcher.destroy()
709
+ })
710
+ })
711
+
712
+ // ============================================================================
713
+ // Re-render on Tier Change Tests
714
+ // ============================================================================
715
+
716
+ describe('Re-render on Tier Change', () => {
717
+ it('emits change event with re-render flag', () => {
718
+ const listener = vi.fn()
719
+ const switcher = createTierSwitcher({
720
+ capabilities: mockFullCapabilities,
721
+ initialTier: 'text',
722
+ })
723
+
724
+ switcher.on('change', listener)
725
+ switcher.setTier('ansi')
726
+
727
+ expect(listener).toHaveBeenCalledWith(
728
+ expect.objectContaining({
729
+ shouldRerender: true,
730
+ })
731
+ )
732
+
733
+ switcher.destroy()
734
+ })
735
+
736
+ it('provides requestRerender callback', () => {
737
+ const rerender = vi.fn()
738
+ const switcher = createTierSwitcher({
739
+ capabilities: mockFullCapabilities,
740
+ initialTier: 'text',
741
+ onRerender: rerender,
742
+ })
743
+
744
+ switcher.setTier('ansi')
745
+
746
+ expect(rerender).toHaveBeenCalled()
747
+
748
+ switcher.destroy()
749
+ })
750
+
751
+ it('allows suppressing re-render', () => {
752
+ const rerender = vi.fn()
753
+ const switcher = createTierSwitcher({
754
+ capabilities: mockFullCapabilities,
755
+ initialTier: 'text',
756
+ onRerender: rerender,
757
+ })
758
+
759
+ switcher.setTier('ansi', { rerender: false })
760
+
761
+ expect(rerender).not.toHaveBeenCalled()
762
+
763
+ switcher.destroy()
764
+ })
765
+
766
+ it('batches multiple tier changes', () => {
767
+ const rerender = vi.fn()
768
+ const switcher = createTierSwitcher({
769
+ capabilities: mockFullCapabilities,
770
+ initialTier: 'text',
771
+ onRerender: rerender,
772
+ batchRerenders: true,
773
+ })
774
+
775
+ switcher.batch(() => {
776
+ switcher.setTier('ascii')
777
+ switcher.setTier('unicode')
778
+ switcher.setTier('ansi')
779
+ })
780
+
781
+ // Should only call rerender once at end of batch
782
+ expect(rerender).toHaveBeenCalledTimes(1)
783
+
784
+ switcher.destroy()
785
+ })
786
+ })
787
+
788
+ // ============================================================================
789
+ // Event System Tests
790
+ // ============================================================================
791
+
792
+ describe('Event System', () => {
793
+ let switcher: TierSwitcher
794
+
795
+ beforeEach(() => {
796
+ switcher = createTierSwitcher({
797
+ capabilities: mockFullCapabilities,
798
+ initialTier: 'text',
799
+ })
800
+ })
801
+
802
+ afterEach(() => {
803
+ switcher.destroy()
804
+ })
805
+
806
+ describe('on', () => {
807
+ it('registers change event listener', () => {
808
+ const listener = vi.fn()
809
+ switcher.on('change', listener)
810
+
811
+ switcher.setTier('ansi')
812
+
813
+ expect(listener).toHaveBeenCalled()
814
+ })
815
+
816
+ it('registers fallback event listener', () => {
817
+ const limitedSwitcher = createTierSwitcher({
818
+ capabilities: mockBasicCapabilities,
819
+ fallbackBehavior: 'downgrade',
820
+ })
821
+
822
+ const listener = vi.fn()
823
+ limitedSwitcher.on('fallback', listener)
824
+
825
+ limitedSwitcher.setTier('ansi', { fallback: true })
826
+
827
+ expect(listener).toHaveBeenCalled()
828
+
829
+ limitedSwitcher.destroy()
830
+ })
831
+
832
+ it('returns unsubscribe function', () => {
833
+ const listener = vi.fn()
834
+ const unsubscribe = switcher.on('change', listener)
835
+
836
+ unsubscribe()
837
+ switcher.setTier('ansi')
838
+
839
+ expect(listener).not.toHaveBeenCalled()
840
+ })
841
+
842
+ it('supports multiple listeners', () => {
843
+ const listener1 = vi.fn()
844
+ const listener2 = vi.fn()
845
+
846
+ switcher.on('change', listener1)
847
+ switcher.on('change', listener2)
848
+
849
+ switcher.setTier('ansi')
850
+
851
+ expect(listener1).toHaveBeenCalled()
852
+ expect(listener2).toHaveBeenCalled()
853
+ })
854
+ })
855
+
856
+ describe('off', () => {
857
+ it('removes event listener', () => {
858
+ const listener = vi.fn()
859
+ switcher.on('change', listener)
860
+ switcher.off('change', listener)
861
+
862
+ switcher.setTier('ansi')
863
+
864
+ expect(listener).not.toHaveBeenCalled()
865
+ })
866
+
867
+ it('handles removing non-existent listener', () => {
868
+ const listener = vi.fn()
869
+
870
+ // Should not throw
871
+ expect(() => switcher.off('change', listener)).not.toThrow()
872
+ })
873
+ })
874
+
875
+ describe('once', () => {
876
+ it('listener only fires once', () => {
877
+ const listener = vi.fn()
878
+ switcher.once('change', listener)
879
+
880
+ switcher.setTier('ascii')
881
+ switcher.setTier('unicode')
882
+
883
+ expect(listener).toHaveBeenCalledTimes(1)
884
+ })
885
+ })
886
+ })
887
+
888
+ // ============================================================================
889
+ // TierChangeEvent Tests
890
+ // ============================================================================
891
+
892
+ describe('TierChangeEvent', () => {
893
+ it('contains previousTier and newTier', () => {
894
+ const listener = vi.fn()
895
+ const switcher = createTierSwitcher({
896
+ capabilities: mockFullCapabilities,
897
+ initialTier: 'text',
898
+ })
899
+
900
+ switcher.on('change', listener)
901
+ switcher.setTier('ansi')
902
+
903
+ const event: TierChangeEvent = listener.mock.calls[0][0]
904
+
905
+ expect(event.previousTier).toBe('text')
906
+ expect(event.newTier).toBe('ansi')
907
+
908
+ switcher.destroy()
909
+ })
910
+
911
+ it('contains timestamp', () => {
912
+ const listener = vi.fn()
913
+ const switcher = createTierSwitcher({
914
+ capabilities: mockFullCapabilities,
915
+ initialTier: 'text',
916
+ })
917
+
918
+ const beforeTime = Date.now()
919
+ switcher.on('change', listener)
920
+ switcher.setTier('ansi')
921
+ const afterTime = Date.now()
922
+
923
+ const event: TierChangeEvent = listener.mock.calls[0][0]
924
+
925
+ expect(event.timestamp).toBeGreaterThanOrEqual(beforeTime)
926
+ expect(event.timestamp).toBeLessThanOrEqual(afterTime)
927
+
928
+ switcher.destroy()
929
+ })
930
+
931
+ it('contains capabilities snapshot', () => {
932
+ const listener = vi.fn()
933
+ const switcher = createTierSwitcher({
934
+ capabilities: mockFullCapabilities,
935
+ initialTier: 'text',
936
+ })
937
+
938
+ switcher.on('change', listener)
939
+ switcher.setTier('ansi')
940
+
941
+ const event: TierChangeEvent = listener.mock.calls[0][0]
942
+
943
+ expect(event.capabilities).toEqual(mockFullCapabilities)
944
+
945
+ switcher.destroy()
946
+ })
947
+
948
+ it('contains shouldRerender flag', () => {
949
+ const listener = vi.fn()
950
+ const switcher = createTierSwitcher({
951
+ capabilities: mockFullCapabilities,
952
+ initialTier: 'text',
953
+ })
954
+
955
+ switcher.on('change', listener)
956
+ switcher.setTier('ansi')
957
+
958
+ const event: TierChangeEvent = listener.mock.calls[0][0]
959
+
960
+ expect(typeof event.shouldRerender).toBe('boolean')
961
+
962
+ switcher.destroy()
963
+ })
964
+ })
965
+
966
+ // ============================================================================
967
+ // Lifecycle Tests
968
+ // ============================================================================
969
+
970
+ describe('Lifecycle', () => {
971
+ describe('destroy', () => {
972
+ it('removes all event listeners', () => {
973
+ const listener = vi.fn()
974
+ const switcher = createTierSwitcher({
975
+ capabilities: mockFullCapabilities,
976
+ })
977
+
978
+ switcher.on('change', listener)
979
+ switcher.destroy()
980
+
981
+ // Attempting to use after destroy should be safe
982
+ expect(() => switcher.setTier('ansi')).not.toThrow()
983
+ expect(listener).not.toHaveBeenCalled()
984
+ })
985
+
986
+ it('prevents further tier changes', () => {
987
+ const switcher = createTierSwitcher({
988
+ capabilities: mockFullCapabilities,
989
+ initialTier: 'text',
990
+ })
991
+
992
+ switcher.destroy()
993
+ const result = switcher.setTier('ansi')
994
+
995
+ expect(result).toBe(false)
996
+ expect(switcher.getCurrentTier()).toBe('text')
997
+ })
998
+
999
+ it('is idempotent', () => {
1000
+ const switcher = createTierSwitcher()
1001
+
1002
+ // Multiple destroy calls should not throw
1003
+ expect(() => {
1004
+ switcher.destroy()
1005
+ switcher.destroy()
1006
+ switcher.destroy()
1007
+ }).not.toThrow()
1008
+ })
1009
+ })
1010
+
1011
+ describe('isDestroyed', () => {
1012
+ it('returns false initially', () => {
1013
+ const switcher = createTierSwitcher()
1014
+
1015
+ expect(switcher.isDestroyed()).toBe(false)
1016
+
1017
+ switcher.destroy()
1018
+ })
1019
+
1020
+ it('returns true after destroy', () => {
1021
+ const switcher = createTierSwitcher()
1022
+ switcher.destroy()
1023
+
1024
+ expect(switcher.isDestroyed()).toBe(true)
1025
+ })
1026
+ })
1027
+ })
1028
+
1029
+ // ============================================================================
1030
+ // Integration Tests
1031
+ // ============================================================================
1032
+
1033
+ describe('Integration', () => {
1034
+ it('full tier switching workflow', () => {
1035
+ const changes: TierChangeEvent[] = []
1036
+ const switcher = createTierSwitcher({
1037
+ capabilities: mockFullCapabilities,
1038
+ autoDetect: true,
1039
+ onChange: (event) => changes.push(event),
1040
+ })
1041
+
1042
+ // Start at highest (interactive)
1043
+ expect(switcher.getCurrentTier()).toBe('interactive')
1044
+
1045
+ // Downgrade to ANSI for piped output
1046
+ switcher.setTier('ansi')
1047
+ expect(switcher.getCurrentTier()).toBe('ansi')
1048
+
1049
+ // Set user preference
1050
+ switcher.setUserPreference('unicode')
1051
+ expect(switcher.getCurrentTier()).toBe('unicode')
1052
+
1053
+ // Clear preference, return to last set tier
1054
+ switcher.clearUserPreference()
1055
+ // Note: clearing preference doesn't auto-upgrade
1056
+
1057
+ expect(changes.length).toBeGreaterThan(0)
1058
+
1059
+ switcher.destroy()
1060
+ })
1061
+
1062
+ it('graceful degradation when capabilities change', () => {
1063
+ const switcher = createTierSwitcher({
1064
+ capabilities: mockFullCapabilities,
1065
+ initialTier: 'interactive',
1066
+ })
1067
+
1068
+ // Simulate capabilities being reduced (e.g., output piped)
1069
+ switcher.updateCapabilities(mockAnsiCapabilities)
1070
+
1071
+ // Should automatically fall back to highest available
1072
+ expect(switcher.getCurrentTier()).toBe('ansi')
1073
+
1074
+ switcher.destroy()
1075
+ })
1076
+
1077
+ it('handles rapid tier changes', () => {
1078
+ const rerender = vi.fn()
1079
+ const switcher = createTierSwitcher({
1080
+ capabilities: mockFullCapabilities,
1081
+ initialTier: 'text',
1082
+ onRerender: rerender,
1083
+ batchRerenders: true,
1084
+ batchDelay: 10,
1085
+ })
1086
+
1087
+ // Rapid changes
1088
+ for (const tier of ALL_TIERS) {
1089
+ switcher.setTier(tier)
1090
+ }
1091
+
1092
+ // Wait for batch to complete
1093
+ return new Promise<void>((resolve) => {
1094
+ setTimeout(() => {
1095
+ // Should have batched all re-renders
1096
+ expect(rerender.mock.calls.length).toBeLessThan(ALL_TIERS.length)
1097
+ expect(switcher.getCurrentTier()).toBe('interactive')
1098
+ switcher.destroy()
1099
+ resolve()
1100
+ }, 50)
1101
+ })
1102
+ })
1103
+ })