@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.
- package/CHANGELOG.md +26 -0
- package/components/card/card.a2ui.json +6 -3
- package/components/card/card.css +15 -1
- package/components/card/card.yaml +5 -3
- package/components/nav-group/nav-group.a2ui.json +5 -0
- package/components/nav-group/nav-group.class.js +14 -6
- package/components/nav-group/nav-group.css +5 -2
- package/components/nav-group/nav-group.d.ts +2 -0
- package/components/nav-group/nav-group.yaml +4 -0
- package/components/select/select.a2ui.json +3 -2
- package/components/select/select.class.js +14 -1
- package/components/select/select.css +13 -1
- package/components/select/select.yaml +5 -4
- package/components/table/table.class.js +7 -3
- package/components/table/table.css +7 -5
- package/dist/theme-provider.min.js +1 -1
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +19 -19
- package/dist/web-components.sheet.js +1 -1
- package/package.json +2 -1
- package/components/adia-mark/adia-mark.test.js +0 -69
- package/components/adia-wordmark/adia-wordmark.test.js +0 -69
- package/components/agent-artifact/agent-artifact.test.js +0 -61
- package/components/alert/alert.test.js +0 -364
- package/components/color-picker/color-picker.test.js +0 -96
- package/components/combobox/combobox.test.js +0 -256
- package/components/command/command.test.js +0 -99
- package/components/date-range-picker/date-range-picker.test.js +0 -387
- package/components/datetime-picker/datetime-picker.test.js +0 -454
- package/components/demo-toggle/demo-toggle.test.js +0 -102
- package/components/description-list/description-list.test.js +0 -72
- package/components/display-field/display-field.test.js +0 -115
- package/components/empty-state/empty-state.test.js +0 -77
- package/components/field/field.test.js +0 -260
- package/components/fields/fields.test.js +0 -88
- package/components/inline-message/inline-message.test.js +0 -203
- package/components/input/input.test.js +0 -335
- package/components/integration-card/integration-card.test.js +0 -318
- package/components/link/link.test.js +0 -143
- package/components/list/list.test.js +0 -106
- package/components/list-window/list-window.test.js +0 -303
- package/components/loading-overlay/loading-overlay.test.js +0 -257
- package/components/menu/menu.test.js +0 -176
- package/components/nav/nav.test.js +0 -428
- package/components/nav-group/nav-group.test.js +0 -67
- package/components/option-card/option-card.test.js +0 -32
- package/components/pagination/pagination.test.js +0 -195
- package/components/select/select.test.js +0 -481
- package/components/slider/slider.test.js +0 -202
- package/components/spinner/spinner.test.js +0 -272
- package/components/stat/stat.test.js +0 -108
- package/components/step-progress/step-progress.test.js +0 -129
- package/components/swatch/swatch.test.js +0 -563
- package/components/table/table.test.js +0 -479
- package/components/table-toolbar/table-toolbar.test.js +0 -152
- package/components/tag/tag.test.js +0 -137
- package/components/tags-input/tags-input.test.js +0 -477
- package/components/text/text.test.js +0 -202
- package/components/theme-provider/theme-provider.test.js +0 -94
- package/components/time-picker/time-picker.test.js +0 -287
- package/components/timeline/timeline.test.js +0 -31
- package/components/toggle-scheme/toggle-scheme.test.js +0 -110
- package/components/tree/tree.test.js +0 -312
- package/core/element.test.js +0 -389
- package/core/icons.test.js +0 -187
- package/core/logical-children.test.js +0 -54
- package/core/markdown.test.js +0 -47
- package/core/responsive.test.js +0 -121
- package/core/template.test.js +0 -611
- package/traits/active-state/active-state.test.js +0 -28
- package/traits/anchor-positioning/anchor-positioning.test.js +0 -122
- package/traits/announcer/announcer.test.js +0 -268
- package/traits/arrow-grid-nav/arrow-grid-nav.test.js +0 -375
- package/traits/attention-pulse/attention-pulse.test.js +0 -26
- package/traits/confetti/confetti.test.js +0 -46
- package/traits/confetti-burst/confetti-burst.test.js +0 -46
- package/traits/count-up/count-up.test.js +0 -54
- package/traits/declarative.test.js +0 -138
- package/traits/dirty-state/dirty-state.test.js +0 -45
- package/traits/drag-ghost/drag-ghost.test.js +0 -19
- package/traits/draggable/draggable.test.js +0 -60
- package/traits/draggable-list-item/draggable-list-item.test.js +0 -51
- package/traits/drop-target/drop-target.test.js +0 -241
- package/traits/droppable/droppable.test.js +0 -54
- package/traits/droppable-collection/droppable-collection.test.js +0 -99
- package/traits/error-shake/error-shake.test.js +0 -114
- package/traits/fade-presence/fade-presence.test.js +0 -20
- package/traits/focus-restore/focus-restore.test.js +0 -202
- package/traits/focus-trap/focus-trap.test.js +0 -42
- package/traits/focusable/focusable.test.js +0 -53
- package/traits/glow-focus/glow-focus.test.js +0 -31
- package/traits/gradient-shift/gradient-shift.test.js +0 -22
- package/traits/haptic-feedback/haptic-feedback.test.js +0 -52
- package/traits/hotkey/hotkey.test.js +0 -61
- package/traits/hoverable/hoverable.test.js +0 -24
- package/traits/inertia-drag/inertia-drag.test.js +0 -33
- package/traits/input-mask/input-mask.test.js +0 -151
- package/traits/intersection-observer/intersection-observer.test.js +0 -38
- package/traits/keyboard-nav/keyboard-nav.test.js +0 -41
- package/traits/keyboard-reorderable/keyboard-reorderable.test.js +0 -45
- package/traits/layout-animation/layout-animation.test.js +0 -114
- package/traits/long-press/long-press.test.js +0 -244
- package/traits/magnetic-hover/magnetic-hover.test.js +0 -30
- package/traits/noise-texture/noise-texture.test.js +0 -20
- package/traits/parallax/parallax.test.js +0 -26
- package/traits/portal/portal.test.js +0 -30
- package/traits/pressable/pressable.test.js +0 -73
- package/traits/resettable/resettable.test.js +0 -100
- package/traits/resizable/resizable.test.js +0 -39
- package/traits/resize-observer/resize-observer.test.js +0 -38
- package/traits/ripple/ripple.test.js +0 -32
- package/traits/roving-tabindex/roving-tabindex.test.js +0 -28
- package/traits/scale-press/scale-press.test.js +0 -39
- package/traits/scroll-lock/scroll-lock.test.js +0 -45
- package/traits/scroll-progress/scroll-progress.test.js +0 -182
- package/traits/shimmer-loading/shimmer-loading.test.js +0 -43
- package/traits/smoke.test.js +0 -111
- package/traits/snap-to-grid/snap-to-grid.test.js +0 -40
- package/traits/sound-feedback/sound-feedback.test.js +0 -26
- package/traits/spring-animate/spring-animate.test.js +0 -28
- package/traits/success-checkmark/success-checkmark.test.js +0 -120
- package/traits/tilt-hover/tilt-hover.test.js +0 -32
- package/traits/tossable/tossable.test.js +0 -31
- package/traits/traits-host.test.js +0 -73
- package/traits/typeahead/typeahead.test.js +0 -38
- package/traits/typewriter/typewriter.test.js +0 -47
- package/traits/validation/validation.test.js +0 -93
- 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
|
-
});
|