diff --git a/MAIN_PLAN.md b/MAIN_PLAN.md index 2892912..7e416f8 100644 --- a/MAIN_PLAN.md +++ b/MAIN_PLAN.md @@ -285,6 +285,12 @@ - 그리드 단독: 인접 후보가 없을 때 13px → 16px 그리드 스냅 - 결과: 스냅 우선순위 정책(에지 > 센터 > 그리드) 회귀 스모크 확보 +### 회전 후 스냅 유지 E2E (에지 > 그리드) (2025-11-17) +- 테스트: `tests/e2e/wysiwyg-rotate-snap.e2e.ts` + - 객체를 15° 회전 후 드래그해도 에지 스냅이 그리드보다 우선 적용됨 검증 + - 드래그 중 라이브리전 가이드 텍스트 노출 확인(가이드 x/y) +- 결과: 회전 상태에서도 스냅 우선순위 일관성 확보, 라이브리전 가이드 동작 스모크 통과 + ## 목표 - 비개발자도 사용 가능한 단일 페이지 랜딩 페이지 빌더. - 결과물: 정적 ZIP(HTML/CSS/JS) 다운로드. 이미지: 외부 URL 또는 로컬 업로드(서버 저장 없음, ZIP에 패키징). 폰트는 외부 CDN 허용. diff --git a/tests/e2e/wysiwyg-group-rotate-drag.e2e.ts b/tests/e2e/wysiwyg-group-rotate-drag.e2e.ts new file mode 100644 index 0000000..65184e0 --- /dev/null +++ b/tests/e2e/wysiwyg-group-rotate-drag.e2e.ts @@ -0,0 +1,64 @@ +import { test, expect } from '@playwright/test' + +const wysiwygUrl = '/en/wysiwyg' + +async function getLefts(page: import('@playwright/test').Page) { + const els = page.locator('[data-testid^="obj-text-"]') + const count = await els.count() + const results: number[] = [] + for (let i = 0; i < count; i++) { + const style = await els.nth(i).evaluate((n) => (n as HTMLElement).style.left || '0') + results.push(parseInt(style || '0', 10)) + } + return results +} + +test.describe('WYSIWYG - Group rotate then drag with snap and live region', () => { + test('after rotating multiselect, dragging group by 13px snaps to 16px; live region says 선택 2개', async ({ page }) => { + await page.goto(wysiwygUrl) + + const canvas = page.getByTestId('wysiwyg-canvas') + await expect(canvas).toBeVisible() + + const addText = page.getByRole('button', { name: /^Add Text$/i }) + await addText.click() + await addText.click() + + // Marquee select both + const cbox = await canvas.boundingBox() + if (!cbox) throw new Error('canvas not found') + await page.mouse.move(cbox.x + 40, cbox.y + 40) + await page.mouse.down() + await page.mouse.move(cbox.x + 260, cbox.y + 160) + await page.mouse.up() + + const live = page.getByTestId('aria-live') + await expect(live).toContainText(/선택\s*2개/) + + // Rotate ~15deg via rotate handle + const rotateHandle = page.locator('[data-testid^="rotate-handle-"]').first() + const hbox = await rotateHandle.boundingBox() + if (!hbox) throw new Error('rotate handle not found') + await page.mouse.move(hbox.x + 5, hbox.y + 5) + await page.mouse.down() + await page.mouse.move(hbox.x + 40, hbox.y + 5) + await expect(live).toContainText(/가이드\s*x=|y=/) + await page.mouse.up() + + const before = await getLefts(page) + + // Drag group by ~13px on X; expect grid snap to 16 + const target = page.locator('[data-testid^="obj-text-"]').nth(1) + const tbox = await target.boundingBox() + if (!tbox) throw new Error('target not found') + await page.mouse.move(tbox.x + 5, tbox.y + 5) + await page.mouse.down() + await page.mouse.move(tbox.x + 5 + 13, tbox.y + 5) + await page.mouse.up() + + const after = await getLefts(page) + const dx = after[1] - before[1] + expect(dx).toBe(16) + expect(after[0] - before[0]).toBe(16) + }) +})