@adia-ai/web-components 0.8.19 → 0.8.20

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 (119) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/components/nav-group/nav-group.a2ui.json +5 -0
  3. package/components/nav-group/nav-group.class.js +14 -6
  4. package/components/nav-group/nav-group.css +5 -2
  5. package/components/nav-group/nav-group.d.ts +2 -0
  6. package/components/nav-group/nav-group.yaml +4 -0
  7. package/dist/theme-provider.min.js +1 -1
  8. package/dist/web-components.min.css +1 -1
  9. package/dist/web-components.min.js +2 -2
  10. package/dist/web-components.sheet.js +1 -1
  11. package/package.json +2 -1
  12. package/components/adia-mark/adia-mark.test.js +0 -69
  13. package/components/adia-wordmark/adia-wordmark.test.js +0 -69
  14. package/components/agent-artifact/agent-artifact.test.js +0 -61
  15. package/components/alert/alert.test.js +0 -364
  16. package/components/color-picker/color-picker.test.js +0 -96
  17. package/components/combobox/combobox.test.js +0 -256
  18. package/components/command/command.test.js +0 -99
  19. package/components/date-range-picker/date-range-picker.test.js +0 -387
  20. package/components/datetime-picker/datetime-picker.test.js +0 -454
  21. package/components/demo-toggle/demo-toggle.test.js +0 -102
  22. package/components/description-list/description-list.test.js +0 -72
  23. package/components/display-field/display-field.test.js +0 -115
  24. package/components/empty-state/empty-state.test.js +0 -77
  25. package/components/field/field.test.js +0 -260
  26. package/components/fields/fields.test.js +0 -88
  27. package/components/inline-message/inline-message.test.js +0 -203
  28. package/components/input/input.test.js +0 -335
  29. package/components/integration-card/integration-card.test.js +0 -318
  30. package/components/link/link.test.js +0 -143
  31. package/components/list/list.test.js +0 -106
  32. package/components/list-window/list-window.test.js +0 -303
  33. package/components/loading-overlay/loading-overlay.test.js +0 -257
  34. package/components/menu/menu.test.js +0 -176
  35. package/components/nav/nav.test.js +0 -428
  36. package/components/nav-group/nav-group.test.js +0 -67
  37. package/components/option-card/option-card.test.js +0 -32
  38. package/components/pagination/pagination.test.js +0 -195
  39. package/components/select/select.test.js +0 -481
  40. package/components/slider/slider.test.js +0 -202
  41. package/components/spinner/spinner.test.js +0 -272
  42. package/components/stat/stat.test.js +0 -108
  43. package/components/step-progress/step-progress.test.js +0 -129
  44. package/components/swatch/swatch.test.js +0 -563
  45. package/components/table/table.test.js +0 -479
  46. package/components/table-toolbar/table-toolbar.test.js +0 -152
  47. package/components/tag/tag.test.js +0 -137
  48. package/components/tags-input/tags-input.test.js +0 -477
  49. package/components/text/text.test.js +0 -202
  50. package/components/theme-provider/theme-provider.test.js +0 -94
  51. package/components/time-picker/time-picker.test.js +0 -287
  52. package/components/timeline/timeline.test.js +0 -31
  53. package/components/toggle-scheme/toggle-scheme.test.js +0 -110
  54. package/components/tree/tree.test.js +0 -312
  55. package/core/element.test.js +0 -389
  56. package/core/icons.test.js +0 -187
  57. package/core/logical-children.test.js +0 -54
  58. package/core/markdown.test.js +0 -47
  59. package/core/responsive.test.js +0 -121
  60. package/core/template.test.js +0 -611
  61. package/traits/active-state/active-state.test.js +0 -28
  62. package/traits/anchor-positioning/anchor-positioning.test.js +0 -122
  63. package/traits/announcer/announcer.test.js +0 -268
  64. package/traits/arrow-grid-nav/arrow-grid-nav.test.js +0 -375
  65. package/traits/attention-pulse/attention-pulse.test.js +0 -26
  66. package/traits/confetti/confetti.test.js +0 -46
  67. package/traits/confetti-burst/confetti-burst.test.js +0 -46
  68. package/traits/count-up/count-up.test.js +0 -54
  69. package/traits/declarative.test.js +0 -138
  70. package/traits/dirty-state/dirty-state.test.js +0 -45
  71. package/traits/drag-ghost/drag-ghost.test.js +0 -19
  72. package/traits/draggable/draggable.test.js +0 -60
  73. package/traits/draggable-list-item/draggable-list-item.test.js +0 -51
  74. package/traits/drop-target/drop-target.test.js +0 -241
  75. package/traits/droppable/droppable.test.js +0 -54
  76. package/traits/droppable-collection/droppable-collection.test.js +0 -99
  77. package/traits/error-shake/error-shake.test.js +0 -114
  78. package/traits/fade-presence/fade-presence.test.js +0 -20
  79. package/traits/focus-restore/focus-restore.test.js +0 -202
  80. package/traits/focus-trap/focus-trap.test.js +0 -42
  81. package/traits/focusable/focusable.test.js +0 -53
  82. package/traits/glow-focus/glow-focus.test.js +0 -31
  83. package/traits/gradient-shift/gradient-shift.test.js +0 -22
  84. package/traits/haptic-feedback/haptic-feedback.test.js +0 -52
  85. package/traits/hotkey/hotkey.test.js +0 -61
  86. package/traits/hoverable/hoverable.test.js +0 -24
  87. package/traits/inertia-drag/inertia-drag.test.js +0 -33
  88. package/traits/input-mask/input-mask.test.js +0 -151
  89. package/traits/intersection-observer/intersection-observer.test.js +0 -38
  90. package/traits/keyboard-nav/keyboard-nav.test.js +0 -41
  91. package/traits/keyboard-reorderable/keyboard-reorderable.test.js +0 -45
  92. package/traits/layout-animation/layout-animation.test.js +0 -114
  93. package/traits/long-press/long-press.test.js +0 -244
  94. package/traits/magnetic-hover/magnetic-hover.test.js +0 -30
  95. package/traits/noise-texture/noise-texture.test.js +0 -20
  96. package/traits/parallax/parallax.test.js +0 -26
  97. package/traits/portal/portal.test.js +0 -30
  98. package/traits/pressable/pressable.test.js +0 -73
  99. package/traits/resettable/resettable.test.js +0 -100
  100. package/traits/resizable/resizable.test.js +0 -39
  101. package/traits/resize-observer/resize-observer.test.js +0 -38
  102. package/traits/ripple/ripple.test.js +0 -32
  103. package/traits/roving-tabindex/roving-tabindex.test.js +0 -28
  104. package/traits/scale-press/scale-press.test.js +0 -39
  105. package/traits/scroll-lock/scroll-lock.test.js +0 -45
  106. package/traits/scroll-progress/scroll-progress.test.js +0 -182
  107. package/traits/shimmer-loading/shimmer-loading.test.js +0 -43
  108. package/traits/smoke.test.js +0 -111
  109. package/traits/snap-to-grid/snap-to-grid.test.js +0 -40
  110. package/traits/sound-feedback/sound-feedback.test.js +0 -26
  111. package/traits/spring-animate/spring-animate.test.js +0 -28
  112. package/traits/success-checkmark/success-checkmark.test.js +0 -120
  113. package/traits/tilt-hover/tilt-hover.test.js +0 -32
  114. package/traits/tossable/tossable.test.js +0 -31
  115. package/traits/traits-host.test.js +0 -73
  116. package/traits/typeahead/typeahead.test.js +0 -38
  117. package/traits/typewriter/typewriter.test.js +0 -47
  118. package/traits/validation/validation.test.js +0 -93
  119. package/traits/view-transition/view-transition.test.js +0 -268
