@adia-ai/web-components 0.8.19 → 0.8.20

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