Skip to content

Commit 4131e08

Browse files
committed
test: add vitest suites
Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
1 parent 640c719 commit 4131e08

8 files changed

Lines changed: 1252 additions & 0 deletions

File tree

Lines changed: 359 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,359 @@
1+
// SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
2+
// SPDX-License-Identifier: AGPL-3.0-or-later
3+
4+
import { describe, it, expect, vi } from 'vitest'
5+
import { mount } from '@vue/test-utils'
6+
import DraggableElement from '../../src/components/DraggableElement.vue'
7+
8+
const baseObject = { id: 'a', x: 10, y: 10, width: 20, height: 20 }
9+
10+
const mountElement = (props = {}) => {
11+
return mount(DraggableElement, {
12+
props: {
13+
object: baseObject,
14+
pageWidth: 100,
15+
pageHeight: 100,
16+
...props,
17+
},
18+
})
19+
}
20+
21+
describe('DraggableElement business rules', () => {
22+
it('does not select when readOnly', async () => {
23+
const wrapper = mountElement({ readOnly: true })
24+
await wrapper.find('.draggable-element').trigger('mousedown', {
25+
clientX: 5,
26+
clientY: 5,
27+
})
28+
expect(wrapper.vm.isSelected).toBe(false)
29+
})
30+
31+
it('selects element and notifies drag start', async () => {
32+
const onDragStart = vi.fn()
33+
const wrapper = mountElement({ onDragStart })
34+
const element = wrapper.find('.draggable-element').element as HTMLElement
35+
element.getBoundingClientRect = () => ({
36+
x: 0,
37+
y: 0,
38+
left: 0,
39+
top: 0,
40+
right: 20,
41+
bottom: 20,
42+
width: 20,
43+
height: 20,
44+
toJSON: () => ({}),
45+
})
46+
47+
await wrapper.find('.draggable-element').trigger('mousedown', {
48+
clientX: 12,
49+
clientY: 14,
50+
})
51+
52+
expect(wrapper.vm.isSelected).toBe(true)
53+
expect(onDragStart).toHaveBeenCalled()
54+
const args = onDragStart.mock.calls[0]
55+
expect(args[0]).toBe(12)
56+
expect(args[1]).toBe(14)
57+
expect(args[2]).toEqual({ x: 12, y: 14 })
58+
expect(args[3]).toEqual({ x: 0, y: 0 })
59+
})
60+
61+
it('ignores click on action toolbar', () => {
62+
const wrapper = mountElement()
63+
const toolbar = document.createElement('div')
64+
toolbar.className = 'actions-toolbar'
65+
66+
wrapper.vm.handleElementClick({
67+
preventDefault: () => {},
68+
target: toolbar,
69+
})
70+
71+
expect(wrapper.vm.isSelected).toBe(false)
72+
})
73+
74+
it('clamps drag updates within page bounds', () => {
75+
const onUpdate = vi.fn()
76+
const wrapper = mountElement({
77+
object: { id: 'a', x: 70, y: 70, width: 40, height: 40 },
78+
onUpdate,
79+
})
80+
81+
wrapper.vm.mode = 'drag'
82+
wrapper.vm.offsetX = 20
83+
wrapper.vm.offsetY = 10
84+
85+
wrapper.vm.stopInteraction()
86+
87+
expect(onUpdate).toHaveBeenCalledWith({ x: 60, y: 60 })
88+
})
89+
90+
it('emits global drag update on stop when dragging globally', () => {
91+
const onUpdate = vi.fn()
92+
const wrapper = mountElement({ onUpdate, isBeingDraggedGlobally: true })
93+
94+
wrapper.vm.mode = 'drag'
95+
wrapper.vm.offsetX = 5
96+
wrapper.vm.offsetY = 5
97+
wrapper.vm.lastMouseX = 120
98+
wrapper.vm.lastMouseY = 140
99+
100+
wrapper.vm.stopInteraction()
101+
102+
expect(onUpdate).toHaveBeenCalledWith({
103+
_globalDrag: true,
104+
_mouseX: 120,
105+
_mouseY: 140,
106+
})
107+
})
108+
109+
it('calls onDragMove while dragging', () => {
110+
const onDragMove = vi.fn()
111+
const wrapper = mountElement({ onDragMove })
112+
wrapper.vm.mode = 'drag'
113+
wrapper.vm.startX = 0
114+
wrapper.vm.startY = 0
115+
116+
const rafSpy = vi.spyOn(globalThis, 'requestAnimationFrame').mockImplementation((cb) => {
117+
cb(0)
118+
return 1
119+
})
120+
121+
wrapper.vm.handleMove({
122+
type: 'mousemove',
123+
clientX: 10,
124+
clientY: 20,
125+
preventDefault: () => {},
126+
})
127+
128+
expect(onDragMove).toHaveBeenCalledWith(10, 20)
129+
130+
rafSpy.mockRestore()
131+
})
132+
133+
it('applies resize updates', () => {
134+
const onUpdate = vi.fn()
135+
const wrapper = mountElement({ onUpdate })
136+
137+
wrapper.vm.mode = 'resize'
138+
wrapper.vm.resizeOffsetX = -10
139+
wrapper.vm.resizeOffsetY = -10
140+
wrapper.vm.resizeOffsetW = 5
141+
wrapper.vm.resizeOffsetH = 5
142+
143+
wrapper.vm.stopInteraction()
144+
145+
expect(onUpdate).toHaveBeenCalledWith({ x: 0, y: 0, width: 25, height: 25 })
146+
})
147+
148+
it('does not emit update when idle', () => {
149+
const onUpdate = vi.fn()
150+
const wrapper = mountElement({ onUpdate })
151+
152+
wrapper.vm.mode = 'idle'
153+
wrapper.vm.stopInteraction()
154+
155+
expect(onUpdate).not.toHaveBeenCalled()
156+
})
157+
158+
it('resets offsets after interaction', () => {
159+
const wrapper = mountElement()
160+
wrapper.vm.mode = 'drag'
161+
wrapper.vm.offsetX = 5
162+
wrapper.vm.offsetY = 6
163+
164+
wrapper.vm.stopInteraction()
165+
166+
expect(wrapper.vm.mode).toBe('idle')
167+
expect(wrapper.vm.offsetX).toBe(0)
168+
expect(wrapper.vm.offsetY).toBe(0)
169+
})
170+
171+
it('always calls onDragEnd when stopping interaction', () => {
172+
const onDragEnd = vi.fn()
173+
const wrapper = mountElement({ onDragEnd })
174+
wrapper.vm.mode = 'drag'
175+
wrapper.vm.offsetX = 1
176+
wrapper.vm.offsetY = 2
177+
178+
wrapper.vm.stopInteraction()
179+
180+
expect(onDragEnd).toHaveBeenCalled()
181+
})
182+
183+
it('deselects on outside click unless ignored', () => {
184+
const wrapper = mountElement()
185+
wrapper.vm.isSelected = true
186+
187+
const outside = document.createElement('div')
188+
document.body.appendChild(outside)
189+
wrapper.vm.handleClickOutside({ target: outside })
190+
191+
expect(wrapper.vm.isSelected).toBe(false)
192+
193+
const wrapperIgnored = mountElement({ ignoreClickOutsideSelectors: ['.keep-selected'] })
194+
wrapperIgnored.vm.isSelected = true
195+
const keep = document.createElement('div')
196+
keep.className = 'keep-selected'
197+
document.body.appendChild(keep)
198+
199+
wrapperIgnored.vm.handleClickOutside({ target: keep })
200+
expect(wrapperIgnored.vm.isSelected).toBe(true)
201+
202+
keep.remove()
203+
outside.remove()
204+
})
205+
206+
it('keeps selection on ignored selector via DOM event', async () => {
207+
const wrapper = mountElement({ ignoreClickOutsideSelectors: ['.keep'] })
208+
wrapper.vm.isSelected = true
209+
210+
const keep = document.createElement('div')
211+
keep.className = 'keep'
212+
document.body.appendChild(keep)
213+
214+
keep.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }))
215+
await wrapper.vm.$nextTick()
216+
217+
expect(wrapper.vm.isSelected).toBe(true)
218+
219+
keep.remove()
220+
})
221+
222+
it('keeps resize above minimum size', () => {
223+
const wrapper = mountElement({
224+
object: { id: 'a', x: 0, y: 0, width: 30, height: 30 },
225+
pagesScale: 1,
226+
})
227+
228+
wrapper.vm.mode = 'resize'
229+
wrapper.vm.direction = 'top-left'
230+
wrapper.vm.startX = 100
231+
wrapper.vm.startY = 100
232+
wrapper.vm.startLeft = 0
233+
wrapper.vm.startTop = 0
234+
wrapper.vm.startWidth = 30
235+
wrapper.vm.startHeight = 30
236+
wrapper.vm.aspectRatio = 1
237+
238+
const rafSpy = vi.spyOn(globalThis, 'requestAnimationFrame').mockImplementation((cb) => {
239+
cb(0)
240+
return 1
241+
})
242+
243+
wrapper.vm.handleMove({
244+
type: 'mousemove',
245+
clientX: 200,
246+
clientY: 200,
247+
preventDefault: () => {},
248+
})
249+
250+
expect(wrapper.vm.resizeOffsetW).toBeGreaterThanOrEqual(-14)
251+
expect(wrapper.vm.resizeOffsetH).toBeGreaterThanOrEqual(-14)
252+
253+
rafSpy.mockRestore()
254+
})
255+
256+
it('clamps resize within page bounds', () => {
257+
const wrapper = mountElement({
258+
object: { id: 'a', x: 60, y: 60, width: 30, height: 30 },
259+
pagesScale: 1,
260+
pageWidth: 100,
261+
pageHeight: 100,
262+
})
263+
264+
wrapper.vm.mode = 'resize'
265+
wrapper.vm.direction = 'bottom-right'
266+
wrapper.vm.startX = 0
267+
wrapper.vm.startY = 0
268+
wrapper.vm.startLeft = 60
269+
wrapper.vm.startTop = 60
270+
wrapper.vm.startWidth = 30
271+
wrapper.vm.startHeight = 30
272+
wrapper.vm.aspectRatio = 1
273+
274+
const rafSpy = vi.spyOn(globalThis, 'requestAnimationFrame').mockImplementation((cb) => {
275+
cb(0)
276+
return 1
277+
})
278+
279+
wrapper.vm.handleMove({
280+
type: 'mousemove',
281+
clientX: 200,
282+
clientY: 200,
283+
preventDefault: () => {},
284+
})
285+
286+
const newWidth = wrapper.vm.object.width + wrapper.vm.resizeOffsetW
287+
const newHeight = wrapper.vm.object.height + wrapper.vm.resizeOffsetH
288+
289+
expect(newWidth).toBeLessThanOrEqual(40)
290+
expect(newHeight).toBeLessThanOrEqual(40)
291+
292+
rafSpy.mockRestore()
293+
})
294+
295+
it('scales drag delta using pagesScale', () => {
296+
const wrapper = mountElement({ pagesScale: 2 })
297+
wrapper.vm.mode = 'drag'
298+
wrapper.vm.startX = 0
299+
wrapper.vm.startY = 0
300+
301+
const rafSpy = vi.spyOn(globalThis, 'requestAnimationFrame').mockImplementation((cb) => {
302+
cb(0)
303+
return 1
304+
})
305+
306+
wrapper.vm.handleMove({
307+
type: 'mousemove',
308+
clientX: 20,
309+
clientY: 10,
310+
preventDefault: () => {},
311+
})
312+
313+
expect(wrapper.vm.offsetX).toBe(10)
314+
expect(wrapper.vm.offsetY).toBe(5)
315+
316+
rafSpy.mockRestore()
317+
})
318+
319+
it('computes drag offsets using page rect with scaling', async () => {
320+
const onDragStart = vi.fn()
321+
const wrapper = mountElement({ pagesScale: 2, onDragStart })
322+
323+
const element = wrapper.find('.draggable-element').element as HTMLElement
324+
element.getBoundingClientRect = () => ({
325+
x: 20,
326+
y: 40,
327+
left: 20,
328+
top: 40,
329+
right: 40,
330+
bottom: 60,
331+
width: 20,
332+
height: 20,
333+
toJSON: () => ({}),
334+
})
335+
336+
const wrapperEl = wrapper.find('.draggable-wrapper').element as HTMLElement
337+
wrapperEl.closest = () => ({
338+
querySelector: () => ({
339+
getBoundingClientRect: () => ({ left: 0, top: 0, right: 200, bottom: 200 }),
340+
}),
341+
}) as any
342+
343+
await wrapper.find('.draggable-element').trigger('mousedown', {
344+
clientX: 30,
345+
clientY: 50,
346+
})
347+
348+
expect(onDragStart).toHaveBeenCalled()
349+
expect(wrapper.vm.pointerOffsetDoc).toEqual({ x: 10, y: 10 })
350+
})
351+
352+
it('hides toolbar while dragged globally', async () => {
353+
const wrapper = mountElement({ isBeingDraggedGlobally: true })
354+
wrapper.vm.isSelected = true
355+
await wrapper.vm.$nextTick()
356+
357+
expect(wrapper.find('.actions-toolbar').exists()).toBe(false)
358+
})
359+
})

0 commit comments

Comments
 (0)