@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adia-ai/web-components",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.21",
|
|
4
4
|
"description": "AdiaUI web components \u2014 vanilla custom elements. A2UI runtime (renderer, registry, streams, wiring) lives in @adia-ai/a2ui-runtime.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./index.d.ts",
|
|
@@ -99,6 +99,7 @@
|
|
|
99
99
|
"!traits/**/*.html",
|
|
100
100
|
"!traits/**/*.examples.js",
|
|
101
101
|
"!traits/_api-table.js",
|
|
102
|
+
"!**/*.test.js",
|
|
102
103
|
"index.js",
|
|
103
104
|
"index.css",
|
|
104
105
|
"index.d.ts",
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import '../../core/element.js';
|
|
3
|
-
import './adia-mark.js';
|
|
4
|
-
|
|
5
|
-
const tick = () => new Promise((r) => queueMicrotask(r));
|
|
6
|
-
|
|
7
|
-
function mount(html) {
|
|
8
|
-
const wrap = document.createElement('div');
|
|
9
|
-
wrap.innerHTML = html;
|
|
10
|
-
document.body.appendChild(wrap);
|
|
11
|
-
return wrap.firstElementChild;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
describe('adia-mark-ui', () => {
|
|
15
|
-
beforeEach(() => { document.body.innerHTML = ''; });
|
|
16
|
-
|
|
17
|
-
it('registers adia-mark-ui as a custom element', () => {
|
|
18
|
-
expect(customElements.get('adia-mark-ui')).toBeDefined();
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it('defaults to size="md"', async () => {
|
|
22
|
-
const el = mount('<adia-mark-ui></adia-mark-ui>');
|
|
23
|
-
await tick();
|
|
24
|
-
expect(el.size).toBe('md');
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('reflects an explicit [size]', async () => {
|
|
28
|
-
const el = mount('<adia-mark-ui size="lg"></adia-mark-ui>');
|
|
29
|
-
await tick();
|
|
30
|
-
expect(el.size).toBe('lg');
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('defaults to inline=false and reflects an explicit [inline]', async () => {
|
|
34
|
-
const a = mount('<adia-mark-ui></adia-mark-ui>');
|
|
35
|
-
const b = mount('<adia-mark-ui inline></adia-mark-ui>');
|
|
36
|
-
await tick();
|
|
37
|
-
expect(a.inline).toBe(false);
|
|
38
|
-
expect(b.inline).toBe(true);
|
|
39
|
-
expect(b.getAttribute('inline')).toBe('');
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('sets role="img" and a default aria-label on connect', async () => {
|
|
43
|
-
const el = mount('<adia-mark-ui></adia-mark-ui>');
|
|
44
|
-
await tick();
|
|
45
|
-
expect(el.getAttribute('role')).toBe('img');
|
|
46
|
-
expect(el.getAttribute('aria-label')).toBe('Adia');
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('does not overwrite a consumer-authored aria-label', async () => {
|
|
50
|
-
const el = mount('<adia-mark-ui aria-label="Adia — home"></adia-mark-ui>');
|
|
51
|
-
await tick();
|
|
52
|
-
expect(el.getAttribute('aria-label')).toBe('Adia — home');
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('stamps the wordmark SVG with five glyph paths, no consumer-content conflict', async () => {
|
|
56
|
-
const el = mount('<adia-mark-ui></adia-mark-ui>');
|
|
57
|
-
await tick();
|
|
58
|
-
const svg = el.querySelector('svg');
|
|
59
|
-
expect(svg).toBeTruthy();
|
|
60
|
-
expect(el.querySelectorAll('[data-mark-glyph]').length).toBe(5);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('renders identically regardless of size — static template, no interpolation', async () => {
|
|
64
|
-
const a = mount('<adia-mark-ui size="sm"></adia-mark-ui>');
|
|
65
|
-
const b = mount('<adia-mark-ui size="lg"></adia-mark-ui>');
|
|
66
|
-
await tick();
|
|
67
|
-
expect(a.innerHTML).toBe(b.innerHTML);
|
|
68
|
-
});
|
|
69
|
-
});
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import '../../core/element.js';
|
|
3
|
-
import './adia-wordmark.js';
|
|
4
|
-
|
|
5
|
-
const tick = () => new Promise((r) => queueMicrotask(r));
|
|
6
|
-
|
|
7
|
-
function mount(html) {
|
|
8
|
-
const wrap = document.createElement('div');
|
|
9
|
-
wrap.innerHTML = html;
|
|
10
|
-
document.body.appendChild(wrap);
|
|
11
|
-
return wrap.firstElementChild;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
describe('adia-wordmark-ui', () => {
|
|
15
|
-
beforeEach(() => { document.body.innerHTML = ''; });
|
|
16
|
-
|
|
17
|
-
it('registers adia-wordmark-ui as a custom element', () => {
|
|
18
|
-
expect(customElements.get('adia-wordmark-ui')).toBeDefined();
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it('defaults to size="md"', async () => {
|
|
22
|
-
const el = mount('<adia-wordmark-ui></adia-wordmark-ui>');
|
|
23
|
-
await tick();
|
|
24
|
-
expect(el.size).toBe('md');
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it('reflects an explicit [size]', async () => {
|
|
28
|
-
const el = mount('<adia-wordmark-ui size="lg"></adia-wordmark-ui>');
|
|
29
|
-
await tick();
|
|
30
|
-
expect(el.size).toBe('lg');
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('defaults to inline=false and reflects an explicit [inline]', async () => {
|
|
34
|
-
const a = mount('<adia-wordmark-ui></adia-wordmark-ui>');
|
|
35
|
-
const b = mount('<adia-wordmark-ui inline></adia-wordmark-ui>');
|
|
36
|
-
await tick();
|
|
37
|
-
expect(a.inline).toBe(false);
|
|
38
|
-
expect(b.inline).toBe(true);
|
|
39
|
-
expect(b.getAttribute('inline')).toBe('');
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('sets role="img" and a default aria-label on connect', async () => {
|
|
43
|
-
const el = mount('<adia-wordmark-ui></adia-wordmark-ui>');
|
|
44
|
-
await tick();
|
|
45
|
-
expect(el.getAttribute('role')).toBe('img');
|
|
46
|
-
expect(el.getAttribute('aria-label')).toBe('Adia');
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('does not overwrite a consumer-authored aria-label', async () => {
|
|
50
|
-
const el = mount('<adia-wordmark-ui aria-label="Adia — home"></adia-wordmark-ui>');
|
|
51
|
-
await tick();
|
|
52
|
-
expect(el.getAttribute('aria-label')).toBe('Adia — home');
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('stamps the wordmark SVG with five glyph paths, no consumer-content conflict', async () => {
|
|
56
|
-
const el = mount('<adia-wordmark-ui></adia-wordmark-ui>');
|
|
57
|
-
await tick();
|
|
58
|
-
const svg = el.querySelector('svg');
|
|
59
|
-
expect(svg).toBeTruthy();
|
|
60
|
-
expect(el.querySelectorAll('[data-wordmark-glyph]').length).toBe(5);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('renders identically regardless of size — static template, no interpolation', async () => {
|
|
64
|
-
const a = mount('<adia-wordmark-ui size="sm"></adia-wordmark-ui>');
|
|
65
|
-
const b = mount('<adia-wordmark-ui size="lg"></adia-wordmark-ui>');
|
|
66
|
-
await tick();
|
|
67
|
-
expect(a.innerHTML).toBe(b.innerHTML);
|
|
68
|
-
});
|
|
69
|
-
});
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import '../../core/element.js';
|
|
3
|
-
import './agent-artifact.js';
|
|
4
|
-
|
|
5
|
-
const tick = () => new Promise((r) => queueMicrotask(r));
|
|
6
|
-
|
|
7
|
-
function mount(html) {
|
|
8
|
-
const wrap = document.createElement('div');
|
|
9
|
-
wrap.innerHTML = html;
|
|
10
|
-
document.body.appendChild(wrap);
|
|
11
|
-
return wrap.firstElementChild;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
describe('agent-artifact-ui', () => {
|
|
15
|
-
beforeEach(() => { document.body.innerHTML = ''; });
|
|
16
|
-
|
|
17
|
-
// Baseline — static slot="primary"/"secondary" buttons route into the header
|
|
18
|
-
// actions cluster, and the default-slot content lands in the body.
|
|
19
|
-
it('routes static action buttons into the header actions cluster (not the body)', async () => {
|
|
20
|
-
const a = mount(`
|
|
21
|
-
<agent-artifact-ui title="Build">
|
|
22
|
-
<button-ui slot="primary">Run</button-ui>
|
|
23
|
-
<button-ui slot="secondary">Cancel</button-ui>
|
|
24
|
-
<p>Body content</p>
|
|
25
|
-
</agent-artifact-ui>
|
|
26
|
-
`);
|
|
27
|
-
await tick();
|
|
28
|
-
const actions = a.querySelector('[data-artifact-actions]');
|
|
29
|
-
const body = a.querySelector('[data-artifact-body]');
|
|
30
|
-
expect(actions).toBeTruthy();
|
|
31
|
-
expect(actions.querySelectorAll('[slot="primary"], [slot="secondary"]').length).toBe(2);
|
|
32
|
-
expect(body.querySelectorAll('[slot="primary"], [slot="secondary"]').length).toBe(0);
|
|
33
|
-
expect(body.textContent).toContain('Body content');
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
// Wrapper-trap regression (audit-wrapper-trap arm 2): action buttons rendered
|
|
37
|
-
// via `.map()`/`repeat()` arrive nested in the template engine's
|
|
38
|
-
// display:contents / role="presentation" wrapper spans. The old `:scope >
|
|
39
|
-
// [slot="primary"]` direct-child grab missed them, so `innerHTML = ''`
|
|
40
|
-
// stranded them in the body. They must reach the actions cluster.
|
|
41
|
-
it('routes WRAPPED (interpolated) action buttons into the actions cluster, not the body', async () => {
|
|
42
|
-
const a = mount(`
|
|
43
|
-
<agent-artifact-ui title="Build">
|
|
44
|
-
<span style="display:contents" role="presentation">
|
|
45
|
-
<span style="display:contents" role="presentation"><button-ui slot="primary">Run</button-ui></span>
|
|
46
|
-
<span style="display:contents" role="presentation"><button-ui slot="secondary">Cancel</button-ui></span>
|
|
47
|
-
</span>
|
|
48
|
-
<p>Body content</p>
|
|
49
|
-
</agent-artifact-ui>
|
|
50
|
-
`);
|
|
51
|
-
await tick();
|
|
52
|
-
const actions = a.querySelector('[data-artifact-actions]');
|
|
53
|
-
const body = a.querySelector('[data-artifact-body]');
|
|
54
|
-
expect(actions.querySelectorAll('[slot="primary"], [slot="secondary"]').length).toBe(2);
|
|
55
|
-
// The regression: before the wrapper-pierce fix these landed in the body.
|
|
56
|
-
expect(body.querySelectorAll('[slot="primary"], [slot="secondary"]').length).toBe(0);
|
|
57
|
-
expect(body.textContent).toContain('Body content');
|
|
58
|
-
// No empty wrapper spans left dangling — they're flattened, not re-appended.
|
|
59
|
-
expect(a.querySelectorAll('[role="presentation"]').length).toBe(0);
|
|
60
|
-
});
|
|
61
|
-
});
|
|
@@ -1,364 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* alert-ui tests
|
|
3
|
-
*
|
|
4
|
-
* Three content modes (precedence order):
|
|
5
|
-
* 1. Author-provided <span slot="content">…</span> — wins
|
|
6
|
-
* 2. [title] + [description] — bolded headline + body paragraph
|
|
7
|
-
* 3. [text] — single-line message
|
|
8
|
-
*
|
|
9
|
-
* Two alias categories:
|
|
10
|
-
* - First-class aliases (public, no warn): [dismissible] ↔ [closable]
|
|
11
|
-
* - Hallucination aliases (warn-once): variant="error" → "danger";
|
|
12
|
-
* [closeable] → [closable]
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
16
|
-
import '../../core/element.js';
|
|
17
|
-
import './alert.js';
|
|
18
|
-
|
|
19
|
-
const tick = () => new Promise((r) => queueMicrotask(r));
|
|
20
|
-
|
|
21
|
-
function mount(html) {
|
|
22
|
-
const wrap = document.createElement('div');
|
|
23
|
-
wrap.innerHTML = html;
|
|
24
|
-
document.body.appendChild(wrap);
|
|
25
|
-
return wrap.firstElementChild;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
describe('alert-ui — canonical props', () => {
|
|
29
|
-
beforeEach(() => { document.body.innerHTML = ''; });
|
|
30
|
-
|
|
31
|
-
it('renders text from the canonical `text` prop', async () => {
|
|
32
|
-
const a = mount('<alert-ui text="Hello" variant="danger"></alert-ui>');
|
|
33
|
-
await tick();
|
|
34
|
-
const content = a.querySelector(':scope > [slot="content"]');
|
|
35
|
-
expect(content).not.toBeNull();
|
|
36
|
-
expect(content.textContent).toBe('Hello');
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('sets role=alert for danger variant', async () => {
|
|
40
|
-
const a = mount('<alert-ui text="Error" variant="danger"></alert-ui>');
|
|
41
|
-
await tick();
|
|
42
|
-
expect(a.getAttribute('role')).toBe('alert');
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('sets role=status for non-danger variants', async () => {
|
|
46
|
-
const a = mount('<alert-ui text="Info" variant="info"></alert-ui>');
|
|
47
|
-
await tick();
|
|
48
|
-
expect(a.getAttribute('role')).toBe('status');
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('renders [title] + [description] as bold-headline + body (first-class, no warn)', async () => {
|
|
52
|
-
const a = mount('<alert-ui title="Heads up" description="A warning was raised." variant="warning"></alert-ui>');
|
|
53
|
-
await tick();
|
|
54
|
-
const content = a.querySelector(':scope > [slot="content"]');
|
|
55
|
-
expect(content).not.toBeNull();
|
|
56
|
-
expect(content.textContent).toContain('Heads up');
|
|
57
|
-
expect(content.textContent).toContain('A warning was raised.');
|
|
58
|
-
const strong = content.querySelector('strong');
|
|
59
|
-
expect(strong).not.toBeNull();
|
|
60
|
-
expect(strong.textContent).toBe('Heads up');
|
|
61
|
-
// No warn for canonical title/description (they're first-class)
|
|
62
|
-
expect(a.getAttribute('role')).toBe('alert'); // warning → alert role
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('renders [title] alone (description omitted)', async () => {
|
|
66
|
-
const a = mount('<alert-ui title="Heads up" variant="warning"></alert-ui>');
|
|
67
|
-
await tick();
|
|
68
|
-
const content = a.querySelector(':scope > [slot="content"]');
|
|
69
|
-
expect(content).not.toBeNull();
|
|
70
|
-
expect(content.textContent.trim()).toBe('Heads up');
|
|
71
|
-
expect(content.querySelector('strong').textContent).toBe('Heads up');
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('renders [description] alone (title omitted)', async () => {
|
|
75
|
-
const a = mount('<alert-ui description="A quiet notice." variant="info"></alert-ui>');
|
|
76
|
-
await tick();
|
|
77
|
-
const content = a.querySelector(':scope > [slot="content"]');
|
|
78
|
-
expect(content).not.toBeNull();
|
|
79
|
-
expect(content.textContent.trim()).toBe('A quiet notice.');
|
|
80
|
-
expect(content.querySelector('strong')).toBeNull();
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('[dismissible] is first-class — same effect as [closable], no warn', async () => {
|
|
84
|
-
const a = mount('<alert-ui text="Banner" dismissible></alert-ui>');
|
|
85
|
-
await tick();
|
|
86
|
-
expect(a.hasAttribute('closable')).toBe(true);
|
|
87
|
-
// dismissible attribute is preserved on the host for forward-compat
|
|
88
|
-
// (consumers can read either)
|
|
89
|
-
expect(a.querySelector('[slot="close"]')).not.toBeNull();
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('[closable] works canonically without [dismissible]', async () => {
|
|
93
|
-
const a = mount('<alert-ui text="Banner" closable></alert-ui>');
|
|
94
|
-
await tick();
|
|
95
|
-
expect(a.hasAttribute('closable')).toBe(true);
|
|
96
|
-
expect(a.querySelector('[slot="close"]')).not.toBeNull();
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
describe('alert-ui — hallucination aliases (warn-once)', () => {
|
|
101
|
-
beforeEach(() => { document.body.innerHTML = ''; });
|
|
102
|
-
|
|
103
|
-
it('maps variant="error" to variant="danger" (warns)', async () => {
|
|
104
|
-
const a = mount('<alert-ui text="Oops" variant="error"></alert-ui>');
|
|
105
|
-
await tick();
|
|
106
|
-
expect(a.getAttribute('variant')).toBe('danger');
|
|
107
|
-
expect(a.getAttribute('role')).toBe('alert');
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('maps [closeable] (misspelling) to [closable] (warns)', async () => {
|
|
111
|
-
const a = mount('<alert-ui text="Banner" closeable></alert-ui>');
|
|
112
|
-
await tick();
|
|
113
|
-
expect(a.hasAttribute('closable')).toBe(true);
|
|
114
|
-
expect(a.hasAttribute('closeable')).toBe(false);
|
|
115
|
-
expect(a.querySelector('[slot="close"]')).not.toBeNull();
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
describe('alert-ui — full corpus shape (the §34 scenario)', () => {
|
|
120
|
-
beforeEach(() => { document.body.innerHTML = ''; });
|
|
121
|
-
|
|
122
|
-
it('renders the exact alert-banner.json corpus pattern correctly', async () => {
|
|
123
|
-
// This is `patterns/agent/alert-banner.json` rendered through the
|
|
124
|
-
// component. All four props are now first-class / aliased; the
|
|
125
|
-
// alert renders with full visible content.
|
|
126
|
-
const a = mount(`<alert-ui
|
|
127
|
-
variant="info"
|
|
128
|
-
icon="info-circle"
|
|
129
|
-
title="System Update"
|
|
130
|
-
description="A new version is available. Please refresh to get the latest features."
|
|
131
|
-
dismissible
|
|
132
|
-
></alert-ui>`);
|
|
133
|
-
await tick();
|
|
134
|
-
// Variant stays canonical
|
|
135
|
-
expect(a.getAttribute('variant')).toBe('info');
|
|
136
|
-
// Dismissible maps to closable
|
|
137
|
-
expect(a.hasAttribute('closable')).toBe(true);
|
|
138
|
-
// Role is status (info, not danger/warning)
|
|
139
|
-
expect(a.getAttribute('role')).toBe('status');
|
|
140
|
-
// Content composed correctly
|
|
141
|
-
const content = a.querySelector(':scope > [slot="content"]');
|
|
142
|
-
expect(content.textContent).toContain('System Update');
|
|
143
|
-
expect(content.textContent).toContain('A new version is available.');
|
|
144
|
-
expect(content.querySelector('strong').textContent).toBe('System Update');
|
|
145
|
-
// Close button present (dismissible → closable)
|
|
146
|
-
expect(a.querySelector('[slot="close"]')).not.toBeNull();
|
|
147
|
-
// ARIA composed
|
|
148
|
-
expect(a.getAttribute('aria-label')).toContain('System Update');
|
|
149
|
-
expect(a.getAttribute('aria-label')).toContain('A new version is available');
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it('renders the exact destructive-confirm.json corpus pattern correctly', async () => {
|
|
153
|
-
// This is the shape used in compositions/forms/destructive-confirm.json:
|
|
154
|
-
// variant=danger + title + description (no close button)
|
|
155
|
-
const a = mount(`<alert-ui
|
|
156
|
-
variant="danger"
|
|
157
|
-
title="This action is permanent"
|
|
158
|
-
description="Deleting your account cannot be undone. All your data will be removed within 30 days."
|
|
159
|
-
></alert-ui>`);
|
|
160
|
-
await tick();
|
|
161
|
-
expect(a.getAttribute('variant')).toBe('danger');
|
|
162
|
-
expect(a.getAttribute('role')).toBe('alert');
|
|
163
|
-
const content = a.querySelector(':scope > [slot="content"]');
|
|
164
|
-
expect(content.querySelector('strong').textContent).toBe('This action is permanent');
|
|
165
|
-
expect(content.textContent).toContain('Deleting your account cannot be undone');
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it('preserves author-provided [slot="content"] when [title]+[description] are also set', async () => {
|
|
169
|
-
// Authored rich content wins; canonical props become metadata only
|
|
170
|
-
// (still mirrored to aria-label for screen reader semantics).
|
|
171
|
-
const a = mount(`<alert-ui title="hdr" description="body">
|
|
172
|
-
<span slot="content">Custom <a href="/foo">link</a></span>
|
|
173
|
-
</alert-ui>`);
|
|
174
|
-
await tick();
|
|
175
|
-
const content = a.querySelector(':scope > [slot="content"]');
|
|
176
|
-
// Should preserve the author's link
|
|
177
|
-
expect(content.querySelector('a')).not.toBeNull();
|
|
178
|
-
expect(content.querySelector('a').getAttribute('href')).toBe('/foo');
|
|
179
|
-
});
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
describe('alert-ui — SPEC-006 dunning pattern', () => {
|
|
183
|
-
beforeEach(() => { document.body.innerHTML = ''; });
|
|
184
|
-
|
|
185
|
-
it('formats amount via Intl.NumberFormat with currency', async () => {
|
|
186
|
-
const a = mount(`<alert-ui pattern="dunning" amount="24.50" currency="USD"
|
|
187
|
-
due-at="2026-05-20" card-last4="4242"
|
|
188
|
-
reason="declined" variant="danger"></alert-ui>`);
|
|
189
|
-
await tick();
|
|
190
|
-
const amt = a.querySelector('[data-dunning-amount]');
|
|
191
|
-
expect(amt).not.toBeNull();
|
|
192
|
-
// en-US default formatting (jsdom): "$24.50"
|
|
193
|
-
expect(amt.textContent).toMatch(/\$24\.50|USD\s*24\.50/);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it('formats due date via Intl.DateTimeFormat', async () => {
|
|
197
|
-
const a = mount(`<alert-ui pattern="dunning" amount="24.50" currency="USD"
|
|
198
|
-
due-at="2026-05-20T00:00:00Z"
|
|
199
|
-
reason="declined" variant="danger"></alert-ui>`);
|
|
200
|
-
await tick();
|
|
201
|
-
const meta = a.querySelector('[data-dunning-meta]');
|
|
202
|
-
expect(meta).not.toBeNull();
|
|
203
|
-
// Expect month name + day + year somewhere in the meta
|
|
204
|
-
expect(meta.textContent).toMatch(/May/);
|
|
205
|
-
expect(meta.textContent).toMatch(/2026/);
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
it('stamps the cardLast4 metadata', async () => {
|
|
209
|
-
const a = mount(`<alert-ui pattern="dunning" amount="24.50" currency="USD"
|
|
210
|
-
due-at="2026-05-20" card-last4="4242"
|
|
211
|
-
reason="declined" variant="danger"></alert-ui>`);
|
|
212
|
-
await tick();
|
|
213
|
-
const meta = a.querySelector('[data-dunning-meta]');
|
|
214
|
-
expect(meta.textContent).toContain('ending 4242');
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
it('stamps x-circle icon for reason="declined"', async () => {
|
|
218
|
-
const a = mount(`<alert-ui pattern="dunning" reason="declined" variant="danger"
|
|
219
|
-
amount="10" currency="USD"></alert-ui>`);
|
|
220
|
-
await tick();
|
|
221
|
-
const icon = a.querySelector('[slot="leading"]');
|
|
222
|
-
expect(icon).not.toBeNull();
|
|
223
|
-
expect(icon.getAttribute('name')).toBe('x-circle');
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it('stamps clock icon for reason="expired"', async () => {
|
|
227
|
-
const a = mount(`<alert-ui pattern="dunning" reason="expired" variant="danger"
|
|
228
|
-
amount="10" currency="USD"></alert-ui>`);
|
|
229
|
-
await tick();
|
|
230
|
-
const icon = a.querySelector('[slot="leading"]');
|
|
231
|
-
expect(icon.getAttribute('name')).toBe('clock');
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
it('dispatches dunning-action event on descendant [data-dunning-action] click', async () => {
|
|
235
|
-
const a = mount(`<alert-ui pattern="dunning" amount="24.50" currency="USD"
|
|
236
|
-
due-at="2026-05-20" reason="declined" variant="danger">
|
|
237
|
-
<button-ui slot="actions" data-dunning-action="update" text="Update"></button-ui>
|
|
238
|
-
<button-ui slot="actions" data-dunning-action="retry" text="Retry"></button-ui>
|
|
239
|
-
</alert-ui>`);
|
|
240
|
-
await tick();
|
|
241
|
-
|
|
242
|
-
const received = [];
|
|
243
|
-
a.addEventListener('dunning-action', (e) => received.push(e.detail));
|
|
244
|
-
|
|
245
|
-
const updateBtn = a.querySelector('[data-dunning-action="update"]');
|
|
246
|
-
updateBtn.dispatchEvent(new Event('press', { bubbles: true }));
|
|
247
|
-
await tick();
|
|
248
|
-
|
|
249
|
-
expect(received.length).toBe(1);
|
|
250
|
-
expect(received[0].action).toBe('update');
|
|
251
|
-
expect(received[0].amount).toBe(24.5);
|
|
252
|
-
expect(received[0].currency).toBe('USD');
|
|
253
|
-
expect(received[0].dueAt).toBe('2026-05-20');
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
it('dispatches dunning-action with action="retry" when retry button is pressed', async () => {
|
|
257
|
-
const a = mount(`<alert-ui pattern="dunning" amount="10" currency="USD"
|
|
258
|
-
due-at="2026-05-20" reason="declined" variant="danger">
|
|
259
|
-
<button-ui slot="actions" data-dunning-action="retry" text="Retry"></button-ui>
|
|
260
|
-
</alert-ui>`);
|
|
261
|
-
await tick();
|
|
262
|
-
|
|
263
|
-
const received = [];
|
|
264
|
-
a.addEventListener('dunning-action', (e) => received.push(e.detail));
|
|
265
|
-
|
|
266
|
-
const retryBtn = a.querySelector('[data-dunning-action="retry"]');
|
|
267
|
-
retryBtn.dispatchEvent(new Event('press', { bubbles: true }));
|
|
268
|
-
await tick();
|
|
269
|
-
|
|
270
|
-
expect(received.length).toBe(1);
|
|
271
|
-
expect(received[0].action).toBe('retry');
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
it('re-formats amount when the [amount] attribute changes', async () => {
|
|
275
|
-
const a = mount(`<alert-ui pattern="dunning" amount="10" currency="USD"
|
|
276
|
-
due-at="2026-05-20" reason="declined" variant="danger"></alert-ui>`);
|
|
277
|
-
await tick();
|
|
278
|
-
let amt = a.querySelector('[data-dunning-amount]');
|
|
279
|
-
expect(amt.textContent).toMatch(/10/);
|
|
280
|
-
|
|
281
|
-
a.amount = 99.99;
|
|
282
|
-
await tick();
|
|
283
|
-
amt = a.querySelector('[data-dunning-amount]');
|
|
284
|
-
expect(amt.textContent).toMatch(/99\.99/);
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
it('inherits role="alert" for variant="danger" in dunning mode', async () => {
|
|
288
|
-
const a = mount(`<alert-ui pattern="dunning" variant="danger" amount="10" currency="USD"
|
|
289
|
-
due-at="2026-05-20" reason="declined"></alert-ui>`);
|
|
290
|
-
await tick();
|
|
291
|
-
expect(a.getAttribute('role')).toBe('alert');
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
it('inherits role="alert" for variant="warning" (grace period)', async () => {
|
|
295
|
-
const a = mount(`<alert-ui pattern="dunning" variant="warning" amount="10" currency="USD"
|
|
296
|
-
due-at="2026-06-01"></alert-ui>`);
|
|
297
|
-
await tick();
|
|
298
|
-
// warning maps to role=alert (same as danger via #updateRole)
|
|
299
|
-
expect(a.getAttribute('role')).toBe('alert');
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
it('grace-period (warning) uses "due" prefix in date meta, not "failed"', async () => {
|
|
303
|
-
const a = mount(`<alert-ui pattern="dunning" variant="warning" amount="10" currency="USD"
|
|
304
|
-
due-at="2026-06-01"></alert-ui>`);
|
|
305
|
-
await tick();
|
|
306
|
-
const meta = a.querySelector('[data-dunning-meta]');
|
|
307
|
-
expect(meta.textContent).toContain('due');
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
it('default title for declined is "Payment failed"', async () => {
|
|
311
|
-
const a = mount(`<alert-ui pattern="dunning" reason="declined" variant="danger"
|
|
312
|
-
amount="10" currency="USD"></alert-ui>`);
|
|
313
|
-
await tick();
|
|
314
|
-
const titleEl = a.querySelector('[data-dunning-title]');
|
|
315
|
-
expect(titleEl.textContent).toBe('Payment failed');
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
it('default title for expired is "Card expired"', async () => {
|
|
319
|
-
const a = mount(`<alert-ui pattern="dunning" reason="expired" variant="danger"
|
|
320
|
-
amount="10" currency="USD"></alert-ui>`);
|
|
321
|
-
await tick();
|
|
322
|
-
const titleEl = a.querySelector('[data-dunning-title]');
|
|
323
|
-
expect(titleEl.textContent).toBe('Card expired');
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
it('explicit [title] overrides the per-reason default', async () => {
|
|
327
|
-
const a = mount(`<alert-ui pattern="dunning" reason="declined" title="Custom message"
|
|
328
|
-
amount="10" currency="USD" variant="danger"></alert-ui>`);
|
|
329
|
-
await tick();
|
|
330
|
-
const titleEl = a.querySelector('[data-dunning-title]');
|
|
331
|
-
expect(titleEl.textContent).toBe('Custom message');
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
it('explicit [icon] overrides the per-reason icon map', async () => {
|
|
335
|
-
const a = mount(`<alert-ui pattern="dunning" reason="declined" icon="credit-card"
|
|
336
|
-
amount="10" currency="USD" variant="danger"></alert-ui>`);
|
|
337
|
-
await tick();
|
|
338
|
-
const icon = a.querySelector('[slot="leading"]');
|
|
339
|
-
expect(icon.getAttribute('name')).toBe('credit-card');
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
it('composes aria-label from title + amount + meta', async () => {
|
|
343
|
-
const a = mount(`<alert-ui pattern="dunning" reason="declined" amount="24.50"
|
|
344
|
-
currency="USD" due-at="2026-05-20" card-last4="4242"
|
|
345
|
-
variant="danger"></alert-ui>`);
|
|
346
|
-
await tick();
|
|
347
|
-
const aria = a.getAttribute('aria-label');
|
|
348
|
-
expect(aria).toContain('Payment failed');
|
|
349
|
-
expect(aria).toMatch(/24\.50/);
|
|
350
|
-
expect(aria).toMatch(/4242/);
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
it('does not stamp dunning content when pattern is empty (standard mode)', async () => {
|
|
354
|
-
const a = mount(`<alert-ui variant="danger" amount="24.50" currency="USD"
|
|
355
|
-
due-at="2026-05-20" reason="declined"
|
|
356
|
-
text="standard message"></alert-ui>`);
|
|
357
|
-
await tick();
|
|
358
|
-
// No dunning markers — uses the standard text-only render path
|
|
359
|
-
expect(a.querySelector('[data-dunning-amount]')).toBeNull();
|
|
360
|
-
expect(a.querySelector('[data-dunning-title]')).toBeNull();
|
|
361
|
-
const content = a.querySelector(':scope > [slot="content"]');
|
|
362
|
-
expect(content.textContent.trim()).toBe('standard message');
|
|
363
|
-
});
|
|
364
|
-
});
|