@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
@@ -1,43 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
2
- import { shimmerLoading } from './shimmer-loading.js';
3
- import { mountHost, connectTrait, resetDOM } from '../test-helpers.js';
4
-
5
- describe('shimmer-loading', () => {
6
- beforeEach(resetDOM);
7
-
8
- it('sets data-shimmer-loading-active and appends an overlay', () => {
9
- const host = mountHost();
10
- connectTrait(shimmerLoading, host);
11
- expect(host.hasAttribute('data-shimmer-loading-active')).toBe(true);
12
- // Either a static muted overlay (reduced-motion) or the shimmer overlay
13
- // is appended as a child div.
14
- const overlay = host.querySelector('div');
15
- expect(overlay).toBeTruthy();
16
- });
17
-
18
- it('disconnect removes overlay + cleans active attribute', () => {
19
- const host = mountHost();
20
- const inst = shimmerLoading();
21
- inst.connect(host);
22
- expect(host.querySelector('div')).toBeTruthy();
23
- inst.disconnect(host);
24
- expect(host.hasAttribute('data-shimmer-loading-active')).toBe(false);
25
- expect(host.querySelector('div')).toBeFalsy();
26
- });
27
-
28
- it('host gets position:relative if not already set (so the overlay anchors)', () => {
29
- const host = mountHost();
30
- connectTrait(shimmerLoading, host);
31
- expect(host.style.position).toBe('relative');
32
- });
33
-
34
- it('parallel attach to two hosts: each gets its own overlay', () => {
35
- const a = mountHost();
36
- const b = mountHost();
37
- shimmerLoading().connect(a);
38
- shimmerLoading().connect(b);
39
- expect(a.querySelector('div')).toBeTruthy();
40
- expect(b.querySelector('div')).toBeTruthy();
41
- expect(a.querySelector('div')).not.toBe(b.querySelector('div'));
42
- });
43
- });
@@ -1,111 +0,0 @@
1
- /**
2
- * Universal trait contract — runs against every trait imported from
3
- * traits/index.js. Asserts schema integrity, attribute cleanup on
4
- * disconnect, and idempotent connect/disconnect cycles.
5
- *
6
- * Per-trait behavior tests live in `<trait>.test.js` siblings.
7
- */
8
-
9
- import { describe, it, expect, beforeEach } from 'vitest';
10
- import * as traits from './index.js';
11
- import { mountHost, connectTrait, expectValidSchema, resetDOM } from './test-helpers.js';
12
-
13
- const KNOWN_CATEGORIES = new Set([
14
- 'input-interaction',
15
- 'keyboard-navigation',
16
- 'forms-data',
17
- 'layout-measurement',
18
- 'motion-positioning',
19
- 'animation-feedback',
20
- 'visual-dynamics',
21
- 'interaction-delight',
22
- 'audio-haptics-sensory',
23
- ]);
24
-
25
- const ALL_TRAITS = Object.entries(traits).filter(([, fn]) => typeof fn === 'function' && fn.schema);
26
-
27
- describe('trait registry — universal contract', () => {
28
- beforeEach(resetDOM);
29
-
30
- it('exports at least 40 traits', () => {
31
- expect(ALL_TRAITS.length).toBeGreaterThanOrEqual(40);
32
- });
33
-
34
- it('every trait declares a known category', () => {
35
- for (const [exportName, trait] of ALL_TRAITS) {
36
- expect(KNOWN_CATEGORIES.has(trait.schema.category), `${exportName} category=${trait.schema.category}`).toBe(true);
37
- }
38
- });
39
-
40
- it('every trait has a non-empty description', () => {
41
- for (const [exportName, trait] of ALL_TRAITS) {
42
- expect(trait.schema.description, `${exportName} missing description`).toBeTruthy();
43
- expect(trait.schema.description.length, `${exportName} description too short`).toBeGreaterThan(10);
44
- }
45
- });
46
-
47
- it('every trait passes the schema-shape contract', () => {
48
- for (const [, trait] of ALL_TRAITS) expectValidSchema(trait);
49
- });
50
-
51
- it('attribute, event, and config names use kebab-case', () => {
52
- const KEBAB_OR_DATA = /^(?:data-)?[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
53
- for (const [exportName, trait] of ALL_TRAITS) {
54
- for (const a of trait.schema.attributes) {
55
- expect(KEBAB_OR_DATA.test(a), `${exportName} attribute "${a}" not kebab-case`).toBe(true);
56
- expect(a.startsWith('data-'), `${exportName} attribute "${a}" must start with data-`).toBe(true);
57
- }
58
- for (const e of trait.schema.events) {
59
- expect(KEBAB_OR_DATA.test(e), `${exportName} event "${e}" not kebab-case`).toBe(true);
60
- }
61
- for (const c of trait.schema.config) {
62
- expect(c.startsWith('data-'), `${exportName} config "${c}" must start with data-`).toBe(true);
63
- }
64
- }
65
- });
66
-
67
- it('connect → disconnect leaves no managed attributes behind', () => {
68
- for (const [exportName, trait] of ALL_TRAITS) {
69
- const host = mountHost();
70
- const inst = connectTrait(trait, host);
71
- inst.disconnect(host);
72
- for (const attr of trait.schema.attributes) {
73
- expect(host.hasAttribute(attr), `${exportName} left "${attr}" after disconnect`).toBe(false);
74
- }
75
- host.remove();
76
- }
77
- });
78
-
79
- it('connect → disconnect → connect again does not throw', () => {
80
- for (const [exportName, trait] of ALL_TRAITS) {
81
- const host = mountHost();
82
- const inst1 = connectTrait(trait, host);
83
- inst1.disconnect(host);
84
- expect(() => {
85
- const inst2 = connectTrait(trait, host);
86
- inst2.disconnect(host);
87
- }, `${exportName} reconnect threw`).not.toThrow();
88
- host.remove();
89
- }
90
- });
91
-
92
- it('two parallel instances on different hosts do not interfere', () => {
93
- for (const [exportName, trait] of ALL_TRAITS) {
94
- const a = mountHost();
95
- const b = mountHost();
96
- const instA = connectTrait(trait, a);
97
- const instB = connectTrait(trait, b);
98
- // Disconnect one — the other should still clean up correctly.
99
- expect(() => instA.disconnect(a), `${exportName} disconnect-A threw`).not.toThrow();
100
- expect(() => instB.disconnect(b), `${exportName} disconnect-B threw`).not.toThrow();
101
- a.remove();
102
- b.remove();
103
- }
104
- });
105
-
106
- it('catalog count matches export count', async () => {
107
- const mod = await import('./_catalog.json', { with: { type: 'json' } });
108
- const catalog = mod.default || mod;
109
- expect(catalog.count, `catalog says ${catalog.count}, exports has ${ALL_TRAITS.length}`).toBe(ALL_TRAITS.length);
110
- });
111
- });
@@ -1,40 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
2
- import { snapToGrid } from './snap-to-grid.js';
3
- import { mountHost, connectTrait, resetDOM } from '../test-helpers.js';
4
-
5
- describe('snap-to-grid', () => {
6
- beforeEach(resetDOM);
7
-
8
- it('drag-end with x,y rounds to nearest grid increment (default 16)', () => {
9
- const host = mountHost();
10
- connectTrait(snapToGrid, host);
11
- host.dispatchEvent(new CustomEvent('drag-end', { detail: { x: 23, y: 41 } }));
12
- expect(host.style.translate).toBe('16px 48px');
13
- expect(host.hasAttribute('data-snap-to-grid-snapped')).toBe(true);
14
- });
15
-
16
- it('respects data-snap-grid override', () => {
17
- const host = mountHost('div', { 'data-snap-grid': '50' });
18
- connectTrait(snapToGrid, host);
19
- host.dispatchEvent(new CustomEvent('drag-end', { detail: { x: 73, y: 124 } }));
20
- expect(host.style.translate).toBe('50px 100px');
21
- });
22
-
23
- it('drag-end without detail is a no-op', () => {
24
- const host = mountHost();
25
- connectTrait(snapToGrid, host);
26
- host.dispatchEvent(new CustomEvent('drag-end', { detail: {} }));
27
- // happy-dom may return undefined or '' for unset style.translate; both
28
- // mean "untouched" which is the contract here.
29
- expect(host.style.translate || '').toBe('');
30
- expect(host.hasAttribute('data-snap-to-grid-snapped')).toBe(false);
31
- });
32
-
33
- it('disconnect clears the snapped attribute', () => {
34
- const host = mountHost();
35
- const inst = connectTrait(snapToGrid, host);
36
- host.dispatchEvent(new CustomEvent('drag-end', { detail: { x: 0, y: 0 } }));
37
- inst.disconnect(host);
38
- expect(host.hasAttribute('data-snap-to-grid-snapped')).toBe(false);
39
- });
40
- });
@@ -1,26 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
2
- import { soundFeedback } from './sound-feedback.js';
3
- import { mountHost, connectTrait, resetDOM } from '../test-helpers.js';
4
-
5
- describe('sound-feedback', () => {
6
- beforeEach(resetDOM);
7
-
8
- it('connect sets data-sound-feedback-active', () => {
9
- const host = mountHost();
10
- connectTrait(soundFeedback, host);
11
- expect(host.hasAttribute('data-sound-feedback-active')).toBe(true);
12
- });
13
-
14
- it('disconnect clears active + stops listening', () => {
15
- const host = mountHost();
16
- const inst = connectTrait(soundFeedback, host);
17
- inst.disconnect(host);
18
- expect(host.hasAttribute('data-sound-feedback-active')).toBe(false);
19
- });
20
-
21
- it('without data-sound-type: press fires no audio (no throw)', () => {
22
- const host = mountHost();
23
- connectTrait(soundFeedback, host);
24
- expect(() => host.dispatchEvent(new CustomEvent('press', { bubbles: true }))).not.toThrow();
25
- });
26
- });
@@ -1,28 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
2
- import { springAnimate } from './spring-animate.js';
3
- import { mountHost, connectTrait, resetDOM } from '../test-helpers.js';
4
-
5
- describe('spring-animate', () => {
6
- beforeEach(resetDOM);
7
-
8
- it('connect sets data-spring-animate-active', () => {
9
- const host = mountHost();
10
- connectTrait(springAnimate, host);
11
- expect(host.hasAttribute('data-spring-animate-active')).toBe(true);
12
- });
13
-
14
- it('disconnect clears active', () => {
15
- const host = mountHost();
16
- const inst = connectTrait(springAnimate, host);
17
- inst.disconnect(host);
18
- expect(host.hasAttribute('data-spring-animate-active')).toBe(false);
19
- });
20
-
21
- it('respects data-spring-stiffness + data-spring-damping config', () => {
22
- const host = mountHost('div', {
23
- 'data-spring-stiffness': '200',
24
- 'data-spring-damping': '0.6',
25
- });
26
- expect(() => connectTrait(springAnimate, host)).not.toThrow();
27
- });
28
- });
@@ -1,120 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
2
- import { successCheckmark } from './success-checkmark.js';
3
- import { mountHost, connectTrait, spyEvent, resetDOM, wait } from '../test-helpers.js';
4
-
5
- // Helper: query the SVG that the trait stamps. As of the overflow:hidden
6
- // fix, the SVG renders at body level (position: fixed) so it can't be
7
- // clipped by the host's ancestors — query document.body, not the host.
8
- const findSvg = () => document.body.querySelector('svg');
9
-
10
- describe('success-checkmark', () => {
11
- beforeEach(resetDOM);
12
-
13
- it('schema declares animation-feedback category', () => {
14
- expect(successCheckmark.schema.category).toBe('animation-feedback');
15
- expect(successCheckmark.schema.events).toContain('success-checkmark-done');
16
- expect(successCheckmark.schema.attributes).toContain('data-success-checkmark-active');
17
- });
18
-
19
- it('connect → disconnect leaves no managed attribute', () => {
20
- const host = mountHost();
21
- const inst = connectTrait(successCheckmark, host);
22
- inst.disconnect(host);
23
- expect(host.hasAttribute('data-success-checkmark-active')).toBe(false);
24
- });
25
-
26
- it('stamps an SVG on validated event with detail.valid === true', () => {
27
- const host = mountHost();
28
- connectTrait(successCheckmark, host);
29
-
30
- host.dispatchEvent(new CustomEvent('validated', {
31
- detail: { valid: true, errors: [] },
32
- }));
33
-
34
- expect(host.hasAttribute('data-success-checkmark-active')).toBe(true);
35
- const svg = findSvg();
36
- expect(svg).not.toBeNull();
37
- expect(svg.querySelector('path')).not.toBeNull();
38
- });
39
-
40
- it('does not stamp on validated event with detail.valid === false', () => {
41
- const host = mountHost();
42
- connectTrait(successCheckmark, host);
43
-
44
- host.dispatchEvent(new CustomEvent('validated', {
45
- detail: { valid: false, errors: ['nope'] },
46
- }));
47
-
48
- expect(host.hasAttribute('data-success-checkmark-active')).toBe(false);
49
- expect(findSvg()).toBeNull();
50
- });
51
-
52
- it('fires on data-validation-valid (false → true)', async () => {
53
- const host = mountHost();
54
- connectTrait(successCheckmark, host);
55
-
56
- host.setAttribute('data-validation-valid', '');
57
- await wait(0);
58
- expect(host.hasAttribute('data-success-checkmark-active')).toBe(true);
59
- expect(findSvg()).not.toBeNull();
60
- });
61
-
62
- it('respects data-success-checkmark-trigger attribute toggle', async () => {
63
- const host = mountHost();
64
- connectTrait(successCheckmark, host);
65
-
66
- host.setAttribute('data-success-checkmark-trigger', '');
67
- await wait(0);
68
- expect(findSvg()).not.toBeNull();
69
- });
70
-
71
- it('renders position: fixed at viewport coordinates (no longer clipped by host overflow)', () => {
72
- const host = mountHost();
73
- connectTrait(successCheckmark, host);
74
-
75
- host.dispatchEvent(new CustomEvent('validated', {
76
- detail: { valid: true, errors: [] },
77
- }));
78
-
79
- const svg = findSvg();
80
- expect(svg).not.toBeNull();
81
- // Body-level rendering with position: fixed escapes any
82
- // overflow:hidden ancestor that the host might be inside.
83
- expect(svg.style.position).toBe('fixed');
84
- expect(svg.parentElement).toBe(document.body);
85
- });
86
-
87
- it('removes the SVG and fires success-checkmark-done after the visible+fade window', async () => {
88
- const host = mountHost();
89
- connectTrait(successCheckmark, host);
90
- const spy = spyEvent(host, 'success-checkmark-done');
91
-
92
- host.dispatchEvent(new CustomEvent('validated', { detail: { valid: true } }));
93
- expect(findSvg()).not.toBeNull();
94
-
95
- // 1500ms hold + 300ms fade = ~1800ms; give a small grace window.
96
- await wait(1900);
97
- expect(findSvg()).toBeNull();
98
- expect(spy.count).toBe(1);
99
- expect(host.hasAttribute('data-success-checkmark-active')).toBe(false);
100
- });
101
-
102
- it('disconnect removes any in-flight SVG', () => {
103
- const host = mountHost();
104
- const inst = connectTrait(successCheckmark, host);
105
- host.dispatchEvent(new CustomEvent('validated', { detail: { valid: true } }));
106
- expect(findSvg()).not.toBeNull();
107
-
108
- inst.disconnect(host);
109
- expect(findSvg()).toBeNull();
110
- expect(host.hasAttribute('data-success-checkmark-active')).toBe(false);
111
- });
112
-
113
- it('does not mutate host.style.position (body-level rendering means the host needs no positioning context)', () => {
114
- const host = mountHost();
115
- const before = host.style.position;
116
- connectTrait(successCheckmark, host);
117
- host.dispatchEvent(new CustomEvent('validated', { detail: { valid: true } }));
118
- expect(host.style.position).toBe(before);
119
- });
120
- });
@@ -1,32 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
2
- import { tiltHover } from './tilt-hover.js';
3
- import { mountHost, connectTrait, resetDOM } from '../test-helpers.js';
4
-
5
- describe('tilt-hover', () => {
6
- beforeEach(resetDOM);
7
-
8
- it('pointermove sets data-tilt-hover-active and applies transform', () => {
9
- const host = mountHost();
10
- connectTrait(tiltHover, host);
11
- host.dispatchEvent(new PointerEvent('pointermove', { clientX: 100, clientY: 100, bubbles: true }));
12
- expect(host.hasAttribute('data-tilt-hover-active')).toBe(true);
13
- expect(host.style.transform).toContain('perspective');
14
- });
15
-
16
- it('pointerleave clears active', () => {
17
- const host = mountHost();
18
- connectTrait(tiltHover, host);
19
- host.dispatchEvent(new PointerEvent('pointermove', { clientX: 50, clientY: 50, bubbles: true }));
20
- host.dispatchEvent(new PointerEvent('pointerleave', { bubbles: true }));
21
- expect(host.hasAttribute('data-tilt-hover-active')).toBe(false);
22
- });
23
-
24
- it('disconnect mid-tilt clears active + transform', () => {
25
- const host = mountHost();
26
- const inst = connectTrait(tiltHover, host);
27
- host.dispatchEvent(new PointerEvent('pointermove', { clientX: 50, clientY: 50, bubbles: true }));
28
- inst.disconnect(host);
29
- expect(host.hasAttribute('data-tilt-hover-active')).toBe(false);
30
- expect(host.style.transform).toBe('');
31
- });
32
- });
@@ -1,31 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
2
- import { tossable } from './tossable.js';
3
- import { mountHost, connectTrait, resetDOM } from '../test-helpers.js';
4
-
5
- function pointer(host, type, x, y) {
6
- host.dispatchEvent(new PointerEvent(type, { clientX: x, clientY: y, pointerId: 1, bubbles: true }));
7
- }
8
-
9
- describe('tossable', () => {
10
- beforeEach(resetDOM);
11
-
12
- it('connect attaches without throwing', () => {
13
- const host = mountHost();
14
- expect(() => connectTrait(tossable, host)).not.toThrow();
15
- });
16
-
17
- it('drag sequence completes without throwing + disconnect cleans up', () => {
18
- const host = mountHost();
19
- const inst = connectTrait(tossable, host);
20
- pointer(host, 'pointerdown', 0, 0);
21
- pointer(host, 'pointermove', 50, 0);
22
- pointer(host, 'pointerup', 50, 0);
23
- expect(() => inst.disconnect(host)).not.toThrow();
24
- expect(host.hasAttribute('data-tossable-flying')).toBe(false);
25
- });
26
-
27
- it('respects data-tossable-bounds config', () => {
28
- const host = mountHost('div', { 'data-tossable-bounds': 'parent' });
29
- expect(() => connectTrait(tossable, host)).not.toThrow();
30
- });
31
- });
@@ -1,73 +0,0 @@
1
- /**
2
- * <traits-host> behavior tests — focus on the wrapper-specific contract:
3
- * - children pass through visually (display: contents)
4
- * - declarative traits attach to the wrapper
5
- * - traits attribute swaps work the same as on UIElement subclasses
6
- * - events from children bubble up to the wrapper for trait capture
7
- */
8
-
9
- import { describe, it, expect, beforeEach } from 'vitest';
10
- import './traits-host.js';
11
- import './pressable/pressable.js';
12
- import './hoverable/hoverable.js';
13
- import { resetDOM } from './test-helpers.js';
14
-
15
- describe('<traits-host>', () => {
16
- beforeEach(resetDOM);
17
-
18
- it('is registered as a UIElement subclass', () => {
19
- const ctor = customElements.get('traits-host');
20
- expect(ctor).toBeTruthy();
21
- });
22
-
23
- it('declarative traits=" " attaches to the wrapper itself', () => {
24
- document.body.innerHTML = '<traits-host traits="pressable"><div>Raw</div></traits-host>';
25
- const host = document.body.firstElementChild;
26
- host.dispatchEvent(new PointerEvent('pointerdown'));
27
- expect(host.hasAttribute('data-pressable-pressed')).toBe(true);
28
- host.dispatchEvent(new PointerEvent('pointerup'));
29
- expect(host.hasAttribute('data-pressable-pressed')).toBe(false);
30
- });
31
-
32
- it('events from children bubble up so the trait captures them', () => {
33
- document.body.innerHTML = '<traits-host traits="hoverable"><span class="inner">Hover</span></traits-host>';
34
- const host = document.body.firstElementChild;
35
- const inner = host.querySelector('.inner');
36
- // Synthesize a pointerenter on the wrapper (DOM bubbling up from inner
37
- // would also work but happy-dom doesn't always reflect that for hover).
38
- host.dispatchEvent(new PointerEvent('pointerenter', { bubbles: true }));
39
- expect(host.hasAttribute('data-hoverable-hover')).toBe(true);
40
- host.dispatchEvent(new PointerEvent('pointerleave', { bubbles: true }));
41
- expect(host.hasAttribute('data-hoverable-hover')).toBe(false);
42
- });
43
-
44
- it('changing traits attribute swaps the trait stack', () => {
45
- document.body.innerHTML = '<traits-host traits="pressable"><div>x</div></traits-host>';
46
- const host = document.body.firstElementChild;
47
- host.dispatchEvent(new PointerEvent('pointerdown'));
48
- expect(host.hasAttribute('data-pressable-pressed')).toBe(true);
49
- host.dispatchEvent(new PointerEvent('pointerup'));
50
-
51
- host.setAttribute('traits', 'hoverable');
52
- host.dispatchEvent(new PointerEvent('pointerdown'));
53
- expect(host.hasAttribute('data-pressable-pressed')).toBe(false);
54
- host.dispatchEvent(new PointerEvent('pointerenter'));
55
- expect(host.hasAttribute('data-hoverable-hover')).toBe(true);
56
- });
57
-
58
- it('removing the host cleans up trait attributes', () => {
59
- document.body.innerHTML = '<traits-host traits="pressable"><div>x</div></traits-host>';
60
- const host = document.body.firstElementChild;
61
- host.dispatchEvent(new PointerEvent('pointerdown'));
62
- expect(host.hasAttribute('data-pressable-pressed')).toBe(true);
63
- host.remove();
64
- expect(host.hasAttribute('data-pressable-pressed')).toBe(false);
65
- });
66
-
67
- it('with no traits attribute: pure pass-through, children render', () => {
68
- document.body.innerHTML = '<traits-host><span class="x">child</span></traits-host>';
69
- const host = document.body.firstElementChild;
70
- expect(host.querySelector('.x')).toBeTruthy();
71
- expect(host.querySelector('.x').textContent).toBe('child');
72
- });
73
- });
@@ -1,38 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
2
- import { typeahead } from './typeahead.js';
3
- import { mountHost, connectTrait, spyEvent, resetDOM, wait } from '../test-helpers.js';
4
-
5
- function child(host, text) {
6
- const li = document.createElement('div');
7
- li.textContent = text;
8
- li.setAttribute('role', 'option');
9
- host.appendChild(li);
10
- return li;
11
- }
12
-
13
- describe('typeahead', () => {
14
- beforeEach(resetDOM);
15
-
16
- it('typing first letter matches a child by textContent', async () => {
17
- const host = mountHost();
18
- child(host, 'Apple');
19
- child(host, 'Banana');
20
- child(host, 'Cherry');
21
- connectTrait(typeahead, host);
22
- const spy = spyEvent(host, 'typeahead-match');
23
- host.dispatchEvent(new KeyboardEvent('keydown', { key: 'b' }));
24
- await wait(20);
25
- expect(spy.count).toBeGreaterThanOrEqual(1);
26
- expect(spy.last.element?.textContent).toBe('Banana');
27
- });
28
-
29
- it('disconnect removes the keydown listener', () => {
30
- const host = mountHost();
31
- child(host, 'Apple');
32
- const inst = connectTrait(typeahead, host);
33
- const spy = spyEvent(host, 'typeahead-match');
34
- inst.disconnect(host);
35
- host.dispatchEvent(new KeyboardEvent('keydown', { key: 'a' }));
36
- expect(spy.count).toBe(0);
37
- });
38
- });
@@ -1,47 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
2
- import { typewriter } from './typewriter.js';
3
- import { mountHost, connectTrait, spyEvent, resetDOM, wait } from '../test-helpers.js';
4
-
5
- describe('typewriter', () => {
6
- beforeEach(resetDOM);
7
-
8
- it('starts with empty content + active attribute on connect', () => {
9
- const host = mountHost();
10
- host.textContent = 'Hello';
11
- connectTrait(typewriter, host, { speed: 5 });
12
- host.setAttribute('data-typewriter-speed', '5');
13
- // Re-connect after setting speed (setup reads on connect)
14
- typewriter().connect(host);
15
- expect(host.hasAttribute('data-typewriter-active')).toBe(true);
16
- });
17
-
18
- it('reveals full text and fires typewriter-done', async () => {
19
- const host = mountHost();
20
- host.textContent = 'Hi';
21
- host.setAttribute('data-typewriter-speed', '5');
22
- const spy = spyEvent(host, 'typewriter-done');
23
- typewriter().connect(host);
24
- await wait(80);
25
- expect(host.textContent).toBe('Hi');
26
- expect(spy.count).toBeGreaterThanOrEqual(1);
27
- });
28
-
29
- it('disconnect mid-reveal restores original text', async () => {
30
- const host = mountHost();
31
- host.textContent = 'A long sentence to reveal';
32
- host.setAttribute('data-typewriter-speed', '20');
33
- const inst = typewriter();
34
- inst.connect(host);
35
- await wait(40);
36
- inst.disconnect(host);
37
- expect(host.textContent).toBe('A long sentence to reveal');
38
- expect(host.hasAttribute('data-typewriter-active')).toBe(false);
39
- });
40
-
41
- it('respects data-typewriter-speed default (50ms)', () => {
42
- const host = mountHost();
43
- host.textContent = 'X';
44
- typewriter().connect(host);
45
- expect(host.hasAttribute('data-typewriter-active')).toBe(true);
46
- });
47
- });