@adia-ai/web-components 0.8.19 → 0.8.21

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