From 971b6093054bd39bdbefbea5284b5a761917b0b2 Mon Sep 17 00:00:00 2001 From: Jaybe Date: Mon, 17 Nov 2025 01:28:51 +0900 Subject: [PATCH] =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8:=20=EB=A9=80?= =?UTF-8?q?=ED=8B=B0=EC=84=A0=ED=83=9D=20=EC=8A=A4=EB=83=85=20=ED=99=95?= =?UTF-8?q?=EB=8C=80=20=EC=BC=80=EC=9D=B4=EC=8A=A4(=EC=B6=95=20=EB=8F=85?= =?UTF-8?q?=EB=A6=BD/=EC=9E=84=EA=B3=84=20=EA=B2=BD=EA=B3=84/=EC=B5=9C?= =?UTF-8?q?=EA=B7=BC=EC=A0=91=20=EC=9A=B0=EC=84=A0)=20TDD=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=EC=95=88=EC=A0=95=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Canvas.multigroup.snap.expanded.test.tsx | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 components/wysiwyg/Canvas.multigroup.snap.expanded.test.tsx diff --git a/components/wysiwyg/Canvas.multigroup.snap.expanded.test.tsx b/components/wysiwyg/Canvas.multigroup.snap.expanded.test.tsx new file mode 100644 index 0000000..7732bc7 --- /dev/null +++ b/components/wysiwyg/Canvas.multigroup.snap.expanded.test.tsx @@ -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) => { + capture(props) + return
+ } + 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() + + // 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() + + // 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?.() }) + }) +})