@adia-ai/web-components 0.8.19 → 0.8.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/components/nav-group/nav-group.a2ui.json +5 -0
  3. package/components/nav-group/nav-group.class.js +14 -6
  4. package/components/nav-group/nav-group.css +5 -2
  5. package/components/nav-group/nav-group.d.ts +2 -0
  6. package/components/nav-group/nav-group.yaml +4 -0
  7. package/dist/theme-provider.min.js +1 -1
  8. package/dist/web-components.min.css +1 -1
  9. package/dist/web-components.min.js +2 -2
  10. package/dist/web-components.sheet.js +1 -1
  11. package/package.json +2 -1
  12. package/components/adia-mark/adia-mark.test.js +0 -69
  13. package/components/adia-wordmark/adia-wordmark.test.js +0 -69
  14. package/components/agent-artifact/agent-artifact.test.js +0 -61
  15. package/components/alert/alert.test.js +0 -364
  16. package/components/color-picker/color-picker.test.js +0 -96
  17. package/components/combobox/combobox.test.js +0 -256
  18. package/components/command/command.test.js +0 -99
  19. package/components/date-range-picker/date-range-picker.test.js +0 -387
  20. package/components/datetime-picker/datetime-picker.test.js +0 -454
  21. package/components/demo-toggle/demo-toggle.test.js +0 -102
  22. package/components/description-list/description-list.test.js +0 -72
  23. package/components/display-field/display-field.test.js +0 -115
  24. package/components/empty-state/empty-state.test.js +0 -77
  25. package/components/field/field.test.js +0 -260
  26. package/components/fields/fields.test.js +0 -88
  27. package/components/inline-message/inline-message.test.js +0 -203
  28. package/components/input/input.test.js +0 -335
  29. package/components/integration-card/integration-card.test.js +0 -318
  30. package/components/link/link.test.js +0 -143
  31. package/components/list/list.test.js +0 -106
  32. package/components/list-window/list-window.test.js +0 -303
  33. package/components/loading-overlay/loading-overlay.test.js +0 -257
  34. package/components/menu/menu.test.js +0 -176
  35. package/components/nav/nav.test.js +0 -428
  36. package/components/nav-group/nav-group.test.js +0 -67
  37. package/components/option-card/option-card.test.js +0 -32
  38. package/components/pagination/pagination.test.js +0 -195
  39. package/components/select/select.test.js +0 -481
  40. package/components/slider/slider.test.js +0 -202
  41. package/components/spinner/spinner.test.js +0 -272
  42. package/components/stat/stat.test.js +0 -108
  43. package/components/step-progress/step-progress.test.js +0 -129
  44. package/components/swatch/swatch.test.js +0 -563
  45. package/components/table/table.test.js +0 -479
  46. package/components/table-toolbar/table-toolbar.test.js +0 -152
  47. package/components/tag/tag.test.js +0 -137
  48. package/components/tags-input/tags-input.test.js +0 -477
  49. package/components/text/text.test.js +0 -202
  50. package/components/theme-provider/theme-provider.test.js +0 -94
  51. package/components/time-picker/time-picker.test.js +0 -287
  52. package/components/timeline/timeline.test.js +0 -31
  53. package/components/toggle-scheme/toggle-scheme.test.js +0 -110
  54. package/components/tree/tree.test.js +0 -312
  55. package/core/element.test.js +0 -389
  56. package/core/icons.test.js +0 -187
  57. package/core/logical-children.test.js +0 -54
  58. package/core/markdown.test.js +0 -47
  59. package/core/responsive.test.js +0 -121
  60. package/core/template.test.js +0 -611
  61. package/traits/active-state/active-state.test.js +0 -28
  62. package/traits/anchor-positioning/anchor-positioning.test.js +0 -122
  63. package/traits/announcer/announcer.test.js +0 -268
  64. package/traits/arrow-grid-nav/arrow-grid-nav.test.js +0 -375
  65. package/traits/attention-pulse/attention-pulse.test.js +0 -26
  66. package/traits/confetti/confetti.test.js +0 -46
  67. package/traits/confetti-burst/confetti-burst.test.js +0 -46
  68. package/traits/count-up/count-up.test.js +0 -54
  69. package/traits/declarative.test.js +0 -138
  70. package/traits/dirty-state/dirty-state.test.js +0 -45
  71. package/traits/drag-ghost/drag-ghost.test.js +0 -19
  72. package/traits/draggable/draggable.test.js +0 -60
  73. package/traits/draggable-list-item/draggable-list-item.test.js +0 -51
  74. package/traits/drop-target/drop-target.test.js +0 -241
  75. package/traits/droppable/droppable.test.js +0 -54
  76. package/traits/droppable-collection/droppable-collection.test.js +0 -99
  77. package/traits/error-shake/error-shake.test.js +0 -114
  78. package/traits/fade-presence/fade-presence.test.js +0 -20
  79. package/traits/focus-restore/focus-restore.test.js +0 -202
  80. package/traits/focus-trap/focus-trap.test.js +0 -42
  81. package/traits/focusable/focusable.test.js +0 -53
  82. package/traits/glow-focus/glow-focus.test.js +0 -31
  83. package/traits/gradient-shift/gradient-shift.test.js +0 -22
  84. package/traits/haptic-feedback/haptic-feedback.test.js +0 -52
  85. package/traits/hotkey/hotkey.test.js +0 -61
  86. package/traits/hoverable/hoverable.test.js +0 -24
  87. package/traits/inertia-drag/inertia-drag.test.js +0 -33
  88. package/traits/input-mask/input-mask.test.js +0 -151
  89. package/traits/intersection-observer/intersection-observer.test.js +0 -38
  90. package/traits/keyboard-nav/keyboard-nav.test.js +0 -41
  91. package/traits/keyboard-reorderable/keyboard-reorderable.test.js +0 -45
  92. package/traits/layout-animation/layout-animation.test.js +0 -114
  93. package/traits/long-press/long-press.test.js +0 -244
  94. package/traits/magnetic-hover/magnetic-hover.test.js +0 -30
  95. package/traits/noise-texture/noise-texture.test.js +0 -20
  96. package/traits/parallax/parallax.test.js +0 -26
  97. package/traits/portal/portal.test.js +0 -30
  98. package/traits/pressable/pressable.test.js +0 -73
  99. package/traits/resettable/resettable.test.js +0 -100
  100. package/traits/resizable/resizable.test.js +0 -39
  101. package/traits/resize-observer/resize-observer.test.js +0 -38
  102. package/traits/ripple/ripple.test.js +0 -32
  103. package/traits/roving-tabindex/roving-tabindex.test.js +0 -28
  104. package/traits/scale-press/scale-press.test.js +0 -39
  105. package/traits/scroll-lock/scroll-lock.test.js +0 -45
  106. package/traits/scroll-progress/scroll-progress.test.js +0 -182
  107. package/traits/shimmer-loading/shimmer-loading.test.js +0 -43
  108. package/traits/smoke.test.js +0 -111
  109. package/traits/snap-to-grid/snap-to-grid.test.js +0 -40
  110. package/traits/sound-feedback/sound-feedback.test.js +0 -26
  111. package/traits/spring-animate/spring-animate.test.js +0 -28
  112. package/traits/success-checkmark/success-checkmark.test.js +0 -120
  113. package/traits/tilt-hover/tilt-hover.test.js +0 -32
  114. package/traits/tossable/tossable.test.js +0 -31
  115. package/traits/traits-host.test.js +0 -73
  116. package/traits/typeahead/typeahead.test.js +0 -38
  117. package/traits/typewriter/typewriter.test.js +0 -47
  118. package/traits/validation/validation.test.js +0 -93
  119. package/traits/view-transition/view-transition.test.js +0 -268
