WYSIWYG: 멀티선택/그룹 스냅·가이드(그룹 bbox)/마퀴 선택 TDD·구현 및 문서 업데이트
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
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 }
|
||||
})
|
||||
|
||||
// 시나리오 1: 같은 축에서 중심과 에지가 모두 임계 내에 들어오면 에지가 우선되어야 함
|
||||
const frameEdgeOverCenter: Frame = {
|
||||
id: 'f-mixed-1',
|
||||
width: 500,
|
||||
height: 400,
|
||||
background: '#fff',
|
||||
layers: [
|
||||
{ id: 'l1', name: 'L1', visible: true, locked: false, objects: [
|
||||
// 선택 대상(o1)
|
||||
{ id: 'o1', type: 'text', x: 10, y: 10, width: 40, height: 30, rotate: 0, zIndex: 2, props: { text: 'A', fontSize: 16, color: '#000' } },
|
||||
// 기준1(o2): 중심 X=240, left=200, right=280
|
||||
{ id: 'o2', type: 'text', x: 200, y: 50, width: 80, height: 30, rotate: 0, zIndex: 1, props: { text: 'B', fontSize: 16, color: '#000' } },
|
||||
]}
|
||||
]
|
||||
}
|
||||
|
||||
// 시나리오 2: 같은 축에서 두 개의 다른 객체가 근접하나, 더 가까운 선으로 스냅되어야 함
|
||||
const frameNearestWins: Frame = {
|
||||
id: 'f-mixed-2',
|
||||
width: 600,
|
||||
height: 400,
|
||||
background: '#fff',
|
||||
layers: [
|
||||
{ id: 'l1', name: 'L1', visible: true, locked: false, objects: [
|
||||
// 선택 대상(o1)
|
||||
{ id: 'o1', type: 'text', x: 50, y: 20, width: 40, height: 30, rotate: 0, zIndex: 3, props: { text: 'A', fontSize: 16, color: '#000' } },
|
||||
// 기준 가까움(o2): left=220
|
||||
{ id: 'o2', type: 'text', x: 220, y: 60, width: 60, height: 30, rotate: 0, zIndex: 2, props: { text: 'B', fontSize: 16, color: '#000' } },
|
||||
// 기준 멀음(o3): left=300
|
||||
{ id: 'o3', type: 'text', x: 300, y: 60, width: 60, height: 30, rotate: 0, zIndex: 1, props: { text: 'C', fontSize: 16, color: '#000' } },
|
||||
]}
|
||||
]
|
||||
}
|
||||
|
||||
describe('Canvas guidelines mixed cases', () => {
|
||||
beforeEach(() => capture.mockReset())
|
||||
|
||||
it('prefers edge snap over center snap on the same axis when both are within threshold', async () => {
|
||||
render(<Canvas frame={frameEdgeOverCenter} />)
|
||||
const last = capture.mock.calls.at(-1)?.[0] as MProps
|
||||
expect(last).toBeTruthy()
|
||||
|
||||
await act(async () => { last.onDragStart?.() })
|
||||
// o2 중심X=240. o1 중심이 240에 근접하도록 left=220 제안(중심 스냅 후보)
|
||||
// 동시에 o2 왼쪽 에지X=200에도 8px 임계 내로 근접시키기 위해 205 근방을 거쳐 테스트.
|
||||
await act(async () => { last.onDrag?.({ left: 205, top: 10 }) })
|
||||
|
||||
const obj = screen.getByTestId('obj-o1') as HTMLElement
|
||||
await waitFor(() => {
|
||||
// 에지 우선: 200으로 스냅되어야 함(중심 240보다 우선)
|
||||
expect(obj.style.left).toBe('200px')
|
||||
})
|
||||
await act(async () => { last.onDragEnd?.() })
|
||||
})
|
||||
|
||||
it('snaps to the nearest object edge when multiple objects are within threshold on same axis', async () => {
|
||||
render(<Canvas frame={frameNearestWins} />)
|
||||
const last = capture.mock.calls.at(-1)?.[0] as MProps
|
||||
expect(last).toBeTruthy()
|
||||
|
||||
await act(async () => { last.onDragStart?.() })
|
||||
// o1을 X축으로 이동. o2.left=220, o3.left=300. 8px 임계 내 근접 중 더 가까운 o2로 스냅되어야 함
|
||||
await act(async () => { last.onDrag?.({ left: 223, top: 20 }) })
|
||||
|
||||
const obj = screen.getByTestId('obj-o1') as HTMLElement
|
||||
await waitFor(() => {
|
||||
expect(obj.style.left).toBe('220px')
|
||||
})
|
||||
await act(async () => { last.onDragEnd?.() })
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user