@spectral_labs/ui 1.6.1 → 1.8.0-next.0

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 (163) hide show
  1. package/LICENSE +21 -0
  2. package/THIRD_PARTY_NOTICES.md +201 -0
  3. package/dist/components/command/command.svelte.d.ts +30 -0
  4. package/dist/components/command/command.svelte.d.ts.map +1 -1
  5. package/dist/components/command/command.svelte.js +85 -4
  6. package/dist/components/command/components/command-group.svelte +25 -15
  7. package/dist/components/command/components/command-group.svelte.d.ts.map +1 -1
  8. package/dist/components/command/components/command-input.svelte +54 -5
  9. package/dist/components/command/components/command-input.svelte.d.ts +1 -1
  10. package/dist/components/command/components/command-input.svelte.d.ts.map +1 -1
  11. package/dist/components/command/components/command-list.svelte +22 -3
  12. package/dist/components/command/components/command-list.svelte.d.ts.map +1 -1
  13. package/dist/components/command/components/command-root.svelte +43 -4
  14. package/dist/components/command/components/command-root.svelte.d.ts.map +1 -1
  15. package/dist/components/command/index.d.ts +1 -0
  16. package/dist/components/command/index.d.ts.map +1 -1
  17. package/dist/components/command/types.d.ts +59 -4
  18. package/dist/components/command/types.d.ts.map +1 -1
  19. package/package.json +8 -2
  20. package/dist/components/__tests__/overlay-scrim-source.test.js +0 -134
  21. package/dist/components/accordion/__tests__/AccordionTest.svelte +0 -31
  22. package/dist/components/accordion/__tests__/accordion.test.svelte.js +0 -139
  23. package/dist/components/alert-dialog/__tests__/AlertDialogTest.svelte +0 -28
  24. package/dist/components/alert-dialog/__tests__/alert-dialog.a11y.test.js +0 -42
  25. package/dist/components/alert-dialog/__tests__/alert-dialog.integration.test.svelte.js +0 -51
  26. package/dist/components/alert-dialog/__tests__/alert-dialog.test.svelte.js +0 -113
  27. package/dist/components/app-bar/__tests__/app-bar.test.svelte.js +0 -30
  28. package/dist/components/aspect-ratio/__tests__/aspect-ratio.test.svelte.js +0 -22
  29. package/dist/components/avatar/__tests__/avatar.test.svelte.js +0 -26
  30. package/dist/components/badge/__tests__/badge.test.svelte.js +0 -30
  31. package/dist/components/bottom-sheet/__tests__/BottomSheetTest.svelte +0 -22
  32. package/dist/components/bottom-sheet/__tests__/bottom-sheet.test.svelte.js +0 -83
  33. package/dist/components/button/__tests__/button.test.svelte.js +0 -63
  34. package/dist/components/calendar/__tests__/CalendarTest.svelte +0 -56
  35. package/dist/components/calendar/__tests__/calendar.test.svelte.js +0 -190
  36. package/dist/components/card/__tests__/card.test.svelte.js +0 -30
  37. package/dist/components/checkbox/__tests__/checkbox.test.svelte.js +0 -44
  38. package/dist/components/chips/__tests__/chips.test.svelte.js +0 -26
  39. package/dist/components/collapsible/__tests__/CollapsibleTest.svelte +0 -18
  40. package/dist/components/collapsible/__tests__/collapsible.test.svelte.js +0 -125
  41. package/dist/components/combobox/__tests__/ComboboxTest.svelte +0 -40
  42. package/dist/components/combobox/__tests__/combobox.test.svelte.js +0 -255
  43. package/dist/components/command/__tests__/CommandTest.svelte +0 -77
  44. package/dist/components/command/__tests__/command-item-passthrough.test.svelte.js +0 -284
  45. package/dist/components/command/__tests__/command.a11y.test.js +0 -30
  46. package/dist/components/command/__tests__/command.integration.test.svelte.js +0 -68
  47. package/dist/components/command/__tests__/command.test.svelte.js +0 -230
  48. package/dist/components/context-menu/__tests__/ContextMenuNonInteractiveTest.svelte +0 -32
  49. package/dist/components/context-menu/__tests__/ContextMenuTest.svelte +0 -25
  50. package/dist/components/context-menu/__tests__/context-menu.test.svelte.js +0 -141
  51. package/dist/components/date-field/__tests__/DateFieldTest.svelte +0 -20
  52. package/dist/components/date-field/__tests__/date-field.test.svelte.js +0 -139
  53. package/dist/components/date-picker/__tests__/DatePickerTest.svelte +0 -20
  54. package/dist/components/date-picker/__tests__/date-picker.test.svelte.js +0 -190
  55. package/dist/components/date-range-field/__tests__/DateRangeFieldTest.svelte +0 -20
  56. package/dist/components/date-range-field/__tests__/date-range-field.test.svelte.js +0 -130
  57. package/dist/components/date-range-picker/__tests__/DateRangePickerTest.svelte +0 -18
  58. package/dist/components/date-range-picker/__tests__/date-range-picker.test.svelte.js +0 -156
  59. package/dist/components/dialog/__tests__/DialogTest.svelte +0 -34
  60. package/dist/components/dialog/__tests__/dialog.test.svelte.js +0 -184
  61. package/dist/components/drop-zone/__tests__/drop-zone.test.svelte.js +0 -26
  62. package/dist/components/dropdown-menu/__tests__/DropdownMenuRadioTest.svelte +0 -90
  63. package/dist/components/dropdown-menu/__tests__/DropdownMenuTest.svelte +0 -71
  64. package/dist/components/dropdown-menu/__tests__/dropdown-menu-radio-group.test.svelte.js +0 -306
  65. package/dist/components/dropdown-menu/__tests__/dropdown-menu.test.svelte.js +0 -567
  66. package/dist/components/empty-state/__tests__/empty-state.test.svelte.js +0 -26
  67. package/dist/components/fab/__tests__/fab.test.svelte.js +0 -30
  68. package/dist/components/field/__tests__/FieldTest.svelte +0 -26
  69. package/dist/components/field/__tests__/field.test.svelte.js +0 -74
  70. package/dist/components/grid-list/__tests__/GridListTest.svelte +0 -46
  71. package/dist/components/grid-list/__tests__/grid-list.test.svelte.js +0 -231
  72. package/dist/components/hover-card/__tests__/HoverCardTest.svelte +0 -30
  73. package/dist/components/hover-card/__tests__/hover-card.test.svelte.js +0 -236
  74. package/dist/components/indicator/__tests__/indicator.test.svelte.js +0 -30
  75. package/dist/components/input/__tests__/input.test.svelte.js +0 -67
  76. package/dist/components/label/__tests__/label.test.svelte.js +0 -22
  77. package/dist/components/language-select/__tests__/language-select.test.svelte.js +0 -199
  78. package/dist/components/language-select/__tests__/languages.test.js +0 -87
  79. package/dist/components/link-preview/__tests__/LinkPreviewTest.svelte +0 -37
  80. package/dist/components/link-preview/__tests__/link-preview.a11y.test.js +0 -28
  81. package/dist/components/link-preview/__tests__/link-preview.integration.test.svelte.js +0 -54
  82. package/dist/components/link-preview/__tests__/link-preview.test.svelte.js +0 -215
  83. package/dist/components/list/__tests__/list.test.svelte.js +0 -22
  84. package/dist/components/menubar/__tests__/MenubarTest.svelte +0 -52
  85. package/dist/components/menubar/__tests__/menubar.a11y.test.js +0 -28
  86. package/dist/components/menubar/__tests__/menubar.integration.test.svelte.js +0 -57
  87. package/dist/components/menubar/__tests__/menubar.test.svelte.js +0 -328
  88. package/dist/components/meter/__tests__/meter.test.svelte.js +0 -26
  89. package/dist/components/nav-bar/__tests__/nav-bar.test.svelte.js +0 -23
  90. package/dist/components/nav-drawer/__tests__/nav-drawer.test.svelte.js +0 -28
  91. package/dist/components/nav-rail/__tests__/nav-rail.test.svelte.js +0 -28
  92. package/dist/components/pagination/__tests__/pagination.test.svelte.js +0 -26
  93. package/dist/components/pin-input/__tests__/pin-input.test.svelte.js +0 -71
  94. package/dist/components/popover/__tests__/PopoverTest.svelte +0 -27
  95. package/dist/components/popover/__tests__/popover.test.svelte.js +0 -196
  96. package/dist/components/progress/__tests__/progress.test.svelte.js +0 -32
  97. package/dist/components/radio-group/__tests__/radio-group.test.svelte.js +0 -22
  98. package/dist/components/range-calendar/__tests__/RangeCalendarTest.svelte +0 -23
  99. package/dist/components/range-calendar/__tests__/range-calendar.test.svelte.js +0 -105
  100. package/dist/components/rating-group/__tests__/rating-group.test.svelte.js +0 -26
  101. package/dist/components/scroll-area/__tests__/scroll-area.test.svelte.js +0 -34
  102. package/dist/components/search-field/__tests__/SearchFieldTest.svelte +0 -34
  103. package/dist/components/search-field/__tests__/search-field.test.svelte.js +0 -118
  104. package/dist/components/select/__tests__/SelectLabelPersistenceTest.svelte +0 -31
  105. package/dist/components/select/__tests__/SelectTest.svelte +0 -33
  106. package/dist/components/select/__tests__/select.test.svelte.js +0 -221
  107. package/dist/components/separator/__tests__/separator.test.svelte.js +0 -39
  108. package/dist/components/sheet/__tests__/SheetTest.svelte +0 -34
  109. package/dist/components/sheet/__tests__/sheet.test.svelte.js +0 -193
  110. package/dist/components/side-sheet/__tests__/side-sheet.test.svelte.js +0 -138
  111. package/dist/components/sidebar/__tests__/SidebarFullTest.svelte +0 -45
  112. package/dist/components/sidebar/__tests__/SidebarTest.svelte +0 -9
  113. package/dist/components/sidebar/__tests__/sidebar.test.svelte.js +0 -186
  114. package/dist/components/simple-dialog/__tests__/SimpleDialogTest.svelte +0 -40
  115. package/dist/components/simple-dialog/__tests__/simple-dialog.test.svelte.js +0 -50
  116. package/dist/components/simple-tooltip/__tests__/SimpleTooltipTest.svelte +0 -19
  117. package/dist/components/simple-tooltip/__tests__/simple-tooltip.test.svelte.js +0 -61
  118. package/dist/components/slider/__tests__/slider.test.svelte.js +0 -42
  119. package/dist/components/sortable-list/__tests__/NestedSortableListTest.svelte +0 -49
  120. package/dist/components/sortable-list/__tests__/SortableListTest.svelte +0 -42
  121. package/dist/components/sortable-list/__tests__/sortable-list.nested.test.svelte.js +0 -112
  122. package/dist/components/sortable-list/__tests__/sortable-list.test.svelte.js +0 -175
  123. package/dist/components/splitter/__tests__/splitter.test.svelte.js +0 -26
  124. package/dist/components/switch/__tests__/switch.test.svelte.js +0 -44
  125. package/dist/components/table/__tests__/table.test.svelte.js +0 -22
  126. package/dist/components/tabs/__tests__/TabsTest.svelte +0 -32
  127. package/dist/components/tabs/__tests__/tabs.test.svelte.js +0 -227
  128. package/dist/components/textarea/__tests__/textarea.test.svelte.js +0 -55
  129. package/dist/components/time-field/__tests__/TimeFieldTest.svelte +0 -20
  130. package/dist/components/time-field/__tests__/time-field.test.svelte.js +0 -100
  131. package/dist/components/time-range-field/__tests__/TimeRangeFieldTest.svelte +0 -20
  132. package/dist/components/time-range-field/__tests__/time-range-field.test.svelte.js +0 -124
  133. package/dist/components/toast/__tests__/toast.test.svelte.js +0 -46
  134. package/dist/components/toggle/__tests__/toggle.test.svelte.js +0 -38
  135. package/dist/components/toggle-group/__tests__/toggle-group.test.svelte.js +0 -22
  136. package/dist/components/toolbar/__tests__/ToolbarCompositionTest.svelte +0 -18
  137. package/dist/components/toolbar/__tests__/ToolbarTabstopTest.svelte +0 -10
  138. package/dist/components/toolbar/__tests__/toolbar.test.svelte.js +0 -63
  139. package/dist/components/tooltip/__tests__/TooltipTest.svelte +0 -22
  140. package/dist/components/tooltip/__tests__/tooltip.test.svelte.js +0 -225
  141. package/dist/components/tree/__tests__/TreeLabelContentTest.svelte +0 -21
  142. package/dist/components/tree/__tests__/TreeTest.svelte +0 -24
  143. package/dist/components/tree/__tests__/tree.test.svelte.js +0 -229
  144. package/dist/components/virtualizer/__tests__/waterfall-layout.test.js +0 -82
  145. package/dist/internal/announcer/__tests__/announcer.test.js +0 -42
  146. package/dist/internal/dismissible-layer/__tests__/dismissible-layer.test.js +0 -196
  147. package/dist/internal/drag-drop/__tests__/auto-scroll.test.js +0 -104
  148. package/dist/internal/drag-drop/__tests__/drag-session.test.svelte.js +0 -803
  149. package/dist/internal/drag-drop/__tests__/drag-source.test.svelte.js +0 -130
  150. package/dist/internal/drag-drop/__tests__/drop-delegate.test.js +0 -223
  151. package/dist/internal/drag-drop/__tests__/types.test.js +0 -20
  152. package/dist/internal/escape-layer/__tests__/escape-layer.test.js +0 -113
  153. package/dist/internal/focus-scope/__tests__/focus-scope.test.js +0 -225
  154. package/dist/internal/grace-area/__tests__/grace-area.test.js +0 -67
  155. package/dist/internal/helpers/__tests__/helpers.test.js +0 -123
  156. package/dist/internal/portal/__tests__/PortalTest.svelte +0 -21
  157. package/dist/internal/portal/__tests__/portal.test.js +0 -29
  158. package/dist/internal/portal/__tests__/portal.test.svelte.js +0 -57
  159. package/dist/internal/scroll-lock/__tests__/scroll-lock.test.js +0 -160
  160. package/dist/internal/typeahead/__tests__/typeahead.test.js +0 -61
  161. package/src/themes/__tests__/carbon-dark-contrast.test.ts +0 -170
  162. package/src/themes/__tests__/generated-css-drift.test.ts +0 -53
  163. package/src/themes/__tests__/scrim-opaque.test.ts +0 -52