@@ -1,151 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
2
- import { inputMask } from './input-mask.js';
3
- import { mountHost, connectTrait, spyEvent, resetDOM } from '../test-helpers.js';
4
-
5
- /**
6
- * Drive an `input` event with a new value and a caret-position guess.
7
- * Mirrors what a real browser dispatches when the user types.
8
- */
9
- function type(host, value, caret) {
10
- host.value = value;
11
- if (caret != null) {
12
- try { host.setSelectionRange(caret, caret); } catch { /* number-typed inputs throw */ }
13
- }
14
- host.dispatchEvent(new Event('input'));
15
- }
16
-
17
- /**
18
- * Microtask-flush helper — caret writes are queued via queueMicrotask
19
- * so the DOM write doesn't fight the browser's own caret reposition.
20
- */
21
- const flush = () => new Promise(r => queueMicrotask(r));
22
-
23
- describe('input-mask', () => {
24
- beforeEach(resetDOM);
25
-
26
- it('schema declares forms-data category + the documented surface', () => {
27
- expect(inputMask.schema.category).toBe('forms-data');
28
- expect(inputMask.schema.attributes).toContain('data-input-mask-active');
29
- expect(inputMask.schema.attributes).toContain('data-input-mask-complete');
30
- expect(inputMask.schema.events).toContain('mask-commit');
31
- expect(inputMask.schema.config).toContain('data-mask-pattern');
32
- expect(inputMask.schema.config).toContain('data-mask-strip-on-commit');
33
- });
34
-
35
- it('phone-us named pattern: 4155551234 → (415) 555-1234', () => {
36
- const host = mountHost('input', { 'data-mask-pattern': 'phone-us' });
37
- connectTrait(inputMask, host);
38
- type(host, '4155551234');
39
- expect(host.value).toBe('(415) 555-1234');
40
- expect(host.hasAttribute('data-input-mask-complete')).toBe(true);
41
- });
42
-
43
- it('credit-card pattern: 16 digits → 4-4-4-4 grouping', () => {
44
- const host = mountHost('input', { 'data-mask-pattern': 'credit-card' });
45
- connectTrait(inputMask, host);
46
- type(host, '4111111111111111');
47
- expect(host.value).toBe('4111 1111 1111 1111');
48
- expect(host.hasAttribute('data-input-mask-complete')).toBe(true);
49
- });
50
-
51
- it('partial input does not set complete; full input does', () => {
52
- const host = mountHost('input', { 'data-mask-pattern': 'date-iso' });
53
- connectTrait(inputMask, host);
54
- type(host, '2026');
55
- expect(host.value).toBe('2026');
56
- expect(host.hasAttribute('data-input-mask-complete')).toBe(false);
57
- type(host, '20260504');
58
- expect(host.value).toBe('2026-05-04');
59
- expect(host.hasAttribute('data-input-mask-complete')).toBe(true);
60
- });
61
-
62
- it('user-supplied pattern overrides any naming collision', () => {
63
- // Pure literal pattern — should be taken verbatim, not matched as a name.
64
- const host = mountHost('input', { 'data-mask-pattern': 'AAA-####' });
65
- connectTrait(inputMask, host);
66
- type(host, 'ABC1234');
67
- expect(host.value).toBe('ABC-1234');
68
- });
69
-
70
- it('rejects placeholder-mismatching chars (digit-only mask drops letters)', () => {
71
- const host = mountHost('input', { 'data-mask-pattern': 'cvv' });
72
- connectTrait(inputMask, host);
73
- type(host, '12a3b4');
74
- // letters skipped, only the 4 digits land in the 4 # slots
75
- expect(host.value).toBe('1234');
76
- expect(host.hasAttribute('data-input-mask-complete')).toBe(true);
77
- });
78
-
79
- it('caret preservation: typing in the middle keeps the cursor anchored', async () => {
80
- const host = mountHost('input', { 'data-mask-pattern': 'phone-us' });
81
- connectTrait(inputMask, host);
82
- // Seed with 4 digits → "(415) " has 4 fillable slots reached with
83
- // 3 area-code digits + 1 exchange digit: "(415) 5".
84
- type(host, '4155');
85
- expect(host.value).toBe('(415) 5');
86
- // Append a 5th digit. The trait should reformat to "(415) 55" and
87
- // place the caret at the position after 5 real chars (index 8).
88
- type(host, '41555', 8);
89
- await flush();
90
- expect(host.value).toBe('(415) 55');
91
- expect(host.selectionStart).toBe(8);
92
- });
93
-
94
- it('mask-commit on blur fires with raw + formatted detail', () => {
95
- const host = mountHost('input', { 'data-mask-pattern': 'phone-us' });
96
- connectTrait(inputMask, host);
97
- const spy = spyEvent(host, 'mask-commit');
98
- type(host, '4155551234');
99
- host.dispatchEvent(new Event('blur'));
100
- expect(spy.count).toBe(1);
101
- expect(spy.last.formatted).toBe('(415) 555-1234');
102
- expect(spy.last.raw).toBe('4155551234');
103
- });
104
-
105
- it('strip-on-commit replaces the value with the raw form on blur', () => {
106
- const host = mountHost('input', {
107
- 'data-mask-pattern': 'phone-us',
108
- 'data-mask-strip-on-commit': '',
109
- });
110
- connectTrait(inputMask, host);
111
- type(host, '4155551234');
112
- expect(host.value).toBe('(415) 555-1234');
113
- host.dispatchEvent(new Event('blur'));
114
- expect(host.value).toBe('4155551234');
115
- });
116
-
117
- it('disconnect clears managed attributes', () => {
118
- const host = mountHost('input', { 'data-mask-pattern': 'phone-us' });
119
- const inst = connectTrait(inputMask, host);
120
- type(host, '4155551234');
121
- expect(host.hasAttribute('data-input-mask-active')).toBe(true);
122
- expect(host.hasAttribute('data-input-mask-complete')).toBe(true);
123
- inst.disconnect(host);
124
- expect(host.hasAttribute('data-input-mask-active')).toBe(false);
125
- expect(host.hasAttribute('data-input-mask-complete')).toBe(false);
126
- });
127
-
128
- it('missing pattern is a no-op (value untouched, no active flag)', () => {
129
- const host = mountHost('input');
130
- connectTrait(inputMask, host);
131
- type(host, 'whatever-the-user-types');
132
- expect(host.value).toBe('whatever-the-user-types');
133
- expect(host.hasAttribute('data-input-mask-active')).toBe(false);
134
- });
135
-
136
- it('AAA letter slot only accepts letters (digits skipped)', () => {
137
- const host = mountHost('input', { 'data-mask-pattern': 'AAA' });
138
- connectTrait(inputMask, host);
139
- type(host, 'A1B2C3');
140
- expect(host.value).toBe('ABC');
141
- expect(host.hasAttribute('data-input-mask-complete')).toBe(true);
142
- });
143
-
144
- it('star slot accepts any char', () => {
145
- const host = mountHost('input', { 'data-mask-pattern': '***-***' });
146
- connectTrait(inputMask, host);
147
- type(host, 'aB1xY9');
148
- expect(host.value).toBe('aB1-xY9');
149
- expect(host.hasAttribute('data-input-mask-complete')).toBe(true);
150
- });
151
- });
@@ -1,38 +0,0 @@
1
- import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
2
- import { intersectionObserver } from './intersection-observer.js';
3
- import { mountHost, connectTrait, resetDOM } from '../test-helpers.js';
4
-
5
- describe('intersection-observer', () => {
6
- let originalIO;
7
-
8
- beforeEach(() => {
9
- resetDOM();
10
- originalIO = globalThis.IntersectionObserver;
11
- });
12
-
13
- afterEach(() => {
14
- if (originalIO) globalThis.IntersectionObserver = originalIO;
15
- else delete globalThis.IntersectionObserver;
16
- });
17
-
18
- it('without IntersectionObserver: gracefully no-ops on connect', () => {
19
- delete globalThis.IntersectionObserver;
20
- const host = mountHost();
21
- expect(() => connectTrait(intersectionObserver, host)).not.toThrow();
22
- });
23
-
24
- it('with mocked IntersectionObserver: observe() is called on connect', () => {
25
- const observe = vi.fn();
26
- const ioDisconnect = vi.fn();
27
- globalThis.IntersectionObserver = function MockIO() {
28
- this.observe = observe;
29
- this.disconnect = ioDisconnect;
30
- this.unobserve = vi.fn();
31
- };
32
- const host = mountHost('div', { 'data-intersection-threshold': '0.5' });
33
- const inst = connectTrait(intersectionObserver, host);
34
- expect(observe).toHaveBeenCalledWith(host);
35
- inst.disconnect(host);
36
- expect(ioDisconnect).toHaveBeenCalled();
37
- });
38
- });
@@ -1,41 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
2
- import { keyboardNav } from './keyboard-nav.js';
3
- import { mountHost, connectTrait, spyEvent, resetDOM } from '../test-helpers.js';
4
-
5
- describe('keyboard-nav', () => {
6
- beforeEach(resetDOM);
7
-
8
- it.each([
9
- ['ArrowUp', 'nav-up'],
10
- ['ArrowDown', 'nav-down'],
11
- ['ArrowLeft', 'nav-left'],
12
- ['ArrowRight', 'nav-right'],
13
- ['Enter', 'nav-enter'],
14
- ['Escape', 'nav-escape'],
15
- ['Home', 'nav-home'],
16
- ['End', 'nav-end'],
17
- ])('%s keydown fires %s event', (key, eventName) => {
18
- const host = mountHost();
19
- connectTrait(keyboardNav, host);
20
- const spy = spyEvent(host, eventName);
21
- host.dispatchEvent(new KeyboardEvent('keydown', { key }));
22
- expect(spy.count).toBe(1);
23
- });
24
-
25
- it('unrelated key does not fire any nav event', () => {
26
- const host = mountHost();
27
- connectTrait(keyboardNav, host);
28
- const spy = spyEvent(host, 'nav-up');
29
- host.dispatchEvent(new KeyboardEvent('keydown', { key: 'a' }));
30
- expect(spy.count).toBe(0);
31
- });
32
-
33
- it('disconnect removes the keydown listener', () => {
34
- const host = mountHost();
35
- const inst = connectTrait(keyboardNav, host);
36
- const spy = spyEvent(host, 'nav-up');
37
- inst.disconnect(host);
38
- host.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp' }));
39
- expect(spy.count).toBe(0);
40
- });
41
- });
@@ -1,45 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
2
- import { keyboardReorderable } from './keyboard-reorderable.js';
3
- import { mountHost, connectTrait, resetDOM } from '../test-helpers.js';
4
-
5
- // Minimal smoke coverage. Keyboard-reorderable is the WCAG 2.5.7 +
6
- // ARIA 1.1 alternative to draggable-list-item for the
7
- // apps/tasks/ DnD model. Cases below verify
8
- // only the trait's own surface (schema, attribute lifecycle, id
9
- // assignment, tabindex injection). Multi-container keyboard flow
10
- // belongs in the playground integration suite.
11
-
12
- describe('keyboard-reorderable', () => {
13
- beforeEach(resetDOM);
14
-
15
- it('schema shape is defined', () => {
16
- expect(keyboardReorderable.schema.name).toBe('keyboard-reorderable');
17
- expect(keyboardReorderable.schema.category).toBe('keyboard-navigation');
18
- expect(keyboardReorderable.schema.description.length).toBeGreaterThanOrEqual(11);
19
- expect(keyboardReorderable.schema.events).toContain('dnd-lift');
20
- expect(keyboardReorderable.schema.events).toContain('dnd-drop');
21
- });
22
-
23
- it('connect assigns an id + ensures tabindex', () => {
24
- const host = mountHost('div');
25
- connectTrait(keyboardReorderable, host);
26
- expect(host.getAttribute('data-keyboard-reorderable-id')).toBeTruthy();
27
- expect(host.getAttribute('tabindex')).toBe('0');
28
- });
29
-
30
- it('connect respects pre-existing tabindex', () => {
31
- const host = mountHost('div', { tabindex: '-1' });
32
- connectTrait(keyboardReorderable, host);
33
- expect(host.getAttribute('tabindex')).toBe('-1');
34
- });
35
-
36
- it('disconnect clears its attributes', () => {
37
- const host = mountHost('div');
38
- const inst = keyboardReorderable();
39
- inst.connect(host);
40
- host.setAttribute('data-keyboard-reorderable-lifting', '');
41
- inst.disconnect(host);
42
- expect(host.hasAttribute('data-keyboard-reorderable-lifting')).toBe(false);
43
- expect(host.hasAttribute('data-keyboard-reorderable-id')).toBe(false);
44
- });
45
- });
@@ -1,114 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
2
- import { layoutAnimation } from './layout-animation.js';
3
- import { mountHost, connectTrait, spyEvent, resetDOM, raf } from '../test-helpers.js';
4
-
5
- describe('layout-animation', () => {
6
- beforeEach(resetDOM);
7
-
8
- it('connect does not throw and registers no leftover attribute', () => {
9
- const host = mountHost();
10
- const inst = connectTrait(layoutAnimation, host);
11
- expect(host.hasAttribute('data-layout-animation-active')).toBe(false);
12
- inst.disconnect(host);
13
- });
14
-
15
- it('disconnect cleans up the active attribute and is idempotent', () => {
16
- const host = mountHost();
17
- const inst = connectTrait(layoutAnimation, host);
18
- host.setAttribute('data-layout-animation-active', '');
19
- inst.disconnect(host);
20
- expect(host.hasAttribute('data-layout-animation-active')).toBe(false);
21
- // A second disconnect must not throw.
22
- expect(() => inst.disconnect(host)).not.toThrow();
23
- });
24
-
25
- it('reads data-layout-animate-duration + data-layout-animate-easing without throwing', () => {
26
- const host = mountHost('div', {
27
- 'data-layout-animate-duration': '500',
28
- 'data-layout-animate-easing': 'ease-in-out',
29
- });
30
- expect(() => connectTrait(layoutAnimation, host)).not.toThrow();
31
- });
32
-
33
- it('handles a missing duration / easing — defaults apply silently', () => {
34
- const host = mountHost();
35
- expect(() => connectTrait(layoutAnimation, host)).not.toThrow();
36
- });
37
-
38
- it('exposes the expected schema surface', () => {
39
- expect(layoutAnimation.schema.name).toBe('layout-animation');
40
- expect(layoutAnimation.schema.category).toBe('motion-positioning');
41
- expect(layoutAnimation.schema.attributes).toContain('data-layout-animation-active');
42
- expect(layoutAnimation.schema.events).toContain('layout-animation-done');
43
- expect(layoutAnimation.schema.config).toEqual([
44
- 'data-layout-animate-duration',
45
- 'data-layout-animate-easing',
46
- 'data-layout-animate-trigger',
47
- ]);
48
- });
49
-
50
- it('parent-childList mutation invokes the FLIP path without throwing', async () => {
51
- // Build a parent + sibling so we can mutate child order.
52
- const parent = document.createElement('div');
53
- document.body.appendChild(parent);
54
- const sib = document.createElement('div');
55
- const host = document.createElement('div');
56
- parent.appendChild(sib);
57
- parent.appendChild(host);
58
-
59
- const inst = layoutAnimation();
60
- inst.connect(host);
61
-
62
- // Reorder siblings — should drive the MutationObserver callback.
63
- expect(() => parent.insertBefore(host, sib)).not.toThrow();
64
-
65
- // Allow the MutationObserver microtask + rAF capture to settle.
66
- await Promise.resolve();
67
- await raf();
68
-
69
- inst.disconnect(host);
70
- expect(host.hasAttribute('data-layout-animation-active')).toBe(false);
71
- });
72
-
73
- it('attribute-trigger flip is wired (toggling data-layout-animate-trigger does not throw)', async () => {
74
- const host = mountHost();
75
- const inst = connectTrait(layoutAnimation, host);
76
-
77
- // Set the trigger — observer should fire a flip(); no DOM rect change in
78
- // the test env means it short-circuits to "no meaningful delta", which we
79
- // assert by absence of throws + no leftover active attribute after settle.
80
- host.setAttribute('data-layout-animate-trigger', '1');
81
-
82
- await Promise.resolve();
83
- await raf();
84
-
85
- inst.disconnect(host);
86
- expect(host.hasAttribute('data-layout-animation-active')).toBe(false);
87
- });
88
-
89
- it('layout-animation-done event listener attaches without error', () => {
90
- const host = mountHost();
91
- const spy = spyEvent(host, 'layout-animation-done');
92
- connectTrait(layoutAnimation, host);
93
- // We don't assert spy.count > 0 here — the test environment (happy-dom)
94
- // returns zero rects, so the FLIP path short-circuits at the no-delta
95
- // gate. The contract we care about is that subscribing doesn't throw.
96
- expect(spy.count).toBe(0);
97
- });
98
-
99
- it('two parallel instances on different hosts do not interfere', () => {
100
- const a = mountHost();
101
- const b = mountHost();
102
- const ia = connectTrait(layoutAnimation, a);
103
- const ib = connectTrait(layoutAnimation, b);
104
- expect(() => ia.disconnect(a)).not.toThrow();
105
- expect(() => ib.disconnect(b)).not.toThrow();
106
- });
107
-
108
- it('disconnect cancels any scheduled snapshot capture', () => {
109
- const host = mountHost();
110
- const inst = connectTrait(layoutAnimation, host);
111
- // Disconnect synchronously after connect — exercises the rAF-cancel branch.
112
- expect(() => inst.disconnect(host)).not.toThrow();
113
- });
114
- });
@@ -1,244 +0,0 @@
1
- import { describe, it, expect, beforeEach, vi } from 'vitest';
2
- import { longPress } from './long-press.js';
3
- import { mountHost, connectTrait, spyEvent, resetDOM, wait } from '../test-helpers.js';
4
-
5
- function pointerDown(host, x = 0, y = 0) {
6
- host.dispatchEvent(new PointerEvent('pointerdown', { clientX: x, clientY: y, bubbles: true }));
7
- }
8
- function pointerUp(host, x = 0, y = 0) {
9
- host.dispatchEvent(new PointerEvent('pointerup', { clientX: x, clientY: y, bubbles: true }));
10
- }
11
- function pointerMove(host, x, y) {
12
- host.dispatchEvent(new PointerEvent('pointermove', { clientX: x, clientY: y, bubbles: true }));
13
- }
14
- function pointerLeave(host) {
15
- host.dispatchEvent(new PointerEvent('pointerleave', { bubbles: true }));
16
- }
17
-
18
- describe('long-press', () => {
19
- beforeEach(() => {
20
- resetDOM();
21
- vi.useFakeTimers();
22
- });
23
-
24
- it('sets data-long-press-active on pointerdown', () => {
25
- const host = mountHost('button', {
26
- 'data-long-press-duration': '100',
27
- 'data-long-press-progress-interval': '20',
28
- });
29
- connectTrait(longPress, host);
30
- pointerDown(host);
31
- expect(host.hasAttribute('data-long-press-active')).toBe(true);
32
- expect(host.getAttribute('data-long-press-progress')).toBe('0');
33
- });
34
-
35
- it('fires "long-press" after the configured duration elapses', () => {
36
- const host = mountHost('button', {
37
- 'data-long-press-duration': '100',
38
- 'data-long-press-progress-interval': '20',
39
- });
40
- connectTrait(longPress, host);
41
- const spy = spyEvent(host, 'long-press');
42
- pointerDown(host);
43
- vi.advanceTimersByTime(100);
44
- expect(spy.count).toBe(1);
45
- expect(host.hasAttribute('data-long-press-fired')).toBe(true);
46
- });
47
-
48
- it('emits "long-press-progress" at intervals while held', () => {
49
- const host = mountHost('button', {
50
- 'data-long-press-duration': '100',
51
- 'data-long-press-progress-interval': '20',
52
- });
53
- connectTrait(longPress, host);
54
- const spy = spyEvent(host, 'long-press-progress');
55
- pointerDown(host);
56
- vi.advanceTimersByTime(60); // 3 ticks at 20ms
57
- expect(spy.count).toBeGreaterThanOrEqual(2);
58
- // Each tick should report progress in [0, 1].
59
- for (const detail of spy.captures) {
60
- expect(detail.progress).toBeGreaterThanOrEqual(0);
61
- expect(detail.progress).toBeLessThanOrEqual(1);
62
- }
63
- });
64
-
65
- it('fires "long-press-cancelled" when pointerup arrives early', () => {
66
- const host = mountHost('button', {
67
- 'data-long-press-duration': '200',
68
- 'data-long-press-progress-interval': '50',
69
- });
70
- connectTrait(longPress, host);
71
- const fireSpy = spyEvent(host, 'long-press');
72
- const cancelSpy = spyEvent(host, 'long-press-cancelled');
73
- pointerDown(host);
74
- vi.advanceTimersByTime(80); // well short of 200
75
- pointerUp(host);
76
- expect(fireSpy.count).toBe(0);
77
- expect(cancelSpy.count).toBe(1);
78
- expect(cancelSpy.last.reason).toBe('release');
79
- });
80
-
81
- it('fires "long-press-cancelled" on pointerleave', () => {
82
- const host = mountHost('button', {
83
- 'data-long-press-duration': '200',
84
- });
85
- connectTrait(longPress, host);
86
- const cancelSpy = spyEvent(host, 'long-press-cancelled');
87
- pointerDown(host);
88
- vi.advanceTimersByTime(40);
89
- pointerLeave(host);
90
- expect(cancelSpy.count).toBe(1);
91
- expect(cancelSpy.last.reason).toBe('leave');
92
- expect(host.hasAttribute('data-long-press-active')).toBe(false);
93
- });
94
-
95
- it('cancels when pointermove exceeds the tolerance', () => {
96
- const host = mountHost('button', {
97
- 'data-long-press-duration': '300',
98
- 'data-long-press-tolerance': '8',
99
- });
100
- connectTrait(longPress, host);
101
- const cancelSpy = spyEvent(host, 'long-press-cancelled');
102
- pointerDown(host, 0, 0);
103
- pointerMove(host, 20, 0); // > 8px
104
- expect(cancelSpy.count).toBe(1);
105
- expect(cancelSpy.last.reason).toBe('move');
106
- });
107
-
108
- it('does NOT cancel when pointermove stays within tolerance', () => {
109
- const host = mountHost('button', {
110
- 'data-long-press-duration': '100',
111
- 'data-long-press-tolerance': '8',
112
- });
113
- connectTrait(longPress, host);
114
- const cancelSpy = spyEvent(host, 'long-press-cancelled');
115
- const fireSpy = spyEvent(host, 'long-press');
116
- pointerDown(host, 0, 0);
117
- pointerMove(host, 5, 5); // hypot ~7.07, under 8
118
- vi.advanceTimersByTime(100);
119
- expect(cancelSpy.count).toBe(0);
120
- expect(fireSpy.count).toBe(1);
121
- });
122
-
123
- it('respects [disabled] — no listeners fire, no attributes set', () => {
124
- const host = mountHost('button', {
125
- disabled: '',
126
- 'data-long-press-duration': '50',
127
- });
128
- connectTrait(longPress, host);
129
- const fireSpy = spyEvent(host, 'long-press');
130
- pointerDown(host);
131
- expect(host.hasAttribute('data-long-press-active')).toBe(false);
132
- vi.advanceTimersByTime(100);
133
- expect(fireSpy.count).toBe(0);
134
- });
135
-
136
- it('suppresses the synthetic click that follows a committed long-press', () => {
137
- const host = mountHost('button', {
138
- 'data-long-press-duration': '100',
139
- });
140
- connectTrait(longPress, host);
141
- let clickHits = 0;
142
- host.addEventListener('click', () => clickHits++);
143
- pointerDown(host);
144
- vi.advanceTimersByTime(100);
145
- pointerUp(host);
146
- host.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
147
- expect(clickHits).toBe(0);
148
- });
149
-
150
- it('does NOT suppress click after a cancelled press', () => {
151
- const host = mountHost('button', {
152
- 'data-long-press-duration': '300',
153
- });
154
- connectTrait(longPress, host);
155
- let clickHits = 0;
156
- host.addEventListener('click', () => clickHits++);
157
- pointerDown(host);
158
- vi.advanceTimersByTime(60);
159
- pointerUp(host); // early release → cancelled
160
- host.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
161
- expect(clickHits).toBe(1);
162
- });
163
-
164
- it('uses default duration (600ms) when attribute missing', () => {
165
- const host = mountHost('button');
166
- connectTrait(longPress, host);
167
- const spy = spyEvent(host, 'long-press');
168
- pointerDown(host);
169
- vi.advanceTimersByTime(599);
170
- expect(spy.count).toBe(0);
171
- vi.advanceTimersByTime(2);
172
- expect(spy.count).toBe(1);
173
- });
174
-
175
- it('uses default tolerance (8px) when attribute missing', () => {
176
- const host = mountHost('button', { 'data-long-press-duration': '300' });
177
- connectTrait(longPress, host);
178
- const cancelSpy = spyEvent(host, 'long-press-cancelled');
179
- pointerDown(host, 0, 0);
180
- pointerMove(host, 7, 0); // under default 8
181
- expect(cancelSpy.count).toBe(0);
182
- pointerMove(host, 9, 0); // over default 8
183
- expect(cancelSpy.count).toBe(1);
184
- });
185
-
186
- it('disconnect clears active timers + attributes mid-press', () => {
187
- const host = mountHost('button', {
188
- 'data-long-press-duration': '200',
189
- 'data-long-press-progress-interval': '50',
190
- });
191
- const inst = connectTrait(longPress, host);
192
- pointerDown(host);
193
- expect(host.hasAttribute('data-long-press-active')).toBe(true);
194
- inst.disconnect(host);
195
- expect(host.hasAttribute('data-long-press-active')).toBe(false);
196
- expect(host.hasAttribute('data-long-press-progress')).toBe(false);
197
- // After disconnect, advancing time must not fire long-press.
198
- const fireSpy = spyEvent(host, 'long-press');
199
- vi.advanceTimersByTime(500);
200
- expect(fireSpy.count).toBe(0);
201
- });
202
-
203
- it('reconnect after disconnect picks up fresh state', () => {
204
- const host = mountHost('button', {
205
- 'data-long-press-duration': '100',
206
- });
207
- const inst = connectTrait(longPress, host);
208
- pointerDown(host);
209
- inst.disconnect(host);
210
-
211
- connectTrait(longPress, host);
212
- const spy = spyEvent(host, 'long-press');
213
- pointerDown(host);
214
- vi.advanceTimersByTime(100);
215
- expect(spy.count).toBe(1);
216
- });
217
-
218
- it('schema declares the input-interaction category and full attr/event/config sets', () => {
219
- expect(longPress.schema.name).toBe('long-press');
220
- expect(longPress.schema.category).toBe('input-interaction');
221
- expect(longPress.schema.attributes).toContain('data-long-press-active');
222
- expect(longPress.schema.attributes).toContain('data-long-press-progress');
223
- expect(longPress.schema.attributes).toContain('data-long-press-fired');
224
- expect(longPress.schema.events).toEqual(
225
- expect.arrayContaining(['long-press', 'long-press-cancelled', 'long-press-progress']),
226
- );
227
- expect(longPress.schema.config).toEqual(
228
- expect.arrayContaining([
229
- 'data-long-press-duration',
230
- 'data-long-press-tolerance',
231
- 'data-long-press-progress-interval',
232
- ]),
233
- );
234
- });
235
-
236
- it('contextmenu is suppressed while a press is in flight', () => {
237
- const host = mountHost('button', { 'data-long-press-duration': '200' });
238
- connectTrait(longPress, host);
239
- pointerDown(host);
240
- const ev = new Event('contextmenu', { bubbles: true, cancelable: true });
241
- host.dispatchEvent(ev);
242
- expect(ev.defaultPrevented).toBe(true);
243
- });
244
- });
@@ -1,30 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
2
- import { magneticHover } from './magnetic-hover.js';
3
- import { mountHost, connectTrait, resetDOM } from '../test-helpers.js';
4
-
5
- describe('magnetic-hover', () => {
6
- beforeEach(resetDOM);
7
-
8
- it('pointermove sets data-magnetic-hover-active', () => {
9
- const host = mountHost();
10
- connectTrait(magneticHover, host);
11
- host.dispatchEvent(new PointerEvent('pointermove', { clientX: 50, clientY: 50, bubbles: true }));
12
- expect(host.hasAttribute('data-magnetic-hover-active')).toBe(true);
13
- });
14
-
15
- it('pointerleave clears active + resets translate', () => {
16
- const host = mountHost();
17
- connectTrait(magneticHover, host);
18
- host.dispatchEvent(new PointerEvent('pointermove', { clientX: 50, clientY: 50, bubbles: true }));
19
- host.dispatchEvent(new PointerEvent('pointerleave', { bubbles: true }));
20
- expect(host.hasAttribute('data-magnetic-hover-active')).toBe(false);
21
- });
22
-
23
- it('disconnect clears all listeners + active state', () => {
24
- const host = mountHost();
25
- const inst = connectTrait(magneticHover, host);
26
- host.dispatchEvent(new PointerEvent('pointermove', { clientX: 0, clientY: 0, bubbles: true }));
27
- inst.disconnect(host);
28
- expect(host.hasAttribute('data-magnetic-hover-active')).toBe(false);
29
- });
30
- });