@@ -1,479 +0,0 @@
1
- /**
2
- * table-ui — focused unit tests for the v0.6.18 loading=skeleton-rows
3
- * behavior change (FB-12 P2 resolution).
4
- *
5
- * Pre-v0.6.18: `loading=true` rendered a `<progress-ui>` spinner overlay
6
- * inside the body via `[data-loading]`. The yaml description always said
7
- * "Shows a loading overlay AND skeleton rows" but the impl only did the
8
- * overlay — yaml-vs-impl drift since the initial table cut.
9
- *
10
- * v0.6.18 changes the loading branch to render N ghost skeleton rows
11
- * (N = paginate if set, else 5) inside the [data-body] rowgroup. Header +
12
- * columns stay intact so layout is preserved. Sets aria-busy="true" on the
13
- * host. Old `[data-loading]` overlay element is removed if it lingers.
14
- *
15
- * Note: table-ui needs columns + data set imperatively (via the .columns /
16
- * .data properties on the element) — declarative <col-def> children also
17
- * work but require the col-def element to be registered first. Tests use
18
- * the imperative path.
19
- */
20
-
21
- import { describe, it, expect, beforeEach } from 'vitest';
22
- import '../../core/element.js';
23
- import './table.js';
24
- import '../skeleton/skeleton.js';
25
-
26
- const tick = () => new Promise((r) => queueMicrotask(r));
27
- const raf = () => new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
28
-
29
- function mount(html) {
30
- const wrap = document.createElement('div');
31
- wrap.innerHTML = html;
32
- document.body.appendChild(wrap);
33
- return wrap.firstElementChild;
34
- }
35
-
36
- const COLS = [
37
- { key: 'id', label: 'ID' },
38
- { key: 'name', label: 'Name' },
39
- { key: 'email', label: 'Email' },
40
- ];
41
- const ROWS = [
42
- { id: 1, name: 'Alice', email: 'alice@acme.com' },
43
- { id: 2, name: 'Bob', email: 'bob@acme.com' },
44
- ];
45
-
46
- describe('table-ui — v0.6.18 loading=skeleton-rows (FB-12 P2)', () => {
47
- beforeEach(() => { document.body.innerHTML = ''; });
48
-
49
- it('renders skeleton rows in [data-body] when [loading] is set', async () => {
50
- const el = mount('<table-ui></table-ui>');
51
- el.columns = COLS;
52
- el.data = ROWS;
53
- await tick();
54
- el.setAttribute('loading', '');
55
- await tick();
56
- const body = el.querySelector(':scope > [data-body]');
57
- expect(body).not.toBeNull();
58
- const skRows = body.querySelectorAll('[data-skeleton-row]');
59
- expect(skRows.length).toBeGreaterThan(0);
60
- });
61
-
62
- it('sets aria-busy="true" on the host when [loading]', async () => {
63
- const el = mount('<table-ui></table-ui>');
64
- el.columns = COLS;
65
- el.data = ROWS;
66
- await tick();
67
- el.setAttribute('loading', '');
68
- await tick();
69
- expect(el.getAttribute('aria-busy')).toBe('true');
70
- });
71
-
72
- it('replaces real body rows with skeleton rows when [loading]', async () => {
73
- const el = mount('<table-ui></table-ui>');
74
- el.columns = COLS;
75
- el.data = ROWS;
76
- // table-ui uses requestAnimationFrame in #requestRender(), not microtasks.
77
- // queueMicrotask-based tick() won't drain RAF callbacks — must await raf().
78
- // Loop up to 5 RAF cycles in case the initial mount needs multiple renders
79
- // to settle (columns set → render → data set → render).
80
- for (let i = 0; i < 5; i++) {
81
- await raf();
82
- const body = el.querySelector(':scope > [data-body]');
83
- if (body && body.children.length >= 2) break;
84
- }
85
- const body = el.querySelector(':scope > [data-body]');
86
- expect(body).not.toBeNull();
87
- // Real rows present (2)
88
- const realRows = body.querySelectorAll(':scope > [role="row"]:not([data-skeleton-row])');
89
- expect(realRows.length).toBe(2);
90
- el.setAttribute('loading', '');
91
- await raf();
92
- // Real rows gone, skeleton rows present
93
- expect(body.querySelectorAll(':scope > [role="row"]:not([data-skeleton-row])').length).toBe(0);
94
- expect(body.querySelectorAll(':scope > [data-skeleton-row]').length).toBeGreaterThan(0);
95
- });
96
-
97
- it('each skeleton row has cell count matching column count', async () => {
98
- const el = mount('<table-ui></table-ui>');
99
- el.columns = COLS; // 3 columns
100
- el.data = ROWS;
101
- await tick();
102
- el.setAttribute('loading', '');
103
- await tick();
104
- const skRow = el.querySelector('[data-skeleton-row]');
105
- expect(skRow.children.length).toBe(3);
106
- });
107
-
108
- it('each skeleton cell contains a <skeleton-ui> shimmer element', async () => {
109
- const el = mount('<table-ui></table-ui>');
110
- el.columns = COLS;
111
- el.data = ROWS;
112
- await tick();
113
- el.setAttribute('loading', '');
114
- await tick();
115
- const skCells = el.querySelectorAll('[data-skeleton-row] > [role="gridcell"]');
116
- expect(skCells.length).toBeGreaterThan(0);
117
- for (const cell of skCells) {
118
- const sk = cell.querySelector('skeleton-ui');
119
- expect(sk).not.toBeNull();
120
- expect(sk.tagName.toLowerCase()).toBe('skeleton-ui');
121
- }
122
- });
123
-
124
- it('does NOT render a [data-loading] overlay element (old behavior removed)', async () => {
125
- const el = mount('<table-ui></table-ui>');
126
- el.columns = COLS;
127
- el.data = ROWS;
128
- await tick();
129
- el.setAttribute('loading', '');
130
- await tick();
131
- // Old impl created <div data-loading> with <progress-ui> child.
132
- // v0.6.18 does NOT — skeleton rows ARE the loading affordance.
133
- expect(el.querySelector(':scope > [data-loading]')).toBeNull();
134
- });
135
-
136
- it('restores real rows + clears aria-busy when loading toggles off', async () => {
137
- const el = mount('<table-ui></table-ui>');
138
- el.columns = COLS;
139
- el.data = ROWS;
140
- await tick();
141
- el.setAttribute('loading', '');
142
- await tick();
143
- el.removeAttribute('loading');
144
- await tick();
145
- expect(el.getAttribute('aria-busy')).toBeNull();
146
- expect(el.querySelectorAll('[data-body] > [data-skeleton-row]').length).toBe(0);
147
- expect(el.querySelectorAll('[data-body] > [role="row"]').length).toBe(2);
148
- });
149
-
150
- it('skeleton row count tracks paginate when set, capped at 8', async () => {
151
- const el = mount('<table-ui paginate="3"></table-ui>');
152
- el.columns = COLS;
153
- el.data = ROWS;
154
- await tick();
155
- el.setAttribute('loading', '');
156
- await tick();
157
- expect(el.querySelectorAll('[data-body] > [data-skeleton-row]').length).toBe(3);
158
- });
159
-
160
- it('preserves header row when [loading]', async () => {
161
- const el = mount('<table-ui></table-ui>');
162
- el.columns = COLS;
163
- el.data = ROWS;
164
- await tick();
165
- const beforeHeader = el.querySelector(':scope > [data-header]');
166
- expect(beforeHeader).not.toBeNull();
167
- el.setAttribute('loading', '');
168
- await tick();
169
- const afterHeader = el.querySelector(':scope > [data-header]');
170
- expect(afterHeader).not.toBeNull();
171
- // Header cells unchanged in count
172
- expect(afterHeader.children.length).toBe(3);
173
- });
174
- });
175
-
176
- /**
177
- * Body-cell wrap default + opt-in paths (PATCH-class behavior change).
178
- *
179
- * Default: cells truncate single-line with ellipsis (the CSS rule sets
180
- * white-space: nowrap; overflow: hidden; text-overflow: ellipsis). Three
181
- * opt-in surfaces lift the truncation:
182
- * - host [wrap] attribute (reflected from the `wrap` prop)
183
- * - col.wrap=true in columns[] → renderer sets [data-wrap] on each
184
- * body cell in that column
185
- * - [data-wrap] directly on a hand-authored <td>
186
- *
187
- * Tests lock the attribute contract; CSS-rule application is covered by
188
- * the source-grep stylistic invariants of table.css.
189
- */
190
- describe('table-ui — wrap default + opt-in', () => {
191
- beforeEach(() => { document.body.innerHTML = ''; });
192
-
193
- it('host [wrap] attribute reflects from the wrap property', async () => {
194
- const el = mount('<table-ui></table-ui>');
195
- el.columns = COLS;
196
- el.data = ROWS;
197
- await raf();
198
- expect(el.hasAttribute('wrap')).toBe(false);
199
- el.wrap = true;
200
- await raf();
201
- expect(el.hasAttribute('wrap')).toBe(true);
202
- el.wrap = false;
203
- await raf();
204
- expect(el.hasAttribute('wrap')).toBe(false);
205
- });
206
-
207
- it('col.wrap=true marks body cells in that column with [data-wrap]', async () => {
208
- const el = mount('<table-ui></table-ui>');
209
- el.columns = [
210
- { key: 'id', label: 'ID' },
211
- { key: 'name', label: 'Name', wrap: true },
212
- { key: 'email', label: 'Email' },
213
- ];
214
- el.data = ROWS;
215
- await raf();
216
- const bodyRows = el.querySelectorAll(':scope > [data-body] > [role="row"]');
217
- expect(bodyRows.length).toBe(2);
218
- for (const row of bodyRows) {
219
- const cells = row.querySelectorAll('[role="gridcell"]');
220
- expect(cells.length).toBe(3);
221
- expect(cells[0].hasAttribute('data-wrap')).toBe(false);
222
- expect(cells[1].hasAttribute('data-wrap')).toBe(true);
223
- expect(cells[2].hasAttribute('data-wrap')).toBe(false);
224
- }
225
- });
226
-
227
- it('omitting col.wrap leaves all body cells without [data-wrap]', async () => {
228
- const el = mount('<table-ui></table-ui>');
229
- el.columns = COLS;
230
- el.data = ROWS;
231
- await raf();
232
- const cells = el.querySelectorAll(':scope > [data-body] [role="gridcell"]');
233
- expect(cells.length).toBeGreaterThan(0);
234
- for (const cell of cells) {
235
- expect(cell.hasAttribute('data-wrap')).toBe(false);
236
- }
237
- });
238
- });
239
-
240
- describe('table-ui — FB-47 keyboard: Enter activates focused body cell', () => {
241
- beforeEach(() => { document.body.innerHTML = ''; });
242
-
243
- it('Enter on a focused body cell fires cell-click with the row data', async () => {
244
- const el = mount('<table-ui></table-ui>');
245
- el.columns = COLS;
246
- el.data = ROWS;
247
- await raf();
248
-
249
- // ArrowDown from null initialises #focusedCell to {row:0,col:0} then
250
- // increments to {row:1,col:0} and calls #updateFocus → [data-focused] on
251
- // the second body row cell.
252
- el.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
253
- await tick();
254
-
255
- let fired = null;
256
- el.addEventListener('cell-click', (e) => { fired = e.detail; });
257
-
258
- el.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
259
- await tick();
260
-
261
- expect(fired).not.toBeNull();
262
- expect(fired.row).toMatchObject({ id: 2, name: 'Bob' }); // index 1 = second row
263
- });
264
-
265
- it('Enter on a focused header cell does NOT fire cell-click (sort path only)', async () => {
266
- const el = mount('<table-ui></table-ui>');
267
- el.columns = COLS;
268
- el.data = ROWS;
269
- await raf();
270
-
271
- // ArrowUp from null initialises to {row:0,col:0} then decrements to
272
- // {row:-1,col:0} → header row.
273
- el.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true }));
274
- await tick();
275
-
276
- let cellClickFired = false;
277
- el.addEventListener('cell-click', () => { cellClickFired = true; });
278
-
279
- el.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
280
- await tick();
281
-
282
- // Header Enter triggers sort (or no-op if not sortable), never cell-click.
283
- expect(cellClickFired).toBe(false);
284
- });
285
- });
286
-
287
- /**
288
- * Raw mode: visual-only passthrough — consumer owns the body shape.
289
- *
290
- * Pre-v0.6.33 (FB-53 §2), `<table-ui raw>` only applied the CSS chrome
291
- * reset (background/border/radius transparent) but the data lifecycle
292
- * still ran. With `.data === []` (the natural state when wrapping a
293
- * consumer-authored native <table>), `#renderOverlays` injected a
294
- * [data-empty] "No data" placeholder above the consumer's content.
295
- *
296
- * v0.6.33 short-circuits `render()` at the top when `this.raw` is set:
297
- * no header injection, no body reconciliation, no overlays, no
298
- * aggregation, no pagination.
299
- */
300
- describe('table-ui — raw mode passthrough (FB-53 §2)', () => {
301
- beforeEach(() => { document.body.innerHTML = ''; });
302
-
303
- it('raw mode does NOT inject [data-empty] overlay when .data is empty', async () => {
304
- const el = mount('<table-ui raw><table><tr><td>foo</td></tr></table></table-ui>');
305
- // No .data set → empty by default. Without the raw early-return,
306
- // #renderOverlays would create the [data-empty] placeholder.
307
- await raf();
308
- expect(el.querySelector(':scope > [data-empty]')).toBeNull();
309
- // Consumer's native <table> survives untouched.
310
- expect(el.querySelector(':scope > table')).not.toBeNull();
311
- });
312
-
313
- it('raw mode does NOT inject [data-header] or [data-body] rowgroups', async () => {
314
- const el = mount('<table-ui raw><table><tr><td>x</td></tr></table></table-ui>');
315
- await raf();
316
- expect(el.querySelector(':scope > [data-header]')).toBeNull();
317
- expect(el.querySelector(':scope > [data-body]')).toBeNull();
318
- });
319
-
320
- it('raw mode preserves consumer markup even when .columns + .data are set', async () => {
321
- const el = mount('<table-ui raw><table id="consumer"><tr><td>consumer-owned</td></tr></table></table-ui>');
322
- el.columns = COLS;
323
- el.data = ROWS;
324
- await raf();
325
- // The consumer's native <table> is still the only table-shaped child.
326
- expect(el.querySelector('#consumer')).not.toBeNull();
327
- expect(el.querySelectorAll(':scope > [role="rowgroup"]').length).toBe(0);
328
- });
329
-
330
- it('non-raw mode (default) still injects header + body rowgroups', async () => {
331
- const el = mount('<table-ui></table-ui>');
332
- el.columns = COLS;
333
- el.data = ROWS;
334
- await raf();
335
- expect(el.querySelector(':scope > [data-header]')).not.toBeNull();
336
- expect(el.querySelector(':scope > [data-body]')).not.toBeNull();
337
- });
338
- });
339
-
340
- // ── F6 (upstream): per-row expand gate via `rowExpandable` ───────────────────
341
- describe('table-ui — F6 per-row expand suppression (rowExpandable)', () => {
342
- beforeEach(() => { document.body.innerHTML = ''; });
343
-
344
- const bodyRows = (el) =>
345
- el.querySelectorAll(':scope > [data-body] > [role="row"]:not([data-detail-row])');
346
-
347
- it('default (no predicate): every row carries an expand caret', async () => {
348
- const el = mount('<table-ui expandable></table-ui>');
349
- el.columns = COLS;
350
- el.data = ROWS;
351
- await raf();
352
- const rows = bodyRows(el);
353
- expect(rows.length).toBe(2);
354
- for (const row of rows) {
355
- expect(row.querySelector('[data-expand-toggle]')).not.toBeNull();
356
- }
357
- });
358
-
359
- it('predicate suppresses the caret on gated rows but keeps the reserved cell', async () => {
360
- const el = mount('<table-ui expandable></table-ui>');
361
- el.columns = COLS;
362
- el.data = ROWS;
363
- el.rowExpandable = (row) => row.id === 2; // only Bob expandable
364
- await raf();
365
- const rows = bodyRows(el);
366
-
367
- // Alice (id 1) — reserved cell present, NO caret button.
368
- expect(rows[0].querySelector(':scope > [data-expand-col]')).not.toBeNull();
369
- expect(rows[0].querySelector('[data-expand-toggle]')).toBeNull();
370
-
371
- // Bob (id 2) — caret button present.
372
- expect(rows[1].querySelector('[data-expand-toggle]')).not.toBeNull();
373
- });
374
-
375
- it('toggleExpand is a no-op on a gated row (no detail row renders)', async () => {
376
- const el = mount('<table-ui expandable></table-ui>');
377
- el.columns = COLS;
378
- el.data = ROWS;
379
- el.rowExpandable = (row) => row.id === 2;
380
- await raf();
381
-
382
- el.toggleExpand(0); // Alice — gated out
383
- await raf();
384
- expect(el.querySelector('[data-detail-row][data-for="0"]')).toBeNull();
385
- expect(el.expanded).not.toContain(0);
386
-
387
- el.toggleExpand(1); // Bob — allowed
388
- await raf();
389
- expect(el.expanded).toContain(1);
390
- });
391
- });
392
-
393
- describe('table-ui — "markdown" cell type (TKT-0008)', () => {
394
- it('renders inline `code`/**bold**/*italic*/[link](href) as real elements, not literal syntax', async () => {
395
- const el = mount('<table-ui></table-ui>');
396
- el.columns = [{ key: 'desc', label: 'Description', type: 'markdown' }];
397
- el.data = [{ desc: 'Semantic status: `new` | `updated` | **beta** or *deprecated*, see [docs](/site/components/badge).' }];
398
- await raf();
399
-
400
- const cell = el.querySelector('[role="gridcell"][data-key="desc"]');
401
- expect([...cell.querySelectorAll('code')].map((c) => c.textContent)).toEqual(['new', 'updated']);
402
- expect(cell.querySelector('strong').textContent).toBe('beta');
403
- expect(cell.querySelector('em').textContent).toBe('deprecated');
404
- expect(cell.querySelector('a').getAttribute('href')).toBe('/site/components/badge');
405
- expect(cell.textContent).not.toMatch(/`|\*\*|\[docs\]/); // no literal markdown syntax leaked through
406
- });
407
-
408
- it('sets [title] to the plain-text (tag-stripped) version, for the truncation tooltip', async () => {
409
- const el = mount('<table-ui></table-ui>');
410
- el.columns = [{ key: 'prop', label: 'Prop', type: 'markdown' }];
411
- el.data = [{ prop: '`variant`' }];
412
- await raf();
413
-
414
- const cell = el.querySelector('[role="gridcell"][data-key="prop"]');
415
- expect(cell.title).toBe('variant');
416
- });
417
-
418
- it('a plain value with no markdown syntax renders as ordinary text (no stray elements)', async () => {
419
- const el = mount('<table-ui></table-ui>');
420
- el.columns = [{ key: 'desc', label: 'Description', type: 'markdown' }];
421
- el.data = [{ desc: 'Plain sentence, no special characters.' }];
422
- await raf();
423
-
424
- const cell = el.querySelector('[role="gridcell"][data-key="desc"]');
425
- expect(cell.querySelector('code, strong, em, a')).toBeNull();
426
- expect(cell.textContent).toBe('Plain sentence, no special characters.');
427
- });
428
- });
429
-
430
- // gh#288 — declarative `data="[…]"` attribute, mirroring chart-ui's existing
431
- // pattern. Row data previously had no declarative form at all — `columns`
432
- // already had `<col-def>` children; `data` had nothing, so a static-HTML or
433
- // SSR-authored table always rendered its data empty.
434
- describe('table-ui — declarative data="[…]" attribute (gh#288)', () => {
435
- beforeEach(() => { document.body.innerHTML = ''; });
436
-
437
- it('hydrates .data from a JSON-array data attribute at connect', async () => {
438
- const el = mount(
439
- `<table-ui data='${JSON.stringify(ROWS)}'></table-ui>`,
440
- );
441
- el.columns = COLS;
442
- await raf();
443
- expect(el.data).toEqual(ROWS);
444
- expect(el.querySelectorAll('[role="gridcell"][data-key="name"]').length).toBe(2);
445
- });
446
-
447
- it('a malformed data attribute is ignored silently, not thrown', async () => {
448
- expect(() => mount(`<table-ui data='not json'></table-ui>`)).not.toThrow();
449
- const el = document.querySelector('table-ui');
450
- expect(el.data).toEqual([]);
451
- });
452
-
453
- it('a non-array JSON value in the attribute is ignored, not coerced', async () => {
454
- const el = mount(`<table-ui data='{"not":"an array"}'></table-ui>`);
455
- await raf();
456
- expect(el.data).toEqual([]);
457
- });
458
-
459
- it('a programmatic .data set before connect wins over the attribute', () => {
460
- const el = document.createElement('table-ui');
461
- el.setAttribute('data', JSON.stringify(ROWS));
462
- el.data = [{ id: 99, name: 'Override', email: 'override@acme.com' }];
463
- document.body.appendChild(el);
464
- expect(el.data).toEqual([{ id: 99, name: 'Override', email: 'override@acme.com' }]);
465
- });
466
-
467
- it('works together with declarative <col-def> children — fully static-HTML table', async () => {
468
- const el = mount(`
469
- <table-ui data='${JSON.stringify(ROWS)}'>
470
- <col-def key="id" label="ID"></col-def>
471
- <col-def key="name" label="Name"></col-def>
472
- </table-ui>
473
- `);
474
- await raf();
475
- expect(el.data).toEqual(ROWS);
476
- expect(el.columns.length).toBe(2);
477
- expect(el.querySelectorAll('[role="gridcell"][data-key="name"]').length).toBe(2);
478
- });
479
- });
@@ -1,152 +0,0 @@
1
- /**
2
- * table-toolbar-ui tests
3
- *
4
- * Pinning the §381 (v0.6.10 slice F) one-shot dev-mode warning for
5
- * unknown opt-out attributes. Triggered by claims-ui FB-04: consumers
6
- * set `searchable` / `exportable` / `sortable` thinking they toggle
7
- * visibility; canonical attrs are `no-search`/`no-filter`/`no-sort`/
8
- * `no-columns`. Previous behavior: silent no-op.
9
- *
10
- * Contract:
11
- * 1. Unknown attrs matching /^(search|export|sort|filter|column)/i
12
- * → console.warn fires once per element with the attr names listed
13
- * 2. Canonical attrs (no-search/no-filter/no-sort/no-columns) → no warn
14
- * 3. Unrelated attrs (id, class, role, aria-*, data-*) → no warn
15
- * 4. Multiple unknown attrs in one declaration → single warn (one warn
16
- * per element, all unknown attrs in the message)
17
- * 5. Repeated reconnects of the same element → still one warn total
18
- */
19
-
20
- import { describe, it, expect, beforeEach, vi } from 'vitest';
21
- import '../../core/element.js';
22
- import './table-toolbar.js';
23
-
24
- const tick = () => new Promise((r) => queueMicrotask(r));
25
-
26
- function mount(html) {
27
- const wrap = document.createElement('div');
28
- wrap.innerHTML = html;
29
- document.body.appendChild(wrap);
30
- return wrap.firstElementChild;
31
- }
32
-
33
- describe('table-toolbar-ui — unknown opt-out attribute warning (§381 / FB-04)', () => {
34
- beforeEach(() => { document.body.innerHTML = ''; });
35
-
36
- it('warns when an unknown `searchable` attribute is set', async () => {
37
- const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
38
- mount('<table-toolbar-ui searchable></table-toolbar-ui>');
39
- await tick();
40
- expect(warn).toHaveBeenCalledTimes(1);
41
- const msg = warn.mock.calls[0][0];
42
- expect(msg).toContain('[table-toolbar-ui]');
43
- expect(msg).toContain('"searchable"');
44
- expect(msg).toContain('no-search');
45
- warn.mockRestore();
46
- });
47
-
48
- it('warns when an unknown `exportable` attribute is set', async () => {
49
- const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
50
- mount('<table-toolbar-ui exportable></table-toolbar-ui>');
51
- await tick();
52
- expect(warn).toHaveBeenCalledTimes(1);
53
- const msg = warn.mock.calls[0][0];
54
- expect(msg).toContain('"exportable"');
55
- warn.mockRestore();
56
- });
57
-
58
- it('warns when an unknown `sortable` attribute is set', async () => {
59
- const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
60
- mount('<table-toolbar-ui sortable></table-toolbar-ui>');
61
- await tick();
62
- expect(warn).toHaveBeenCalledTimes(1);
63
- expect(warn.mock.calls[0][0]).toContain('"sortable"');
64
- warn.mockRestore();
65
- });
66
-
67
- it('warns when an unknown `filterable` attribute is set', async () => {
68
- const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
69
- mount('<table-toolbar-ui filterable></table-toolbar-ui>');
70
- await tick();
71
- expect(warn).toHaveBeenCalledTimes(1);
72
- expect(warn.mock.calls[0][0]).toContain('"filterable"');
73
- warn.mockRestore();
74
- });
75
-
76
- it('warns when an unknown `columns-visible` attribute is set', async () => {
77
- const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
78
- mount('<table-toolbar-ui columns-visible></table-toolbar-ui>');
79
- await tick();
80
- expect(warn).toHaveBeenCalledTimes(1);
81
- expect(warn.mock.calls[0][0]).toContain('"columns-visible"');
82
- warn.mockRestore();
83
- });
84
-
85
- it('does NOT warn on canonical no-search', async () => {
86
- const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
87
- mount('<table-toolbar-ui no-search></table-toolbar-ui>');
88
- await tick();
89
- expect(warn).not.toHaveBeenCalled();
90
- warn.mockRestore();
91
- });
92
-
93
- it('does NOT warn on canonical no-filter / no-sort / no-columns', async () => {
94
- const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
95
- mount('<table-toolbar-ui no-filter no-sort no-columns></table-toolbar-ui>');
96
- await tick();
97
- expect(warn).not.toHaveBeenCalled();
98
- warn.mockRestore();
99
- });
100
-
101
- it('does NOT warn on unrelated attributes (id, class, role, aria-*, data-*)', async () => {
102
- const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
103
- mount('<table-toolbar-ui id="x" class="y" role="toolbar" aria-label="z" data-test="ok"></table-toolbar-ui>');
104
- await tick();
105
- expect(warn).not.toHaveBeenCalled();
106
- warn.mockRestore();
107
- });
108
-
109
- it('does NOT warn on canonical attrs even when placeholder is also set', async () => {
110
- const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
111
- mount('<table-toolbar-ui no-search no-sort placeholder="Find..."></table-toolbar-ui>');
112
- await tick();
113
- expect(warn).not.toHaveBeenCalled();
114
- warn.mockRestore();
115
- });
116
-
117
- it('emits a single warn that names all unknown opt-out attrs', async () => {
118
- const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
119
- mount('<table-toolbar-ui searchable exportable sortable></table-toolbar-ui>');
120
- await tick();
121
- expect(warn).toHaveBeenCalledTimes(1);
122
- const msg = warn.mock.calls[0][0];
123
- expect(msg).toContain('"searchable"');
124
- expect(msg).toContain('"exportable"');
125
- expect(msg).toContain('"sortable"');
126
- warn.mockRestore();
127
- });
128
-
129
- it('warns at most once per element across reconnects', async () => {
130
- const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
131
- const el = mount('<table-toolbar-ui searchable></table-toolbar-ui>');
132
- await tick();
133
- expect(warn).toHaveBeenCalledTimes(1);
134
- // Disconnect + reconnect.
135
- el.remove();
136
- document.body.appendChild(el);
137
- await tick();
138
- expect(warn).toHaveBeenCalledTimes(1); // unchanged
139
- warn.mockRestore();
140
- });
141
-
142
- it('mixes canonical + unknown — names only the unknown in the warning', async () => {
143
- const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
144
- mount('<table-toolbar-ui no-search searchable></table-toolbar-ui>');
145
- await tick();
146
- expect(warn).toHaveBeenCalledTimes(1);
147
- const msg = warn.mock.calls[0][0];
148
- expect(msg).toContain('"searchable"');
149
- expect(msg).not.toContain('"no-search"');
150
- warn.mockRestore();
151
- });
152
- });