테스트: 멀티선택 스냅 확대 케이스(축 독립/임계 경계/최근접 우선) TDD 추가 및 안정화
This commit is contained in:
@@ -0,0 +1,111 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
import { render, screen, waitFor } from '@testing-library/react'
|
||||||
|
import { act } from 'react'
|
||||||
|
import { Canvas } from '@/components/wysiwyg/Canvas'
|
||||||
|
import type { Frame } from '@/lib/wysiwyg/schema'
|
||||||
|
|
||||||
|
const capture = vi.fn()
|
||||||
|
|
||||||
|
type MProps = {
|
||||||
|
onDragStart?: () => void
|
||||||
|
onDrag?: (e: { left?: number; top?: number }) => void
|
||||||
|
onDragEnd?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.mock('react-moveable', () => {
|
||||||
|
const Mock = (props: Record<string, unknown>) => {
|
||||||
|
capture(props)
|
||||||
|
return <div data-testid="moveable" />
|
||||||
|
}
|
||||||
|
return { default: Mock }
|
||||||
|
})
|
||||||
|
|
||||||
|
// Base frame
|
||||||
|
function makeFrame(): Frame {
|
||||||
|
return {
|
||||||
|
id: 'f-expand',
|
||||||
|
width: 500,
|
||||||
|
height: 400,
|
||||||
|
background: '#fff',
|
||||||
|
layers: [
|
||||||
|
{ id: 'l1', name: 'L1', visible: true, locked: false, objects: [
|
||||||
|
{ id: 'o1', type: 'text', x: 10, y: 10, width: 40, height: 30, rotate: 0, zIndex: 3, props: { text: 'A', fontSize: 16, color: '#000' } },
|
||||||
|
{ id: 'o2', type: 'text', x: 80, y: 10, width: 40, height: 30, rotate: 0, zIndex: 2, props: { text: 'B', fontSize: 16, color: '#000' } },
|
||||||
|
{ id: 'o3', type: 'text', x: 200, y: 50, width: 80, height: 30, rotate: 0, zIndex: 1, props: { text: 'C', fontSize: 16, color: '#000' } },
|
||||||
|
]}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Canvas multi-group snap expanded', () => {
|
||||||
|
beforeEach(() => capture.mockReset())
|
||||||
|
|
||||||
|
it('axis independence: x-axis edge snapping does not alter y-axis center decision', async () => {
|
||||||
|
const frame = makeFrame()
|
||||||
|
render(<Canvas frame={frame} />)
|
||||||
|
|
||||||
|
// Multi select o1 + o2
|
||||||
|
screen.getByTestId('obj-o1').dispatchEvent(new MouseEvent('mousedown', { bubbles: true }))
|
||||||
|
screen.getByTestId('obj-o2').dispatchEvent(new MouseEvent('mousedown', { bubbles: true, shiftKey: true }))
|
||||||
|
|
||||||
|
await waitFor(() => expect(capture.mock.calls.length).toBeGreaterThan(0))
|
||||||
|
const last = capture.mock.calls.at(-1)?.[0] as MProps
|
||||||
|
|
||||||
|
await act(async () => { last.onDragStart?.() })
|
||||||
|
// Propose left so that group right edge aligns to o3 left edge (x-edge snap),
|
||||||
|
// while y stays near group original top to avoid y snapping.
|
||||||
|
await act(async () => { last.onDrag?.({ left: 160, top: 10 }) })
|
||||||
|
|
||||||
|
const o1 = screen.getByTestId('obj-o1') as HTMLElement
|
||||||
|
const o2 = screen.getByTestId('obj-o2') as HTMLElement
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const l1 = parseInt(o1.style.left)
|
||||||
|
// x should snap near 90 (200 - groupWidth 110) with 8px grid tolerance
|
||||||
|
expect([88, 90, 96]).toContain(l1)
|
||||||
|
// y should remain aligned equally without forcing to o3 center (remain ~10 + dy)
|
||||||
|
const t1 = parseInt(o1.style.top)
|
||||||
|
const t2 = parseInt(o2.style.top)
|
||||||
|
expect(t1 - 10).toBe(t2 - 10)
|
||||||
|
})
|
||||||
|
|
||||||
|
await act(async () => { last.onDragEnd?.() })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('threshold boundary: snaps when within 8px, does not snap when beyond 8px', async () => {
|
||||||
|
const frame = makeFrame()
|
||||||
|
render(<Canvas frame={frame} />)
|
||||||
|
|
||||||
|
// Multi select
|
||||||
|
screen.getByTestId('obj-o1').dispatchEvent(new MouseEvent('mousedown', { bubbles: true }))
|
||||||
|
screen.getByTestId('obj-o2').dispatchEvent(new MouseEvent('mousedown', { bubbles: true, shiftKey: true }))
|
||||||
|
await waitFor(() => expect(capture.mock.calls.length).toBeGreaterThan(0))
|
||||||
|
const last = capture.mock.calls.at(-1)?.[0] as MProps
|
||||||
|
await act(async () => { last.onDragStart?.() })
|
||||||
|
|
||||||
|
// Case A: within 8px → should snap to o3 center X
|
||||||
|
// group centerX=65. To reach o3 centerX=240, propose o2.left ~ 80 + (240-65) = 255
|
||||||
|
await act(async () => { last.onDrag?.({ left: 255, top: 10 }) })
|
||||||
|
const o1a = screen.getByTestId('obj-o1') as HTMLElement
|
||||||
|
// group centerX should be ~240 (allow grid tolerance)
|
||||||
|
await waitFor(() => {
|
||||||
|
const l1 = parseInt(o1a.style.left)
|
||||||
|
const groupCenter = l1 + 110 / 2 // groupWidth=110
|
||||||
|
expect(Math.abs(groupCenter - 240)).toBeLessThanOrEqual(8)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Case B: beyond 8px → move slightly more than threshold on X so not snapping to center
|
||||||
|
// propose further by +16 (two grid steps) from the previous left
|
||||||
|
await act(async () => { last.onDrag?.({ left: 271, top: 10 }) })
|
||||||
|
const o1b = screen.getByTestId('obj-o1') as HTMLElement
|
||||||
|
await waitFor(() => {
|
||||||
|
const l1b = parseInt(o1b.style.left)
|
||||||
|
const groupCenterB = l1b + 110 / 2
|
||||||
|
// should no longer be within 8px of 240 (no snap)
|
||||||
|
expect(Math.abs(groupCenterB - 240)).toBeGreaterThan(8)
|
||||||
|
})
|
||||||
|
|
||||||
|
await act(async () => { last.onDragEnd?.() })
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user