@adia-ai/web-components 0.8.19 → 0.8.21

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 (128) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/components/card/card.a2ui.json +6 -3
  3. package/components/card/card.css +15 -1
  4. package/components/card/card.yaml +5 -3
  5. package/components/nav-group/nav-group.a2ui.json +5 -0
  6. package/components/nav-group/nav-group.class.js +14 -6
  7. package/components/nav-group/nav-group.css +5 -2
  8. package/components/nav-group/nav-group.d.ts +2 -0
  9. package/components/nav-group/nav-group.yaml +4 -0
  10. package/components/select/select.a2ui.json +3 -2
  11. package/components/select/select.class.js +14 -1
  12. package/components/select/select.css +13 -1
  13. package/components/select/select.yaml +5 -4
  14. package/components/table/table.class.js +7 -3
  15. package/components/table/table.css +7 -5
  16. package/dist/theme-provider.min.js +1 -1
  17. package/dist/web-components.min.css +1 -1
  18. package/dist/web-components.min.js +19 -19
  19. package/dist/web-components.sheet.js +1 -1
  20. package/package.json +2 -1
  21. package/components/adia-mark/adia-mark.test.js +0 -69
  22. package/components/adia-wordmark/adia-wordmark.test.js +0 -69
  23. package/components/agent-artifact/agent-artifact.test.js +0 -61
  24. package/components/alert/alert.test.js +0 -364
  25. package/components/color-picker/color-picker.test.js +0 -96
  26. package/components/combobox/combobox.test.js +0 -256
  27. package/components/command/command.test.js +0 -99
  28. package/components/date-range-picker/date-range-picker.test.js +0 -387
  29. package/components/datetime-picker/datetime-picker.test.js +0 -454
  30. package/components/demo-toggle/demo-toggle.test.js +0 -102
  31. package/components/description-list/description-list.test.js +0 -72
  32. package/components/display-field/display-field.test.js +0 -115
  33. package/components/empty-state/empty-state.test.js +0 -77
  34. package/components/field/field.test.js +0 -260
  35. package/components/fields/fields.test.js +0 -88
  36. package/components/inline-message/inline-message.test.js +0 -203
  37. package/components/input/input.test.js +0 -335
  38. package/components/integration-card/integration-card.test.js +0 -318
  39. package/components/link/link.test.js +0 -143
  40. package/components/list/list.test.js +0 -106
  41. package/components/list-window/list-window.test.js +0 -303
  42. package/components/loading-overlay/loading-overlay.test.js +0 -257
  43. package/components/menu/menu.test.js +0 -176
  44. package/components/nav/nav.test.js +0 -428
  45. package/components/nav-group/nav-group.test.js +0 -67
  46. package/components/option-card/option-card.test.js +0 -32
  47. package/components/pagination/pagination.test.js +0 -195
  48. package/components/select/select.test.js +0 -481
  49. package/components/slider/slider.test.js +0 -202
  50. package/components/spinner/spinner.test.js +0 -272
  51. package/components/stat/stat.test.js +0 -108
  52. package/components/step-progress/step-progress.test.js +0 -129
  53. package/components/swatch/swatch.test.js +0 -563
  54. package/components/table/table.test.js +0 -479
  55. package/components/table-toolbar/table-toolbar.test.js +0 -152
  56. package/components/tag/tag.test.js +0 -137
  57. package/components/tags-input/tags-input.test.js +0 -477
  58. package/components/text/text.test.js +0 -202
  59. package/components/theme-provider/theme-provider.test.js +0 -94
  60. package/components/time-picker/time-picker.test.js +0 -287
  61. package/components/timeline/timeline.test.js +0 -31
  62. package/components/toggle-scheme/toggle-scheme.test.js +0 -110
  63. package/components/tree/tree.test.js +0 -312
  64. package/core/element.test.js +0 -389
  65. package/core/icons.test.js +0 -187
  66. package/core/logical-children.test.js +0 -54
  67. package/core/markdown.test.js +0 -47
  68. package/core/responsive.test.js +0 -121
  69. package/core/template.test.js +0 -611
  70. package/traits/active-state/active-state.test.js +0 -28
  71. package/traits/anchor-positioning/anchor-positioning.test.js +0 -122
  72. package/traits/announcer/announcer.test.js +0 -268
  73. package/traits/arrow-grid-nav/arrow-grid-nav.test.js +0 -375
  74. package/traits/attention-pulse/attention-pulse.test.js +0 -26
  75. package/traits/confetti/confetti.test.js +0 -46
  76. package/traits/confetti-burst/confetti-burst.test.js +0 -46
  77. package/traits/count-up/count-up.test.js +0 -54
  78. package/traits/declarative.test.js +0 -138
  79. package/traits/dirty-state/dirty-state.test.js +0 -45
  80. package/traits/drag-ghost/drag-ghost.test.js +0 -19
  81. package/traits/draggable/draggable.test.js +0 -60
  82. package/traits/draggable-list-item/draggable-list-item.test.js +0 -51
  83. package/traits/drop-target/drop-target.test.js +0 -241
  84. package/traits/droppable/droppable.test.js +0 -54
  85. package/traits/droppable-collection/droppable-collection.test.js +0 -99
  86. package/traits/error-shake/error-shake.test.js +0 -114
  87. package/traits/fade-presence/fade-presence.test.js +0 -20
  88. package/traits/focus-restore/focus-restore.test.js +0 -202
  89. package/traits/focus-trap/focus-trap.test.js +0 -42
  90. package/traits/focusable/focusable.test.js +0 -53
  91. package/traits/glow-focus/glow-focus.test.js +0 -31
  92. package/traits/gradient-shift/gradient-shift.test.js +0 -22
  93. package/traits/haptic-feedback/haptic-feedback.test.js +0 -52
  94. package/traits/hotkey/hotkey.test.js +0 -61
  95. package/traits/hoverable/hoverable.test.js +0 -24
  96. package/traits/inertia-drag/inertia-drag.test.js +0 -33
  97. package/traits/input-mask/input-mask.test.js +0 -151
  98. package/traits/intersection-observer/intersection-observer.test.js +0 -38
  99. package/traits/keyboard-nav/keyboard-nav.test.js +0 -41
  100. package/traits/keyboard-reorderable/keyboard-reorderable.test.js +0 -45
  101. package/traits/layout-animation/layout-animation.test.js +0 -114
  102. package/traits/long-press/long-press.test.js +0 -244
  103. package/traits/magnetic-hover/magnetic-hover.test.js +0 -30
  104. package/traits/noise-texture/noise-texture.test.js +0 -20
  105. package/traits/parallax/parallax.test.js +0 -26
  106. package/traits/portal/portal.test.js +0 -30
  107. package/traits/pressable/pressable.test.js +0 -73
  108. package/traits/resettable/resettable.test.js +0 -100
  109. package/traits/resizable/resizable.test.js +0 -39
  110. package/traits/resize-observer/resize-observer.test.js +0 -38
  111. package/traits/ripple/ripple.test.js +0 -32
  112. package/traits/roving-tabindex/roving-tabindex.test.js +0 -28
  113. package/traits/scale-press/scale-press.test.js +0 -39
  114. package/traits/scroll-lock/scroll-lock.test.js +0 -45
  115. package/traits/scroll-progress/scroll-progress.test.js +0 -182
  116. package/traits/shimmer-loading/shimmer-loading.test.js +0 -43
  117. package/traits/smoke.test.js +0 -111
  118. package/traits/snap-to-grid/snap-to-grid.test.js +0 -40
  119. package/traits/sound-feedback/sound-feedback.test.js +0 -26
  120. package/traits/spring-animate/spring-animate.test.js +0 -28
  121. package/traits/success-checkmark/success-checkmark.test.js +0 -120
  122. package/traits/tilt-hover/tilt-hover.test.js +0 -32
  123. package/traits/tossable/tossable.test.js +0 -31
  124. package/traits/traits-host.test.js +0 -73
  125. package/traits/typeahead/typeahead.test.js +0 -38
  126. package/traits/typewriter/typewriter.test.js +0 -47
  127. package/traits/validation/validation.test.js +0 -93
  128. 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
- });