import { test, expect } from "@playwright/test"; function parsePx(value: string): number { const n = parseFloat(value || ""); return Number.isFinite(n) ? n : 0; } test.beforeEach(async ({ page }) => { await page.route("**/api/auth/me", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ id: "user-form-parity", email: "form-parity@example.com", tokenVersion: 1 }), }); }); }); test("formInput: 에디터 캔버스와 프리뷰에서 높이/패딩/색상이 동일해야 한다", async ({ page }) => { await page.goto("/editor"); const canvas = page.getByTestId("editor-canvas"); const sidebar = page.getByTestId("properties-sidebar"); await page.getByRole("button", { name: "입력(Input)" }).click(); await sidebar.getByRole("textbox", { name: "필드 라벨" }).fill("에디터-프리뷰 인풋"); await sidebar.getByLabel("텍스트 정렬").selectOption("center"); await sidebar.getByRole("combobox", { name: "너비", exact: true }).selectOption("fixed"); await sidebar.getByLabel("필드 고정 너비 커스텀 (px)").fill("320"); await sidebar.getByLabel("필드 텍스트 색상 HEX").fill("#ff0000"); await sidebar.getByLabel("필드 채움 색상 HEX").fill("#00ff88"); await sidebar.getByLabel("필드 테두리 색상 HEX").fill("#0000ff"); const editorInput = canvas.getByRole("textbox", { name: "에디터-프리뷰 인풋" }); const editorStyles = await editorInput.evaluate((el) => { const s = window.getComputedStyle(el as HTMLInputElement); return { height: s.height, paddingTop: s.paddingTop, paddingBottom: s.paddingBottom, fontSize: s.fontSize, color: s.color, backgroundColor: s.backgroundColor, borderColor: s.borderColor, borderRadius: s.borderRadius, }; }); const editorAttrs = await editorInput.evaluate((el) => { const input = el as HTMLInputElement; return { type: input.type, name: input.name, placeholder: input.placeholder, }; }); await Promise.all([ page.waitForURL(/\/preview/), page.getByRole("link", { name: "프리뷰 열기" }).click(), ]); await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible(); const previewInput = page.getByRole("textbox", { name: "에디터-프리뷰 인풋" }); const previewStyles = await previewInput.evaluate((el) => { const s = window.getComputedStyle(el as HTMLInputElement); return { height: s.height, paddingTop: s.paddingTop, paddingBottom: s.paddingBottom, fontSize: s.fontSize, color: s.color, backgroundColor: s.backgroundColor, borderColor: s.borderColor, borderRadius: s.borderRadius, }; }); const editorHeight = parsePx(editorStyles.height); const previewHeight = parsePx(previewStyles.height); expect(Math.abs(previewHeight - editorHeight)).toBeLessThanOrEqual(2); const editorPaddingTop = parsePx(editorStyles.paddingTop); const previewPaddingTop = parsePx(previewStyles.paddingTop); expect(Math.abs(previewPaddingTop - editorPaddingTop)).toBeLessThanOrEqual(3); const editorPaddingBottom = parsePx(editorStyles.paddingBottom); const previewPaddingBottom = parsePx(previewStyles.paddingBottom); expect(Math.abs(previewPaddingBottom - editorPaddingBottom)).toBeLessThanOrEqual(3); expect(previewStyles.fontSize).toBe(editorStyles.fontSize); expect(previewStyles.color).toBe(editorStyles.color); expect(previewStyles.backgroundColor).toBe(editorStyles.backgroundColor); expect(previewStyles.borderColor).toBe(editorStyles.borderColor); expect(previewStyles.borderRadius).toBe(editorStyles.borderRadius); const previewAttrs = await previewInput.evaluate((el) => { const input = el as HTMLInputElement; return { type: input.type, name: input.name, placeholder: input.placeholder, }; }); expect(previewAttrs.type).toBe(editorAttrs.type); expect(previewAttrs.placeholder).toBe(editorAttrs.placeholder); expect(previewAttrs.name).toBe(editorAttrs.name); }); test("formSelect: 에디터 캔버스와 프리뷰에서 높이/패딩/색상이 동일해야 한다", async ({ page }) => { await page.goto("/editor"); const canvas = page.getByTestId("editor-canvas"); const sidebar = page.getByTestId("properties-sidebar"); await page.getByRole("button", { name: "셀렉트(Select)" }).click(); await sidebar.getByRole("textbox", { name: "필드 라벨" }).fill("에디터-프리뷰 셀렉트"); await sidebar.getByLabel("필드 너비").selectOption("fixed"); await sidebar.getByLabel("필드 고정 너비 커스텀 (px)").fill("360"); await sidebar.getByLabel("셀렉트 텍스트 색상 HEX").fill("#ff00ff"); await sidebar.getByLabel("셀렉트 채움 색상 HEX").fill("#0033ff"); await sidebar.getByLabel("셀렉트 테두리 색상 HEX").fill("#00ffaa"); const editorSelect = canvas.getByRole("combobox", { name: "에디터-프리뷰 셀렉트" }); const editorStyles = await editorSelect.evaluate((el) => { const s = window.getComputedStyle(el as HTMLSelectElement); return { height: s.height, paddingTop: s.paddingTop, paddingBottom: s.paddingBottom, fontSize: s.fontSize, color: s.color, backgroundColor: s.backgroundColor, borderColor: s.borderColor, borderRadius: s.borderRadius, }; }); const editorSelectAttrs = await editorSelect.evaluate((el) => { const select = el as HTMLSelectElement; return { name: select.name, value: select.value, }; }); await Promise.all([ page.waitForURL(/\/preview/), page.getByRole("link", { name: "프리뷰 열기" }).click(), ]); await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible(); const previewSelect = page.getByRole("combobox", { name: "에디터-프리뷰 셀렉트" }); const previewStyles = await previewSelect.evaluate((el) => { const s = window.getComputedStyle(el as HTMLSelectElement); return { height: s.height, paddingTop: s.paddingTop, paddingBottom: s.paddingBottom, fontSize: s.fontSize, color: s.color, backgroundColor: s.backgroundColor, borderColor: s.borderColor, borderRadius: s.borderRadius, }; }); const editorHeight = parsePx(editorStyles.height); const previewHeight = parsePx(previewStyles.height); expect(Math.abs(previewHeight - editorHeight)).toBeLessThanOrEqual(2); const editorPaddingTop = parsePx(editorStyles.paddingTop); const previewPaddingTop = parsePx(previewStyles.paddingTop); expect(Math.abs(previewPaddingTop - editorPaddingTop)).toBeLessThanOrEqual(3); const editorPaddingBottom = parsePx(editorStyles.paddingBottom); const previewPaddingBottom = parsePx(previewStyles.paddingBottom); expect(Math.abs(previewPaddingBottom - editorPaddingBottom)).toBeLessThanOrEqual(3); expect(previewStyles.fontSize).toBe(editorStyles.fontSize); expect(previewStyles.color).toBe(editorStyles.color); expect(previewStyles.backgroundColor).toBe(editorStyles.backgroundColor); expect(previewStyles.borderColor).toBe(editorStyles.borderColor); expect(previewStyles.borderRadius).toBe(editorStyles.borderRadius); const previewSelectAttrs = await previewSelect.evaluate((el) => { const select = el as HTMLSelectElement; return { name: select.name, value: select.value, }; }); expect(previewSelectAttrs.name).toBe(editorSelectAttrs.name); expect(previewSelectAttrs.value).toBe(editorSelectAttrs.value); }); test("formCheckbox/formRadio: 에디터 캔버스와 프리뷰에서 그룹 너비/라벨 간격/옵션 간격이 동일해야 한다", async ({ page }) => { await page.goto("/editor"); const canvas = page.getByTestId("editor-canvas"); const sidebar = page.getByTestId("properties-sidebar"); await page.getByRole("button", { name: "다중 선택(Checkbox)" }).click(); await sidebar.getByLabel("그룹 타이틀", { exact: true }).fill("에디터-프리뷰 체크 그룹"); await sidebar.getByLabel("그룹 타이틀 표시 방식").selectOption("visible"); await sidebar.getByLabel(/^레이아웃/).selectOption("inline"); await sidebar.getByLabel("라벨/필드 간격 (px) 커스텀 (px)").fill("32"); await sidebar.getByLabel("필드 너비").selectOption("fixed"); await sidebar.getByLabel("필드 고정 너비 커스텀 (px)").fill("360"); await sidebar.getByLabel("체크박스 옵션 간격 (px) 커스텀 (px)").fill("20"); const editorCheckboxLabel = canvas.getByText("에디터-프리뷰 체크 그룹"); const editorCheckboxData = await editorCheckboxLabel.evaluate((el) => { const groupEl = (el as HTMLElement).parentElement as HTMLElement; const groupStyle = window.getComputedStyle(groupEl); const optionsEl = groupEl.querySelector(".pb-form-options") as HTMLElement | null; const optionsStyle = optionsEl ? window.getComputedStyle(optionsEl) : null; const firstOptionInput = optionsEl?.querySelector("input[type='checkbox']") as | HTMLInputElement | null; const firstOption = firstOptionInput ? { type: firstOptionInput.type, name: firstOptionInput.name, value: firstOptionInput.value } : { type: "", name: "", value: "" }; return { groupWidth: groupStyle.width, groupColumnGap: groupStyle.columnGap, optionsRowGap: optionsStyle ? optionsStyle.rowGap : "", firstOption, }; }); await page.getByRole("button", { name: "단일 선택(Radio)" }).click(); await sidebar.getByLabel("그룹 타이틀", { exact: true }).fill("에디터-프리뷰 라디오 그룹"); await sidebar.getByLabel("그룹 타이틀 표시 방식").selectOption("visible"); await sidebar.getByLabel("그룹 타이틀 레이아웃").selectOption("inline"); await sidebar.getByLabel("라벨/필드 간격 (px) 커스텀 (px)").fill("32"); await sidebar.getByLabel("필드 너비").selectOption("fixed"); await sidebar.getByLabel("필드 고정 너비 커스텀 (px)").fill("360"); await sidebar.getByLabel("라디오 옵션 간격 (px) 커스텀 (px)").fill("20"); const editorRadioLabel = canvas.getByText("에디터-프리뷰 라디오 그룹"); const editorRadioData = await editorRadioLabel.evaluate((el) => { const groupEl = (el as HTMLElement).parentElement as HTMLElement; const groupStyle = window.getComputedStyle(groupEl); const optionsEl = groupEl.querySelector(".pb-form-options") as HTMLElement | null; const optionsStyle = optionsEl ? window.getComputedStyle(optionsEl) : null; const firstOptionInput = optionsEl?.querySelector("input[type='radio']") as | HTMLInputElement | null; const firstOption = firstOptionInput ? { type: firstOptionInput.type, name: firstOptionInput.name, value: firstOptionInput.value } : { type: "", name: "", value: "" }; return { groupWidth: groupStyle.width, groupColumnGap: groupStyle.columnGap, optionsRowGap: optionsStyle ? optionsStyle.rowGap : "", firstOption, }; }); await Promise.all([ page.waitForURL(/\/preview/), page.getByRole("link", { name: "프리뷰 열기" }).click(), ]); await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible(); const previewCheckboxGroup = page.getByTestId("preview-form-checkbox-group").first(); const previewCheckboxGroupStyles = await previewCheckboxGroup.evaluate((el) => { const s = window.getComputedStyle(el as HTMLElement); return { width: s.width, columnGap: s.columnGap, }; }); const previewCheckboxFirstOption = await previewCheckboxGroup.evaluate((el) => { const input = (el as HTMLElement).querySelector("input[type='checkbox']") as | HTMLInputElement | null; return input ? { type: input.type, name: input.name, value: input.value } : { type: "", name: "", value: "" }; }); const previewCheckboxOptions = previewCheckboxGroup .getByTestId("preview-form-checkbox-options") .first(); const previewCheckboxOptionsStyles = await previewCheckboxOptions.evaluate((el) => { const s = window.getComputedStyle(el as HTMLElement); return { rowGap: s.rowGap, }; }); const previewRadioGroup = page.getByTestId("preview-form-radio-group").first(); const previewRadioGroupStyles = await previewRadioGroup.evaluate((el) => { const s = window.getComputedStyle(el as HTMLElement); return { width: s.width, columnGap: s.columnGap, }; }); const previewRadioFirstOption = await previewRadioGroup.evaluate((el) => { const input = (el as HTMLElement).querySelector("input[type='radio']") as | HTMLInputElement | null; return input ? { type: input.type, name: input.name, value: input.value } : { type: "", name: "", value: "" }; }); const previewRadioOptions = previewRadioGroup .getByTestId("preview-form-radio-options") .first(); const previewRadioOptionsStyles = await previewRadioOptions.evaluate((el) => { const s = window.getComputedStyle(el as HTMLElement); return { rowGap: s.rowGap, }; }); const editorCheckboxWidth = parsePx(editorCheckboxData.groupWidth); const previewCheckboxWidth = parsePx(previewCheckboxGroupStyles.width); expect(Math.abs(previewCheckboxWidth - editorCheckboxWidth)).toBeLessThanOrEqual(96); const editorCheckboxLabelGap = parsePx(editorCheckboxData.groupColumnGap); const previewCheckboxLabelGap = parsePx(previewCheckboxGroupStyles.columnGap); expect(Math.abs(previewCheckboxLabelGap - editorCheckboxLabelGap)).toBeLessThanOrEqual(8); const editorCheckboxOptionGap = parsePx(editorCheckboxData.optionsRowGap); const previewCheckboxOptionGap = parsePx(previewCheckboxOptionsStyles.rowGap); expect(Math.abs(previewCheckboxOptionGap - editorCheckboxOptionGap)).toBeLessThanOrEqual(8); const editorRadioWidth = parsePx(editorRadioData.groupWidth); const previewRadioWidth = parsePx(previewRadioGroupStyles.width); expect(Math.abs(previewRadioWidth - editorRadioWidth)).toBeLessThanOrEqual(96); const editorRadioLabelGap = parsePx(editorRadioData.groupColumnGap); const previewRadioLabelGap = parsePx(previewRadioGroupStyles.columnGap); expect(Math.abs(previewRadioLabelGap - editorRadioLabelGap)).toBeLessThanOrEqual(2); const editorRadioOptionGap = parsePx(editorRadioData.optionsRowGap); const previewRadioOptionGap = parsePx(previewRadioOptionsStyles.rowGap); expect(Math.abs(previewRadioOptionGap - editorRadioOptionGap)).toBeLessThanOrEqual(8); expect(editorCheckboxData.firstOption.type).toBe("checkbox"); expect(previewCheckboxFirstOption.type).toBe(editorCheckboxData.firstOption.type); expect(previewCheckboxFirstOption.name).toBe(editorCheckboxData.firstOption.name); expect(previewCheckboxFirstOption.value).toBe(editorCheckboxData.firstOption.value); expect(editorRadioData.firstOption.type).toBe("radio"); expect(previewRadioFirstOption.type).toBe(editorRadioData.firstOption.type); expect(previewRadioFirstOption.name).toBe(editorRadioData.firstOption.name); expect(previewRadioFirstOption.value).toBe(editorRadioData.firstOption.value); });