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