@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,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
|
+
})
|