@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.
- package/README.md +571 -0
- package/dist/ansi-css-Sk5mWtdK.d.ts +119 -0
- package/dist/ansi-css-V6JIHGsM.d.ts +119 -0
- package/dist/ansi-css-_3eSEU9d.d.ts +119 -0
- package/dist/chunk-3EFDH7PK.js +5235 -0
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/chunk-3X5IR6WE.js +884 -0
- package/dist/chunk-4FV5ZDCE.js +5236 -0
- package/dist/chunk-4OVMSF2J.js +243 -0
- package/dist/chunk-63FEETIS.js +4048 -0
- package/dist/chunk-B43KP7XJ.js +884 -0
- package/dist/chunk-BMTJXWUV.js +655 -0
- package/dist/chunk-C3SVH4N7.js +882 -0
- package/dist/chunk-EVWR7Y47.js +874 -0
- package/dist/chunk-F6A5VWUC.js +1285 -0
- package/dist/chunk-FD7KW7GE.js +882 -0
- package/dist/chunk-GBQ6UD6I.js +655 -0
- package/dist/chunk-GMDD3M6U.js +5227 -0
- package/dist/chunk-JBHRXOXM.js +1058 -0
- package/dist/chunk-JFOO3EYO.js +1182 -0
- package/dist/chunk-JQ5H3WXL.js +1291 -0
- package/dist/chunk-JQD5NASE.js +234 -0
- package/dist/chunk-KRHJP5R7.js +592 -0
- package/dist/chunk-KWF6WVJE.js +962 -0
- package/dist/chunk-LHYQVN3H.js +1038 -0
- package/dist/chunk-M3TLQLGC.js +1032 -0
- package/dist/chunk-MVW4Q5OP.js +240 -0
- package/dist/chunk-NXCZSWLU.js +1294 -0
- package/dist/chunk-O25TNRO6.js +607 -0
- package/dist/chunk-PNECDA2I.js +884 -0
- package/dist/chunk-QIHWRLJR.js +962 -0
- package/dist/chunk-QW5YMQ7K.js +882 -0
- package/dist/chunk-R5U7XKVJ.js +16 -0
- package/dist/chunk-RP2MVQLR.js +962 -0
- package/dist/chunk-TP6RXGXA.js +1087 -0
- package/dist/chunk-TQQSTITZ.js +655 -0
- package/dist/chunk-X24GWXQV.js +1281 -0
- package/dist/components/index.d.ts +802 -0
- package/dist/components/index.js +149 -0
- package/dist/data/index.d.ts +2554 -0
- package/dist/data/index.js +51 -0
- package/dist/forms/index.d.ts +1596 -0
- package/dist/forms/index.js +464 -0
- package/dist/index-CQRFZntR.d.ts +867 -0
- package/dist/index.d.ts +579 -0
- package/dist/index.js +786 -0
- package/dist/interactive-D0JkWosD.d.ts +217 -0
- package/dist/keyboard/index.d.ts +2 -0
- package/dist/keyboard/index.js +43 -0
- package/dist/renderers/index.d.ts +546 -0
- package/dist/renderers/index.js +2157 -0
- package/dist/storybook/index.d.ts +396 -0
- package/dist/storybook/index.js +641 -0
- package/dist/theme/index.d.ts +1339 -0
- package/dist/theme/index.js +123 -0
- package/dist/types-Bxu5PAgA.d.ts +710 -0
- package/dist/types-CIlop5Ji.d.ts +701 -0
- package/dist/types-Ca8p_p5X.d.ts +710 -0
- package/package.json +90 -0
- package/src/__tests__/components/data/card.test.ts +458 -0
- package/src/__tests__/components/data/list.test.ts +473 -0
- package/src/__tests__/components/data/metrics.test.ts +541 -0
- package/src/__tests__/components/data/table.test.ts +448 -0
- package/src/__tests__/components/input/field.test.ts +555 -0
- package/src/__tests__/components/input/form.test.ts +870 -0
- package/src/__tests__/components/input/search.test.ts +1238 -0
- package/src/__tests__/components/input/select.test.ts +658 -0
- package/src/__tests__/components/navigation/breadcrumb.test.ts +923 -0
- package/src/__tests__/components/navigation/command-palette.test.ts +1095 -0
- package/src/__tests__/components/navigation/sidebar.test.ts +1018 -0
- package/src/__tests__/components/navigation/tabs.test.ts +995 -0
- package/src/__tests__/components.test.tsx +1197 -0
- package/src/__tests__/core/compiler.test.ts +986 -0
- package/src/__tests__/core/parser.test.ts +785 -0
- package/src/__tests__/core/tier-switcher.test.ts +1103 -0
- package/src/__tests__/core/types.test.ts +1398 -0
- package/src/__tests__/data/collections.test.ts +1337 -0
- package/src/__tests__/data/db.test.ts +1265 -0
- package/src/__tests__/data/reactive.test.ts +1010 -0
- package/src/__tests__/data/sync.test.ts +1614 -0
- package/src/__tests__/errors.test.ts +660 -0
- package/src/__tests__/forms/integration.test.ts +444 -0
- package/src/__tests__/integration.test.ts +905 -0
- package/src/__tests__/keyboard.test.ts +1791 -0
- package/src/__tests__/renderer.test.ts +489 -0
- package/src/__tests__/renderers/ansi-css.test.ts +948 -0
- package/src/__tests__/renderers/ansi.test.ts +1366 -0
- package/src/__tests__/renderers/ascii.test.ts +1360 -0
- package/src/__tests__/renderers/interactive.test.ts +2353 -0
- package/src/__tests__/renderers/markdown.test.ts +1483 -0
- package/src/__tests__/renderers/text.test.ts +1369 -0
- package/src/__tests__/renderers/unicode.test.ts +1307 -0
- package/src/__tests__/theme.test.ts +639 -0
- package/src/__tests__/utils/assertions.ts +685 -0
- package/src/__tests__/utils/index.ts +115 -0
- package/src/__tests__/utils/test-renderer.ts +381 -0
- package/src/__tests__/utils/utils.test.ts +560 -0
- package/src/components/containers/card.ts +56 -0
- package/src/components/containers/dialog.ts +53 -0
- package/src/components/containers/index.ts +9 -0
- package/src/components/containers/panel.ts +59 -0
- package/src/components/feedback/badge.ts +40 -0
- package/src/components/feedback/index.ts +8 -0
- package/src/components/feedback/spinner.ts +23 -0
- package/src/components/helpers.ts +81 -0
- package/src/components/index.ts +153 -0
- package/src/components/layout/breadcrumb.ts +31 -0
- package/src/components/layout/index.ts +10 -0
- package/src/components/layout/list.ts +29 -0
- package/src/components/layout/sidebar.ts +79 -0
- package/src/components/layout/table.ts +62 -0
- package/src/components/primitives/box.ts +95 -0
- package/src/components/primitives/button.ts +54 -0
- package/src/components/primitives/index.ts +11 -0
- package/src/components/primitives/input.ts +88 -0
- package/src/components/primitives/select.ts +97 -0
- package/src/components/primitives/text.ts +60 -0
- package/src/components/render.ts +155 -0
- package/src/components/templates/app.ts +43 -0
- package/src/components/templates/index.ts +8 -0
- package/src/components/templates/site.ts +54 -0
- package/src/components/types.ts +777 -0
- package/src/core/compiler.ts +718 -0
- package/src/core/parser.ts +127 -0
- package/src/core/tier-switcher.ts +607 -0
- package/src/core/types.ts +672 -0
- package/src/data/collection.ts +316 -0
- package/src/data/collections.ts +50 -0
- package/src/data/context.tsx +174 -0
- package/src/data/db.ts +127 -0
- package/src/data/hooks.ts +532 -0
- package/src/data/index.ts +138 -0
- package/src/data/reactive.ts +1225 -0
- package/src/data/saas-collections.ts +375 -0
- package/src/data/sync.ts +1213 -0
- package/src/data/types.ts +660 -0
- package/src/forms/converters.ts +512 -0
- package/src/forms/index.ts +133 -0
- package/src/forms/schemas.ts +403 -0
- package/src/forms/types.ts +476 -0
- package/src/index.ts +542 -0
- package/src/keyboard/focus.ts +748 -0
- package/src/keyboard/index.ts +96 -0
- package/src/keyboard/integration.ts +371 -0
- package/src/keyboard/manager.ts +377 -0
- package/src/keyboard/presets.ts +90 -0
- package/src/renderers/ansi-css.ts +576 -0
- package/src/renderers/ansi.ts +802 -0
- package/src/renderers/ascii.ts +680 -0
- package/src/renderers/breadcrumb.ts +480 -0
- package/src/renderers/command-palette.ts +802 -0
- package/src/renderers/components/field.ts +210 -0
- package/src/renderers/components/form.ts +327 -0
- package/src/renderers/components/index.ts +21 -0
- package/src/renderers/components/search.ts +449 -0
- package/src/renderers/components/select.ts +222 -0
- package/src/renderers/index.ts +101 -0
- package/src/renderers/interactive/component-handlers.ts +622 -0
- package/src/renderers/interactive/cursor-manager.ts +147 -0
- package/src/renderers/interactive/focus-manager.ts +279 -0
- package/src/renderers/interactive/index.ts +661 -0
- package/src/renderers/interactive/input-handler.ts +164 -0
- package/src/renderers/interactive/keyboard-handler.ts +212 -0
- package/src/renderers/interactive/mouse-handler.ts +167 -0
- package/src/renderers/interactive/state-manager.ts +109 -0
- package/src/renderers/interactive/types.ts +338 -0
- package/src/renderers/interactive-string.ts +299 -0
- package/src/renderers/interactive.ts +59 -0
- package/src/renderers/markdown.ts +950 -0
- package/src/renderers/sidebar.ts +549 -0
- package/src/renderers/tabs.ts +682 -0
- package/src/renderers/text.ts +791 -0
- package/src/renderers/unicode.ts +917 -0
- package/src/renderers/utils.ts +942 -0
- package/src/router/adapters.ts +383 -0
- package/src/router/types.ts +140 -0
- package/src/router/utils.ts +452 -0
- package/src/schemas.ts +205 -0
- package/src/storybook/index.ts +91 -0
- package/src/storybook/interactive-decorator.tsx +659 -0
- package/src/storybook/keyboard-simulator.ts +501 -0
- package/src/theme/ansi-codes.ts +80 -0
- package/src/theme/box-drawing.ts +132 -0
- package/src/theme/color-convert.ts +254 -0
- package/src/theme/color-support.ts +321 -0
- package/src/theme/index.ts +134 -0
- package/src/theme/strip-ansi.ts +50 -0
- package/src/theme/tailwind-map.ts +469 -0
- package/src/theme/text-styles.ts +206 -0
- package/src/theme/theme-system.ts +568 -0
- 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
|
+
})
|