@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,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
- });