feat: WYSIWYG 빌더 1차 기능 완성 및 테스트 추가
Auto PR / open-pr (push) Successful in 20s
Auto Label PR / add-automerge-label (pull_request) Successful in 7s
CI / test (pull_request) Successful in 1m34s

This commit is contained in:
2025-11-17 10:04:01 +09:00
parent 9ced249015
commit 1c607f6331
13 changed files with 660 additions and 28 deletions
@@ -0,0 +1,149 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { WysiwygBuilderPage } from '@/components/wysiwyg/WysiwygBuilderPage'
const createObjectURL = global.URL.createObjectURL
const revokeObjectURL = global.URL.revokeObjectURL
describe('WYSIWYG Builder acceptance', () => {
beforeEach(() => {
// @ts-expect-error - test shim
global.URL.createObjectURL = () => 'blob:mock'
// @ts-expect-error - test shim
global.URL.revokeObjectURL = () => {}
})
afterEach(() => {
global.URL.createObjectURL = createObjectURL
global.URL.revokeObjectURL = revokeObjectURL
})
it('Add → Edit → Export smoke', async () => {
render(<WysiwygBuilderPage />)
// Add Text
fireEvent.click(screen.getByRole('button', { name: /Add Text/i }))
const obj = await screen.findByTestId('obj-text-1')
// Select and edit via Properties panel
fireEvent.mouseDown(obj)
const inputX = await screen.findByLabelText('X')
fireEvent.change(inputX, { target: { value: '80' } })
await waitFor(() => {
// style left updated on canvas object (snap may apply, accept near 80)
const left = parseInt((obj as HTMLElement).style.left || '0', 10)
expect(Math.abs(left - 80)).toBeLessThanOrEqual(8)
})
// Export exists and triggers download
const exportBtn = screen.getByRole('button', { name: /Export/i })
fireEvent.click(exportBtn)
// No throw means success; optionally wait a tick
await new Promise((r) => setTimeout(r, 0))
})
it('uploads a local image and renders it via AssetManager path', async () => {
render(<WysiwygBuilderPage />)
const input = screen.getByTestId('wysiwyg-image-input') as HTMLInputElement
const file = new File([new Uint8Array([1, 2, 3])], 'hero.png', { type: 'image/png' })
fireEvent.change(input, { target: { files: [file] } })
const img = await screen.findByRole('img')
const src = (img as HTMLImageElement).src
expect(src).toContain('/assets/')
})
it('syncs width/height/rotate from Properties panel to Canvas for a text object', async () => {
render(<WysiwygBuilderPage />)
// Add Text and select
const addTextBtn = screen.getByRole('button', { name: /Add Text/i })
fireEvent.click(addTextBtn)
const [obj] = await screen.findAllByTestId(/obj-text-/)
fireEvent.mouseDown(obj)
// Edit width/height/rotate via Properties
const inputW = await screen.findByLabelText('Width')
const inputH = await screen.findByLabelText('Height')
const inputR = await screen.findByLabelText('Rotate')
fireEvent.change(inputW, { target: { value: '200' } })
fireEvent.change(inputH, { target: { value: '60' } })
fireEvent.change(inputR, { target: { value: '30' } })
await waitFor(() => {
const style = (obj as HTMLElement).style
expect(style.width).toBe('200px')
expect(style.height).toBe('60px')
expect(style.transform).toContain('30deg')
})
})
it('syncs text/button specific props from Properties to Canvas content', async () => {
render(<WysiwygBuilderPage />)
// Text
fireEvent.click(screen.getByRole('button', { name: /Add Text/i }))
const [textObj] = await screen.findAllByTestId(/obj-text-/)
fireEvent.mouseDown(textObj)
const textInput = await screen.findByLabelText('Text')
fireEvent.change(textInput, { target: { value: 'Hello World' } })
await waitFor(() => {
expect(textObj).toHaveTextContent('Hello World')
})
// Button
fireEvent.click(screen.getByRole('button', { name: /Add Button/i }))
const [buttonObj] = await screen.findAllByTestId(/obj-button-/)
fireEvent.mouseDown(buttonObj)
const labelInput = await screen.findByLabelText('Label')
fireEvent.change(labelInput, { target: { value: 'CTA' } })
await waitFor(() => {
expect(buttonObj).toHaveTextContent('CTA')
})
})
it('supports Shift+click multiselect and group drag on the builder canvas', async () => {
render(<WysiwygBuilderPage />)
// 두 텍스트 추가
const addTextBtn = screen.getByRole('button', { name: /Add Text/i })
fireEvent.click(addTextBtn)
fireEvent.click(addTextBtn)
const [obj1, obj2] = await screen.findAllByTestId(/obj-text-/)
// 첫 번째 선택
fireEvent.mouseDown(obj1, { clientX: 70, clientY: 70 })
// Shift+클릭으로 두 번째 추가 선택
fireEvent.mouseDown(obj2, { clientX: 80, clientY: 120, shiftKey: true })
// 멀티선택 상태에서 두 객체 모두 outline 스타일을 갖는지만 확인
expect((obj1 as HTMLElement).style.outline).toContain('2px solid')
expect((obj2 as HTMLElement).style.outline).toContain('2px solid')
})
it('supports marquee selection on the builder canvas', async () => {
render(<WysiwygBuilderPage />)
// 텍스트 두 개 배치
const addTextBtn = screen.getByRole('button', { name: /Add Text/i })
fireEvent.click(addTextBtn)
fireEvent.click(addTextBtn)
const [obj1, obj2] = await screen.findAllByTestId(/obj-text-/)
// 마퀴 드래그 박스로 두 객체 모두 포함되도록 드래그
const canvas = screen.getByTestId('wysiwyg-canvas')
fireEvent.mouseDown(canvas, { clientX: 40, clientY: 40 })
fireEvent.mouseMove(canvas, { clientX: 300, clientY: 300 })
fireEvent.mouseUp(canvas)
// 선택된 두 객체가 모두 outline 스타일을 갖는지 확인
expect((obj1 as HTMLElement).style.outline).toContain('2px solid')
expect((obj2 as HTMLElement).style.outline).toContain('2px solid')
})
})