@@ -1,803 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
- import { EscapeLayerState, resetEscapeLayerStack } from '../../escape-layer/index.js';
3
- import { dragSession } from '../drag-session.svelte.js';
4
- function createSourceConfig(id, overrides = {}) {
5
- return {
6
- id,
7
- getItems: () => [{ 'test-type': id, 'text/plain': `Item ${id}` }],
8
- onDragStart: vi.fn(),
9
- onDragEnd: vi.fn(),
10
- ...overrides,
11
- };
12
- }
13
- function createTargetConfig(id, overrides = {}) {
14
- return {
15
- id,
16
- onDrop: vi.fn(),
17
- ...overrides,
18
- };
19
- }
20
- function createElement() {
21
- const el = document.createElement('div');
22
- Object.defineProperty(el, 'getBoundingClientRect', {
23
- value: () => ({
24
- top: 0,
25
- left: 0,
26
- bottom: 100,
27
- right: 200,
28
- width: 200,
29
- height: 100,
30
- x: 0,
31
- y: 0,
32
- toJSON() { },
33
- }),
34
- });
35
- document.body.appendChild(el);
36
- return el;
37
- }
38
- /** Polyfill PointerEvent for jsdom. */
39
- function ensurePointerEventPolyfill() {
40
- if (typeof globalThis.PointerEvent === 'undefined') {
41
- ;
42
- globalThis.PointerEvent = class PointerEvent extends MouseEvent {
43
- pointerId;
44
- pointerType;
45
- constructor(type, init = {}) {
46
- super(type, init);
47
- this.pointerId = init.pointerId ?? 0;
48
- this.pointerType = init.pointerType ?? '';
49
- }
50
- };
51
- }
52
- }
53
- function pointerEvent(type, x, y) {
54
- return new PointerEvent(type, {
55
- clientX: x,
56
- clientY: y,
57
- pointerId: 1,
58
- pointerType: 'mouse',
59
- bubbles: true,
60
- });
61
- }
62
- function startPointerDrag(id) {
63
- const config = createSourceConfig(id);
64
- const el = createElement();
65
- el.setPointerCapture = vi.fn();
66
- el.releasePointerCapture = vi.fn();
67
- dragSession.registerSource(config, el);
68
- dragSession.beginPointerDrag(id, pointerEvent('pointerdown', 100, 100));
69
- el.dispatchEvent(pointerEvent('pointermove', 110, 110)); // crosses the 5px threshold
70
- return { config, el };
71
- }
72
- function pressEscape() {
73
- document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
74
- }
75
- describe('dragSession', () => {
76
- beforeEach(() => {
77
- dragSession._resetForTests();
78
- });
79
- describe('initial state', () => {
80
- it('is not dragging initially', () => {
81
- expect(dragSession.isDragging).toBe(false);
82
- });
83
- it('has no source', () => {
84
- expect(dragSession.sourceId).toBeNull();
85
- });
86
- it('has no current target', () => {
87
- expect(dragSession.currentTarget).toBeNull();
88
- });
89
- it('has empty items', () => {
90
- expect(dragSession.items).toEqual([]);
91
- });
92
- it('has empty types', () => {
93
- expect(dragSession.types.size).toBe(0);
94
- });
95
- });
96
- describe('source registration', () => {
97
- it('registers and unregisters a source', () => {
98
- const config = createSourceConfig('source-1');
99
- const el = createElement();
100
- const unregister = dragSession.registerSource(config, el);
101
- expect(dragSession.getSource('source-1')).toBeDefined();
102
- expect(dragSession.getSource('source-1').config).toBe(config);
103
- unregister();
104
- expect(dragSession.getSource('source-1')).toBeUndefined();
105
- el.remove();
106
- });
107
- });
108
- describe('target registration', () => {
109
- it('registers and unregisters a target', () => {
110
- const config = createTargetConfig('target-1');
111
- const el = createElement();
112
- const unregister = dragSession.registerTarget(config, el);
113
- expect(dragSession.getTarget('target-1')).toBeDefined();
114
- expect(dragSession.getTarget('target-1').config).toBe(config);
115
- unregister();
116
- expect(dragSession.getTarget('target-1')).toBeUndefined();
117
- el.remove();
118
- });
119
- });
120
- describe('keyboard drag', () => {
121
- it('starts a keyboard drag session', () => {
122
- const config = createSourceConfig('kb-source');
123
- const el = createElement();
124
- dragSession.registerSource(config, el);
125
- dragSession.beginKeyboardDrag('kb-source');
126
- expect(dragSession.isDragging).toBe(true);
127
- expect(dragSession.sourceId).toBe('kb-source');
128
- expect(dragSession.modality).toBe('keyboard');
129
- expect(dragSession.items).toHaveLength(1);
130
- expect(dragSession.types.has('test-type')).toBe(true);
131
- expect(config.onDragStart).toHaveBeenCalled();
132
- dragSession.cancel();
133
- el.remove();
134
- });
135
- it('does not start if source is disabled', () => {
136
- const config = createSourceConfig('disabled-source', { isDisabled: true });
137
- const el = createElement();
138
- dragSession.registerSource(config, el);
139
- dragSession.beginKeyboardDrag('disabled-source');
140
- expect(dragSession.isDragging).toBe(false);
141
- el.remove();
142
- });
143
- it('does not start if already dragging', () => {
144
- const config1 = createSourceConfig('src-1');
145
- const config2 = createSourceConfig('src-2');
146
- const el1 = createElement();
147
- const el2 = createElement();
148
- dragSession.registerSource(config1, el1);
149
- dragSession.registerSource(config2, el2);
150
- dragSession.beginKeyboardDrag('src-1');
151
- dragSession.beginKeyboardDrag('src-2');
152
- expect(dragSession.sourceId).toBe('src-1'); // first one wins
153
- dragSession.cancel();
154
- el1.remove();
155
- el2.remove();
156
- });
157
- });
158
- describe('cancel', () => {
159
- it('resets state and calls onDragEnd with cancel', () => {
160
- const config = createSourceConfig('cancel-src');
161
- const el = createElement();
162
- dragSession.registerSource(config, el);
163
- dragSession.beginKeyboardDrag('cancel-src');
164
- expect(dragSession.isDragging).toBe(true);
165
- dragSession.cancel();
166
- expect(dragSession.isDragging).toBe(false);
167
- expect(dragSession.sourceId).toBeNull();
168
- expect(dragSession.items).toEqual([]);
169
- expect(config.onDragEnd).toHaveBeenCalledWith('cancel');
170
- el.remove();
171
- });
172
- });
173
- describe('drop', () => {
174
- it('calls onDrop on the target and onDragEnd on the source', async () => {
175
- const sourceConfig = createSourceConfig('drop-src');
176
- const targetConfig = createTargetConfig('drop-tgt', {
177
- getDropOperation: () => 'move',
178
- onDrop: vi.fn(),
179
- });
180
- const srcEl = createElement();
181
- const tgtEl = createElement();
182
- dragSession.registerSource(sourceConfig, srcEl);
183
- dragSession.registerTarget(targetConfig, tgtEl);
184
- dragSession.beginKeyboardDrag('drop-src');
185
- dragSession.focusNextTarget(); // navigate to the target
186
- // Verify we're on a target
187
- expect(dragSession.currentTargetId).toBe('drop-tgt');
188
- await dragSession.drop();
189
- expect(targetConfig.onDrop).toHaveBeenCalled();
190
- expect(sourceConfig.onDragEnd).toHaveBeenCalledWith('move');
191
- expect(dragSession.isDragging).toBe(false);
192
- srcEl.remove();
193
- tgtEl.remove();
194
- });
195
- it('cancels if no target is selected', async () => {
196
- const config = createSourceConfig('no-tgt-src');
197
- const el = createElement();
198
- dragSession.registerSource(config, el);
199
- dragSession.beginKeyboardDrag('no-tgt-src');
200
- await dragSession.drop();
201
- expect(config.onDragEnd).toHaveBeenCalledWith('cancel');
202
- expect(dragSession.isDragging).toBe(false);
203
- el.remove();
204
- });
205
- });
206
- describe('target navigation', () => {
207
- it('cycles through targets with focusNextTarget', () => {
208
- const sourceConfig = createSourceConfig('nav-src');
209
- const tgt1Config = createTargetConfig('nav-tgt-1');
210
- const tgt2Config = createTargetConfig('nav-tgt-2');
211
- const srcEl = createElement();
212
- const tgt1El = createElement();
213
- const tgt2El = createElement();
214
- dragSession.registerSource(sourceConfig, srcEl);
215
- dragSession.registerTarget(tgt1Config, tgt1El);
216
- dragSession.registerTarget(tgt2Config, tgt2El);
217
- dragSession.beginKeyboardDrag('nav-src');
218
- dragSession.focusNextTarget();
219
- const first = dragSession.currentTargetId;
220
- expect(first).toBeTruthy();
221
- dragSession.focusNextTarget();
222
- const second = dragSession.currentTargetId;
223
- expect(second).toBeTruthy();
224
- expect(second).not.toBe(first);
225
- // Cycle back
226
- dragSession.focusNextTarget();
227
- expect(dragSession.currentTargetId).toBe(first);
228
- dragSession.cancel();
229
- srcEl.remove();
230
- tgt1El.remove();
231
- tgt2El.remove();
232
- });
233
- });
234
- describe('subscriptions', () => {
235
- it('notifies subscribers on state change', () => {
236
- const callback = vi.fn();
237
- const unsub = dragSession.subscribe(callback);
238
- const config = createSourceConfig('sub-src');
239
- const el = createElement();
240
- dragSession.registerSource(config, el);
241
- dragSession.beginKeyboardDrag('sub-src');
242
- expect(callback).toHaveBeenCalled();
243
- dragSession.cancel();
244
- unsub();
245
- el.remove();
246
- });
247
- it('unsubscribes correctly', () => {
248
- const callback = vi.fn();
249
- const unsub = dragSession.subscribe(callback);
250
- unsub();
251
- const config = createSourceConfig('unsub-src');
252
- const el = createElement();
253
- dragSession.registerSource(config, el);
254
- dragSession.beginKeyboardDrag('unsub-src');
255
- expect(callback).not.toHaveBeenCalled();
256
- dragSession.cancel();
257
- el.remove();
258
- });
259
- });
260
- describe('collection context', () => {
261
- it('sets and checks source collection', () => {
262
- const config = createSourceConfig('coll-src');
263
- const el = createElement();
264
- dragSession.registerSource(config, el);
265
- dragSession.beginKeyboardDrag('coll-src');
266
- dragSession.setSourceCollection('list-A', new Set(['item-1', 'item-2']));
267
- expect(dragSession.sourceCollectionId).toBe('list-A');
268
- expect(dragSession.draggingKeys.has('item-1')).toBe(true);
269
- expect(dragSession.draggingKeys.has('item-2')).toBe(true);
270
- expect(dragSession.isInternalDrop('list-A')).toBe(true);
271
- expect(dragSession.isInternalDrop('list-B')).toBe(false);
272
- // wasDroppedExternally — no drop yet
273
- expect(dragSession.wasDroppedExternally('list-A')).toBe(false);
274
- dragSession.cancel();
275
- el.remove();
276
- });
277
- it('wasDroppedExternally detects cross-list drops correctly', async () => {
278
- const sourceConfig = createSourceConfig('cross-src');
279
- const targetConfig = createTargetConfig('cross-tgt', {
280
- getDropOperation: () => 'move',
281
- onDrop: vi.fn(),
282
- });
283
- const srcEl = createElement();
284
- const tgtEl = createElement();
285
- dragSession.registerSource(sourceConfig, srcEl);
286
- dragSession.registerTarget(targetConfig, tgtEl);
287
- dragSession.beginKeyboardDrag('cross-src');
288
- dragSession.setSourceCollection('list-A', new Set(['item-1']));
289
- dragSession.focusNextTarget();
290
- // Before drop, dropCollectionId is null
291
- expect(dragSession.wasDroppedExternally('list-A')).toBe(false);
292
- await dragSession.drop();
293
- // After drop, source's onDragEnd was called — check that the session
294
- // correctly tracked that the drop went to a DIFFERENT collection
295
- // (cross-tgt is NOT list-A, so wasDroppedExternally should be true)
296
- // Note: by this point resetSession was called, so we test indirectly
297
- // via the onDragEnd callback behavior
298
- expect(sourceConfig.onDragEnd).toHaveBeenCalledWith('move');
299
- srcEl.remove();
300
- tgtEl.remove();
301
- });
302
- });
303
- describe('touch long-press', () => {
304
- beforeEach(() => {
305
- vi.useFakeTimers();
306
- ensurePointerEventPolyfill();
307
- });
308
- afterEach(() => {
309
- vi.useRealTimers();
310
- });
311
- function createTouchPointerEvent(type, x, y, pointerId = 1) {
312
- return new PointerEvent(type, {
313
- clientX: x,
314
- clientY: y,
315
- pointerId,
316
- pointerType: 'touch',
317
- bubbles: true,
318
- });
319
- }
320
- it('does not start drag immediately on touch pointerdown', () => {
321
- const config = createSourceConfig('touch-src');
322
- const el = createElement();
323
- // Stub setPointerCapture / releasePointerCapture
324
- el.setPointerCapture = vi.fn();
325
- el.releasePointerCapture = vi.fn();
326
- dragSession.registerSource(config, el);
327
- const event = createTouchPointerEvent('pointerdown', 100, 100);
328
- dragSession.beginPointerDrag('touch-src', event);
329
- // Should NOT be dragging yet — waiting for long-press timer
330
- expect(dragSession.isDragging).toBe(false);
331
- dragSession.cancel();
332
- el.remove();
333
- });
334
- it('starts drag after 300ms long-press without movement', () => {
335
- const config = createSourceConfig('touch-lp');
336
- const el = createElement();
337
- el.setPointerCapture = vi.fn();
338
- el.releasePointerCapture = vi.fn();
339
- dragSession.registerSource(config, el);
340
- const event = createTouchPointerEvent('pointerdown', 100, 100);
341
- dragSession.beginPointerDrag('touch-lp', event);
342
- // Advance past the long-press delay
343
- vi.advanceTimersByTime(300);
344
- expect(dragSession.isDragging).toBe(true);
345
- expect(dragSession.sourceId).toBe('touch-lp');
346
- expect(config.onDragStart).toHaveBeenCalled();
347
- dragSession.cancel();
348
- el.remove();
349
- });
350
- it('cancels if touch moves too far before long-press fires', () => {
351
- const config = createSourceConfig('touch-scroll');
352
- const el = createElement();
353
- el.setPointerCapture = vi.fn();
354
- el.releasePointerCapture = vi.fn();
355
- dragSession.registerSource(config, el);
356
- const downEvent = createTouchPointerEvent('pointerdown', 100, 100);
357
- dragSession.beginPointerDrag('touch-scroll', downEvent);
358
- // Move more than 10px (LONG_PRESS_TOLERANCE) before timer fires
359
- const moveEvent = createTouchPointerEvent('pointermove', 100, 120);
360
- el.dispatchEvent(moveEvent);
361
- // Even after timer would fire, drag should not start
362
- vi.advanceTimersByTime(300);
363
- expect(dragSession.isDragging).toBe(false);
364
- expect(config.onDragStart).not.toHaveBeenCalled();
365
- el.remove();
366
- });
367
- it('allows small touch jitter within tolerance', () => {
368
- const config = createSourceConfig('touch-jitter');
369
- const el = createElement();
370
- el.setPointerCapture = vi.fn();
371
- el.releasePointerCapture = vi.fn();
372
- dragSession.registerSource(config, el);
373
- const downEvent = createTouchPointerEvent('pointerdown', 100, 100);
374
- dragSession.beginPointerDrag('touch-jitter', downEvent);
375
- // Small movement within tolerance (< 10px)
376
- const moveEvent = createTouchPointerEvent('pointermove', 103, 105);
377
- el.dispatchEvent(moveEvent);
378
- // Timer fires → drag should start despite small jitter
379
- vi.advanceTimersByTime(300);
380
- expect(dragSession.isDragging).toBe(true);
381
- dragSession.cancel();
382
- el.remove();
383
- });
384
- });
385
- describe('exception safety (F-013)', () => {
386
- it('resets session even if onDrop throws', async () => {
387
- const sourceConfig = createSourceConfig('exc-src');
388
- const targetConfig = createTargetConfig('exc-tgt', {
389
- getDropOperation: () => 'move',
390
- onDrop: vi.fn(() => {
391
- throw new Error('callback error');
392
- }),
393
- });
394
- const srcEl = createElement();
395
- const tgtEl = createElement();
396
- dragSession.registerSource(sourceConfig, srcEl);
397
- dragSession.registerTarget(targetConfig, tgtEl);
398
- dragSession.beginKeyboardDrag('exc-src');
399
- dragSession.focusNextTarget();
400
- await expect(dragSession.drop()).rejects.toThrow('callback error');
401
- // Session MUST be cleaned up despite the exception
402
- expect(dragSession.isDragging).toBe(false);
403
- expect(dragSession.sourceId).toBeNull();
404
- expect(dragSession.currentTarget).toBeNull();
405
- srcEl.remove();
406
- tgtEl.remove();
407
- });
408
- it('resets session even if onDragEnd throws on cancel', () => {
409
- const config = createSourceConfig('exc-cancel', {
410
- onDragEnd: vi.fn(() => {
411
- throw new Error('cancel callback error');
412
- }),
413
- });
414
- const el = createElement();
415
- dragSession.registerSource(config, el);
416
- dragSession.beginKeyboardDrag('exc-cancel');
417
- expect(() => dragSession.cancel()).toThrow('cancel callback error');
418
- // Session MUST be cleaned up
419
- expect(dragSession.isDragging).toBe(false);
420
- expect(dragSession.sourceId).toBeNull();
421
- el.remove();
422
- });
423
- });
424
- describe('keyboard focus on targets (F-014)', () => {
425
- it('focuses target element on Tab navigation', () => {
426
- const sourceConfig = createSourceConfig('focus-src');
427
- const targetConfig = createTargetConfig('focus-tgt');
428
- const srcEl = createElement();
429
- const tgtEl = createElement();
430
- tgtEl.tabIndex = 0;
431
- tgtEl.focus = vi.fn();
432
- dragSession.registerSource(sourceConfig, srcEl);
433
- dragSession.registerTarget(targetConfig, tgtEl);
434
- dragSession.beginKeyboardDrag('focus-src');
435
- dragSession.focusNextTarget();
436
- expect(tgtEl.focus).toHaveBeenCalled();
437
- dragSession.cancel();
438
- srcEl.remove();
439
- tgtEl.remove();
440
- });
441
- });
442
- describe('pagehide cleanup (F-005)', () => {
443
- it('resets session on pagehide event during active drag', () => {
444
- const config = createSourceConfig('ph-src');
445
- const el = createElement();
446
- dragSession.registerSource(config, el);
447
- dragSession.beginKeyboardDrag('ph-src');
448
- expect(dragSession.isDragging).toBe(true);
449
- // Simulate page navigation
450
- window.dispatchEvent(new Event('pagehide'));
451
- expect(dragSession.isDragging).toBe(false);
452
- expect(dragSession.sourceId).toBeNull();
453
- el.remove();
454
- });
455
- it('does nothing on pagehide when not dragging', () => {
456
- // Just verify no error is thrown
457
- window.dispatchEvent(new Event('pagehide'));
458
- expect(dragSession.isDragging).toBe(false);
459
- });
460
- });
461
- describe('pointer drag re-entrance (#190)', () => {
462
- beforeEach(() => {
463
- ensurePointerEventPolyfill();
464
- });
465
- function createMousePointerEvent(type, x, y) {
466
- return new PointerEvent(type, {
467
- clientX: x,
468
- clientY: y,
469
- pointerId: 1,
470
- pointerType: 'mouse',
471
- bubbles: true,
472
- });
473
- }
474
- function registerSource(id) {
475
- const config = createSourceConfig(id);
476
- const el = createElement();
477
- el.setPointerCapture = vi.fn();
478
- el.releasePointerCapture = vi.fn();
479
- dragSession.registerSource(config, el);
480
- return { config, el };
481
- }
482
- it('a second beginPointerDrag while one is pending does not clobber the first', () => {
483
- const inner = registerSource('inner-src');
484
- const outer = registerSource('outer-src');
485
- // Same pointerdown delivered to both sources (bubbling in a nested DOM)
486
- const down = createMousePointerEvent('pointerdown', 100, 100);
487
- dragSession.beginPointerDrag('inner-src', down);
488
- expect(dragSession.sourceId).toBe('inner-src');
489
- dragSession.beginPointerDrag('outer-src', down);
490
- expect(dragSession.sourceId).toBe('inner-src'); // first claim wins
491
- // Crossing the threshold activates the drag for the FIRST source
492
- inner.el.dispatchEvent(createMousePointerEvent('pointermove', 110, 110));
493
- expect(dragSession.isDragging).toBe(true);
494
- expect(dragSession.sourceId).toBe('inner-src');
495
- expect(inner.config.onDragStart).toHaveBeenCalled();
496
- expect(outer.config.onDragStart).not.toHaveBeenCalled();
497
- // And the session ends cleanly — no stuck ghost
498
- inner.el.dispatchEvent(createMousePointerEvent('pointerup', 110, 110));
499
- expect(dragSession.isDragging).toBe(false);
500
- expect(dragSession.sourceId).toBeNull();
501
- expect(document.querySelector('.sp-drag-ghost')).toBeNull();
502
- inner.el.remove();
503
- outer.el.remove();
504
- });
505
- it('releasing before the threshold clears the pending claim for the next drag', () => {
506
- const a = registerSource('pending-a');
507
- const b = registerSource('pending-b');
508
- dragSession.beginPointerDrag('pending-a', createMousePointerEvent('pointerdown', 100, 100));
509
- a.el.dispatchEvent(createMousePointerEvent('pointerup', 100, 100));
510
- expect(dragSession.sourceId).toBeNull();
511
- dragSession.beginPointerDrag('pending-b', createMousePointerEvent('pointerdown', 100, 100));
512
- expect(dragSession.sourceId).toBe('pending-b');
513
- dragSession.cancel();
514
- a.el.remove();
515
- b.el.remove();
516
- });
517
- it('keyboard drag cannot start while a pointer drag is pending', () => {
518
- const a = registerSource('kb-block-a');
519
- const b = registerSource('kb-block-b');
520
- dragSession.beginPointerDrag('kb-block-a', createMousePointerEvent('pointerdown', 100, 100));
521
- dragSession.beginKeyboardDrag('kb-block-b');
522
- expect(dragSession.isDragging).toBe(false);
523
- expect(dragSession.sourceId).toBe('kb-block-a');
524
- dragSession.cancel();
525
- a.el.remove();
526
- b.el.remove();
527
- });
528
- it('pagehide during a pending claim resets it so future drags are not blocked', () => {
529
- const src = registerSource('ph-pending');
530
- // Pointer down, threshold NOT crossed — claim is pending
531
- dragSession.beginPointerDrag('ph-pending', createMousePointerEvent('pointerdown', 100, 100));
532
- expect(dragSession.isDragging).toBe(false);
533
- expect(dragSession.sourceId).toBe('ph-pending');
534
- // Page navigates during the pending claim
535
- window.dispatchEvent(new Event('pagehide'));
536
- expect(dragSession.sourceId).toBeNull();
537
- // The re-entrance guard must not block the next drag
538
- dragSession.beginPointerDrag('ph-pending', createMousePointerEvent('pointerdown', 100, 100));
539
- expect(dragSession.sourceId).toBe('ph-pending');
540
- dragSession.cancel();
541
- src.el.remove();
542
- });
543
- });
544
- describe('async onDrop (F-001)', () => {
545
- it('awaits async onDrop callback before cleanup', async () => {
546
- let dropResolved = false;
547
- const sourceConfig = createSourceConfig('async-src');
548
- const targetConfig = createTargetConfig('async-tgt', {
549
- getDropOperation: () => 'move',
550
- onDrop: vi.fn(async () => {
551
- await new Promise((resolve) => setTimeout(resolve, 10));
552
- dropResolved = true;
553
- }),
554
- });
555
- const srcEl = createElement();
556
- const tgtEl = createElement();
557
- dragSession.registerSource(sourceConfig, srcEl);
558
- dragSession.registerTarget(targetConfig, tgtEl);
559
- dragSession.beginKeyboardDrag('async-src');
560
- dragSession.focusNextTarget();
561
- await dragSession.drop();
562
- // The async callback should have resolved before we get here
563
- expect(dropResolved).toBe(true);
564
- expect(targetConfig.onDrop).toHaveBeenCalled();
565
- expect(dragSession.isDragging).toBe(false);
566
- srcEl.remove();
567
- tgtEl.remove();
568
- });
569
- });
570
- describe('escape cancels a POINTER drag (PR #138)', () => {
571
- beforeEach(() => {
572
- // The session is a module singleton: a test that left one active would
573
- // make `beginPointerDrag` refuse re-entry here, and the keystroke would
574
- // then be handled by the INHERITED session — a green that proves nothing.
575
- dragSession.cancel();
576
- ensurePointerEventPolyfill();
577
- });
578
- it('cancels the session, releases the capture and drops no item', () => {
579
- const { config, el } = startPointerDrag('esc-src');
580
- const target = createTargetConfig('esc-tgt');
581
- const tgtEl = createElement();
582
- dragSession.registerTarget(target, tgtEl);
583
- expect(dragSession.isDragging).toBe(true);
584
- pressEscape();
585
- expect(dragSession.isDragging).toBe(false);
586
- expect(config.onDragEnd).toHaveBeenCalledWith('cancel');
587
- expect(target.onDrop).not.toHaveBeenCalled();
588
- expect(el.releasePointerCapture).toHaveBeenCalled();
589
- expect(document.documentElement.classList.contains('sp-drag-active')).toBe(false);
590
- expect(document.querySelector('.sp-drag-ghost')).toBeNull();
591
- el.remove();
592
- tgtEl.remove();
593
- });
594
- it('does not resurrect the drag on the next pointermove', () => {
595
- const { config, el } = startPointerDrag('esc-resurrect');
596
- pressEscape();
597
- expect(dragSession.isDragging).toBe(false);
598
- // The listeners must be gone: _startX/_startY survive, so a surviving
599
- // onMove would re-activate the session past the 5px threshold.
600
- el.dispatchEvent(pointerEvent('pointermove', 300, 300));
601
- expect(dragSession.isDragging).toBe(false);
602
- expect(dragSession.sourceId).toBeNull();
603
- expect(config.onDragStart).toHaveBeenCalledTimes(1);
604
- el.remove();
605
- });
606
- it('leaves Escape to the application while the claim is still PENDING', () => {
607
- const config = createSourceConfig('esc-pending');
608
- const el = createElement();
609
- el.setPointerCapture = vi.fn();
610
- el.releasePointerCapture = vi.fn();
611
- dragSession.registerSource(config, el);
612
- dragSession.beginPointerDrag('esc-pending', pointerEvent('pointerdown', 100, 100));
613
- // no pointermove: under the threshold, the session is not dragging
614
- const event = new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true });
615
- document.dispatchEvent(event);
616
- expect(event.defaultPrevented).toBe(false); // the keystroke belongs to the app
617
- expect(config.onDragEnd).not.toHaveBeenCalled();
618
- dragSession.cancel();
619
- el.remove();
620
- });
621
- });
622
- describe('escape priority over an already-mounted escape layer (see internal/escape-layer)', () => {
623
- beforeEach(() => {
624
- dragSession.cancel();
625
- resetEscapeLayerStack();
626
- ensurePointerEventPolyfill();
627
- });
628
- afterEach(() => {
629
- resetEscapeLayerStack();
630
- });
631
- /**
632
- * Mounts a layer BEFORE the drag starts — the order that exposes the bug.
633
- * `EscapeLayer` registers its global handler on the first mount; a drag
634
- * listener added later on the SAME node in the SAME phase runs after it,
635
- * so `preventDefault()` arrives once the dialog has already closed.
636
- */
637
- function mountLayerFirst() {
638
- const onEscapeKeydown = vi.fn();
639
- const layer = new EscapeLayerState({ enabled: true, onEscapeKeydown });
640
- layer.mount();
641
- return { layer, onEscapeKeydown };
642
- }
643
- it('gives the first escape to the pointer drag and the second to the dialog', () => {
644
- const { layer, onEscapeKeydown } = mountLayerFirst();
645
- const { config, el } = startPointerDrag('esc-layer-pointer');
646
- expect(dragSession.isDragging).toBe(true);
647
- pressEscape();
648
- expect(dragSession.isDragging).toBe(false);
649
- expect(config.onDragEnd).toHaveBeenCalledWith('cancel');
650
- expect(onEscapeKeydown).not.toHaveBeenCalled(); // the dialog must survive
651
- pressEscape();
652
- expect(onEscapeKeydown).toHaveBeenCalledTimes(1); // now it closes normally
653
- layer.unmount();
654
- el.remove();
655
- });
656
- it('gives the first escape to the keyboard drag and the second to the dialog', () => {
657
- const { layer, onEscapeKeydown } = mountLayerFirst();
658
- const config = createSourceConfig('esc-layer-keyboard');
659
- const el = createElement();
660
- dragSession.registerSource(config, el);
661
- dragSession.beginKeyboardDrag('esc-layer-keyboard');
662
- expect(dragSession.isDragging).toBe(true);
663
- pressEscape();
664
- expect(dragSession.isDragging).toBe(false);
665
- expect(config.onDragEnd).toHaveBeenCalledWith('cancel');
666
- expect(onEscapeKeydown).not.toHaveBeenCalled();
667
- pressEscape();
668
- expect(onEscapeKeydown).toHaveBeenCalledTimes(1);
669
- layer.unmount();
670
- el.remove();
671
- });
672
- it('keeps that priority when the layer mounts AFTER the drag has started', () => {
673
- // The mirror order. It was already green by accident — the drag listener
674
- // happened to be registered first. The point of the pair is that the
675
- // outcome no longer depends on which one registered first.
676
- const { config, el } = startPointerDrag('esc-layer-after');
677
- const { layer, onEscapeKeydown } = mountLayerFirst();
678
- expect(dragSession.isDragging).toBe(true);
679
- pressEscape();
680
- expect(dragSession.isDragging).toBe(false);
681
- expect(config.onDragEnd).toHaveBeenCalledWith('cancel');
682
- expect(onEscapeKeydown).not.toHaveBeenCalled();
683
- pressEscape();
684
- expect(onEscapeKeydown).toHaveBeenCalledTimes(1);
685
- layer.unmount();
686
- el.remove();
687
- });
688
- it('leaves the dialog closable immediately while the claim is still pending', () => {
689
- const { layer, onEscapeKeydown } = mountLayerFirst();
690
- const config = createSourceConfig('esc-layer-pending');
691
- const el = createElement();
692
- el.setPointerCapture = vi.fn();
693
- el.releasePointerCapture = vi.fn();
694
- dragSession.registerSource(config, el);
695
- dragSession.beginPointerDrag('esc-layer-pending', pointerEvent('pointerdown', 100, 100));
696
- // no pointermove: under the 5px threshold the session is only PENDING
697
- pressEscape();
698
- expect(onEscapeKeydown).toHaveBeenCalledTimes(1); // closing stays immediate
699
- expect(config.onDragEnd).not.toHaveBeenCalled();
700
- dragSession.cancel();
701
- layer.unmount();
702
- el.remove();
703
- });
704
- });
705
- describe('pointer drop target is computed at release', () => {
706
- beforeEach(() => {
707
- dragSession.cancel();
708
- ensurePointerEventPolyfill();
709
- });
710
- it('drops on the target under the pointerup when no animation frame has run', async () => {
711
- const { config, el } = startPointerDrag('release-src');
712
- const target = createTargetConfig('release-tgt');
713
- const onPointerUpdate = vi.fn();
714
- const tgtEl = createElement(); // rect 0,0 → 200,100
715
- dragSession.registerTarget(target, tgtEl, onPointerUpdate);
716
- // move then up inside the SAME frame: no requestAnimationFrame callback
717
- // has run, so the throttled hit-test never saw either position.
718
- el.dispatchEvent(pointerEvent('pointermove', 150, 70));
719
- el.dispatchEvent(pointerEvent('pointerup', 150, 70));
720
- expect(onPointerUpdate).toHaveBeenCalledWith(150, 70);
721
- expect(target.onDrop).toHaveBeenCalledTimes(1);
722
- // completeDrop awaits onDrop, so the end-of-drag callback lands one
723
- // microtask later even when onDrop is synchronous.
724
- await Promise.resolve();
725
- expect(config.onDragEnd).toHaveBeenCalledWith('move');
726
- el.remove();
727
- tgtEl.remove();
728
- });
729
- it('cancels the pending frame so a stale position cannot land during an async drop', async () => {
730
- const { el } = startPointerDrag('release-async-src');
731
- let resolveDrop = null;
732
- const onDrop = vi.fn(() => new Promise((resolve) => {
733
- resolveDrop = resolve;
734
- }));
735
- const target = createTargetConfig('release-async-tgt', { onDrop });
736
- const onPointerUpdate = vi.fn();
737
- const tgtEl = createElement();
738
- dragSession.registerTarget(target, tgtEl, onPointerUpdate);
739
- // 50,70 is INSIDE the target, so a surviving frame would call
740
- // onPointerUpdate again with those stale coordinates.
741
- el.dispatchEvent(pointerEvent('pointermove', 50, 70));
742
- el.dispatchEvent(pointerEvent('pointerup', 150, 70));
743
- expect(onPointerUpdate).toHaveBeenCalledTimes(1);
744
- expect(onPointerUpdate).toHaveBeenLastCalledWith(150, 70);
745
- // completeDrop is still awaiting onDrop — the session is alive, which is
746
- // exactly the window a surviving frame would fire in.
747
- await new Promise((resolve) => setTimeout(resolve, 48));
748
- expect(onPointerUpdate).toHaveBeenCalledTimes(1);
749
- expect(onPointerUpdate).toHaveBeenLastCalledWith(150, 70);
750
- resolveDrop();
751
- el.remove();
752
- tgtEl.remove();
753
- });
754
- });
755
- describe('pointer hit-test honours acceptedTypes', () => {
756
- beforeEach(() => {
757
- dragSession.cancel();
758
- ensurePointerEventPolyfill();
759
- });
760
- it('never makes a target whose accepted types miss the drag the current one', async () => {
761
- const { el } = startPointerDrag('types-src'); // carries 'test-type' + 'text/plain'
762
- // Registered FIRST and covering the same rect: without the filter the
763
- // hit-test stops here, because it breaks at the first rect containing
764
- // the point.
765
- const foreign = createTargetConfig('types-foreign', { acceptedTypes: ['other-type'] });
766
- const foreignEl = createElement();
767
- const foreignUpdate = vi.fn();
768
- dragSession.registerTarget(foreign, foreignEl, foreignUpdate);
769
- const own = createTargetConfig('types-own', { acceptedTypes: ['test-type'] });
770
- const ownEl = createElement();
771
- const ownUpdate = vi.fn();
772
- dragSession.registerTarget(own, ownEl, ownUpdate);
773
- el.dispatchEvent(pointerEvent('pointerup', 150, 70));
774
- expect(foreignUpdate).not.toHaveBeenCalled();
775
- expect(foreign.onDrop).not.toHaveBeenCalled();
776
- expect(ownUpdate).toHaveBeenCalledWith(150, 70);
777
- expect(own.onDrop).toHaveBeenCalledTimes(1);
778
- await Promise.resolve();
779
- el.remove();
780
- foreignEl.remove();
781
- ownEl.remove();
782
- });
783
- it('still accepts everything when acceptedTypes is absent or empty', async () => {
784
- // Non-regression guard for the catch-all: green before and after the
785
- // filter, and it must never turn red.
786
- const { el } = startPointerDrag('types-catchall-src');
787
- const catchAll = createTargetConfig('types-catchall');
788
- const catchAllEl = createElement();
789
- const catchAllUpdate = vi.fn();
790
- dragSession.registerTarget(catchAll, catchAllEl, catchAllUpdate);
791
- const empty = createTargetConfig('types-empty', { acceptedTypes: [] });
792
- const emptyEl = createElement();
793
- dragSession.registerTarget(empty, emptyEl, vi.fn());
794
- el.dispatchEvent(pointerEvent('pointerup', 150, 70));
795
- expect(catchAllUpdate).toHaveBeenCalledWith(150, 70);
796
- expect(catchAll.onDrop).toHaveBeenCalledTimes(1);
797
- await Promise.resolve();
798
- el.remove();
799
- catchAllEl.remove();
800
- emptyEl.remove();
801
- });
802
- });
803
- });