텍스트 스타일 및 프리뷰 렌더링 정리
CI / test (push) Successful in 11m5s
CI / pr_and_merge (push) Successful in 1m40s
CI / test (pull_request) Failing after 35m42s
CI / pr_and_merge (pull_request) Has been skipped

This commit is contained in:
2025-11-20 23:36:47 +09:00
parent 9e40ee405c
commit 2f59e3781e
19 changed files with 3831 additions and 556 deletions
+334 -15
View File
@@ -115,7 +115,7 @@ test("여러 텍스트 블록 중 선택을 전환하면 속성 패널 내용과
await expect(sidebarEditor).toHaveValue("두 번째 블록");
});
test("텍스트 블록의 정렬과 글자 크기를 속성 패널에서 변경하면 캔버스 스타일 바뀌어야 한다", async ({ page }) => {
test("텍스트 블록의 정렬과 글자 크기를 속성 패널에서 변경하면 캔버스 스타일 유틸리티(pf-text-*)가 바뀌어야 한다", async ({ page }) => {
await page.goto("/editor");
// 텍스트 블록을 하나 추가하고 선택한다.
@@ -132,12 +132,12 @@ test("텍스트 블록의 정렬과 글자 크기를 속성 패널에서 변경
const sizeSelect = page.getByRole("combobox", { name: "글자 크기" });
await sizeSelect.selectOption("lg");
// 캔버스 블록에 text-center와 text-lg 클래스가 적용되어야 한다.
await expect(block).toHaveClass(/text-center/);
await expect(block).toHaveClass(/text-lg/);
// 캔버스 블록에 pb-text-center pb-text-lg 클래스가 적용되어야 한다.
await expect(block).toHaveClass(/pb-text-center/);
await expect(block).toHaveClass(/pb-text-lg/);
});
test("텍스트 블록 인라인 툴바에서 정렬과 글자 크기를 변경할 수 있어야 한다", async ({ page }) => {
test("텍스트 블록 인라인 툴바에서 정렬과 글자 크기를 변경할 수 있어야 한다 (pb-text-*)", async ({ page }) => {
test.skip(process.env.CI === "true");
await page.goto("/editor");
@@ -160,9 +160,9 @@ test("텍스트 블록 인라인 툴바에서 정렬과 글자 크기를 변경
// 편집을 종료하기 위해 블록 밖을 클릭한다.
await canvas.click({ position: { x: 5, y: 5 } });
// 캔버스 블록에 text-center 와 text-lg 클래스가 적용되어 있어야 한다.
await expect(block).toHaveClass(/text-center/);
await expect(block).toHaveClass(/text-lg/);
// 캔버스 블록에 pb-text-center 와 pb-text-lg 클래스가 적용되어 있어야 한다.
await expect(block).toHaveClass(/pb-text-center/);
await expect(block).toHaveClass(/pb-text-lg/);
});
test("에디터 상태를 JSON으로 내보내고 다시 불러오면 동일한 블록 상태가 복원되어야 한다", async ({ page }) => {
@@ -217,12 +217,12 @@ test("에디터 상태를 JSON으로 내보내고 다시 불러오면 동일한
const restoredSecond = restoredBlocks.nth(1);
await expect(restoredFirst).toContainText("첫 번째 JSON 블록");
await expect(restoredFirst).toHaveClass(/text-left/);
await expect(restoredFirst).toHaveClass(/text-sm/);
await expect(restoredFirst).toHaveClass(/pb-text-left/);
await expect(restoredFirst).toHaveClass(/pb-text-sm/);
await expect(restoredSecond).toContainText("두 번째 JSON 블록");
await expect(restoredSecond).toHaveClass(/text-right/);
await expect(restoredSecond).toHaveClass(/text-lg/);
await expect(restoredSecond).toHaveClass(/pb-text-right/);
await expect(restoredSecond).toHaveClass(/pb-text-lg/);
});
test("각 캔버스 블록에 드래그 핸들 UI가 표시되고 선택 상태가 aria-selected로 노출되어야 한다", async ({ page }) => {
@@ -596,12 +596,12 @@ test("구분선 블록을 추가하면 캔버스에 구분선이 렌더되고
const dividerBlock = blocks.nth(0);
// 블록 추가 시 자동으로 선택된다고 가정하고 기본 정렬이 가운데인지 확인한다.
await expect(dividerBlock).toHaveClass(/text-center/);
await expect(dividerBlock).toHaveClass(/pb-text-center/);
// 속성 패널에서 정렬을 오른쪽으로 변경하면 클래스가 바뀌어야 한다.
const alignSelect = page.getByRole("combobox", { name: "구분선 정렬" });
await alignSelect.selectOption("right");
await expect(dividerBlock).toHaveClass(/text-right/);
await expect(dividerBlock).toHaveClass(/pb-text-right/);
// 두께를 보통으로 변경해도 에러 없이 동작해야 한다.
const thicknessSelect = page.getByRole("combobox", { name: "구분선 두께" });
@@ -633,5 +633,324 @@ test("리스트 블록을 추가하고 첫 번째 아이템과 번호 매기기/
const alignSelect = page.getByRole("combobox", { name: "리스트 정렬" });
await alignSelect.selectOption("center");
await expect(listBlock).toHaveClass(/text-center/);
await expect(listBlock).toHaveClass(/pb-text-center/);
});
test("폼 요소 사이드바에서 폼 입력/셀렉트/라디오/체크박스 블록을 추가할 수 있어야 한다", async ({ page }) => {
await page.goto("/editor");
const canvas = page.getByTestId("editor-canvas");
// 좌측 사이드바에 "폼 요소" 섹션 제목이 표시되어야 한다.
await expect(page.getByRole("heading", { name: "폼 요소" })).toBeVisible();
// 폼 입력 블록 추가 버튼을 클릭하면 formInput 블록이 캔버스에 생성되어야 한다.
await page.getByRole("button", { name: "폼 입력 추가" }).click();
let blocks = canvas.getByTestId("editor-block");
await expect(blocks).toHaveCount(1);
// 폼 셀렉트 블록 추가 버튼을 클릭하면 formSelect 블록이 추가되어야 한다.
await page.getByRole("button", { name: "폼 셀렉트 추가" }).click();
blocks = canvas.getByTestId("editor-block");
await expect(blocks).toHaveCount(2);
// 폼 라디오 그룹 블록 추가 버튼을 클릭하면 formRadio 블록이 추가되어야 한다.
await page.getByRole("button", { name: "폼 라디오 그룹 추가" }).click();
blocks = canvas.getByTestId("editor-block");
await expect(blocks).toHaveCount(3);
// 폼 체크박스 블록 추가 버튼을 클릭하면 formCheckbox 블록이 추가되어야 한다.
await page.getByRole("button", { name: "폼 체크박스 추가" }).click();
blocks = canvas.getByTestId("editor-block");
await expect(blocks).toHaveCount(4);
});
test("폼 블록을 선택하면 우측 속성 패널에서 폼 필드/버튼 매핑 UI가 보여야 한다", async ({ page }) => {
await page.goto("/editor");
const canvas = page.getByTestId("editor-canvas");
// 폼 요소와 버튼 블록을 몇 개 추가한다.
await page.getByRole("button", { name: "폼 입력 추가" }).click();
await page.getByRole("button", { name: "폼 셀렉트 추가" }).click();
await page.getByRole("button", { name: "버튼 블록 추가" }).click();
// 폼 블록을 추가한다.
await page.getByRole("button", { name: "폼 블록 추가" }).click();
const blocks = canvas.getByTestId("editor-block");
const formBlock = blocks.nth((await blocks.count()) - 1);
// 폼 블록을 선택한다.
await formBlock.click({ force: true });
// 우측 속성 패널에 폼 컨트롤러 섹션 제목이 보여야 한다.
await expect(page.getByRole("heading", { name: "폼 컨트롤러" })).toBeVisible();
// 필드 매핑 영역: 폼 입력/셀렉트 블록을 대상으로 하는 체크박스가 있어야 한다.
const fieldMappingGroup = page.getByRole("group", { name: "폼 필드 매핑" });
await expect(fieldMappingGroup).toBeVisible();
// 이 그룹 안에 최소 2개의 체크박스(입력/셀렉트)가 있어야 한다.
const fieldCheckboxes = fieldMappingGroup.getByRole("checkbox");
await expect(fieldCheckboxes).toHaveCount(2);
// 첫 번째 필드 체크박스를 클릭하면 체크 상태가 되어야 한다.
await fieldCheckboxes.nth(0).check();
await expect(fieldCheckboxes.nth(0)).toBeChecked();
// 버튼 매핑 셀렉트 박스: 추가한 버튼 블록을 submit 버튼으로 선택할 수 있어야 한다.
const submitSelect = page.getByRole("combobox", { name: "Submit 버튼" });
await expect(submitSelect).toBeVisible();
// 옵션 중 하나를 선택해도 에러 없이 동작해야 한다.
const options = await submitSelect.locator("option").allTextContents();
if (options.length > 0) {
await submitSelect.selectOption({ label: options[0] });
}
});
test("폼 입력 블록의 라벨/전송 키/필수 여부를 우측 패널에서 수정할 수 있어야 한다", async ({ page }) => {
await page.goto("/editor");
const canvas = page.getByTestId("editor-canvas");
// 폼 입력 블록을 하나 추가한다.
await page.getByRole("button", { name: "폼 입력 추가" }).click();
const blocks = canvas.getByTestId("editor-block");
const inputBlock = blocks.nth(0);
// 폼 입력 블록을 선택한다.
await inputBlock.click({ force: true });
// 우측 속성 패널에 폼 입력 필드 설정 섹션이 보여야 한다.
const labelInput = page.getByRole("textbox", { name: "필드 라벨" });
const nameInput = page.getByRole("textbox", { name: "전송 키" });
const requiredCheckbox = page.getByRole("checkbox", { name: "필수 필드" });
await expect(labelInput).toBeVisible();
await expect(nameInput).toBeVisible();
await expect(requiredCheckbox).toBeVisible();
// 라벨과 전송 키를 수정하고, 필수 여부를 토글할 수 있어야 한다.
await labelInput.fill("이메일 주소");
await nameInput.fill("email_address");
await requiredCheckbox.check();
});
test("폼 셀렉트/라디오/체크박스 블록도 우측 패널에서 기본 필드 속성을 편집할 수 있어야 한다", async ({ page }) => {
await page.goto("/editor");
const canvas = page.getByTestId("editor-canvas");
// 폼 셀렉트 블록
await page.getByRole("button", { name: "폼 셀렉트 추가" }).click();
let blocks = canvas.getByTestId("editor-block");
const selectBlock = blocks.nth((await blocks.count()) - 1);
await selectBlock.click({ force: true });
let labelInput = page.getByRole("textbox", { name: "필드 라벨" });
let nameInput = page.getByRole("textbox", { name: "전송 키" });
let requiredCheckbox = page.getByRole("checkbox", { name: "필수 필드" });
await expect(labelInput).toBeVisible();
await expect(nameInput).toBeVisible();
await expect(requiredCheckbox).toBeVisible();
await labelInput.fill("셀렉트 라벨");
await nameInput.fill("select_field");
await requiredCheckbox.check();
// 폼 라디오 블록
await page.getByRole("button", { name: "폼 라디오 그룹 추가" }).click();
blocks = canvas.getByTestId("editor-block");
const radioBlock = blocks.nth((await blocks.count()) - 1);
await radioBlock.click({ force: true });
labelInput = page.getByRole("textbox", { name: "그룹 타이틀" });
nameInput = page.getByRole("textbox", { name: "전송 키" });
requiredCheckbox = page.getByRole("checkbox", { name: "필수 필드" });
await labelInput.fill("라디오 라벨");
await nameInput.fill("radio_field");
await requiredCheckbox.check();
// 라디오 그룹 타이틀 텍스트 필드는 그대로 편집 가능해야 한다.
// 폼 체크박스 블록
await page.getByRole("button", { name: "폼 체크박스 추가" }).click();
blocks = canvas.getByTestId("editor-block");
const checkboxBlock = blocks.nth((await blocks.count()) - 1);
await checkboxBlock.click({ force: true });
labelInput = page.getByRole("textbox", { name: "그룹 타이틀" });
nameInput = page.getByRole("textbox", { name: "전송 키" });
requiredCheckbox = page.getByRole("checkbox", { name: "필수 필드" });
await labelInput.fill("체크박스 라벨");
await nameInput.fill("checkbox_field");
await requiredCheckbox.check();
// 체크박스 그룹 타이틀 텍스트 필드도 동일하게 동작해야 한다.
});
test("폼 입력 블록을 추가하면 에디터 캔버스에 라벨과 입력 필드 UI가 보여야 한다", async ({ page }) => {
await page.goto("/editor");
const canvas = page.getByTestId("editor-canvas");
// 폼 입력 블록을 하나 추가한다.
await page.getByRole("button", { name: "폼 입력 추가" }).click();
const blocks = canvas.getByTestId("editor-block");
const inputBlock = blocks.nth(0);
// 블록 안에 기본 라벨 텍스트가 보여야 한다.
await expect(inputBlock.getByText("입력 필드")).toBeVisible();
// 블록 안에 시각적인 입력 UI(텍스트 입력 또는 textarea)가 있어야 한다.
const textboxes = inputBlock.getByRole("textbox");
await expect(textboxes).toHaveCount(1);
});
test("폼 셀렉트 블록을 추가하면 에디터 캔버스에 라벨과 셀렉트 UI가 보여야 한다", async ({ page }) => {
await page.goto("/editor");
const canvas = page.getByTestId("editor-canvas");
// 폼 셀렉트 블록을 하나 추가한다.
await page.getByRole("button", { name: "폼 셀렉트 추가" }).click();
const blocks = canvas.getByTestId("editor-block");
const selectBlock = blocks.nth(0);
// 블록 안에 기본 라벨 텍스트가 보여야 한다.
await expect(selectBlock.getByText("선택 필드")).toBeVisible();
// 블록 안에 셀렉트 UI가 있어야 한다.
const combobox = selectBlock.getByRole("combobox");
await expect(combobox).toBeVisible();
});
test("폼 라디오 블록을 추가하면 에디터 캔버스에 그룹 라벨과 라디오 버튼들이 보여야 한다", async ({ page }) => {
await page.goto("/editor");
const canvas = page.getByTestId("editor-canvas");
// 폼 라디오 블록을 하나 추가한다.
await page.getByRole("button", { name: "폼 라디오 그룹 추가" }).click();
const blocks = canvas.getByTestId("editor-block");
const radioBlock = blocks.nth(0);
// 블록 안에 기본 그룹 라벨 텍스트가 보여야 한다.
await expect(radioBlock.getByText("라디오 그룹")).toBeVisible();
// 블록 안에 라디오 버튼이 하나 이상 있어야 한다.
const radios = radioBlock.getByRole("radio");
await expect(radios).toHaveCount(1);
});
test("폼 체크박스 블록을 추가하면 에디터 캔버스에 체크박스와 라벨이 보여야 한다", async ({ page }) => {
await page.goto("/editor");
const canvas = page.getByTestId("editor-canvas");
// 폼 체크박스 블록을 하나 추가한다.
await page.getByRole("button", { name: "폼 체크박스 추가" }).click();
const blocks = canvas.getByTestId("editor-block");
const checkboxBlock = blocks.nth(0);
// 블록 안에 기본 라벨 텍스트가 보여야 한다.
await expect(checkboxBlock.getByText("체크박스")).toBeVisible();
// 블록 안에 체크박스 UI가 있어야 한다.
const checkbox = checkboxBlock.getByRole("checkbox");
await expect(checkbox).toBeVisible();
});
test("폼 입력 필드의 타입과 Placeholder 를 우측 패널에서 변경하면 캔버스 UI에 반영되어야 한다", async ({ page }) => {
await page.goto("/editor");
const canvas = page.getByTestId("editor-canvas");
await page.getByRole("button", { name: "폼 입력 추가" }).click();
const blocks = canvas.getByTestId("editor-block");
const inputBlock = blocks.nth(0);
await inputBlock.click({ force: true });
// 우측 패널에서 필드 타입과 placeholder 를 수정한다.
const typeSelect = page.getByRole("combobox", { name: "필드 타입" });
await typeSelect.selectOption("email");
const placeholderInput = page.getByRole("textbox", { name: "Placeholder" });
await placeholderInput.fill("이메일을 입력하세요");
// 캔버스 안 인풋이 type="email" 이고 placeholder 가 반영되어야 한다.
const emailInput = inputBlock.locator('input[type="email"]');
await expect(emailInput).toHaveAttribute("placeholder", "이메일을 입력하세요");
});
test("폼 셀렉트 블록의 옵션 목록을 우측 패널에서 수정하면 캔버스 셀렉트 옵션에 반영되어야 한다", async ({ page }) => {
await page.goto("/editor");
const canvas = page.getByTestId("editor-canvas");
await page.getByRole("button", { name: "폼 셀렉트 추가" }).click();
const blocks = canvas.getByTestId("editor-block");
const selectBlock = blocks.nth(0);
await selectBlock.click({ force: true });
// 우측 패널에서 옵션 목록을 수정한다.
const optionsTextarea = page.getByRole("textbox", { name: "옵션 목록" });
await optionsTextarea.fill("옵션 A\n옵션 B\n옵션 C");
// 캔버스 셀렉트 박스 옵션에 동일한 텍스트가 반영되어야 한다.
const selectEl = selectBlock.getByRole("combobox");
const optionLocators = selectEl.locator("option");
await expect(optionLocators).toHaveCount(3);
await expect(optionLocators.nth(0)).toHaveText("옵션 A");
await expect(optionLocators.nth(1)).toHaveText("옵션 B");
await expect(optionLocators.nth(2)).toHaveText("옵션 C");
});
test("폼 입력 블록의 스타일(텍스트/배경/모서리)을 우측 패널에서 변경하면 캔버스 필드 UI에 반영되어야 한다", async ({ page }) => {
await page.goto("/editor");
const canvas = page.getByTestId("editor-canvas");
// 폼 입력 블록을 하나 추가한다.
await page.getByRole("button", { name: "폼 입력 추가" }).click();
const blocks = canvas.getByTestId("editor-block");
const inputBlock = blocks.nth(0);
await inputBlock.click({ force: true });
// 우측 패널에서 필드 텍스트/배경 색상과 모서리 둥글기를 변경한다.
// ColorPickerField와 NumericPropertyControl은 기존 버튼 스타일과 비슷한 패턴의 라벨을 사용한다.
// 텍스트 색상 피커에서 HEX 입력을 직접 변경한다고 가정한다.
const textColorHexInput = page.getByRole("textbox", { name: "필드 텍스트 색상 HEX" });
await textColorHexInput.fill("#ff0000");
const fillColorHexInput = page.getByRole("textbox", { name: "필드 채움 색상 HEX" });
await fillColorHexInput.fill("#0000ff");
// 모서리 둥글기 슬라이더/입력 값을 조정한다.
const radiusNumericInput = page.getByRole("spinbutton", { name: "필드 모서리 둥글기" });
await radiusNumericInput.fill("4");
// 캔버스 안 실제 입력 필드 wrapper에 스타일이 반영되어야 한다.
const fieldWrapper = inputBlock.getByTestId("form-input-field");
await expect(fieldWrapper).toHaveCSS("color", "rgb(255, 0, 0)");
await expect(fieldWrapper).toHaveCSS("background-color", "rgb(0, 0, 255)");
// border-radius 는 브라우저에서 px 단위로 계산되므로 4px 이상인지 정도만 확인한다.
const borderRadius = await fieldWrapper.evaluate((el) => getComputedStyle(el).borderRadius);
expect(Number.parseFloat(borderRadius)).toBeGreaterThanOrEqual(4);
});