@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,923 @@
1
+ /**
2
+ * @mdxui/terminal Breadcrumb Navigation Component Tests
3
+ *
4
+ * TDD RED Phase: Tests for the Breadcrumb navigation component.
5
+ * All tests should FAIL initially because the Breadcrumb renderers don't exist yet.
6
+ *
7
+ * The Breadcrumb component shows navigation path hierarchy with:
8
+ * - Segments (path items)
9
+ * - Current segment indication
10
+ * - Clickable links (except current)
11
+ * - Customizable separators
12
+ * - Responsive truncation for long paths
13
+ *
14
+ * This is part of the Universal Terminal UI 6-tier rendering system.
15
+ */
16
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
17
+ import type { UINode, RenderTier, RenderContext, ThemeTokens } from '../../../core/types'
18
+
19
+ // ============================================================================
20
+ // Test Utilities
21
+ // ============================================================================
22
+
23
+ const RENDER_TIERS: RenderTier[] = ['text', 'markdown', 'ascii', 'unicode', 'ansi', 'interactive']
24
+
25
+ function createTestTheme(): ThemeTokens {
26
+ return {
27
+ primary: '\x1b[34m',
28
+ secondary: '\x1b[36m',
29
+ muted: '\x1b[90m',
30
+ foreground: '\x1b[37m',
31
+ background: '\x1b[40m',
32
+ border: '\x1b[90m',
33
+ success: '\x1b[32m',
34
+ warning: '\x1b[33m',
35
+ error: '\x1b[31m',
36
+ info: '\x1b[34m',
37
+ }
38
+ }
39
+
40
+ function createContext(tier: RenderTier): RenderContext {
41
+ return {
42
+ tier,
43
+ width: 80,
44
+ height: 24,
45
+ depth: 0,
46
+ theme: createTestTheme(),
47
+ interactive: tier === 'interactive',
48
+ }
49
+ }
50
+
51
+ // ============================================================================
52
+ // Breadcrumb Types
53
+ // ============================================================================
54
+
55
+ interface BreadcrumbSegment {
56
+ label: string
57
+ path?: string
58
+ icon?: string
59
+ }
60
+
61
+ interface BreadcrumbProps {
62
+ segments: BreadcrumbSegment[]
63
+ separator?: string
64
+ maxItems?: number
65
+ showHome?: boolean
66
+ homeLabel?: string
67
+ homeIcon?: string
68
+ onNavigate?: (path: string) => void
69
+ }
70
+
71
+ function createBreadcrumbNode(props: BreadcrumbProps): UINode {
72
+ return {
73
+ type: 'breadcrumb',
74
+ props,
75
+ children: props.segments.map((segment, index) => ({
76
+ type: 'breadcrumb-segment',
77
+ props: {
78
+ label: segment.label,
79
+ path: segment.path,
80
+ icon: segment.icon,
81
+ isCurrent: index === props.segments.length - 1,
82
+ },
83
+ })),
84
+ }
85
+ }
86
+
87
+ // ============================================================================
88
+ // Test Suite: Basic Breadcrumb Rendering
89
+ // ============================================================================
90
+
91
+ describe('Breadcrumb Component', () => {
92
+ describe('basic rendering', () => {
93
+ it('renders breadcrumb with segments', async () => {
94
+ const { renderBreadcrumb } = await import('../../../renderers/breadcrumb')
95
+
96
+ const node = createBreadcrumbNode({
97
+ segments: [
98
+ { label: 'Home', path: '/' },
99
+ { label: 'Products', path: '/products' },
100
+ { label: 'Electronics' },
101
+ ],
102
+ })
103
+
104
+ const ctx = createContext('unicode')
105
+ const result = renderBreadcrumb(node, ctx)
106
+
107
+ expect(result).toContain('Home')
108
+ expect(result).toContain('Products')
109
+ expect(result).toContain('Electronics')
110
+ })
111
+
112
+ it('renders single segment', async () => {
113
+ const { renderBreadcrumb } = await import('../../../renderers/breadcrumb')
114
+
115
+ const node = createBreadcrumbNode({
116
+ segments: [{ label: 'Dashboard' }],
117
+ })
118
+
119
+ const ctx = createContext('unicode')
120
+ const result = renderBreadcrumb(node, ctx)
121
+
122
+ expect(result).toContain('Dashboard')
123
+ // Single segment should not have separator
124
+ expect(result).not.toMatch(/[\/>\u203A\u2192]/)
125
+ })
126
+
127
+ it('returns empty string for empty segments', async () => {
128
+ const { renderBreadcrumb } = await import('../../../renderers/breadcrumb')
129
+
130
+ const node = createBreadcrumbNode({ segments: [] })
131
+ const ctx = createContext('unicode')
132
+ const result = renderBreadcrumb(node, ctx)
133
+
134
+ expect(result).toBe('')
135
+ })
136
+
137
+ it('renders segments with icons', async () => {
138
+ const { renderBreadcrumb } = await import('../../../renderers/breadcrumb')
139
+
140
+ const node = createBreadcrumbNode({
141
+ segments: [
142
+ { label: 'Home', path: '/', icon: '\u2302' },
143
+ { label: 'Settings', path: '/settings', icon: '\u2699' },
144
+ { label: 'Profile', icon: '\u263A' },
145
+ ],
146
+ })
147
+
148
+ const ctx = createContext('unicode')
149
+ const result = renderBreadcrumb(node, ctx)
150
+
151
+ expect(result).toContain('\u2302')
152
+ expect(result).toContain('\u2699')
153
+ expect(result).toContain('\u263A')
154
+ })
155
+ })
156
+
157
+ // ============================================================================
158
+ // Separator Tests
159
+ // ============================================================================
160
+
161
+ describe('separators', () => {
162
+ it('uses default separator', async () => {
163
+ const { renderBreadcrumb } = await import('../../../renderers/breadcrumb')
164
+
165
+ const node = createBreadcrumbNode({
166
+ segments: [
167
+ { label: 'Home', path: '/' },
168
+ { label: 'About' },
169
+ ],
170
+ })
171
+
172
+ const ctx = createContext('unicode')
173
+ const result = renderBreadcrumb(node, ctx)
174
+
175
+ // Default separator should be right-pointing arrow or slash
176
+ expect(result).toMatch(/[\/>\u203A\u2192\u25B8\u276F]/)
177
+ })
178
+
179
+ it('uses custom separator', async () => {
180
+ const { renderBreadcrumb } = await import('../../../renderers/breadcrumb')
181
+
182
+ const node = createBreadcrumbNode({
183
+ segments: [
184
+ { label: 'Home', path: '/' },
185
+ { label: 'Products', path: '/products' },
186
+ { label: 'Item' },
187
+ ],
188
+ separator: '::',
189
+ })
190
+
191
+ const ctx = createContext('unicode')
192
+ const result = renderBreadcrumb(node, ctx)
193
+
194
+ expect(result).toContain('::')
195
+ expect(result).toContain('Home')
196
+ expect(result).toContain('Products')
197
+ expect(result).toContain('Item')
198
+ })
199
+
200
+ it('text tier uses slash separator', async () => {
201
+ const { renderBreadcrumb } = await import('../../../renderers/breadcrumb')
202
+
203
+ const node = createBreadcrumbNode({
204
+ segments: [
205
+ { label: 'Home', path: '/' },
206
+ { label: 'Page' },
207
+ ],
208
+ })
209
+
210
+ const ctx = createContext('text')
211
+ const result = renderBreadcrumb(node, ctx)
212
+
213
+ // Text tier should use simple / or >
214
+ expect(result).toMatch(/\/|>/)
215
+ })
216
+
217
+ it('ascii tier uses > or / separator', async () => {
218
+ const { renderBreadcrumb } = await import('../../../renderers/breadcrumb')
219
+
220
+ const node = createBreadcrumbNode({
221
+ segments: [
222
+ { label: 'Level 1', path: '/1' },
223
+ { label: 'Level 2' },
224
+ ],
225
+ })
226
+
227
+ const ctx = createContext('ascii')
228
+ const result = renderBreadcrumb(node, ctx)
229
+
230
+ expect(result).toMatch(/[\/>\-]/)
231
+ })
232
+
233
+ it('unicode tier uses chevron or arrow', async () => {
234
+ const { renderBreadcrumb } = await import('../../../renderers/breadcrumb')
235
+
236
+ const node = createBreadcrumbNode({
237
+ segments: [
238
+ { label: 'Root', path: '/' },
239
+ { label: 'Child' },
240
+ ],
241
+ })
242
+
243
+ const ctx = createContext('unicode')
244
+ const result = renderBreadcrumb(node, ctx)
245
+
246
+ // Unicode uses fancy arrows
247
+ expect(result).toMatch(/[\u203A\u2192\u25B8\u276F\u2794]/)
248
+ })
249
+
250
+ it('separators appear between all segments', async () => {
251
+ const { renderBreadcrumb } = await import('../../../renderers/breadcrumb')
252
+
253
+ const node = createBreadcrumbNode({
254
+ segments: [
255
+ { label: 'A', path: '/a' },
256
+ { label: 'B', path: '/b' },
257
+ { label: 'C', path: '/c' },
258
+ { label: 'D' },
259
+ ],
260
+ separator: '|',
261
+ })
262
+
263
+ const ctx = createContext('unicode')
264
+ const result = renderBreadcrumb(node, ctx)
265
+
266
+ // Count separators (should be n-1 where n is number of segments)
267
+ const separatorCount = (result.match(/\|/g) || []).length
268
+ expect(separatorCount).toBe(3)
269
+ })
270
+ })
271
+
272
+ // ============================================================================
273
+ // Current Segment Tests
274
+ // ============================================================================
275
+
276
+ describe('current segment indication', () => {
277
+ it('marks last segment as current', async () => {
278
+ const { renderBreadcrumb } = await import('../../../renderers/breadcrumb')
279
+
280
+ const node = createBreadcrumbNode({
281
+ segments: [
282
+ { label: 'Home', path: '/' },
283
+ { label: 'Products', path: '/products' },
284
+ { label: 'Current Item' },
285
+ ],
286
+ })
287
+
288
+ const ctx = createContext('ansi')
289
+ const result = renderBreadcrumb(node, ctx)
290
+
291
+ // Current segment should have different styling
292
+ expect(result).toContain('Current Item')
293
+
294
+ // Current segment typically bold or different color
295
+ const currentIndex = result.indexOf('Current Item')
296
+ const beforeCurrent = result.slice(Math.max(0, currentIndex - 20), currentIndex)
297
+
298
+ // Should have styling code before current
299
+ expect(beforeCurrent).toMatch(/\x1b\[.*m/)
300
+ })
301
+
302
+ it('current segment is not clickable (no path)', async () => {
303
+ const { renderBreadcrumb } = await import('../../../renderers/breadcrumb')
304
+
305
+ const node = createBreadcrumbNode({
306
+ segments: [
307
+ { label: 'Home', path: '/' },
308
+ { label: 'Current' }, // No path = current page
309
+ ],
310
+ })
311
+
312
+ const ctx = createContext('ansi')
313
+ const result = renderBreadcrumb(node, ctx)
314
+
315
+ // Verify current is present
316
+ expect(result).toContain('Current')
317
+ })
318
+
319
+ it('text tier shows current with different marker', async () => {
320
+ const { renderBreadcrumb } = await import('../../../renderers/breadcrumb')
321
+
322
+ const node = createBreadcrumbNode({
323
+ segments: [
324
+ { label: 'Home', path: '/' },
325
+ { label: 'Current Page' },
326
+ ],
327
+ })
328
+
329
+ const ctx = createContext('text')
330
+ const result = renderBreadcrumb(node, ctx)
331
+
332
+ // Current might be marked with brackets, bold indicator, or no link
333
+ // The current page typically appears plainly while others might be [bracketed]
334
+ expect(result).toContain('Current Page')
335
+ })
336
+
337
+ it('previous segments are shown as links', async () => {
338
+ const { renderBreadcrumb } = await import('../../../renderers/breadcrumb')
339
+
340
+ const node = createBreadcrumbNode({
341
+ segments: [
342
+ { label: 'Home', path: '/' },
343
+ { label: 'Products', path: '/products' },
344
+ { label: 'Current' },
345
+ ],
346
+ })
347
+
348
+ const ctx = createContext('ansi')
349
+ const result = renderBreadcrumb(node, ctx)
350
+
351
+ // Links should have underline or different color
352
+ expect(result).toContain('Home')
353
+ expect(result).toContain('Products')
354
+
355
+ // Link styling (underline code is 4m)
356
+ expect(result).toMatch(/\x1b\[4m|\x1b\[34m|\x1b\[36m/)
357
+ })
358
+ })
359
+
360
+ // ============================================================================
361
+ // Link Navigation Tests
362
+ // ============================================================================
363
+
364
+ describe('link navigation', () => {
365
+ it('markdown tier renders links as markdown', async () => {
366
+ const { renderBreadcrumb } = await import('../../../renderers/breadcrumb')
367
+
368
+ const node = createBreadcrumbNode({
369
+ segments: [
370
+ { label: 'Home', path: '/' },
371
+ { label: 'Products', path: '/products' },
372
+ { label: 'Current' },
373
+ ],
374
+ })
375
+
376
+ const ctx = createContext('markdown')
377
+ const result = renderBreadcrumb(node, ctx)
378
+
379
+ // Markdown links: [label](path)
380
+ expect(result).toMatch(/\[Home\]\(\/\)/)
381
+ expect(result).toMatch(/\[Products\]\(\/products\)/)
382
+ })
383
+
384
+ it('segments with paths are navigable', async () => {
385
+ const { createBreadcrumbState, handleBreadcrumbClick } = await import('../../../renderers/breadcrumb')
386
+
387
+ const onNavigate = vi.fn()
388
+
389
+ const state = createBreadcrumbState({
390
+ segments: [
391
+ { label: 'Home', path: '/' },
392
+ { label: 'Products', path: '/products' },
393
+ { label: 'Current' },
394
+ ],
395
+ onNavigate,
396
+ })
397
+
398
+ // Click on Products
399
+ handleBreadcrumbClick(state, 1)
400
+
401
+ expect(onNavigate).toHaveBeenCalledWith('/products')
402
+ })
403
+
404
+ it('clicking current segment does nothing', async () => {
405
+ const { createBreadcrumbState, handleBreadcrumbClick } = await import('../../../renderers/breadcrumb')
406
+
407
+ const onNavigate = vi.fn()
408
+
409
+ const state = createBreadcrumbState({
410
+ segments: [
411
+ { label: 'Home', path: '/' },
412
+ { label: 'Current' },
413
+ ],
414
+ onNavigate,
415
+ })
416
+
417
+ // Click on current (index 1, last item)
418
+ handleBreadcrumbClick(state, 1)
419
+
420
+ expect(onNavigate).not.toHaveBeenCalled()
421
+ })
422
+ })
423
+
424
+ // ============================================================================
425
+ // Home Segment Tests
426
+ // ============================================================================
427
+
428
+ describe('home segment', () => {
429
+ it('can show home icon automatically', async () => {
430
+ const { renderBreadcrumb } = await import('../../../renderers/breadcrumb')
431
+
432
+ const node = createBreadcrumbNode({
433
+ segments: [
434
+ { label: 'Products', path: '/products' },
435
+ { label: 'Item' },
436
+ ],
437
+ showHome: true,
438
+ homeIcon: '\u2302',
439
+ })
440
+
441
+ const ctx = createContext('unicode')
442
+ const result = renderBreadcrumb(node, ctx)
443
+
444
+ // Home icon should be prepended
445
+ expect(result).toContain('\u2302')
446
+
447
+ // Products should come after home
448
+ const homeIndex = result.indexOf('\u2302')
449
+ const productsIndex = result.indexOf('Products')
450
+ expect(homeIndex).toBeLessThan(productsIndex)
451
+ })
452
+
453
+ it('uses custom home label', async () => {
454
+ const { renderBreadcrumb } = await import('../../../renderers/breadcrumb')
455
+
456
+ const node = createBreadcrumbNode({
457
+ segments: [
458
+ { label: 'Section', path: '/section' },
459
+ { label: 'Page' },
460
+ ],
461
+ showHome: true,
462
+ homeLabel: 'Dashboard',
463
+ })
464
+
465
+ const ctx = createContext('unicode')
466
+ const result = renderBreadcrumb(node, ctx)
467
+
468
+ expect(result).toContain('Dashboard')
469
+ })
470
+
471
+ it('home is always first segment when shown', async () => {
472
+ const { renderBreadcrumb } = await import('../../../renderers/breadcrumb')
473
+
474
+ const node = createBreadcrumbNode({
475
+ segments: [
476
+ { label: 'A', path: '/a' },
477
+ { label: 'B', path: '/b' },
478
+ { label: 'C' },
479
+ ],
480
+ showHome: true,
481
+ homeLabel: 'Home',
482
+ })
483
+
484
+ const ctx = createContext('text')
485
+ const result = renderBreadcrumb(node, ctx)
486
+
487
+ // Home should appear before A
488
+ const homeIndex = result.indexOf('Home')
489
+ const aIndex = result.indexOf('A')
490
+
491
+ expect(homeIndex).toBeGreaterThanOrEqual(0)
492
+ expect(homeIndex).toBeLessThan(aIndex)
493
+ })
494
+ })
495
+
496
+ // ============================================================================
497
+ // Truncation Tests
498
+ // ============================================================================
499
+
500
+ describe('truncation', () => {
501
+ it('truncates long paths with ellipsis', async () => {
502
+ const { renderBreadcrumb } = await import('../../../renderers/breadcrumb')
503
+
504
+ const node = createBreadcrumbNode({
505
+ segments: [
506
+ { label: 'Level 1', path: '/1' },
507
+ { label: 'Level 2', path: '/2' },
508
+ { label: 'Level 3', path: '/3' },
509
+ { label: 'Level 4', path: '/4' },
510
+ { label: 'Level 5', path: '/5' },
511
+ { label: 'Current' },
512
+ ],
513
+ maxItems: 3,
514
+ })
515
+
516
+ const ctx = createContext('unicode')
517
+ const result = renderBreadcrumb(node, ctx)
518
+
519
+ // Should show ellipsis
520
+ expect(result).toMatch(/\u2026|\.\.\./)
521
+
522
+ // Should show first and last segments
523
+ expect(result).toContain('Level 1')
524
+ expect(result).toContain('Current')
525
+
526
+ // Middle items should be hidden
527
+ expect(result).not.toContain('Level 3')
528
+ expect(result).not.toContain('Level 4')
529
+ })
530
+
531
+ it('shows all segments when under maxItems', async () => {
532
+ const { renderBreadcrumb } = await import('../../../renderers/breadcrumb')
533
+
534
+ const node = createBreadcrumbNode({
535
+ segments: [
536
+ { label: 'Home', path: '/' },
537
+ { label: 'Products', path: '/products' },
538
+ { label: 'Current' },
539
+ ],
540
+ maxItems: 5,
541
+ })
542
+
543
+ const ctx = createContext('unicode')
544
+ const result = renderBreadcrumb(node, ctx)
545
+
546
+ // All segments should be visible
547
+ expect(result).toContain('Home')
548
+ expect(result).toContain('Products')
549
+ expect(result).toContain('Current')
550
+
551
+ // No ellipsis
552
+ expect(result).not.toMatch(/\u2026|\.\.\./)
553
+ })
554
+
555
+ it('respects context width for responsive truncation', async () => {
556
+ const { renderBreadcrumb } = await import('../../../renderers/breadcrumb')
557
+
558
+ const node = createBreadcrumbNode({
559
+ segments: [
560
+ { label: 'Very Long First Segment Name', path: '/1' },
561
+ { label: 'Another Long Segment', path: '/2' },
562
+ { label: 'Final Long Current Segment' },
563
+ ],
564
+ })
565
+
566
+ const narrowCtx = createContext('unicode')
567
+ narrowCtx.width = 40
568
+
569
+ const result = renderBreadcrumb(node, narrowCtx)
570
+
571
+ // Result should fit within width
572
+ const lines = result.split('\n')
573
+ for (const line of lines) {
574
+ const stripped = line.replace(/\x1b\[[\d;]*m/g, '')
575
+ expect(stripped.length).toBeLessThanOrEqual(40)
576
+ }
577
+ })
578
+
579
+ it('truncates individual segment labels when too long', async () => {
580
+ const { renderBreadcrumb } = await import('../../../renderers/breadcrumb')
581
+
582
+ const node = createBreadcrumbNode({
583
+ segments: [
584
+ { label: 'This Is An Extremely Long Breadcrumb Label That Should Be Truncated', path: '/1' },
585
+ { label: 'Short' },
586
+ ],
587
+ })
588
+
589
+ const ctx = createContext('unicode')
590
+ ctx.width = 50
591
+
592
+ const result = renderBreadcrumb(node, ctx)
593
+
594
+ // Long label should be truncated with ellipsis
595
+ expect(result).toMatch(/\u2026|\.\.\./)
596
+ })
597
+ })
598
+
599
+ // ============================================================================
600
+ // Tier-Specific Rendering Tests
601
+ // ============================================================================
602
+
603
+ describe('tier-specific rendering', () => {
604
+ const sampleNode = createBreadcrumbNode({
605
+ segments: [
606
+ { label: 'Home', path: '/' },
607
+ { label: 'Products', path: '/products' },
608
+ { label: 'Electronics', path: '/products/electronics' },
609
+ { label: 'Phones' },
610
+ ],
611
+ })
612
+
613
+ it('text tier renders plain text', async () => {
614
+ const { renderBreadcrumb } = await import('../../../renderers/breadcrumb')
615
+
616
+ const ctx = createContext('text')
617
+ const result = renderBreadcrumb(sampleNode, ctx)
618
+
619
+ // Should contain all labels
620
+ expect(result).toContain('Home')
621
+ expect(result).toContain('Products')
622
+ expect(result).toContain('Electronics')
623
+ expect(result).toContain('Phones')
624
+
625
+ // No ANSI codes
626
+ expect(result).not.toContain('\x1b[')
627
+ })
628
+
629
+ it('markdown tier uses markdown links', async () => {
630
+ const { renderBreadcrumb } = await import('../../../renderers/breadcrumb')
631
+
632
+ const ctx = createContext('markdown')
633
+ const result = renderBreadcrumb(sampleNode, ctx)
634
+
635
+ // Markdown links
636
+ expect(result).toMatch(/\[.*\]\(.*\)/)
637
+ })
638
+
639
+ it('ascii tier uses simple characters', async () => {
640
+ const { renderBreadcrumb } = await import('../../../renderers/breadcrumb')
641
+
642
+ const ctx = createContext('ascii')
643
+ const result = renderBreadcrumb(sampleNode, ctx)
644
+
645
+ // Should use ASCII separators
646
+ expect(result).toMatch(/[\/>\-]/)
647
+
648
+ // Should not use unicode
649
+ expect(result).not.toMatch(/[\u203A\u2192\u25B8]/)
650
+ })
651
+
652
+ it('unicode tier uses fancy arrows', async () => {
653
+ const { renderBreadcrumb } = await import('../../../renderers/breadcrumb')
654
+
655
+ const ctx = createContext('unicode')
656
+ const result = renderBreadcrumb(sampleNode, ctx)
657
+
658
+ // Unicode arrows or chevrons
659
+ expect(result).toMatch(/[\u203A\u2192\u25B8\u276F]/)
660
+ })
661
+
662
+ it('ansi tier includes colors', async () => {
663
+ const { renderBreadcrumb } = await import('../../../renderers/breadcrumb')
664
+
665
+ const ctx = createContext('ansi')
666
+ const result = renderBreadcrumb(sampleNode, ctx)
667
+
668
+ // ANSI color codes
669
+ expect(result).toContain('\x1b[')
670
+ })
671
+
672
+ it('interactive tier includes focus indicators', async () => {
673
+ const { renderBreadcrumb } = await import('../../../renderers/breadcrumb')
674
+
675
+ const ctx = createContext('interactive')
676
+ const result = renderBreadcrumb(sampleNode, ctx)
677
+
678
+ // Should have interactive styling
679
+ expect(result).toContain('\x1b[')
680
+ })
681
+ })
682
+
683
+ // ============================================================================
684
+ // Keyboard Navigation Tests (INTERACTIVE Tier)
685
+ // ============================================================================
686
+
687
+ describe('keyboard navigation (interactive tier)', () => {
688
+ it('provides keyboard bindings', async () => {
689
+ const { getBreadcrumbKeyBindings } = await import('../../../renderers/breadcrumb')
690
+
691
+ const bindings = getBreadcrumbKeyBindings()
692
+
693
+ expect(bindings.h).toBe('focus-prev')
694
+ expect(bindings.l).toBe('focus-next')
695
+ expect(bindings.left).toBe('focus-prev')
696
+ expect(bindings.right).toBe('focus-next')
697
+ expect(bindings.enter).toBe('navigate')
698
+ expect(bindings.escape).toBe('blur')
699
+ })
700
+
701
+ it('h/l or left/right moves focus between segments', async () => {
702
+ const { createBreadcrumbState, handleBreadcrumbKey } = await import('../../../renderers/breadcrumb')
703
+
704
+ const state = createBreadcrumbState({
705
+ segments: [
706
+ { label: 'Home', path: '/' },
707
+ { label: 'Products', path: '/products' },
708
+ { label: 'Current' },
709
+ ],
710
+ focusedIndex: 0,
711
+ })
712
+
713
+ // Move right
714
+ const afterL = handleBreadcrumbKey(state, 'l')
715
+ expect(afterL.focusedIndex).toBe(1)
716
+
717
+ const afterL2 = handleBreadcrumbKey(afterL, 'right')
718
+ expect(afterL2.focusedIndex).toBe(2)
719
+
720
+ // Move left
721
+ const afterH = handleBreadcrumbKey(afterL2, 'h')
722
+ expect(afterH.focusedIndex).toBe(1)
723
+
724
+ const afterLeft = handleBreadcrumbKey(afterH, 'left')
725
+ expect(afterLeft.focusedIndex).toBe(0)
726
+ })
727
+
728
+ it('enter navigates to focused segment', async () => {
729
+ const { createBreadcrumbState, handleBreadcrumbKey } = await import('../../../renderers/breadcrumb')
730
+
731
+ const onNavigate = vi.fn()
732
+
733
+ const state = createBreadcrumbState({
734
+ segments: [
735
+ { label: 'Home', path: '/' },
736
+ { label: 'Products', path: '/products' },
737
+ { label: 'Current' },
738
+ ],
739
+ focusedIndex: 1,
740
+ onNavigate,
741
+ })
742
+
743
+ handleBreadcrumbKey(state, 'enter')
744
+
745
+ expect(onNavigate).toHaveBeenCalledWith('/products')
746
+ })
747
+
748
+ it('enter on current segment does nothing', async () => {
749
+ const { createBreadcrumbState, handleBreadcrumbKey } = await import('../../../renderers/breadcrumb')
750
+
751
+ const onNavigate = vi.fn()
752
+
753
+ const state = createBreadcrumbState({
754
+ segments: [
755
+ { label: 'Home', path: '/' },
756
+ { label: 'Current' },
757
+ ],
758
+ focusedIndex: 1,
759
+ onNavigate,
760
+ })
761
+
762
+ handleBreadcrumbKey(state, 'enter')
763
+
764
+ expect(onNavigate).not.toHaveBeenCalled()
765
+ })
766
+
767
+ it('focus wraps at boundaries', async () => {
768
+ const { createBreadcrumbState, handleBreadcrumbKey } = await import('../../../renderers/breadcrumb')
769
+
770
+ const state = createBreadcrumbState({
771
+ segments: [
772
+ { label: 'Home', path: '/' },
773
+ { label: 'Products', path: '/products' },
774
+ { label: 'Current' },
775
+ ],
776
+ focusedIndex: 2,
777
+ wrapNavigation: true,
778
+ })
779
+
780
+ // Move right from last should wrap to first
781
+ const afterL = handleBreadcrumbKey(state, 'l')
782
+ expect(afterL.focusedIndex).toBe(0)
783
+ })
784
+
785
+ it('focus stops at boundaries when wrap disabled', async () => {
786
+ const { createBreadcrumbState, handleBreadcrumbKey } = await import('../../../renderers/breadcrumb')
787
+
788
+ const state = createBreadcrumbState({
789
+ segments: [
790
+ { label: 'Home', path: '/' },
791
+ { label: 'Current' },
792
+ ],
793
+ focusedIndex: 0,
794
+ wrapNavigation: false,
795
+ })
796
+
797
+ // Move left from first should stay at first
798
+ const afterH = handleBreadcrumbKey(state, 'h')
799
+ expect(afterH.focusedIndex).toBe(0)
800
+ })
801
+
802
+ it('escape blurs focus', async () => {
803
+ const { createBreadcrumbState, handleBreadcrumbKey } = await import('../../../renderers/breadcrumb')
804
+
805
+ const onBlur = vi.fn()
806
+
807
+ const state = createBreadcrumbState({
808
+ segments: [{ label: 'Home', path: '/' }],
809
+ focusedIndex: 0,
810
+ onBlur,
811
+ })
812
+
813
+ handleBreadcrumbKey(state, 'escape')
814
+
815
+ expect(onBlur).toHaveBeenCalled()
816
+ })
817
+
818
+ it('Home key moves to first segment', async () => {
819
+ const { createBreadcrumbState, handleBreadcrumbKey } = await import('../../../renderers/breadcrumb')
820
+
821
+ const state = createBreadcrumbState({
822
+ segments: [
823
+ { label: 'Home', path: '/' },
824
+ { label: 'Products', path: '/products' },
825
+ { label: 'Current' },
826
+ ],
827
+ focusedIndex: 2,
828
+ })
829
+
830
+ const afterHome = handleBreadcrumbKey(state, 'home')
831
+ expect(afterHome.focusedIndex).toBe(0)
832
+ })
833
+
834
+ it('End key moves to last segment', async () => {
835
+ const { createBreadcrumbState, handleBreadcrumbKey } = await import('../../../renderers/breadcrumb')
836
+
837
+ const state = createBreadcrumbState({
838
+ segments: [
839
+ { label: 'Home', path: '/' },
840
+ { label: 'Products', path: '/products' },
841
+ { label: 'Current' },
842
+ ],
843
+ focusedIndex: 0,
844
+ })
845
+
846
+ const afterEnd = handleBreadcrumbKey(state, 'end')
847
+ expect(afterEnd.focusedIndex).toBe(2)
848
+ })
849
+ })
850
+
851
+ // ============================================================================
852
+ // Accessibility Tests
853
+ // ============================================================================
854
+
855
+ describe('accessibility', () => {
856
+ it('renders with aria-label attribute data', async () => {
857
+ const { renderBreadcrumb } = await import('../../../renderers/breadcrumb')
858
+
859
+ const node = createBreadcrumbNode({
860
+ segments: [
861
+ { label: 'Home', path: '/' },
862
+ { label: 'Current' },
863
+ ],
864
+ })
865
+
866
+ const ctx = createContext('interactive')
867
+ const result = renderBreadcrumb(node, ctx)
868
+
869
+ // Interactive tier should include accessibility info
870
+ // This could be in comments or structured data
871
+ expect(result).toBeDefined()
872
+ })
873
+
874
+ it('current segment is marked appropriately', async () => {
875
+ const { renderBreadcrumb } = await import('../../../renderers/breadcrumb')
876
+
877
+ const node = createBreadcrumbNode({
878
+ segments: [
879
+ { label: 'Home', path: '/' },
880
+ { label: 'Products', path: '/products' },
881
+ { label: 'Current Page' },
882
+ ],
883
+ })
884
+
885
+ const ctx = createContext('ansi')
886
+ const result = renderBreadcrumb(node, ctx)
887
+
888
+ // Current should be distinguishable (different styling)
889
+ expect(result).toContain('Current Page')
890
+ })
891
+ })
892
+
893
+ // ============================================================================
894
+ // All Tiers Rendering Tests
895
+ // ============================================================================
896
+
897
+ describe('renders across all tiers', () => {
898
+ const sampleNode = createBreadcrumbNode({
899
+ segments: [
900
+ { label: 'Root', path: '/' },
901
+ { label: 'Branch', path: '/branch' },
902
+ { label: 'Leaf' },
903
+ ],
904
+ })
905
+
906
+ RENDER_TIERS.forEach((tier) => {
907
+ it(`renders on ${tier} tier`, async () => {
908
+ const { renderBreadcrumb } = await import('../../../renderers/breadcrumb')
909
+
910
+ const ctx = createContext(tier)
911
+ const result = renderBreadcrumb(sampleNode, ctx)
912
+
913
+ // Should produce output
914
+ expect(result.length).toBeGreaterThan(0)
915
+
916
+ // Should contain all segment labels
917
+ expect(result).toContain('Root')
918
+ expect(result).toContain('Branch')
919
+ expect(result).toContain('Leaf')
920
+ })
921
+ })
922
+ })
923
+ })