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() // 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() 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() // 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() // 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() // 두 텍스트 추가 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() // 텍스트 두 개 배치 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') }) })