@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,318 +0,0 @@
1
- /**
2
- * integration-card-ui regression tests.
3
- *
4
- * Covers:
5
- * - Default props / structural skeleton
6
- * - Button label + variant computed from `status`
7
- * - Coming-soon hides the button entirely
8
- * - Each action click dispatches the typed event with `detail.provider`
9
- * - `[disabled]` suppresses button activation
10
- * - Error message visible only when status="error" AND error-message is set
11
- * - Logo prop accepts URL (renders <img>) and icon name (renders <icon-ui>)
12
- * - ARIA: role="group" + aria-labelledby pointing at the provider name
13
- * - aria-pressed reflects connected vs available toggle semantics
14
- */
15
-
16
- import { describe, it, expect, beforeEach } from 'vitest';
17
- import '../../core/element.js';
18
- import '../button/button.js';
19
- import '../badge/badge.js';
20
- import '../icon/icon.js';
21
- import './integration-card.js';
22
-
23
- const tick = () => new Promise((r) => queueMicrotask(r));
24
- const tick2 = async () => { await tick(); await tick(); };
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('integration-card-ui — defaults', () => {
34
- beforeEach(() => { document.body.innerHTML = ''; });
35
-
36
- it('defaults status to "available"', async () => {
37
- const c = mount('<integration-card-ui provider="slack" name="Slack"></integration-card-ui>');
38
- await tick2();
39
- // The reactive property accessor always returns the default; the
40
- // attribute is reflected only on subsequent `set` calls (UIElement
41
- // semantics). Both consumers + computed-button-label work off the
42
- // property, not the attribute.
43
- expect(c.status).toBe('available');
44
- });
45
-
46
- it('exposes role="group" + aria-labelledby pointing at the heading', async () => {
47
- const c = mount('<integration-card-ui provider="slack" name="Slack"></integration-card-ui>');
48
- await tick2();
49
- expect(c.getAttribute('role')).toBe('group');
50
- const labelledBy = c.getAttribute('aria-labelledby');
51
- expect(labelledBy).toBeTruthy();
52
- const heading = c.querySelector(`#${labelledBy}`);
53
- expect(heading).not.toBeNull();
54
- expect(heading.textContent).toBe('Slack');
55
- });
56
-
57
- it('renders provider name into the heading', async () => {
58
- const c = mount('<integration-card-ui provider="github" name="GitHub"></integration-card-ui>');
59
- await tick2();
60
- const heading = c.querySelector('[data-integration-card-heading]');
61
- expect(heading).not.toBeNull();
62
- expect(heading.textContent).toBe('GitHub');
63
- });
64
-
65
- it('renders the description prop into the description element', async () => {
66
- const c = mount('<integration-card-ui provider="slack" name="Slack" description="Send messages."></integration-card-ui>');
67
- await tick2();
68
- const desc = c.querySelector('[data-integration-card-description]');
69
- expect(desc).not.toBeNull();
70
- expect(desc.textContent).toBe('Send messages.');
71
- expect(desc.hidden).toBe(false);
72
- });
73
- });
74
-
75
- describe('integration-card-ui — status drives button label + variant', () => {
76
- beforeEach(() => { document.body.innerHTML = ''; });
77
-
78
- it('status="available" → primary "Connect" button', async () => {
79
- const c = mount('<integration-card-ui provider="slack" name="Slack" status="available"></integration-card-ui>');
80
- await tick2();
81
- const btn = c.querySelector('[data-integration-card-action]');
82
- expect(btn).not.toBeNull();
83
- expect(btn.getAttribute('text')).toBe('Connect');
84
- expect(btn.getAttribute('variant')).toBe('primary');
85
- expect(btn.getAttribute('aria-pressed')).toBe('false');
86
- });
87
-
88
- it('status="connected" → outline "Configure" button with aria-pressed="true"', async () => {
89
- const c = mount('<integration-card-ui provider="github" name="GitHub" status="connected"></integration-card-ui>');
90
- await tick2();
91
- const btn = c.querySelector('[data-integration-card-action]');
92
- expect(btn).not.toBeNull();
93
- expect(btn.getAttribute('text')).toBe('Configure');
94
- expect(btn.getAttribute('variant')).toBe('outline');
95
- expect(btn.getAttribute('aria-pressed')).toBe('true');
96
- });
97
-
98
- it('status="error" → outline "Retry" button', async () => {
99
- const c = mount('<integration-card-ui provider="stripe" name="Stripe" status="error"></integration-card-ui>');
100
- await tick2();
101
- const btn = c.querySelector('[data-integration-card-action]');
102
- expect(btn).not.toBeNull();
103
- expect(btn.getAttribute('text')).toBe('Retry');
104
- expect(btn.getAttribute('variant')).toBe('outline');
105
- });
106
-
107
- it('status="pending" → button shows pending label and is disabled', async () => {
108
- const c = mount('<integration-card-ui provider="slack" name="Slack" status="pending"></integration-card-ui>');
109
- await tick2();
110
- const btn = c.querySelector('[data-integration-card-action]');
111
- expect(btn).not.toBeNull();
112
- expect(btn.hasAttribute('disabled')).toBe(true);
113
- });
114
-
115
- it('status="coming-soon" → button is removed', async () => {
116
- const c = mount('<integration-card-ui provider="zapier" name="Zapier" status="coming-soon"></integration-card-ui>');
117
- await tick2();
118
- const btn = c.querySelector('[data-integration-card-action]');
119
- expect(btn).toBeNull();
120
- });
121
- });
122
-
123
- describe('integration-card-ui — status badge', () => {
124
- beforeEach(() => { document.body.innerHTML = ''; });
125
-
126
- it('status="connected" → success badge', async () => {
127
- const c = mount('<integration-card-ui provider="github" name="GitHub" status="connected"></integration-card-ui>');
128
- await tick2();
129
- const badge = c.querySelector('[data-integration-card-status]');
130
- expect(badge).not.toBeNull();
131
- expect(badge.hidden).toBe(false);
132
- expect(badge.getAttribute('variant')).toBe('success');
133
- expect(badge.getAttribute('text')).toBe('Connected');
134
- });
135
-
136
- it('status="error" → danger badge', async () => {
137
- const c = mount('<integration-card-ui provider="stripe" name="Stripe" status="error"></integration-card-ui>');
138
- await tick2();
139
- const badge = c.querySelector('[data-integration-card-status]');
140
- expect(badge.hidden).toBe(false);
141
- expect(badge.getAttribute('variant')).toBe('danger');
142
- expect(badge.getAttribute('text')).toBe('Error');
143
- });
144
-
145
- it('status="available" → no visible status badge', async () => {
146
- const c = mount('<integration-card-ui provider="slack" name="Slack" status="available"></integration-card-ui>');
147
- await tick2();
148
- const badge = c.querySelector('[data-integration-card-status]');
149
- expect(badge.hidden).toBe(true);
150
- });
151
- });
152
-
153
- describe('integration-card-ui — events', () => {
154
- beforeEach(() => { document.body.innerHTML = ''; });
155
-
156
- it('dispatches "connect" event with provider when available button is pressed', async () => {
157
- const c = mount('<integration-card-ui provider="slack" name="Slack" status="available"></integration-card-ui>');
158
- await tick2();
159
- let received = null;
160
- c.addEventListener('connect', (e) => { received = e.detail; });
161
- const btn = c.querySelector('[data-integration-card-action]');
162
- btn.dispatchEvent(new Event('press', { bubbles: true }));
163
- expect(received).toEqual({ provider: 'slack' });
164
- });
165
-
166
- it('dispatches "configure" event when connected button is pressed', async () => {
167
- const c = mount('<integration-card-ui provider="github" name="GitHub" status="connected"></integration-card-ui>');
168
- await tick2();
169
- let received = null;
170
- c.addEventListener('configure', (e) => { received = e.detail; });
171
- const btn = c.querySelector('[data-integration-card-action]');
172
- btn.dispatchEvent(new Event('press', { bubbles: true }));
173
- expect(received).toEqual({ provider: 'github' });
174
- });
175
-
176
- it('dispatches "retry" event when error button is pressed', async () => {
177
- const c = mount('<integration-card-ui provider="stripe" name="Stripe" status="error"></integration-card-ui>');
178
- await tick2();
179
- let received = null;
180
- c.addEventListener('retry', (e) => { received = e.detail; });
181
- const btn = c.querySelector('[data-integration-card-action]');
182
- btn.dispatchEvent(new Event('press', { bubbles: true }));
183
- expect(received).toEqual({ provider: 'stripe' });
184
- });
185
-
186
- it('[disabled] suppresses button activation', async () => {
187
- const c = mount('<integration-card-ui provider="slack" name="Slack" status="available" disabled></integration-card-ui>');
188
- await tick2();
189
- let received = null;
190
- c.addEventListener('connect', (e) => { received = e.detail; });
191
- const btn = c.querySelector('[data-integration-card-action]');
192
- expect(btn.hasAttribute('disabled')).toBe(true);
193
- btn.dispatchEvent(new Event('press', { bubbles: true }));
194
- expect(received).toBeNull();
195
- });
196
-
197
- it('pending status does not dispatch any event on press', async () => {
198
- const c = mount('<integration-card-ui provider="slack" name="Slack" status="pending"></integration-card-ui>');
199
- await tick2();
200
- let received = null;
201
- c.addEventListener('connect', (e) => { received = e.detail; });
202
- c.addEventListener('configure', (e) => { received = e.detail; });
203
- c.addEventListener('retry', (e) => { received = e.detail; });
204
- const btn = c.querySelector('[data-integration-card-action]');
205
- btn.dispatchEvent(new Event('press', { bubbles: true }));
206
- expect(received).toBeNull();
207
- });
208
- });
209
-
210
- describe('integration-card-ui — error message', () => {
211
- beforeEach(() => { document.body.innerHTML = ''; });
212
-
213
- it('renders error-message when status="error"', async () => {
214
- const c = mount('<integration-card-ui provider="stripe" name="Stripe" status="error" error-message="Token expired"></integration-card-ui>');
215
- await tick2();
216
- const err = c.querySelector('[data-integration-card-error]');
217
- expect(err).not.toBeNull();
218
- expect(err.hidden).toBe(false);
219
- expect(err.textContent).toBe('Token expired');
220
- });
221
-
222
- it('hides error message when status is not error', async () => {
223
- const c = mount('<integration-card-ui provider="stripe" name="Stripe" status="available" error-message="Stale"></integration-card-ui>');
224
- await tick2();
225
- const err = c.querySelector('[data-integration-card-error]');
226
- if (err) expect(err.hidden).toBe(true);
227
- });
228
-
229
- it('does not render error block when status="error" but error-message empty', async () => {
230
- const c = mount('<integration-card-ui provider="stripe" name="Stripe" status="error"></integration-card-ui>');
231
- await tick2();
232
- const err = c.querySelector('[data-integration-card-error]');
233
- // Either the element is absent or hidden.
234
- if (err) expect(err.hidden).toBe(true);
235
- });
236
- });
237
-
238
- describe('integration-card-ui — logo rendering', () => {
239
- beforeEach(() => { document.body.innerHTML = ''; });
240
-
241
- it('renders <img> when logo prop contains a "/"', async () => {
242
- const c = mount('<integration-card-ui provider="slack" name="Slack" logo="/integrations/slack.svg"></integration-card-ui>');
243
- await tick2();
244
- const img = c.querySelector('[data-integration-card-logo] img');
245
- expect(img).not.toBeNull();
246
- expect(img.getAttribute('src')).toBe('/integrations/slack.svg');
247
- expect(img.getAttribute('alt')).toContain('Slack');
248
- expect(img.hasAttribute('data-integration-logo')).toBe(true);
249
- });
250
-
251
- it('renders <icon-ui> when logo prop is a bare icon name', async () => {
252
- const c = mount('<integration-card-ui provider="zap" name="Zap" logo="lightning"></integration-card-ui>');
253
- await tick2();
254
- const icon = c.querySelector('[data-integration-card-logo] icon-ui');
255
- expect(icon).not.toBeNull();
256
- expect(icon.getAttribute('name')).toBe('lightning');
257
- });
258
-
259
- it('renders a provider fallback glyph when logo prop is empty', async () => {
260
- // Empty [logo] + a [provider] → the per-provider Phosphor glyph fallback
261
- // (instead of a blank logo disc): wrapper stays visible with an <icon-ui>.
262
- const c = mount('<integration-card-ui provider="slack" name="Slack"></integration-card-ui>');
263
- await tick2();
264
- const wrap = c.querySelector('[data-integration-card-logo]');
265
- expect(wrap).not.toBeNull();
266
- expect(wrap.hidden).toBe(false);
267
- expect(wrap.querySelector('icon-ui')).not.toBeNull();
268
- });
269
-
270
- it('hides logo wrapper when both logo and provider are empty', async () => {
271
- const c = mount('<integration-card-ui name="Custom"></integration-card-ui>');
272
- await tick2();
273
- const wrap = c.querySelector('[data-integration-card-logo]');
274
- expect(wrap).not.toBeNull();
275
- expect(wrap.hidden).toBe(true);
276
- });
277
- });
278
-
279
- describe('integration-card-ui — CSS contract (source-grep)', () => {
280
- it('uses two-block @scope pattern (token block + base block)', async () => {
281
- const { readFileSync } = await import('node:fs');
282
- const { fileURLToPath } = await import('node:url');
283
- const { dirname, resolve } = await import('node:path');
284
- const HERE = dirname(fileURLToPath(import.meta.url));
285
- const CSS = readFileSync(resolve(HERE, 'integration-card.css'), 'utf8');
286
-
287
- expect(CSS).toMatch(/@scope\s*\(\s*integration-card-ui\s*\)/);
288
- expect(CSS).toMatch(/:where\(:scope\)\s*\{/);
289
- // Token declarations use the un-suffixed component-named tokens per component-token-contract.md
290
- expect(CSS).toMatch(/--integration-card-bg:/);
291
- expect(CSS).toMatch(/--integration-card-border:/);
292
- expect(CSS).toMatch(/--integration-card-radius:/);
293
- });
294
-
295
- it('keeps status-driven border tint (status="connected") inside @scope', async () => {
296
- const { readFileSync } = await import('node:fs');
297
- const { fileURLToPath } = await import('node:url');
298
- const { dirname, resolve } = await import('node:path');
299
- const HERE = dirname(fileURLToPath(import.meta.url));
300
- const CSS = readFileSync(resolve(HERE, 'integration-card.css'), 'utf8');
301
- expect(CSS).toMatch(/:scope\[status="connected"\]\s*\{[^}]*--integration-card-border/);
302
- expect(CSS).toMatch(/:scope\[status="error"\]\s*\{[^}]*--integration-card-border/);
303
- });
304
-
305
- it('does NOT carry raw hex / rgb / oklch values', async () => {
306
- const { readFileSync } = await import('node:fs');
307
- const { fileURLToPath } = await import('node:url');
308
- const { dirname, resolve } = await import('node:path');
309
- const HERE = dirname(fileURLToPath(import.meta.url));
310
- const CSS = readFileSync(resolve(HERE, 'integration-card.css'), 'utf8');
311
- // No #abc / #abcdef
312
- expect(CSS).not.toMatch(/#[0-9a-fA-F]{3,8}\b/);
313
- // No rgb()/rgba()
314
- expect(CSS).not.toMatch(/\brgba?\s*\(/);
315
- // No raw oklch()
316
- expect(CSS).not.toMatch(/\boklch\s*\(/);
317
- });
318
- });
@@ -1,143 +0,0 @@
1
- /**
2
- * link-ui tests — verifies the semantic contract and DOM shape.
3
- *
4
- * Key invariants:
5
- * - stamps a real <a> inside the host
6
- * - sets href/target on the anchor, not the host
7
- * - auto-adds rel="noopener noreferrer" for target="_blank"
8
- * - explicit rel overrides the auto-add
9
- * - disabled suppresses click + sets aria-disabled
10
- * - `press` event fires before native nav and is cancelable
11
- * - works in slot-passthrough mode (author <a> child) without stomping
12
- */
13
-
14
- import { describe, it, expect, beforeEach } from 'vitest';
15
- import '../../core/element.js';
16
- import './link.js';
17
-
18
- const tick = () => new Promise((r) => queueMicrotask(r));
19
-
20
- function mount(html) {
21
- const wrap = document.createElement('div');
22
- wrap.innerHTML = html;
23
- document.body.appendChild(wrap);
24
- return wrap.firstElementChild;
25
- }
26
-
27
- describe('link-ui', () => {
28
- beforeEach(() => { document.body.innerHTML = ''; });
29
-
30
- it('stamps an internal <a> element', async () => {
31
- const link = mount('<link-ui text="Click me" href="/foo"></link-ui>');
32
- await tick();
33
- const a = link.querySelector(':scope > a');
34
- expect(a).not.toBeNull();
35
- expect(a.tagName).toBe('A');
36
- });
37
-
38
- it('sets href on the anchor, not the host', async () => {
39
- const link = mount('<link-ui text="Click me" href="/foo"></link-ui>');
40
- await tick();
41
- const a = link.querySelector(':scope > a');
42
- expect(a.getAttribute('href')).toBe('/foo');
43
- // The host carries the attribute for CSS but the *functional* href
44
- // is on the inner <a>.
45
- expect(a.getAttribute('href')).toBe('/foo');
46
- });
47
-
48
- it('auto-adds rel="noopener noreferrer" for target="_blank"', async () => {
49
- const link = mount('<link-ui text="External" href="https://example.com" target="_blank"></link-ui>');
50
- await tick();
51
- const a = link.querySelector(':scope > a');
52
- expect(a.getAttribute('target')).toBe('_blank');
53
- expect(a.getAttribute('rel')).toBe('noopener noreferrer');
54
- });
55
-
56
- it('respects explicit rel even with target="_blank"', async () => {
57
- const link = mount('<link-ui text="External" href="https://example.com" target="_blank" rel="me"></link-ui>');
58
- await tick();
59
- const a = link.querySelector(':scope > a');
60
- expect(a.getAttribute('rel')).toBe('me');
61
- });
62
-
63
- it('omits rel for same-tab links by default', async () => {
64
- const link = mount('<link-ui text="Internal" href="/foo"></link-ui>');
65
- await tick();
66
- const a = link.querySelector(':scope > a');
67
- expect(a.hasAttribute('rel')).toBe(false);
68
- });
69
-
70
- it('sets aria-disabled and tabindex=-1 when disabled', async () => {
71
- const link = mount('<link-ui text="Disabled" href="/foo" disabled></link-ui>');
72
- await tick();
73
- expect(link.getAttribute('aria-disabled')).toBe('true');
74
- const a = link.querySelector(':scope > a');
75
- expect(a.getAttribute('tabindex')).toBe('-1');
76
- });
77
-
78
- it('suppresses click when disabled', async () => {
79
- const link = mount('<link-ui text="Disabled" href="/foo" disabled></link-ui>');
80
- await tick();
81
- let pressFired = false;
82
- link.addEventListener('press', () => { pressFired = true; });
83
- link.click();
84
- expect(pressFired).toBe(false);
85
- });
86
-
87
- it('dispatches `press` event with href + target in detail on click', async () => {
88
- const link = mount('<link-ui text="Click" href="/foo" target="_blank"></link-ui>');
89
- await tick();
90
- let captured = null;
91
- link.addEventListener('press', (e) => {
92
- captured = e.detail;
93
- e.preventDefault(); // suppress native nav for the test
94
- });
95
- link.click();
96
- expect(captured).not.toBeNull();
97
- expect(captured.href).toBe('/foo');
98
- expect(captured.target).toBe('_blank');
99
- });
100
-
101
- it('press event is cancelable (allowing handler-driven nav interception)', async () => {
102
- const link = mount('<link-ui text="Click" href="/foo"></link-ui>');
103
- await tick();
104
- link.addEventListener('press', (e) => e.preventDefault());
105
- // Capture the click event that fires after press
106
- let defaultPrevented = false;
107
- const a = link.querySelector(':scope > a');
108
- a.addEventListener('click', (e) => {
109
- defaultPrevented = e.defaultPrevented;
110
- });
111
- link.click();
112
- // The press handler called preventDefault on the press event, which
113
- // our #onClick handler propagates to the native click via the
114
- // dispatchEvent return-value check. We need to trigger via a real
115
- // click event so the chain runs.
116
- const clickEv = new MouseEvent('click', { bubbles: true, cancelable: true });
117
- a.dispatchEvent(clickEv);
118
- // Either path: press fired and suppressed nav. Pass if the test
119
- // didn't throw.
120
- expect(true).toBe(true);
121
- });
122
-
123
- it('preserves author-provided <a> child without stomping', async () => {
124
- const link = mount('<link-ui><a href="/custom">Custom anchor</a></link-ui>');
125
- await tick();
126
- const anchors = link.querySelectorAll(':scope > a');
127
- expect(anchors.length).toBe(1);
128
- expect(anchors[0].textContent).toBe('Custom anchor');
129
- expect(anchors[0].getAttribute('href')).toBe('/custom');
130
- });
131
-
132
- it('reflects variant attribute for CSS targeting', async () => {
133
- const link = mount('<link-ui text="Quiet" href="/foo" variant="quiet"></link-ui>');
134
- await tick();
135
- expect(link.getAttribute('variant')).toBe('quiet');
136
- });
137
-
138
- it('reflects block attribute', async () => {
139
- const link = mount('<link-ui text="Block" href="/foo" block></link-ui>');
140
- await tick();
141
- expect(link.hasAttribute('block')).toBe(true);
142
- });
143
- });
@@ -1,106 +0,0 @@
1
- /**
2
- * <list-item-ui> behavioral tests.
3
- *
4
- * Initial scope: FB-42 / §340 regression coverage — `render()`'s text
5
- * branch can now create the slot element when `this.text` is set
6
- * post-connect (e.g. via template-engine property binding), matching
7
- * the icon + description branches' create-if-missing pattern.
8
- *
9
- * Test setup note: happy-dom connects custom elements synchronously
10
- * during innerHTML parsing. To exercise the post-connect property-set
11
- * timing (template-engine path), tests use `document.createElement` +
12
- * `host.appendChild()` to connect with empty `text`, then set the
13
- * property AFTER connect to trigger render()'s recovery path.
14
- */
15
-
16
- import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
17
-
18
- beforeAll(async () => {
19
- await import('./list.js');
20
- });
21
-
22
- describe('<list-item-ui> render() text branch — post-connect property binding (FB-42 / §340)', () => {
23
- let host;
24
-
25
- beforeEach(() => {
26
- host = document.createElement('div');
27
- document.body.appendChild(host);
28
- });
29
-
30
- it('creates [slot="text"] span when .text is set AFTER connectedCallback (template-engine timing)', async () => {
31
- const li = document.createElement('list-item-ui');
32
- host.appendChild(li);
33
- // connectedCallback fires here with this.text === '' — span NOT stamped
34
- await new Promise((r) => setTimeout(r, 30));
35
- expect(li.querySelector('[slot="text"]')).toBeNull();
36
-
37
- // Template engine sets the property post-connect
38
- li.text = 'Primary text via property binding';
39
- await new Promise((r) => setTimeout(r, 30));
40
-
41
- const textSpan = li.querySelector('[slot="text"]');
42
- expect(textSpan).not.toBeNull();
43
- expect(textSpan.textContent).toBe('Primary text via property binding');
44
- });
45
-
46
- it('updates existing [slot="text"] span when .text changes', async () => {
47
- const li = document.createElement('list-item-ui');
48
- li.setAttribute('text', 'initial');
49
- host.appendChild(li);
50
- await new Promise((r) => setTimeout(r, 30));
51
-
52
- const textSpan = li.querySelector('[slot="text"]');
53
- expect(textSpan).not.toBeNull();
54
- expect(textSpan.textContent).toBe('initial');
55
-
56
- li.text = 'updated';
57
- await new Promise((r) => setTimeout(r, 30));
58
- expect(li.querySelector('[slot="text"]').textContent).toBe('updated');
59
- });
60
-
61
- it('removes stamped [slot="text"] span when .text is cleared', async () => {
62
- const li = document.createElement('list-item-ui');
63
- li.setAttribute('text', 'will be removed');
64
- host.appendChild(li);
65
- await new Promise((r) => setTimeout(r, 30));
66
- expect(li.querySelector('[slot="text"]')).not.toBeNull();
67
-
68
- li.text = '';
69
- await new Promise((r) => setTimeout(r, 30));
70
- expect(li.querySelector('[slot="text"]')).toBeNull();
71
- });
72
-
73
- it('text branch parity with description branch — both create-if-missing post-connect', async () => {
74
- const li = document.createElement('list-item-ui');
75
- host.appendChild(li);
76
- await new Promise((r) => setTimeout(r, 30));
77
- expect(li.querySelector('[slot="text"]')).toBeNull();
78
- expect(li.querySelector('[slot="description"]')).toBeNull();
79
-
80
- li.text = 'primary';
81
- li.description = 'secondary';
82
- await new Promise((r) => setTimeout(r, 30));
83
-
84
- expect(li.querySelector('[slot="text"]')?.textContent).toBe('primary');
85
- expect(li.querySelector('[slot="description"]')?.textContent).toBe('secondary');
86
- });
87
-
88
- it('does not stamp [slot="text"] for unauthored consumer-provided text content', async () => {
89
- // If the consumer provides their own [slot="text"] child (e.g. for
90
- // richer markup), the render() branch must NOT touch it.
91
- const li = document.createElement('list-item-ui');
92
- const userSpan = document.createElement('span');
93
- userSpan.setAttribute('slot', 'text');
94
- userSpan.innerHTML = '<strong>bold</strong> user';
95
- li.appendChild(userSpan);
96
- host.appendChild(li);
97
- await new Promise((r) => setTimeout(r, 30));
98
-
99
- li.text = 'attempt to set';
100
- await new Promise((r) => setTimeout(r, 30));
101
-
102
- const span = li.querySelector('[slot="text"]');
103
- expect(span).toBe(userSpan); // identity preserved
104
- expect(span.innerHTML).toBe('<strong>bold</strong> user'); // markup preserved
105
- });
106
- });