@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,448 @@
1
+ /**
2
+ * @mdxui/terminal Table Component Tests (RED phase)
3
+ *
4
+ * TDD RED Phase: These tests define the contract for the Table data component
5
+ * across all 6 render tiers (TEXT, MARKDOWN, ASCII, UNICODE, ANSI, INTERACTIVE).
6
+ *
7
+ * Table Component Features:
8
+ * - Column definitions with headers and keys
9
+ * - Row data rendering
10
+ * - Row selection (single/multi)
11
+ * - Column sorting
12
+ * - Row actions
13
+ * - Pagination
14
+ * - Empty state handling
15
+ *
16
+ * NOTE: These tests are expected to FAIL until implementation is complete.
17
+ * Run: pnpm --filter @mdxui/terminal test -- --run src/__tests__/components/data/table.test.ts
18
+ */
19
+ import { describe, it, expect } from 'vitest'
20
+
21
+ import type { UINode, RenderContext, ThemeTokens, RenderTier } from '../../../core/types'
22
+
23
+ // ============================================================================
24
+ // Test Helpers
25
+ // ============================================================================
26
+
27
+ const ALL_TIERS: RenderTier[] = ['text', 'markdown', 'ascii', 'unicode', 'ansi', 'interactive']
28
+
29
+ function createTestTheme(): ThemeTokens {
30
+ return {
31
+ primary: '\x1b[34m',
32
+ secondary: '\x1b[36m',
33
+ muted: '\x1b[90m',
34
+ foreground: '\x1b[37m',
35
+ background: '\x1b[40m',
36
+ border: '\x1b[90m',
37
+ success: '\x1b[32m',
38
+ warning: '\x1b[33m',
39
+ error: '\x1b[31m',
40
+ info: '\x1b[34m',
41
+ }
42
+ }
43
+
44
+ function createTestContext(tier: RenderTier, overrides?: Partial<RenderContext>): RenderContext {
45
+ return {
46
+ tier,
47
+ width: 80,
48
+ height: 24,
49
+ depth: 0,
50
+ theme: createTestTheme(),
51
+ interactive: tier === 'interactive',
52
+ ...overrides,
53
+ }
54
+ }
55
+
56
+ function createTableNode(props: Record<string, unknown>, data?: unknown[]): UINode {
57
+ return {
58
+ type: 'table',
59
+ props,
60
+ data,
61
+ }
62
+ }
63
+
64
+ // ============================================================================
65
+ // Renderer import helper - uses dynamic import for interactive tier
66
+ // ============================================================================
67
+
68
+ async function renderForTier(node: UINode, tier: RenderTier, ctx?: RenderContext): Promise<string> {
69
+ const context = ctx ?? createTestContext(tier)
70
+ switch (tier) {
71
+ case 'text': {
72
+ const { renderText } = await import('../../../renderers/text')
73
+ return renderText(node)
74
+ }
75
+ case 'markdown': {
76
+ const { renderMarkdown } = await import('../../../renderers/markdown')
77
+ return renderMarkdown(node)
78
+ }
79
+ case 'ascii': {
80
+ const { renderASCII } = await import('../../../renderers/ascii')
81
+ return renderASCII(node, context)
82
+ }
83
+ case 'unicode': {
84
+ const { renderUnicode } = await import('../../../renderers/unicode')
85
+ return renderUnicode(node, context)
86
+ }
87
+ case 'ansi': {
88
+ const { renderANSI } = await import('../../../renderers/ansi')
89
+ return renderANSI(node)
90
+ }
91
+ case 'interactive': {
92
+ const { renderInteractive } = await import('../../../renderers/interactive')
93
+ return renderInteractive(node, context)
94
+ }
95
+ }
96
+ }
97
+
98
+ // ============================================================================
99
+ // Basic Table Rendering Tests
100
+ // ============================================================================
101
+
102
+ describe('Table Component', () => {
103
+ describe('basic rendering across all tiers', () => {
104
+ const columns = [
105
+ { key: 'name', header: 'Name' },
106
+ { key: 'email', header: 'Email' },
107
+ { key: 'role', header: 'Role' },
108
+ ]
109
+ const data = [
110
+ { name: 'Alice', email: 'alice@example.com', role: 'Admin' },
111
+ { name: 'Bob', email: 'bob@example.com', role: 'User' },
112
+ { name: 'Carol', email: 'carol@example.com', role: 'User' },
113
+ ]
114
+
115
+ ALL_TIERS.forEach((tier) => {
116
+ describe(`${tier.toUpperCase()} tier`, () => {
117
+ it('renders column headers', async () => {
118
+ const node = createTableNode({ columns }, data)
119
+ const result = await renderForTier(node, tier)
120
+
121
+ expect(result).toContain('Name')
122
+ expect(result).toContain('Email')
123
+ expect(result).toContain('Role')
124
+ })
125
+
126
+ it('renders row data', async () => {
127
+ const node = createTableNode({ columns }, data)
128
+ const result = await renderForTier(node, tier)
129
+
130
+ expect(result).toContain('Alice')
131
+ expect(result).toContain('alice@example.com')
132
+ expect(result).toContain('Admin')
133
+ expect(result).toContain('Bob')
134
+ expect(result).toContain('Carol')
135
+ })
136
+
137
+ it('handles empty data array', async () => {
138
+ const node = createTableNode({ columns }, [])
139
+ const result = await renderForTier(node, tier)
140
+
141
+ expect(result).toContain('Name')
142
+ expect(typeof result).toBe('string')
143
+ })
144
+
145
+ it('handles missing column values', async () => {
146
+ const node = createTableNode({ columns }, [{ name: 'Dave' }])
147
+ const result = await renderForTier(node, tier)
148
+
149
+ expect(result).toContain('Dave')
150
+ expect(typeof result).toBe('string')
151
+ })
152
+ })
153
+ })
154
+ })
155
+
156
+ // ============================================================================
157
+ // Column Configuration Tests
158
+ // ============================================================================
159
+
160
+ describe('column configuration', () => {
161
+ ALL_TIERS.forEach((tier) => {
162
+ describe(`${tier.toUpperCase()} tier`, () => {
163
+ it('respects column width', async () => {
164
+ const columns = [
165
+ { key: 'id', header: 'ID', width: 5 },
166
+ { key: 'name', header: 'Name', width: 20 },
167
+ ]
168
+ const node = createTableNode({ columns }, [{ id: '1', name: 'Alice' }])
169
+ const result = await renderForTier(node, tier)
170
+
171
+ expect(result).toContain('ID')
172
+ expect(result).toContain('Name')
173
+ })
174
+
175
+ it('renders column with custom align', async () => {
176
+ const columns = [
177
+ { key: 'price', header: 'Price', align: 'right' },
178
+ { key: 'name', header: 'Name', align: 'left' },
179
+ ]
180
+ const node = createTableNode({ columns }, [{ price: '$99.99', name: 'Widget' }])
181
+ const result = await renderForTier(node, tier)
182
+
183
+ expect(result).toContain('Price')
184
+ expect(result).toContain('$99.99')
185
+ })
186
+ })
187
+ })
188
+ })
189
+
190
+ // ============================================================================
191
+ // Row Selection Tests
192
+ // ============================================================================
193
+
194
+ describe('row selection', () => {
195
+ const columns = [
196
+ { key: 'id', header: 'ID' },
197
+ { key: 'name', header: 'Name' },
198
+ ]
199
+ const data = [
200
+ { id: '1', name: 'Alice' },
201
+ { id: '2', name: 'Bob' },
202
+ { id: '3', name: 'Carol' },
203
+ ]
204
+
205
+ ALL_TIERS.forEach((tier) => {
206
+ describe(`${tier.toUpperCase()} tier`, () => {
207
+ it('renders selectable rows with selection indicator', async () => {
208
+ const node = createTableNode({ columns, selectable: true, selectedRows: ['1'] }, data)
209
+ const result = await renderForTier(node, tier)
210
+
211
+ expect(result).toContain('Alice')
212
+ expect(result.length).toBeGreaterThan(0)
213
+ })
214
+
215
+ it('renders multi-select with checkbox indicators', async () => {
216
+ const node = createTableNode({ columns, selectable: 'multi', selectedRows: ['1', '3'] }, data)
217
+ const result = await renderForTier(node, tier)
218
+
219
+ expect(result).toContain('Alice')
220
+ expect(result).toContain('Carol')
221
+ })
222
+
223
+ it('highlights currently selected row', async () => {
224
+ const node = createTableNode({ columns, selectable: true, selectedRows: ['2'] }, data)
225
+ const result = await renderForTier(node, tier)
226
+
227
+ expect(result).toContain('Bob')
228
+ })
229
+ })
230
+ })
231
+ })
232
+
233
+ // ============================================================================
234
+ // Sorting Tests
235
+ // ============================================================================
236
+
237
+ describe('column sorting', () => {
238
+ const columns = [
239
+ { key: 'name', header: 'Name', sortable: true },
240
+ { key: 'age', header: 'Age', sortable: true },
241
+ ]
242
+ const data = [
243
+ { name: 'Carol', age: 35 },
244
+ { name: 'Alice', age: 28 },
245
+ { name: 'Bob', age: 32 },
246
+ ]
247
+
248
+ ALL_TIERS.forEach((tier) => {
249
+ describe(`${tier.toUpperCase()} tier`, () => {
250
+ it('shows sort indicator for sorted column (ascending)', async () => {
251
+ const node = createTableNode({ columns, sortBy: 'name', sortDirection: 'asc' }, data)
252
+ const result = await renderForTier(node, tier)
253
+
254
+ expect(result).toContain('Name')
255
+ expect(result.length).toBeGreaterThan(0)
256
+ })
257
+
258
+ it('shows sort indicator for sorted column (descending)', async () => {
259
+ const node = createTableNode({ columns, sortBy: 'age', sortDirection: 'desc' }, data)
260
+ const result = await renderForTier(node, tier)
261
+
262
+ expect(result).toContain('Age')
263
+ })
264
+ })
265
+ })
266
+ })
267
+
268
+ // ============================================================================
269
+ // Row Actions Tests
270
+ // ============================================================================
271
+
272
+ describe('row actions', () => {
273
+ const columns = [
274
+ { key: 'name', header: 'Name' },
275
+ { key: 'status', header: 'Status' },
276
+ ]
277
+ const data = [
278
+ { name: 'Task 1', status: 'Active' },
279
+ { name: 'Task 2', status: 'Pending' },
280
+ ]
281
+ const actions = [
282
+ { key: 'edit', label: 'Edit', icon: 'pencil' },
283
+ { key: 'delete', label: 'Delete', icon: 'trash' },
284
+ ]
285
+
286
+ ALL_TIERS.forEach((tier) => {
287
+ describe(`${tier.toUpperCase()} tier`, () => {
288
+ it('renders action column', async () => {
289
+ const node = createTableNode({ columns, actions }, data)
290
+ const result = await renderForTier(node, tier)
291
+
292
+ expect(result).toContain('Task 1')
293
+ expect(result.length).toBeGreaterThan(0)
294
+ })
295
+
296
+ it('shows action labels or icons', async () => {
297
+ const node = createTableNode({ columns, actions, showActionLabels: true }, data)
298
+ const result = await renderForTier(node, tier)
299
+
300
+ expect(result).toContain('Task 1')
301
+ })
302
+ })
303
+ })
304
+ })
305
+
306
+ // ============================================================================
307
+ // Tier-Specific Rendering Tests
308
+ // ============================================================================
309
+
310
+ describe('tier-specific rendering', () => {
311
+ const columns = [
312
+ { key: 'name', header: 'Name' },
313
+ { key: 'value', header: 'Value' },
314
+ ]
315
+ const data = [
316
+ { name: 'Item 1', value: '100' },
317
+ { name: 'Item 2', value: '200' },
318
+ ]
319
+
320
+ describe('TEXT tier', () => {
321
+ it('renders table without borders', async () => {
322
+ const node = createTableNode({ columns }, data)
323
+ const result = await renderForTier(node, 'text')
324
+
325
+ expect(result).toContain('Name')
326
+ expect(result).toContain('Item 1')
327
+ expect(result).not.toContain('|')
328
+ expect(result).not.toContain('+')
329
+ })
330
+ })
331
+
332
+ describe('MARKDOWN tier', () => {
333
+ it('renders as markdown table syntax', async () => {
334
+ const node = createTableNode({ columns }, data)
335
+ const result = await renderForTier(node, 'markdown')
336
+
337
+ expect(result).toContain('|')
338
+ expect(result).toContain('Name')
339
+ })
340
+
341
+ it('includes header separator row', async () => {
342
+ const node = createTableNode({ columns }, data)
343
+ const result = await renderForTier(node, 'markdown')
344
+
345
+ expect(result).toMatch(/[-|]+/)
346
+ })
347
+ })
348
+
349
+ describe('ASCII tier', () => {
350
+ it('renders table with ASCII box characters', async () => {
351
+ const node = createTableNode({ columns }, data)
352
+ const result = await renderForTier(node, 'ascii')
353
+
354
+ expect(result).toMatch(/[+\-|]/)
355
+ })
356
+ })
357
+
358
+ describe('UNICODE tier', () => {
359
+ it('renders table with unicode box-drawing characters', async () => {
360
+ const node = createTableNode({ columns, border: true }, data)
361
+ const result = await renderForTier(node, 'unicode')
362
+
363
+ expect(result).toMatch(/[─│┌┐└┘├┤┬┴┼]/)
364
+ })
365
+ })
366
+
367
+ describe('ANSI tier', () => {
368
+ it('renders with ANSI color codes', async () => {
369
+ const node = createTableNode({ columns }, data)
370
+ const result = await renderForTier(node, 'ansi')
371
+
372
+ expect(result).toMatch(/\x1b\[/)
373
+ })
374
+
375
+ it('applies header styling', async () => {
376
+ const node = createTableNode({ columns, headerStyle: { bold: true } }, data)
377
+ const result = await renderForTier(node, 'ansi')
378
+
379
+ expect(result).toMatch(/\x1b\[1m|\x1b\[\d+m/)
380
+ })
381
+ })
382
+
383
+ describe('INTERACTIVE tier', () => {
384
+ it('renders selectable rows with cursor', async () => {
385
+ const node = createTableNode({ columns, selectable: true, focusedRow: 0 }, data)
386
+ const result = await renderForTier(node, 'interactive')
387
+
388
+ expect(result).toContain('Item 1')
389
+ })
390
+
391
+ it('highlights focused row', async () => {
392
+ const node = createTableNode({ columns, focusedRow: 1 }, data)
393
+ const result = await renderForTier(node, 'interactive')
394
+
395
+ expect(result).toContain('Item 2')
396
+ })
397
+ })
398
+ })
399
+
400
+ // ============================================================================
401
+ // Edge Cases
402
+ // ============================================================================
403
+
404
+ describe('edge cases', () => {
405
+ ALL_TIERS.forEach((tier) => {
406
+ describe(`${tier.toUpperCase()} tier`, () => {
407
+ it('handles null values in data', async () => {
408
+ const columns = [{ key: 'name', header: 'Name' }]
409
+ const node = createTableNode({ columns }, [{ name: null }])
410
+ const result = await renderForTier(node, tier)
411
+
412
+ expect(typeof result).toBe('string')
413
+ })
414
+
415
+ it('handles very long cell content', async () => {
416
+ const columns = [{ key: 'content', header: 'Content', width: 20 }]
417
+ const node = createTableNode({ columns }, [
418
+ { content: 'This is a very long string that should be truncated or wrapped' },
419
+ ])
420
+ const result = await renderForTier(node, tier)
421
+
422
+ expect(result.length).toBeGreaterThan(0)
423
+ })
424
+
425
+ it('handles many columns', async () => {
426
+ const columns = Array.from({ length: 10 }, (_, i) => ({
427
+ key: `col${i}`,
428
+ header: `Column ${i}`,
429
+ }))
430
+ const node = createTableNode({ columns }, [
431
+ Object.fromEntries(columns.map((c) => [c.key, 'value'])),
432
+ ])
433
+ const result = await renderForTier(node, tier)
434
+
435
+ expect(result).toContain('Column 0')
436
+ })
437
+
438
+ it('handles unicode in cell content', async () => {
439
+ const columns = [{ key: 'name', header: 'Name' }]
440
+ const node = createTableNode({ columns }, [{ name: 'Hello world' }])
441
+ const result = await renderForTier(node, tier)
442
+
443
+ expect(result).toContain('world')
444
+ })
445
+ })
446
+ })
447
+ })
448
+ })