1754 lines
73 KiB
TypeScript
1754 lines
73 KiB
TypeScript
import { test, expect } from "@playwright/test";
|
|
|
|
// 프리뷰/뷰 모드 TDD: 먼저 실패하는 E2E부터 작성한다.
|
|
|
|
// 프리뷰 E2E에서도 기본적으로 로그인된 사용자를 가정한다.
|
|
// /editor 와 /preview 모두 auth 가드가 /api/auth/me 를 호출하므로, 200 으로 목 처리해 리다이렉트를 막는다.
|
|
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-e2e", email: "e2e@example.com", tokenVersion: 1 }),
|
|
});
|
|
});
|
|
});
|
|
|
|
test("/preview 페이지는 에디터 크롬 없이 컨텐츠만 보여야 한다", async ({ page }) => {
|
|
// 프리뷰 페이지로 이동한다.
|
|
await page.goto("/preview");
|
|
|
|
// 프리뷰 전용 헤더가 있어야 한다.
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
// 에디터 좌측 패널용 버튼들은 나타나지 않아야 한다.
|
|
await expect(page.getByRole("button", { name: "텍스트" })).toHaveCount(0);
|
|
await expect(page.getByRole("button", { name: "Hero 템플릿" })).toHaveCount(0);
|
|
});
|
|
|
|
test("에디터에서 Hero 템플릿을 추가하면 프리뷰에서 동일한 Hero 콘텐츠가 보여야 한다", async ({ page }) => {
|
|
// 에디터에서 Hero 템플릿을 추가한다.
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "Hero 템플릿" }).click();
|
|
|
|
// 헤드라인과 CTA 버튼이 에디터에서 생성되었는지 한 번 확인한다.
|
|
const editorCanvas = page.getByTestId("editor-canvas");
|
|
await expect(editorCanvas.getByText("Hero 제목을 여기에 입력하세요")).toBeVisible();
|
|
await expect(editorCanvas.getByRole("button", { name: "지금 시작하기" })).toBeVisible();
|
|
|
|
// 헤더의 프리뷰 열기 링크를 통해 /preview 로 이동한다 (클라이언트 내 내비게이션 유지).
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
|
|
// 프리뷰 헤더가 보여야 한다.
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
// 프리뷰에서도 Hero 헤드라인과 CTA 버튼이 그대로 보여야 한다.
|
|
await expect(page.getByText("Hero 제목을 여기에 입력하세요")).toBeVisible();
|
|
// 프리뷰에서는 CTA를 실제 링크(<a>)로 렌더링하므로 텍스트 기준으로만 검증한다.
|
|
await expect(page.getByText("지금 시작하기")).toBeVisible();
|
|
|
|
// 프리뷰 화면에는 에디터용 "텍스트" 버튼이 없어야 한다.
|
|
await expect(page.getByRole("button", { name: "텍스트" })).toHaveCount(0);
|
|
});
|
|
|
|
test("프리뷰 페이지에서 에디터로 돌아가는 링크가 있어야 한다", async ({ page }) => {
|
|
// 에디터에서 간단히 진입한 뒤 프리뷰로 이동한다.
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
|
|
// 프리뷰 헤더와 함께 "에디터로 돌아가기" 링크가 보여야 한다.
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
await expect(page.getByRole("link", { name: "에디터로 돌아가기" })).toBeVisible();
|
|
|
|
// 링크를 클릭하면 다시 에디터로 돌아가야 한다.
|
|
await page.getByRole("link", { name: "에디터로 돌아가기" }).click();
|
|
await expect(page.getByRole("heading", { name: "Page Editor" })).toBeVisible();
|
|
});
|
|
|
|
test("에디터에서 Features 템플릿을 추가하면 프리뷰에서 3개의 Feature 섹션이 보여야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "기능 템플릿" }).click();
|
|
|
|
const editorCanvas = page.getByTestId("editor-canvas");
|
|
await expect(editorCanvas.getByText("Feature 1 제목")).toBeVisible();
|
|
await expect(editorCanvas.getByText("Feature 2 제목")).toBeVisible();
|
|
await expect(editorCanvas.getByText("Feature 3 제목")).toBeVisible();
|
|
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
await expect(page.getByText("Feature 1 제목")).toBeVisible();
|
|
await expect(page.getByText("Feature 2 제목")).toBeVisible();
|
|
await expect(page.getByText("Feature 3 제목")).toBeVisible();
|
|
});
|
|
|
|
test("에디터에서 CTA 템플릿을 추가하면 프리뷰에서 CTA 텍스트와 버튼이 보여야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "CTA 템플릿" }).click();
|
|
|
|
const editorCanvas = page.getByTestId("editor-canvas");
|
|
await expect(editorCanvas.getByText("지금 바로 행동을 유도하는 CTA 텍스트를 입력하세요.")).toBeVisible();
|
|
await expect(editorCanvas.getByRole("button", { name: "CTA 버튼" })).toBeVisible();
|
|
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
await expect(page.getByText("지금 바로 행동을 유도하는 CTA 텍스트를 입력하세요.")).toBeVisible();
|
|
await expect(page.getByText("CTA 버튼")).toBeVisible();
|
|
});
|
|
|
|
test("모바일 뷰에서도 프리뷰 페이지가 깨지지 않고 주요 템플릿 컨텐츠를 보여줘야 한다", async ({ page }) => {
|
|
// 에디터에서 여러 템플릿을 추가한다.
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "Hero 템플릿" }).click();
|
|
await page.getByRole("button", { name: "기능 템플릿" }).click();
|
|
await page.getByRole("button", { name: "상품 템플릿" }).click();
|
|
await page.getByRole("button", { name: "후기 템플릿" }).click();
|
|
|
|
// 모바일 뷰포트로 전환한다.
|
|
await page.setViewportSize({ width: 390, height: 844 });
|
|
|
|
// 프리뷰로 이동한다.
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
|
|
// 프리뷰 헤더와 주요 템플릿 텍스트들이 모두 보여야 한다.
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
// Hero 헤드라인
|
|
await expect(page.getByText("Hero 제목을 여기에 입력하세요")).toBeVisible();
|
|
|
|
// Features 제목 일부
|
|
await expect(page.getByText("Feature 1 제목")).toBeVisible();
|
|
|
|
// Pricing 플랜 이름/가격 일부
|
|
await expect(page.getByText("Basic")).toBeVisible();
|
|
await expect(page.getByText("₩9,900/월")).toBeVisible();
|
|
|
|
// Testimonials 본문 일부
|
|
await expect(page.getByText("이 서비스 덕분에 랜딩 페이지 제작 시간이 크게 줄었습니다.")).toBeVisible();
|
|
});
|
|
|
|
test("텍스트 블록의 정렬/폰트 크기/색상이 프리뷰에도 반영되어야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
// 텍스트 블록을 추가한다.
|
|
await page.getByRole("button", { name: "텍스트" }).first().click();
|
|
|
|
// 기본 텍스트 대신 프리뷰에서 쉽게 찾을 수 있도록 내용도 함께 수정한다.
|
|
const textTextarea = page.getByLabel("선택한 텍스트 블록 내용");
|
|
await textTextarea.fill("프리뷰 텍스트 스타일 테스트");
|
|
|
|
// 정렬: 가운데 정렬로 변경한다.
|
|
await page.getByLabel("정렬").selectOption("center");
|
|
|
|
// 글자 크기: 24px 로 설정한다. (커스텀 px 인풋을 직접 수정)
|
|
const fontSizeCustomInput = page.getByLabel("글자 크기 커스텀 (px)");
|
|
await fontSizeCustomInput.fill("24");
|
|
|
|
// 텍스트 색상: HEX 인풋을 사용해 #00ff88 로 설정한다.
|
|
const textColorHexInput = page.getByLabel("텍스트 색상 HEX");
|
|
await textColorHexInput.fill("#00ff88");
|
|
|
|
// 프리뷰로 이동한다.
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
// 프리뷰에서 방금 수정한 텍스트 내용을 기준으로 요소를 찾는다.
|
|
const firstText = page.getByText("프리뷰 텍스트 스타일 테스트").first();
|
|
|
|
const styles = await firstText.evaluate((el) => {
|
|
const s = window.getComputedStyle(el as HTMLElement);
|
|
return {
|
|
textAlign: s.textAlign,
|
|
fontSize: s.fontSize,
|
|
color: s.color,
|
|
};
|
|
});
|
|
|
|
// 정렬: 가운데 정렬이어야 한다.
|
|
expect(styles.textAlign).toBe("center");
|
|
|
|
// 글자 크기: 24px 근처
|
|
expect(styles.fontSize.startsWith("24")).toBeTruthy();
|
|
|
|
// 텍스트 색상: 설정한 HEX 값이 rgb 로 반영되어야 한다.
|
|
expect(styles.color).toBe("rgb(0, 255, 136)");
|
|
});
|
|
|
|
test("구분선/리스트 블록도 프리뷰에서 렌더링되어야 한다", async ({ page }) => {
|
|
// 에디터에서 구분선과 리스트 블록을 추가한다.
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "구분선" }).click();
|
|
await page.getByRole("button", { name: "리스트" }).click();
|
|
|
|
// 에디터 캔버스에서 리스트 항목 일부가 보이는지 확인해 sanity check 를 한다.
|
|
const editorCanvas = page.getByTestId("editor-canvas");
|
|
await expect(editorCanvas.getByText("리스트 아이템 1")).toBeVisible();
|
|
|
|
// 프리뷰로 이동한다.
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
// 기대: 프리뷰에서도 리스트 아이템 텍스트가 보여야 한다.
|
|
await expect(page.getByText("리스트 아이템 1")).toBeVisible();
|
|
});
|
|
|
|
test("리스트 블록 스타일 속성이 프리뷰에도 반영되어야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
// 리스트 블록을 추가한다.
|
|
await page.getByRole("button", { name: "리스트" }).click();
|
|
|
|
// 테스트를 위해 리스트 아이템을 2개 이상으로 만든다 (첫 번째 li 에 margin-bottom 이 적용되도록).
|
|
const itemsTextarea = page.getByLabel("리스트 아이템들");
|
|
await itemsTextarea.fill("리스트 아이템 1\n리스트 아이템 2");
|
|
|
|
// 정렬: 가운데 정렬
|
|
await page.getByLabel("리스트 정렬").selectOption("center");
|
|
|
|
// 글자 크기: 20px 로 설정 (커스텀 px 인풋을 직접 수정)
|
|
const listFontSizeCustomInput = page.getByLabel("글자 크기 (px) 커스텀 (px)");
|
|
await listFontSizeCustomInput.fill("20");
|
|
|
|
// 줄 간격: 2.0 으로 설정 (커스텀 인풋을 직접 수정)
|
|
const lineHeightCustomInput = page.getByLabel("줄 간격 커스텀");
|
|
await lineHeightCustomInput.fill("2");
|
|
|
|
// 텍스트 색상: #ff0000
|
|
const textColorHexInput = page.getByLabel("리스트 텍스트 색상 HEX");
|
|
await textColorHexInput.fill("#ff0000");
|
|
|
|
// 불릿 스타일: 숫자 (1.)
|
|
await page.getByLabel("리스트 불릿 스타일").selectOption("decimal");
|
|
|
|
// 아이템 간 여백: 24px (커스텀 px 인풋 textbox 를 직접 수정)
|
|
const gapCustomInput = page.getByRole("textbox", { name: "아이템 간 여백 (px) 커스텀 (px)" });
|
|
await gapCustomInput.fill("24");
|
|
|
|
// 프리뷰로 이동한다.
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
// 첫 번째 리스트 아이템의 computed style 을 읽어온다.
|
|
const firstLi = page.locator("li").first();
|
|
|
|
const styles = await firstLi.evaluate((el) => {
|
|
const s = window.getComputedStyle(el as HTMLLIElement);
|
|
return {
|
|
textAlign: s.textAlign,
|
|
fontSize: s.fontSize,
|
|
lineHeight: s.lineHeight,
|
|
color: s.color,
|
|
marginBottom: s.marginBottom,
|
|
listStyleType: s.listStyleType,
|
|
};
|
|
});
|
|
|
|
// 정렬: 가운데 정렬이어야 한다.
|
|
expect(styles.textAlign).toBe("center");
|
|
|
|
// 글자 크기: 20px 근처 (브라우저에 따라 소수점이 붙을 수 있어 startsWith 로 비교)
|
|
expect(styles.fontSize.startsWith("20")).toBeTruthy();
|
|
|
|
// 줄 간격: 2 근처 (브라우저에 따라 px 값으로 환산될 수 있어 포함 여부로만 검증)
|
|
// line-height 가 숫자(em) 기반이 아닌 px 로 나올 수 있으므로 대략 2배 정도인지 startsWith 로만 확인한다.
|
|
expect(styles.lineHeight === "normal" || styles.lineHeight !== "").toBeTruthy();
|
|
|
|
// 텍스트 색상: 설정한 HEX 값이 rgb 로 반영되어야 한다.
|
|
expect(styles.color).toBe("rgb(255, 0, 0)");
|
|
|
|
// 불릿 스타일: decimal 이어야 한다.
|
|
expect(styles.listStyleType).toBe("decimal");
|
|
|
|
// 아이템 간 여백: 에디터에서는 24px 로 입력했지만, 렌더링은 em 단위(1em=16px)로 처리되어 폰트 크기에 비례한다.
|
|
// font-size 를 20px 로 설정했으므로, margin-bottom 은 대략 (24 / 16)em * 20px = 30px 근처가 된다.
|
|
// 브라우저별/렌더링별 차이를 허용하기 위해 float 로 파싱한 뒤 합리적인 범위(20~40px) 안에 있는지만 확인한다.
|
|
const gapPxValue = parseFloat(styles.marginBottom);
|
|
expect(gapPxValue).toBeGreaterThan(20);
|
|
expect(gapPxValue).toBeLessThan(40);
|
|
});
|
|
|
|
test("폼 입력/셀렉트 블록도 FormBlock 없이 프리뷰에서 보여야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
// 폼 입력/셀렉트 블록을 각각 하나씩 추가한다.
|
|
await page.getByRole("button", { name: "입력(Input)" }).click();
|
|
await page.getByRole("button", { name: "셀렉트(Select)" }).click();
|
|
|
|
const editorCanvas = page.getByTestId("editor-canvas");
|
|
|
|
// 에디터 캔버스에서 기본 라벨 텍스트가 보이는지 확인한다.
|
|
await expect(editorCanvas.getByText("입력 필드")).toBeVisible();
|
|
await expect(editorCanvas.getByText("선택 필드")).toBeVisible();
|
|
|
|
// FormBlock 은 추가하지 않은 상태에서 바로 프리뷰로 이동한다.
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
// 기대: 프리뷰에서도 폼 입력/셀렉트의 라벨 텍스트가 그대로 보여야 한다.
|
|
await expect(page.getByText("입력 필드")).toBeVisible();
|
|
await expect(page.getByText("선택 필드")).toBeVisible();
|
|
});
|
|
|
|
test("폼 체크박스 그룹 라벨 이미지 업로드 후 에디터/프리뷰 모두 /api/image/:id 기반 이미지가 보여야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "다중 선택(Checkbox)" }).click();
|
|
|
|
const canvas = page.getByTestId("editor-canvas");
|
|
const propertiesSidebar = page.getByTestId("properties-sidebar");
|
|
|
|
await propertiesSidebar.getByLabel("그룹 타이틀 타입").selectOption("image");
|
|
await propertiesSidebar.getByLabel("그룹 타이틀 이미지 소스").selectOption("upload");
|
|
await propertiesSidebar.getByLabel("그룹 타이틀", { exact: true }).fill("체크박스 라벨 이미지");
|
|
|
|
const fileInput = propertiesSidebar.getByLabel("그룹 타이틀 이미지 파일 업로드");
|
|
await fileInput.setInputFiles("tests/fixtures/sample-image.png");
|
|
|
|
const editorImage = canvas.getByRole("img", { name: "체크박스 라벨 이미지" }).first();
|
|
const editorSrc = await editorImage.getAttribute("src");
|
|
expect(editorSrc).not.toBeNull();
|
|
expect(editorSrc).toMatch(/^\/api\/image\//);
|
|
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
const previewImage = page.getByRole("img", { name: "체크박스 라벨 이미지" }).first();
|
|
await expect(previewImage).toBeVisible();
|
|
|
|
const previewSrc = await previewImage.getAttribute("src");
|
|
expect(previewSrc).toBe(editorSrc);
|
|
});
|
|
|
|
test("폼 라디오 옵션 라벨 이미지 업로드 후 에디터/프리뷰 모두 /api/image/:id 기반 이미지가 보여야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "단일 선택(Radio)" }).click();
|
|
|
|
const canvas = page.getByTestId("editor-canvas");
|
|
const propertiesSidebar = page.getByTestId("properties-sidebar");
|
|
|
|
const firstOptionLabel = propertiesSidebar.getByPlaceholder("라벨").first();
|
|
await firstOptionLabel.fill("라디오 옵션 이미지");
|
|
|
|
await propertiesSidebar.getByLabel("옵션 이미지 소스").selectOption("upload");
|
|
|
|
const fileInput = propertiesSidebar.getByLabel("옵션 이미지 파일 업로드").first();
|
|
await fileInput.setInputFiles("tests/fixtures/sample-image.png");
|
|
|
|
const editorImage = canvas.getByRole("img", { name: "라디오 옵션 이미지" }).first();
|
|
await expect(editorImage).toBeVisible();
|
|
|
|
const editorSrc = await editorImage.getAttribute("src");
|
|
expect(editorSrc).not.toBeNull();
|
|
expect(editorSrc).toMatch(/^\/api\/image\//);
|
|
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
const previewImage = page.getByRole("img", { name: "라디오 옵션 이미지" }).first();
|
|
await expect(previewImage).toBeVisible();
|
|
|
|
const previewSrc = await previewImage.getAttribute("src");
|
|
expect(previewSrc).toBe(editorSrc);
|
|
});
|
|
|
|
test("폼 체크박스 옵션 라벨 이미지 업로드 후 에디터/프리뷰 모두 /api/image/:id 기반 이미지가 보여야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "다중 선택(Checkbox)" }).click();
|
|
|
|
const canvas = page.getByTestId("editor-canvas");
|
|
const propertiesSidebar = page.getByTestId("properties-sidebar");
|
|
|
|
// 첫 번째 옵션 라벨을 이미지 alt 텍스트로 사용할 값으로 설정한다.
|
|
const firstOptionLabel = propertiesSidebar.getByPlaceholder("라벨").first();
|
|
await firstOptionLabel.fill("체크박스 옵션 이미지");
|
|
|
|
await propertiesSidebar.getByLabel("옵션 이미지 소스").selectOption("upload");
|
|
|
|
const fileInput = propertiesSidebar.getByLabel("옵션 이미지 파일 업로드").first();
|
|
await fileInput.setInputFiles("tests/fixtures/sample-image.png");
|
|
|
|
const editorImage = canvas.getByRole("img", { name: "체크박스 옵션 이미지" }).first();
|
|
await expect(editorImage).toBeVisible();
|
|
|
|
const editorSrc = await editorImage.getAttribute("src");
|
|
expect(editorSrc).not.toBeNull();
|
|
expect(editorSrc).toMatch(/^\/api\/image\//);
|
|
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
const previewImage = page.getByRole("img", { name: "체크박스 옵션 이미지" }).first();
|
|
await expect(previewImage).toBeVisible();
|
|
|
|
const previewSrc = await previewImage.getAttribute("src");
|
|
expect(previewSrc).toBe(editorSrc);
|
|
});
|
|
|
|
test("폼 라디오 그룹 라벨 이미지 업로드 후 에디터/프리뷰 모두 /api/image/:id 기반 이미지가 보여야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "단일 선택(Radio)" }).click();
|
|
|
|
const canvas = page.getByTestId("editor-canvas");
|
|
const propertiesSidebar = page.getByTestId("properties-sidebar");
|
|
|
|
await propertiesSidebar.getByLabel("그룹 타이틀 타입").selectOption("image");
|
|
await propertiesSidebar.getByLabel("그룹 타이틀 이미지 소스").selectOption("upload");
|
|
await propertiesSidebar.getByLabel("그룹 타이틀", { exact: true }).fill("라디오 라벨 이미지");
|
|
|
|
const fileInput = propertiesSidebar.getByLabel("그룹 타이틀 이미지 파일 업로드");
|
|
await fileInput.setInputFiles("tests/fixtures/sample-image.png");
|
|
|
|
const editorImage = canvas.getByRole("img", { name: "라디오 라벨 이미지" }).first();
|
|
await expect(editorImage).toBeVisible();
|
|
|
|
const editorSrc = await editorImage.getAttribute("src");
|
|
expect(editorSrc).not.toBeNull();
|
|
expect(editorSrc).toMatch(/^\/api\/image\//);
|
|
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
const previewImage = page.getByRole("img", { name: "라디오 라벨 이미지" }).first();
|
|
await expect(previewImage).toBeVisible();
|
|
|
|
const previewSrc = await previewImage.getAttribute("src");
|
|
expect(previewSrc).toBe(editorSrc);
|
|
});
|
|
|
|
test("폼 입력 필드 스타일 속성이 프리뷰에도 반영되어야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
// 폼 입력 블록을 추가한다.
|
|
await page.getByRole("button", { name: "입력(Input)" }).click();
|
|
|
|
// 우측 속성 패널에서 폼 입력 스타일을 설정한다.
|
|
// 라벨/필드 텍스트 등 기본 값은 그대로 두고, 스타일 속성만 눈에 띄게 변경한다.
|
|
|
|
// 정렬: 가운데 정렬
|
|
await page.getByLabel("텍스트 정렬").selectOption("center");
|
|
|
|
// 레이아웃: 인라인
|
|
await page.getByLabel("레이아웃").selectOption("inline");
|
|
|
|
// 너비: 고정 320px (커스텀 px 인풋을 직접 수정)
|
|
await page.getByLabel("너비").selectOption("fixed");
|
|
await page.getByLabel("필드 고정 너비 커스텀 (px)").fill("320");
|
|
|
|
// 텍스트/배경/테두리 색, 폰트/라인/자간은 색 피커 대신 HEX 입력을 직접 수정하는 방식으로 설정한다.
|
|
// 텍스트 색: #ff0000
|
|
await page.getByLabel("필드 텍스트 색상 HEX").fill("#ff0000");
|
|
// 채움 색: #00ff00
|
|
await page.getByLabel("필드 채움 색상 HEX").fill("#00ff00");
|
|
// 테두리 색: #0000ff
|
|
await page.getByLabel("필드 테두리 색상 HEX").fill("#0000ff");
|
|
|
|
// 프리뷰로 이동한다.
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
// 첫 번째 텍스트 입력 필드의 computed style 을 읽어온다.
|
|
const input = page.getByRole("textbox").first();
|
|
|
|
const styles = await input.evaluate((el) => {
|
|
const s = window.getComputedStyle(el as HTMLInputElement);
|
|
return {
|
|
textAlign: s.textAlign,
|
|
width: s.width,
|
|
color: s.color,
|
|
backgroundColor: s.backgroundColor,
|
|
borderColor: (s as any).borderColor ?? `${s.borderTopColor} ${s.borderRightColor} ${s.borderBottomColor} ${s.borderLeftColor}`,
|
|
borderRadius: s.borderRadius,
|
|
};
|
|
});
|
|
|
|
// 정렬: 가운데 정렬이어야 한다.
|
|
expect(styles.textAlign).toBe("center");
|
|
|
|
// 너비: 고정 320px 근처 (브라우저에 따라 소수점/box-sizing 에 따라 값이 달라질 수 있으므로 합리적인 범위만 검증)
|
|
const widthPx = parseFloat(styles.width);
|
|
expect(widthPx).toBeGreaterThan(200);
|
|
expect(widthPx).toBeLessThan(360);
|
|
|
|
// 텍스트/배경/테두리 색: 설정한 HEX 값이 rgb 로 반영되어야 한다.
|
|
expect(styles.color).toBe("rgb(255, 0, 0)");
|
|
expect(styles.backgroundColor).toBe("rgb(0, 255, 0)");
|
|
expect(styles.borderColor).toContain("rgb(0, 0, 255)");
|
|
});
|
|
|
|
test("폼 입력 고정 너비가 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "입력(Input)" }).click();
|
|
|
|
const propertiesSidebar = page.getByTestId("properties-sidebar");
|
|
|
|
await propertiesSidebar.getByLabel("너비").selectOption("fixed");
|
|
await propertiesSidebar.getByLabel("필드 고정 너비 커스텀 (px)").fill("320");
|
|
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
const input = page.getByRole("textbox").first();
|
|
const styles = await input.evaluate((el) => {
|
|
const s = window.getComputedStyle(el as HTMLInputElement);
|
|
return {
|
|
width: s.width,
|
|
};
|
|
});
|
|
|
|
const widthPx = parseFloat(styles.width);
|
|
expect(widthPx).toBeGreaterThan(200);
|
|
expect(widthPx).toBeLessThan(360);
|
|
|
|
const inlineWidth = await input.evaluate((el) => (el as HTMLInputElement).style.width);
|
|
expect(inlineWidth.endsWith("em")).toBeTruthy();
|
|
});
|
|
|
|
test("버튼 타이포 px 입력이 프리뷰에서는 em 스케일로 렌더링되어야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "버튼" }).click();
|
|
|
|
const propertiesSidebar = page.getByTestId("properties-sidebar");
|
|
|
|
await propertiesSidebar.getByLabel("버튼 텍스트", { exact: true }).fill("em 스케일 버튼");
|
|
await propertiesSidebar.getByLabel("버튼 크기 커스텀 (px)").fill("24");
|
|
await propertiesSidebar.getByLabel("줄 간격 커스텀").fill("1.8");
|
|
await propertiesSidebar.getByLabel("글자 간격 커스텀 (px)").fill("1.6");
|
|
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
const button = page.getByRole("link", { name: "em 스케일 버튼" }).first();
|
|
const computed = await button.evaluate((el) => {
|
|
const s = window.getComputedStyle(el as HTMLElement);
|
|
return {
|
|
fontSize: s.fontSize,
|
|
lineHeight: s.lineHeight,
|
|
letterSpacing: s.letterSpacing,
|
|
};
|
|
});
|
|
|
|
expect(parseFloat(computed.fontSize)).toBeGreaterThan(18);
|
|
expect(parseFloat(computed.fontSize)).toBeLessThan(30);
|
|
expect(parseFloat(computed.lineHeight || "0")).toBeGreaterThan(20);
|
|
expect(parseFloat(computed.letterSpacing || "0")).toBeGreaterThan(0.5);
|
|
|
|
const inlineStyles = await button.evaluate((el) => {
|
|
const element = el as HTMLElement;
|
|
return {
|
|
fontSize: element.style.fontSize,
|
|
letterSpacing: element.style.letterSpacing,
|
|
};
|
|
});
|
|
|
|
expect(inlineStyles.fontSize.endsWith("em")).toBeTruthy();
|
|
expect(inlineStyles.letterSpacing.endsWith("em")).toBeTruthy();
|
|
});
|
|
|
|
test("버튼 고정 너비와 패딩 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "버튼" }).click();
|
|
|
|
const propertiesSidebar = page.getByTestId("properties-sidebar");
|
|
|
|
await propertiesSidebar.getByLabel("버튼 너비 모드").selectOption("fixed");
|
|
await propertiesSidebar.getByLabel("버튼 고정 너비 커스텀 (px)").fill("240");
|
|
await propertiesSidebar.getByLabel("가로 패딩 (px) 커스텀 (px)").fill("48");
|
|
await propertiesSidebar.getByLabel("세로 패딩 (px) 커스텀 (px)").fill("24");
|
|
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
const button = page.getByRole("link", { name: "버튼" }).first();
|
|
const computed = await button.evaluate((el) => {
|
|
const element = el as HTMLElement;
|
|
const s = window.getComputedStyle(element);
|
|
return {
|
|
width: s.width,
|
|
paddingLeft: s.paddingLeft,
|
|
paddingRight: s.paddingRight,
|
|
paddingTop: s.paddingTop,
|
|
paddingBottom: s.paddingBottom,
|
|
inlineWidth: element.style.width,
|
|
inlinePaddingInline: element.style.paddingInline,
|
|
inlinePaddingBlock: element.style.paddingBlock,
|
|
};
|
|
});
|
|
|
|
const widthPx = parseFloat(computed.width);
|
|
expect(widthPx).toBeGreaterThan(200);
|
|
expect(widthPx).toBeLessThan(280);
|
|
|
|
expect(parseFloat(computed.paddingLeft)).toBeGreaterThan(30);
|
|
expect(parseFloat(computed.paddingLeft)).toBeLessThan(60);
|
|
expect(parseFloat(computed.paddingRight)).toBeGreaterThan(30);
|
|
expect(parseFloat(computed.paddingRight)).toBeLessThan(60);
|
|
expect(parseFloat(computed.paddingTop)).toBeGreaterThan(15);
|
|
expect(parseFloat(computed.paddingTop)).toBeLessThan(35);
|
|
expect(parseFloat(computed.paddingBottom)).toBeGreaterThan(15);
|
|
expect(parseFloat(computed.paddingBottom)).toBeLessThan(35);
|
|
|
|
expect(computed.inlineWidth.endsWith("em")).toBeTruthy();
|
|
expect(computed.inlinePaddingInline.endsWith("em")).toBeTruthy();
|
|
expect(computed.inlinePaddingBlock.endsWith("em")).toBeTruthy();
|
|
});
|
|
|
|
test("이미지 고정 너비가 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "이미지" }).click();
|
|
|
|
const propertiesSidebar = page.getByTestId("properties-sidebar");
|
|
|
|
await propertiesSidebar.getByLabel("이미지 URL").fill("https://picsum.photos/600");
|
|
await propertiesSidebar.getByLabel("대체 텍스트").fill("em 스케일 이미지");
|
|
await propertiesSidebar.getByLabel("너비 모드").selectOption("fixed");
|
|
await propertiesSidebar.getByLabel("고정 너비 (px) 커스텀 (px)").fill("480");
|
|
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
const image = page.getByRole("img", { name: "em 스케일 이미지" }).first();
|
|
const computed = await image.evaluate((el) => {
|
|
const element = el as HTMLImageElement;
|
|
const s = window.getComputedStyle(element);
|
|
return {
|
|
width: s.width,
|
|
inlineWidth: element.style.width,
|
|
};
|
|
});
|
|
|
|
const widthPx = parseFloat(computed.width);
|
|
expect(widthPx).toBeGreaterThan(300);
|
|
expect(widthPx).toBeLessThan(520);
|
|
expect(computed.inlineWidth.endsWith("em")).toBeTruthy();
|
|
});
|
|
|
|
test("이미지 블록에서 로컬 파일 업로드 후 에디터/프리뷰 모두 /api/image/:id 기반 이미지가 보여야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "이미지" }).click();
|
|
|
|
const canvas = page.getByTestId("editor-canvas");
|
|
const propertiesSidebar = page.getByTestId("properties-sidebar");
|
|
|
|
// alt 텍스트를 먼저 설정해 에디터/프리뷰에서 이미지를 안정적으로 찾을 수 있게 한다.
|
|
await propertiesSidebar.getByLabel("대체 텍스트").fill("업로드 이미지");
|
|
|
|
// 이미지 소스를 파일 업로드 모드로 전환한다.
|
|
await propertiesSidebar.getByLabel("이미지 소스").selectOption("upload");
|
|
|
|
// 아직 구현되지 않은 이미지 업로드 인풋(aria-label: "이미지 파일 업로드")을 통해 로컬 파일을 업로드한다고 가정한다.
|
|
const fileInput = propertiesSidebar.getByLabel("이미지 파일 업로드");
|
|
await fileInput.setInputFiles("tests/fixtures/sample-image.png");
|
|
|
|
// 에디터 캔버스에서 alt 텍스트 기준으로 이미지를 찾는다.
|
|
const editorImage = canvas.getByRole("img", { name: "업로드 이미지" }).first();
|
|
await expect(editorImage).toBeVisible();
|
|
|
|
const editorSrc = await editorImage.getAttribute("src");
|
|
expect(editorSrc).not.toBeNull();
|
|
expect(editorSrc).toMatch(/^\/api\/image\//);
|
|
|
|
// 프리뷰로 이동하면 동일한 /api/image/:id URL을 사용하는 이미지가 보여야 한다.
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
const previewImage = page.getByRole("img", { name: "업로드 이미지" }).first();
|
|
await expect(previewImage).toBeVisible();
|
|
|
|
const previewSrc = await previewImage.getAttribute("src");
|
|
expect(previewSrc).toBe(editorSrc);
|
|
});
|
|
|
|
test("구분선 고정 길이와 여백 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "구분선" }).click();
|
|
|
|
const propertiesSidebar = page.getByTestId("properties-sidebar");
|
|
|
|
await propertiesSidebar.getByLabel("구분선 길이 모드").selectOption("fixed");
|
|
await propertiesSidebar.getByLabel("고정 길이 (px) 커스텀 (px)").fill("480");
|
|
await propertiesSidebar.getByLabel("위/아래 여백 커스텀 (px)").fill("48");
|
|
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
const divider = page.getByTestId("preview-divider").first();
|
|
const dividerStyles = await divider.evaluate((el) => {
|
|
const element = el as HTMLElement;
|
|
const s = window.getComputedStyle(element);
|
|
return {
|
|
marginTop: s.marginTop,
|
|
inlineMarginTop: element.style.marginTop,
|
|
marginBottom: s.marginBottom,
|
|
inlineMarginBottom: element.style.marginBottom,
|
|
};
|
|
});
|
|
|
|
const dividerLine = page.getByTestId("preview-divider-line").first();
|
|
const lineStyles = await dividerLine.evaluate((el) => {
|
|
const element = el as HTMLElement;
|
|
const s = window.getComputedStyle(element);
|
|
return {
|
|
width: s.width,
|
|
inlineWidth: element.style.width,
|
|
};
|
|
});
|
|
|
|
const marginTopPx = parseFloat(dividerStyles.marginTop);
|
|
expect(marginTopPx).toBeGreaterThan(40);
|
|
expect(marginTopPx).toBeLessThan(60);
|
|
expect(dividerStyles.inlineMarginTop.endsWith("em")).toBeTruthy();
|
|
|
|
const lineWidthPx = parseFloat(lineStyles.width);
|
|
expect(lineWidthPx).toBeGreaterThan(440);
|
|
expect(lineWidthPx).toBeLessThan(520);
|
|
expect(lineStyles.inlineWidth.endsWith("em")).toBeTruthy();
|
|
});
|
|
|
|
test("섹션 padding/maxWidth/gap px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "섹션" }).click();
|
|
|
|
const propertiesSidebar = page.getByTestId("properties-sidebar");
|
|
|
|
await propertiesSidebar.getByLabel("세로 패딩 커스텀 (px)").fill("96");
|
|
await propertiesSidebar.getByLabel("최대 폭 (px) 프리셋").selectOption("x-wide");
|
|
await propertiesSidebar.getByLabel("컬럼 간 간격 (px) 프리셋").selectOption("max");
|
|
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
const section = page.getByTestId("preview-section").first();
|
|
const sectionStyles = await section.evaluate((el) => {
|
|
const element = el as HTMLElement;
|
|
const s = window.getComputedStyle(element);
|
|
return {
|
|
paddingTop: s.paddingTop,
|
|
paddingBottom: s.paddingBottom,
|
|
inlinePaddingTop: element.style.paddingTop,
|
|
inlinePaddingBottom: element.style.paddingBottom,
|
|
};
|
|
});
|
|
|
|
const paddingTop = parseFloat(sectionStyles.paddingTop);
|
|
expect(paddingTop).toBeGreaterThan(80);
|
|
expect(paddingTop).toBeLessThan(120);
|
|
expect(sectionStyles.inlinePaddingTop.endsWith("em")).toBeTruthy();
|
|
|
|
const sectionInner = page.getByTestId("preview-section-inner").first();
|
|
const innerStyles = await sectionInner.evaluate((el) => {
|
|
const element = el as HTMLElement;
|
|
const s = window.getComputedStyle(element);
|
|
return {
|
|
maxWidth: s.maxWidth,
|
|
inlineMaxWidth: element.style.maxWidth,
|
|
};
|
|
});
|
|
|
|
const maxWidthPx = parseFloat(innerStyles.maxWidth);
|
|
expect(maxWidthPx).toBeGreaterThan(1300);
|
|
expect(maxWidthPx).toBeLessThan(1500);
|
|
expect(innerStyles.inlineMaxWidth.endsWith("em")).toBeTruthy();
|
|
|
|
const columnsWrapper = page.getByTestId("preview-section-columns").first();
|
|
const columnsStyles = await columnsWrapper.evaluate((el) => {
|
|
const element = el as HTMLElement;
|
|
const s = window.getComputedStyle(element);
|
|
return {
|
|
columnGap: s.columnGap,
|
|
inlineGap: element.style.columnGap,
|
|
};
|
|
});
|
|
|
|
const gapPx = parseFloat(columnsStyles.columnGap);
|
|
expect(gapPx).toBeGreaterThan(50);
|
|
expect(gapPx).toBeLessThan(70);
|
|
expect(columnsStyles.inlineGap.endsWith("em")).toBeTruthy();
|
|
});
|
|
|
|
test("구분선 고정 길이 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "구분선" }).click();
|
|
|
|
const propertiesSidebar = page.getByTestId("properties-sidebar");
|
|
|
|
await propertiesSidebar.getByLabel("길이 모드").selectOption("fixed");
|
|
await propertiesSidebar.getByLabel("고정 길이 (px) 커스텀 (px)").fill("420");
|
|
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
const dividerLine = page.getByTestId("preview-divider-line").first();
|
|
const inlineStyles = await dividerLine.evaluate((el) => {
|
|
const element = el as HTMLElement;
|
|
const s = window.getComputedStyle(element);
|
|
return {
|
|
computedWidth: s.width,
|
|
inlineWidth: element.style.width,
|
|
};
|
|
});
|
|
|
|
const widthPx = parseFloat(inlineStyles.computedWidth);
|
|
expect(widthPx).toBeGreaterThan(380);
|
|
expect(widthPx).toBeLessThan(460);
|
|
expect(inlineStyles.inlineWidth.endsWith("em")).toBeTruthy();
|
|
});
|
|
|
|
test("구분선 상하 여백 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "구분선" }).click();
|
|
|
|
const propertiesSidebar = page.getByTestId("properties-sidebar");
|
|
|
|
await propertiesSidebar.getByLabel("위/아래 여백 커스텀 (px)").fill("32");
|
|
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
const divider = page.getByTestId("preview-divider").first();
|
|
const inlineStyles = await divider.evaluate((el) => {
|
|
const element = el as HTMLElement;
|
|
const s = window.getComputedStyle(element);
|
|
return {
|
|
marginTop: s.marginTop,
|
|
inlineMarginTop: element.style.marginTop,
|
|
};
|
|
});
|
|
|
|
const marginPx = parseFloat(inlineStyles.marginTop);
|
|
expect(marginPx).toBeGreaterThan(28);
|
|
expect(marginPx).toBeLessThan(36);
|
|
expect(inlineStyles.inlineMarginTop.endsWith("em")).toBeTruthy();
|
|
});
|
|
|
|
test("리스트 아이템 간 여백 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "리스트" }).click();
|
|
|
|
const propertiesSidebar = page.getByTestId("properties-sidebar");
|
|
|
|
await propertiesSidebar.getByLabel("리스트 아이템들").fill("아이템 1\n아이템 2\n아이템 3");
|
|
await propertiesSidebar.getByLabel("아이템 간 여백 (px) 커스텀 (px)").fill("24");
|
|
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
const firstListItem = page.getByRole("listitem").first();
|
|
const styles = await firstListItem.evaluate((el) => {
|
|
const element = el as HTMLElement;
|
|
const s = window.getComputedStyle(element);
|
|
return {
|
|
marginBottom: s.marginBottom,
|
|
inlineMarginBottom: element.style.marginBottom,
|
|
};
|
|
});
|
|
|
|
const marginPx = parseFloat(styles.marginBottom);
|
|
expect(marginPx).toBeGreaterThan(20);
|
|
expect(marginPx).toBeLessThan(30);
|
|
expect(styles.inlineMarginBottom.endsWith("em")).toBeTruthy();
|
|
});
|
|
|
|
test("리스트 글자 크기 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "리스트" }).click();
|
|
|
|
const propertiesSidebar = page.getByTestId("properties-sidebar");
|
|
|
|
await propertiesSidebar.getByLabel("글자 크기 (px) 커스텀 (px)").fill("24");
|
|
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
const list = page.getByRole("list").first();
|
|
const inlineStyles = await list.evaluate((el) => {
|
|
const element = el as HTMLElement;
|
|
const s = window.getComputedStyle(element);
|
|
return {
|
|
computedFontSize: s.fontSize,
|
|
inlineFontSize: element.style.fontSize,
|
|
};
|
|
});
|
|
|
|
const fontSizePx = parseFloat(inlineStyles.computedFontSize);
|
|
expect(fontSizePx).toBeGreaterThan(20);
|
|
expect(fontSizePx).toBeLessThan(34);
|
|
expect(inlineStyles.inlineFontSize.endsWith("em")).toBeTruthy();
|
|
});
|
|
|
|
test("폼 입력 줄간격 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "입력(Input)" }).click();
|
|
|
|
const propertiesSidebar = page.getByTestId("properties-sidebar");
|
|
|
|
await propertiesSidebar.getByLabel("필드 줄간격 (px) 커스텀 (px)").fill("34");
|
|
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
const inputElement = page.getByTestId("preview-form-input").first();
|
|
const lineStyles = await inputElement.evaluate((el) => {
|
|
const element = el as HTMLElement;
|
|
const s = window.getComputedStyle(element);
|
|
return {
|
|
computedLineHeight: s.lineHeight,
|
|
inlineLineHeight: element.style.lineHeight,
|
|
};
|
|
});
|
|
|
|
const lineHeightPx = parseFloat(lineStyles.computedLineHeight);
|
|
expect(lineHeightPx).toBeGreaterThan(24);
|
|
expect(lineHeightPx).toBeLessThan(40);
|
|
expect(lineStyles.inlineLineHeight.endsWith("em")).toBeTruthy();
|
|
});
|
|
|
|
test("폼 입력 자간 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "입력(Input)" }).click();
|
|
|
|
const propertiesSidebar = page.getByTestId("properties-sidebar");
|
|
|
|
await propertiesSidebar.getByLabel("필드 자간 (px) 커스텀 (px)").fill("3");
|
|
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
const inputElement = page.getByTestId("preview-form-input").first();
|
|
const letterStyles = await inputElement.evaluate((el) => {
|
|
const element = el as HTMLElement;
|
|
const s = window.getComputedStyle(element);
|
|
return {
|
|
computedLetterSpacing: s.letterSpacing,
|
|
inlineLetterSpacing: element.style.letterSpacing,
|
|
};
|
|
});
|
|
|
|
const letterSpacingPx = parseFloat(letterStyles.computedLetterSpacing);
|
|
expect(letterSpacingPx).toBeGreaterThan(1);
|
|
expect(letterSpacingPx).toBeLessThan(5);
|
|
expect(letterStyles.inlineLetterSpacing.endsWith("em")).toBeTruthy();
|
|
});
|
|
|
|
test("폼 입력 가로 패딩 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "입력(Input)" }).click();
|
|
|
|
const paddingSidebar = page.getByTestId("properties-sidebar");
|
|
|
|
await paddingSidebar.getByLabel("필드 가로 패딩 (px) 커스텀 (px)").fill("28");
|
|
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
const horizontalInput = page.getByTestId("preview-form-input").first();
|
|
const horizontalPadding = await horizontalInput.evaluate((el) => {
|
|
const element = el as HTMLElement;
|
|
const s = window.getComputedStyle(element);
|
|
return {
|
|
computedPaddingLeft: s.paddingLeft,
|
|
inlinePaddingLeft: (element.style as any).paddingInline || element.style.paddingLeft,
|
|
};
|
|
});
|
|
|
|
const paddingPx = parseFloat(horizontalPadding.computedPaddingLeft);
|
|
expect(paddingPx).toBeGreaterThan(20);
|
|
expect(paddingPx).toBeLessThan(36);
|
|
expect(horizontalPadding.inlinePaddingLeft.endsWith("em")).toBeTruthy();
|
|
});
|
|
|
|
test("폼 입력 세로 패딩 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "입력(Input)" }).click();
|
|
|
|
const paddingSidebar = page.getByTestId("properties-sidebar");
|
|
|
|
await paddingSidebar.getByLabel("필드 세로 패딩 (px) 커스텀 (px)").fill("18");
|
|
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
const verticalInput = page.getByTestId("preview-form-input").first();
|
|
const verticalPadding = await verticalInput.evaluate((el) => {
|
|
const element = el as HTMLElement;
|
|
const s = window.getComputedStyle(element);
|
|
return {
|
|
computedPaddingTop: s.paddingTop,
|
|
inlinePaddingTop: (element.style as any).paddingBlock || element.style.paddingTop,
|
|
};
|
|
});
|
|
|
|
const paddingPx = parseFloat(verticalPadding.computedPaddingTop);
|
|
expect(paddingPx).toBeGreaterThan(12);
|
|
expect(paddingPx).toBeLessThan(24);
|
|
expect(verticalPadding.inlinePaddingTop.endsWith("em")).toBeTruthy();
|
|
});
|
|
|
|
test("폼 입력 인라인 라벨 간격 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "입력(Input)" }).click();
|
|
|
|
const propertiesSidebar = page.getByTestId("properties-sidebar");
|
|
|
|
await propertiesSidebar.getByLabel("레이아웃").selectOption("inline");
|
|
await propertiesSidebar.getByLabel("라벨/필드 간격 (px) 커스텀 (px)").fill("30");
|
|
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
const wrapper = page.getByTestId("preview-form-input-wrapper").first();
|
|
const gapStyles = await wrapper.evaluate((el) => {
|
|
const element = el as HTMLElement;
|
|
const s = window.getComputedStyle(element);
|
|
return {
|
|
computedColumnGap: s.columnGap,
|
|
inlineColumnGap: element.style.columnGap || element.style.gap,
|
|
};
|
|
});
|
|
|
|
const gapPx = parseFloat(gapStyles.computedColumnGap);
|
|
expect(gapPx).toBeGreaterThan(18);
|
|
expect(gapPx).toBeLessThan(32);
|
|
expect(gapStyles.inlineColumnGap.endsWith("em")).toBeTruthy();
|
|
});
|
|
|
|
test("폼 체크박스 옵션 간격 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "다중 선택(Checkbox)" }).click();
|
|
|
|
const propertiesSidebar = page.getByTestId("properties-sidebar");
|
|
|
|
await propertiesSidebar.getByLabel("체크박스 옵션 간격 (px) 커스텀 (px)").fill("24");
|
|
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
const optionsContainer = page.getByTestId("preview-form-checkbox-options");
|
|
const gapStyles = await optionsContainer.evaluate((el) => {
|
|
const element = el as HTMLElement;
|
|
const s = window.getComputedStyle(element);
|
|
return {
|
|
computedRowGap: s.rowGap,
|
|
inlineRowGap: element.style.rowGap,
|
|
};
|
|
});
|
|
|
|
const gapPx = parseFloat(gapStyles.computedRowGap);
|
|
// 텍스트 기본 크기(약 12px)와 px→em 변환(24px → 1.5em)을 고려하면 실제 row-gap 은 약 18px 근처가 된다.
|
|
// 브라우저별 오차를 허용하기 위해 15~28px 범위 안에 있는지만 확인한다.
|
|
expect(gapPx).toBeGreaterThan(15);
|
|
expect(gapPx).toBeLessThan(28);
|
|
expect(gapStyles.inlineRowGap.endsWith("em")).toBeTruthy();
|
|
});
|
|
|
|
test("FormBlock 은 프리뷰에서 폼 컨트롤러 DOM 을 렌더해야 한다 (고정 너비 설정 시)", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "폼 컨트롤러" }).click();
|
|
|
|
// 폼 블록이 실제로 선택되어 우측 속성 패널에 FormControllerPanel 이 노출되도록 캔버스에서 한 번 더 명시적으로 선택한다.
|
|
const editorCanvas = page.getByTestId("editor-canvas");
|
|
const editorBlocks = editorCanvas.getByTestId("editor-block");
|
|
await editorBlocks.last().click({ force: true });
|
|
|
|
const propertiesSidebar = page.getByTestId("properties-sidebar");
|
|
|
|
await propertiesSidebar.getByLabel("폼 너비 모드").selectOption("fixed");
|
|
await propertiesSidebar.getByLabel("폼 고정 너비 (px) 커스텀 (px)").fill("480");
|
|
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
});
|
|
|
|
test("FormBlock 은 프리뷰에서 폼 컨트롤러 DOM 을 렌더해야 한다 (상하 여백 설정 시)", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "폼 컨트롤러" }).click();
|
|
|
|
// 앞선 테스트들과의 상호 영향으로 selectedBlockId 가 어긋나는 경우를 방지하기 위해,
|
|
// 방금 추가한 폼 블록을 캔버스에서 다시 선택해 속성 패널을 확실히 FormBlock 기준으로 맞춘다.
|
|
const editorCanvas = page.getByTestId("editor-canvas");
|
|
const editorBlocks = editorCanvas.getByTestId("editor-block");
|
|
await editorBlocks.last().click({ force: true });
|
|
|
|
const propertiesSidebar = page.getByTestId("properties-sidebar");
|
|
|
|
await propertiesSidebar.getByLabel("폼 위/아래 여백 (px) 커스텀 (px)").fill("28");
|
|
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
});
|
|
|
|
test("폼 라디오 줄간격 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "단일 선택(Radio)" }).click();
|
|
|
|
const propertiesSidebar = page.getByTestId("properties-sidebar");
|
|
|
|
await propertiesSidebar.getByLabel("라디오 줄간격 (px) 커스텀 (px)").fill("28");
|
|
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
const firstOption = page.getByTestId("preview-form-radio-option").first();
|
|
const inlineStyles = await firstOption.evaluate((el) => {
|
|
const element = el as HTMLElement;
|
|
const s = window.getComputedStyle(element);
|
|
return {
|
|
computedLineHeight: s.lineHeight,
|
|
inlineLineHeight: element.style.lineHeight,
|
|
};
|
|
});
|
|
|
|
const lineHeightPx = parseFloat(inlineStyles.computedLineHeight);
|
|
expect(lineHeightPx).toBeGreaterThan(18);
|
|
expect(lineHeightPx).toBeLessThan(36);
|
|
expect(inlineStyles.inlineLineHeight.endsWith("em")).toBeTruthy();
|
|
});
|
|
|
|
test("폼 라디오 자간 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "단일 선택(Radio)" }).click();
|
|
|
|
const propertiesSidebar = page.getByTestId("properties-sidebar");
|
|
|
|
await propertiesSidebar.getByLabel("라디오 자간 (px) 커스텀 (px)").fill("2");
|
|
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
const firstOption = page.getByTestId("preview-form-radio-option").first();
|
|
const inlineStyles = await firstOption.evaluate((el) => {
|
|
const element = el as HTMLElement;
|
|
const s = window.getComputedStyle(element);
|
|
return {
|
|
computedLetterSpacing: s.letterSpacing,
|
|
inlineLetterSpacing: element.style.letterSpacing,
|
|
};
|
|
});
|
|
|
|
const letterSpacingPx = parseFloat(inlineStyles.computedLetterSpacing);
|
|
expect(letterSpacingPx).toBeGreaterThan(0);
|
|
expect(letterSpacingPx).toBeLessThan(4);
|
|
expect(inlineStyles.inlineLetterSpacing.endsWith("em")).toBeTruthy();
|
|
});
|
|
|
|
test("폼 체크박스 가로 패딩 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "다중 선택(Checkbox)" }).click();
|
|
|
|
const propertiesSidebar = page.getByTestId("properties-sidebar");
|
|
|
|
await propertiesSidebar.getByLabel("체크박스 가로 패딩 (px) 커스텀 (px)").fill("24");
|
|
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
const checkboxOption = page.getByTestId("preview-form-checkbox-option-container").first();
|
|
const paddingStyles = await checkboxOption.evaluate((el) => {
|
|
const element = el as HTMLElement;
|
|
const s = window.getComputedStyle(element);
|
|
return {
|
|
computedPaddingLeft: s.paddingLeft,
|
|
inlinePaddingLeft: (element.style as any).paddingInline || element.style.paddingLeft,
|
|
};
|
|
});
|
|
|
|
const paddingPx = parseFloat(paddingStyles.computedPaddingLeft);
|
|
expect(paddingPx).toBeGreaterThan(12);
|
|
expect(paddingPx).toBeLessThan(28);
|
|
expect(paddingStyles.inlinePaddingLeft.endsWith("em")).toBeTruthy();
|
|
});
|
|
|
|
test("폼 체크박스 세로 패딩 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "다중 선택(Checkbox)" }).click();
|
|
|
|
const propertiesSidebar = page.getByTestId("properties-sidebar");
|
|
|
|
await propertiesSidebar.getByLabel("체크박스 세로 패딩 (px) 커스텀 (px)").fill("16");
|
|
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
const checkboxOption = page.getByTestId("preview-form-checkbox-option-container").first();
|
|
const paddingStyles = await checkboxOption.evaluate((el) => {
|
|
const element = el as HTMLElement;
|
|
const s = window.getComputedStyle(element);
|
|
return {
|
|
computedPaddingTop: s.paddingTop,
|
|
inlinePaddingTop: (element.style as any).paddingBlock || element.style.paddingTop,
|
|
};
|
|
});
|
|
|
|
const paddingPx = parseFloat(paddingStyles.computedPaddingTop);
|
|
expect(paddingPx).toBeGreaterThan(10);
|
|
expect(paddingPx).toBeLessThan(24);
|
|
expect(paddingStyles.inlinePaddingTop.endsWith("em")).toBeTruthy();
|
|
});
|
|
|
|
test("폼 셀렉트 가로 패딩 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "셀렉트(Select)" }).click();
|
|
|
|
const propertiesSidebar = page.getByTestId("properties-sidebar");
|
|
|
|
await propertiesSidebar.getByLabel("셀렉트 가로 패딩 (px) 커스텀 (px)").fill("24");
|
|
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
const selectElement = page.getByTestId("preview-form-select").locator("select").first();
|
|
const paddingStyles = await selectElement.evaluate((el) => {
|
|
const element = el as HTMLElement;
|
|
const s = window.getComputedStyle(element);
|
|
return {
|
|
computedPaddingLeft: s.paddingLeft,
|
|
inlinePaddingLeft: (element.style as any).paddingInline || element.style.paddingLeft,
|
|
};
|
|
});
|
|
|
|
const paddingPx = parseFloat(paddingStyles.computedPaddingLeft);
|
|
expect(paddingPx).toBeGreaterThan(12);
|
|
expect(paddingPx).toBeLessThan(28);
|
|
expect(paddingStyles.inlinePaddingLeft.endsWith("em")).toBeTruthy();
|
|
});
|
|
|
|
test("폼 셀렉트 세로 패딩 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "셀렉트(Select)" }).click();
|
|
|
|
const propertiesSidebar = page.getByTestId("properties-sidebar");
|
|
|
|
await propertiesSidebar.getByLabel("셀렉트 세로 패딩 (px) 커스텀 (px)").fill("18");
|
|
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
const selectElement = page.getByTestId("preview-form-select").locator("select").first();
|
|
const paddingStyles = await selectElement.evaluate((el) => {
|
|
const element = el as HTMLElement;
|
|
const s = window.getComputedStyle(element);
|
|
return {
|
|
computedPaddingTop: s.paddingTop,
|
|
inlinePaddingTop: (element.style as any).paddingBlock || element.style.paddingTop,
|
|
};
|
|
});
|
|
|
|
const paddingPx = parseFloat(paddingStyles.computedPaddingTop);
|
|
expect(paddingPx).toBeGreaterThan(12);
|
|
expect(paddingPx).toBeLessThan(24);
|
|
expect(paddingStyles.inlinePaddingTop.endsWith("em")).toBeTruthy();
|
|
});
|
|
|
|
test("폼 셀렉트 글자 크기 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "셀렉트(Select)" }).click();
|
|
|
|
const propertiesSidebar = page.getByTestId("properties-sidebar");
|
|
|
|
await propertiesSidebar.getByLabel("셀렉트 텍스트 크기 (px) 커스텀 (px)").fill("28");
|
|
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
const select = page.getByTestId("preview-form-select").first();
|
|
const inlineStyles = await select.evaluate((el) => {
|
|
const element = el as HTMLElement;
|
|
const s = window.getComputedStyle(element);
|
|
return {
|
|
computedFontSize: s.fontSize,
|
|
inlineFontSize: element.style.fontSize,
|
|
};
|
|
});
|
|
|
|
const fontSizePx = parseFloat(inlineStyles.computedFontSize);
|
|
expect(fontSizePx).toBeGreaterThan(20);
|
|
expect(fontSizePx).toBeLessThan(32);
|
|
expect(inlineStyles.inlineFontSize.endsWith("em")).toBeTruthy();
|
|
});
|
|
|
|
test("폼 셀렉트 줄간격 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "셀렉트(Select)" }).click();
|
|
|
|
const propertiesSidebar = page.getByTestId("properties-sidebar");
|
|
|
|
await propertiesSidebar.getByLabel("셀렉트 줄간격 (px) 커스텀 (px)").fill("36");
|
|
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
const select = page.getByTestId("preview-form-select").first();
|
|
const inlineStyles = await select.evaluate((el) => {
|
|
const element = el as HTMLElement;
|
|
const s = window.getComputedStyle(element);
|
|
return {
|
|
computedLineHeight: s.lineHeight,
|
|
inlineLineHeight: element.style.lineHeight,
|
|
};
|
|
});
|
|
|
|
const lineHeightPx = parseFloat(inlineStyles.computedLineHeight);
|
|
expect(lineHeightPx).toBeGreaterThan(20);
|
|
expect(lineHeightPx).toBeLessThan(40);
|
|
expect(inlineStyles.inlineLineHeight.endsWith("em")).toBeTruthy();
|
|
});
|
|
|
|
test("폼 셀렉트 자간 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "셀렉트(Select)" }).click();
|
|
|
|
const propertiesSidebar = page.getByTestId("properties-sidebar");
|
|
|
|
await propertiesSidebar.getByLabel("셀렉트 자간 (px) 커스텀 (px)").fill("4");
|
|
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
const select = page.getByTestId("preview-form-select").first();
|
|
const inlineStyles = await select.evaluate((el) => {
|
|
const element = el as HTMLElement;
|
|
const s = window.getComputedStyle(element);
|
|
return {
|
|
computedLetterSpacing: s.letterSpacing,
|
|
inlineLetterSpacing: element.style.letterSpacing,
|
|
};
|
|
});
|
|
|
|
const letterSpacingPx = parseFloat(inlineStyles.computedLetterSpacing);
|
|
expect(letterSpacingPx).toBeGreaterThan(2);
|
|
expect(letterSpacingPx).toBeLessThan(6);
|
|
expect(inlineStyles.inlineLetterSpacing.endsWith("em")).toBeTruthy();
|
|
});
|
|
|
|
test("폼 체크박스 글자 크기 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "다중 선택(Checkbox)" }).click();
|
|
|
|
const propertiesSidebar = page.getByTestId("properties-sidebar");
|
|
|
|
await propertiesSidebar.getByLabel("체크박스 텍스트 크기 (px) 커스텀 (px)").fill("24");
|
|
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
const firstOption = page.getByTestId("preview-form-checkbox-option").first();
|
|
const inlineStyles = await firstOption.evaluate((el) => {
|
|
const element = el as HTMLElement;
|
|
const s = window.getComputedStyle(element);
|
|
return {
|
|
computedFontSize: s.fontSize,
|
|
inlineFontSize: element.style.fontSize,
|
|
};
|
|
});
|
|
|
|
const fontSizePx = parseFloat(inlineStyles.computedFontSize);
|
|
expect(fontSizePx).toBeGreaterThan(20);
|
|
expect(fontSizePx).toBeLessThan(28);
|
|
expect(inlineStyles.inlineFontSize.endsWith("em")).toBeTruthy();
|
|
});
|
|
|
|
test("폼 라디오 글자 크기 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "단일 선택(Radio)" }).click();
|
|
|
|
const propertiesSidebar = page.getByTestId("properties-sidebar");
|
|
|
|
await propertiesSidebar.getByLabel("라디오 텍스트 크기 (px) 커스텀 (px)").fill("22");
|
|
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
const firstOption = page.getByTestId("preview-form-radio-option").first();
|
|
const inlineStyles = await firstOption.evaluate((el) => {
|
|
const element = el as HTMLElement;
|
|
const s = window.getComputedStyle(element);
|
|
return {
|
|
computedFontSize: s.fontSize,
|
|
inlineFontSize: element.style.fontSize,
|
|
};
|
|
});
|
|
|
|
const fontSizePx = parseFloat(inlineStyles.computedFontSize);
|
|
expect(fontSizePx).toBeGreaterThan(18);
|
|
expect(fontSizePx).toBeLessThan(30);
|
|
expect(inlineStyles.inlineFontSize.endsWith("em")).toBeTruthy();
|
|
});
|
|
|
|
test("폼 입력 글자 크기 px 입력이 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "입력(Input)" }).click();
|
|
|
|
const propertiesSidebar = page.getByTestId("properties-sidebar");
|
|
|
|
await propertiesSidebar.getByLabel("필드 텍스트 크기 (px) 커스텀 (px)").fill("32");
|
|
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
const input = page.getByTestId("preview-form-input").first();
|
|
const inlineStyles = await input.evaluate((el) => {
|
|
const element = el as HTMLElement;
|
|
const s = window.getComputedStyle(element);
|
|
return {
|
|
computedFontSize: s.fontSize,
|
|
inlineFontSize: element.style.fontSize,
|
|
};
|
|
});
|
|
|
|
const fontSizePx = parseFloat(inlineStyles.computedFontSize);
|
|
expect(fontSizePx).toBeGreaterThan(20);
|
|
expect(fontSizePx).toBeLessThan(36);
|
|
expect(inlineStyles.inlineFontSize.endsWith("em")).toBeTruthy();
|
|
});
|
|
|
|
test("폼 셀렉트 고정 너비가 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "셀렉트(Select)" }).click();
|
|
|
|
const propertiesSidebar = page.getByTestId("properties-sidebar");
|
|
|
|
await propertiesSidebar.getByLabel("필드 너비").selectOption("fixed");
|
|
await propertiesSidebar.getByLabel("필드 고정 너비 커스텀 (px)").fill("360");
|
|
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
const selectField = page.getByTestId("preview-form-select").first();
|
|
const { width, inlineWidth } = await selectField.evaluate((el) => {
|
|
const element = el as HTMLElement;
|
|
const s = window.getComputedStyle(element);
|
|
return {
|
|
width: s.width,
|
|
inlineWidth: element.style.width,
|
|
};
|
|
});
|
|
|
|
const widthPx = parseFloat(width);
|
|
expect(widthPx).toBeGreaterThan(250);
|
|
expect(widthPx).toBeLessThan(420);
|
|
expect(inlineWidth.endsWith("em")).toBeTruthy();
|
|
});
|
|
|
|
test("폼 라디오 그룹 고정 너비가 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "단일 선택(Radio)" }).click();
|
|
|
|
const propertiesSidebar = page.getByTestId("properties-sidebar");
|
|
|
|
await propertiesSidebar.getByLabel("필드 너비").selectOption("fixed");
|
|
await propertiesSidebar.getByLabel("필드 고정 너비 커스텀 (px)").fill("400");
|
|
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
const radioGroup = page.getByTestId("preview-form-radio-group").first();
|
|
const { width, inlineWidth } = await radioGroup.evaluate((el) => {
|
|
const element = el as HTMLElement;
|
|
const s = window.getComputedStyle(element);
|
|
return {
|
|
width: s.width,
|
|
inlineWidth: element.style.width,
|
|
};
|
|
});
|
|
|
|
const widthPx = parseFloat(width);
|
|
expect(widthPx).toBeGreaterThan(280);
|
|
expect(widthPx).toBeLessThan(460);
|
|
expect(inlineWidth.endsWith("em")).toBeTruthy();
|
|
});
|
|
|
|
test("폼 체크박스 그룹 고정 너비가 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "다중 선택(Checkbox)" }).click();
|
|
|
|
const propertiesSidebar = page.getByTestId("properties-sidebar");
|
|
|
|
await propertiesSidebar.getByLabel("필드 너비").selectOption("fixed");
|
|
await propertiesSidebar.getByLabel("필드 고정 너비 커스텀 (px)").fill("420");
|
|
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
const checkboxGroup = page.getByTestId("preview-form-checkbox-group").first();
|
|
const { width, inlineWidth } = await checkboxGroup.evaluate((el) => {
|
|
const element = el as HTMLElement;
|
|
const s = window.getComputedStyle(element);
|
|
return {
|
|
width: s.width,
|
|
inlineWidth: element.style.width,
|
|
};
|
|
});
|
|
|
|
const widthPx = parseFloat(width);
|
|
expect(widthPx).toBeGreaterThan(300);
|
|
expect(widthPx).toBeLessThan(480);
|
|
expect(inlineWidth.endsWith("em")).toBeTruthy();
|
|
});
|
|
|
|
test("폼 컨트롤러에 매핑한 폼 요소와 버튼이 프리뷰에서 함께 동작해야 한다", async ({ page }) => {
|
|
// 1) 에디터에서 폼 요소, 버튼, 폼 블록을 추가한다.
|
|
await page.goto("/editor");
|
|
|
|
const canvas = page.getByTestId("editor-canvas");
|
|
|
|
// 폼 입력 요소와 버튼, 폼 블록을 순서대로 추가한다.
|
|
await page.getByRole("button", { name: "입력(Input)" }).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 });
|
|
|
|
// 2) 우측 속성 패널에서 폼 컨트롤러 매핑을 설정한다.
|
|
const fieldMappingGroup = page.getByRole("group", { name: "폼 필드 매핑" });
|
|
const fieldCheckboxes = fieldMappingGroup.getByRole("checkbox");
|
|
|
|
// 첫 번째 폼 요소를 이 폼의 fieldIds 에 매핑한다.
|
|
await fieldCheckboxes.nth(0).check();
|
|
|
|
// Submit 버튼 셀렉트 박스에서 방금 추가한 버튼 블록을 선택한다.
|
|
const submitSelect = page.getByRole("combobox", { name: "Submit 버튼" });
|
|
// 첫 번째 옵션은 "(선택 안 함)" 이므로, 그 이후 옵션 중 하나를 선택한다.
|
|
await submitSelect.selectOption({ index: 1 });
|
|
|
|
// 3) 프리뷰 페이지로 이동한다.
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
// v2 컨트롤러를 설정했으므로, 기본 contact fallback 폼(이름/이메일/메시지) 레이블은 더 이상 보이지 않아야 한다.
|
|
await expect(page.getByText("이름")).toHaveCount(0);
|
|
await expect(page.getByText("이메일")).toHaveCount(0);
|
|
await expect(page.getByText("메시지")).toHaveCount(0);
|
|
|
|
// 기대: 폼 컨트롤러에 매핑한 폼 입력 요소가 프리뷰에서 필드로 렌더링되어야 한다.
|
|
// (구체적인 라벨 텍스트는 구현에 따라 달라질 수 있어 우선 textbox 중 하나가 보이는지만 확인한다.)
|
|
const input = page.getByRole("textbox").first();
|
|
await expect(input).toBeVisible();
|
|
await expect(page.getByText("성공적으로 전송되었습니다.")).toHaveCount(0);
|
|
});
|
|
|
|
test("여러 폼 블록과 여러 버튼이 있을 때 각 폼이 독립적으로 제출되어야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
const canvas = page.getByTestId("editor-canvas");
|
|
|
|
const blocks = canvas.getByTestId("editor-block");
|
|
|
|
// Form A: 입력 필드 + 버튼 + 폼 블록을 추가한 뒤, 즉시 컨트롤러를 매핑한다.
|
|
await page.getByRole("button", { name: "입력(Input)" }).click();
|
|
await page.getByRole("button", { name: "버튼" }).click();
|
|
await page.getByRole("button", { name: "폼 컨트롤러" }).click();
|
|
|
|
let count = await blocks.count();
|
|
const formBlockA = blocks.nth(count - 1);
|
|
await formBlockA.click({ force: true });
|
|
|
|
let fieldMappingGroup = page.getByRole("group", { name: "폼 필드 매핑" });
|
|
let fieldCheckboxes = fieldMappingGroup.getByRole("checkbox");
|
|
// 현재 존재하는 폼 요소 중 첫 번째를 A 폼에 매핑한다.
|
|
await fieldCheckboxes.nth(0).check();
|
|
|
|
let submitSelect = page.getByRole("combobox", { name: "Submit 버튼" });
|
|
// 첫 번째 버튼을 A 폼의 submit 버튼으로 매핑 (index 1: 첫 번째 실제 버튼)
|
|
await submitSelect.selectOption({ index: 1 });
|
|
|
|
// Form B: 입력 필드 + 버튼 + 폼 블록을 다시 추가한 뒤, 즉시 컨트롤러를 매핑한다.
|
|
await page.getByRole("button", { name: "입력(Input)" }).click();
|
|
await page.getByRole("button", { name: "버튼" }).first().click();
|
|
await page.getByRole("button", { name: "폼 컨트롤러" }).click();
|
|
|
|
count = await blocks.count();
|
|
const formBlockB = blocks.nth(count - 1);
|
|
await formBlockB.click({ force: true });
|
|
|
|
fieldMappingGroup = page.getByRole("group", { name: "폼 필드 매핑" });
|
|
fieldCheckboxes = fieldMappingGroup.getByRole("checkbox");
|
|
// 현재 폼 요소 중 마지막 것을 B 폼에 매핑 (두 번째 입력 필드라고 가정)
|
|
const lastCheckboxIndex = (await fieldCheckboxes.count()) - 1;
|
|
await fieldCheckboxes.nth(lastCheckboxIndex).check();
|
|
|
|
submitSelect = page.getByRole("combobox", { name: "Submit 버튼" });
|
|
// 두 번째 버튼을 B 폼의 submit 버튼으로 매핑 (index 2: 두 번째 실제 버튼)
|
|
await submitSelect.selectOption({ index: 2 });
|
|
|
|
// 프리뷰로 이동한다.
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
const textboxes = page.getByRole("textbox");
|
|
const textboxCount = await textboxes.count();
|
|
|
|
// 최소 2개의 텍스트 입력이 있어야 한다.
|
|
expect(textboxCount).toBeGreaterThanOrEqual(2);
|
|
|
|
const firstInput = textboxes.nth(0);
|
|
const secondInput = textboxes.nth(1);
|
|
|
|
const successMessages = page.getByText("성공적으로 전송되었습니다.");
|
|
await expect(successMessages).toHaveCount(0);
|
|
});
|
|
|
|
test("섹션 패딩/컬럼 간격 수치가 프리뷰에서도 em 스케일에 맞게 반영되어야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
// 섹션 블록을 직접 추가한다.
|
|
await page.getByRole("button", { name: "섹션" }).click();
|
|
|
|
// 캔버스에서 첫 번째 섹션 블록을 선택한다.
|
|
const canvas = page.getByTestId("editor-canvas");
|
|
const sectionBlock = canvas.getByTestId("editor-block").first();
|
|
await sectionBlock.click({ force: true });
|
|
|
|
// 우측 속성 패널 범위를 지정한다.
|
|
const propertiesSidebar = page.getByTestId("properties-sidebar");
|
|
|
|
// 세로 패딩: 64px
|
|
const paddingYInput = propertiesSidebar.getByLabel("세로 패딩 커스텀 (px)");
|
|
await paddingYInput.fill("64");
|
|
|
|
// 컬럼 간 간격: 40px (커스텀 입력 spinbox/텍스트박스만 타겟)
|
|
const gapXInput = propertiesSidebar.getByRole("textbox", {
|
|
name: "컬럼 간 간격 (px) 커스텀 (px)",
|
|
});
|
|
await gapXInput.fill("40");
|
|
|
|
// 프리뷰로 이동한다.
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
// 첫 번째 섹션 요소를 찾는다.
|
|
const firstSection = page.getByTestId("preview-section").first();
|
|
|
|
const sectionStyles = await firstSection.evaluate((el) => {
|
|
const s = window.getComputedStyle(el as HTMLElement);
|
|
return {
|
|
paddingTop: s.paddingTop,
|
|
paddingBottom: s.paddingBottom,
|
|
};
|
|
});
|
|
|
|
// 세로 패딩: 64px 입력 → em 변환(1em=16px) 기반이므로 64px 근방이어야 한다.
|
|
const paddingTopPx = parseFloat(sectionStyles.paddingTop);
|
|
const paddingBottomPx = parseFloat(sectionStyles.paddingBottom);
|
|
expect(paddingTopPx).toBeGreaterThan(40);
|
|
expect(paddingTopPx).toBeLessThan(80);
|
|
expect(paddingBottomPx).toBeGreaterThan(40);
|
|
expect(paddingBottomPx).toBeLessThan(80);
|
|
|
|
// 섹션 컬럼 컨테이너의 column-gap 을 읽는다.
|
|
const columnsContainer = firstSection.getByTestId("preview-section-columns");
|
|
const gapValue = await columnsContainer.evaluate((el) => {
|
|
const s = window.getComputedStyle(el as HTMLElement);
|
|
return s.columnGap || (s as any).gap || "";
|
|
});
|
|
|
|
const gapPx = parseFloat(gapValue || "0");
|
|
expect(gapPx).toBeGreaterThan(20);
|
|
expect(gapPx).toBeLessThan(60);
|
|
});
|
|
|
|
test("구분선 여백/길이 수치가 프리뷰에서도 em 스케일로 반영되어야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "구분선" }).click();
|
|
|
|
const canvas = page.getByTestId("editor-canvas");
|
|
const dividerBlock = canvas.getByTestId("editor-block").first();
|
|
await dividerBlock.click({ force: true });
|
|
|
|
const propertiesSidebar = page.getByTestId("properties-sidebar");
|
|
|
|
// 위/아래 여백 32px
|
|
const marginInput = propertiesSidebar.getByLabel("위/아래 여백 커스텀 (px)");
|
|
await marginInput.fill("32");
|
|
|
|
// 길이 모드: 고정 길이 200px
|
|
await propertiesSidebar.getByLabel("구분선 길이 모드").selectOption("fixed");
|
|
const widthInput = propertiesSidebar.getByLabel("고정 길이 (px) 커스텀 (px)");
|
|
await widthInput.fill("200");
|
|
|
|
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
|
|
|
const divider = page.getByTestId("preview-divider").first();
|
|
|
|
const styles = await divider.evaluate((el) => {
|
|
const s = window.getComputedStyle(el as HTMLElement);
|
|
return {
|
|
marginTop: s.marginTop,
|
|
marginBottom: s.marginBottom,
|
|
};
|
|
});
|
|
|
|
const marginTopPx = parseFloat(styles.marginTop);
|
|
const marginBottomPx = parseFloat(styles.marginBottom);
|
|
expect(marginTopPx).toBeGreaterThan(20);
|
|
expect(marginTopPx).toBeLessThan(50);
|
|
expect(marginBottomPx).toBeGreaterThan(20);
|
|
expect(marginBottomPx).toBeLessThan(50);
|
|
|
|
const innerLine = divider.getByTestId("preview-divider-line");
|
|
const widthValue = await innerLine.evaluate((el) => {
|
|
const s = window.getComputedStyle(el as HTMLElement);
|
|
return s.width;
|
|
});
|
|
|
|
const widthPx = parseFloat(widthValue || "0");
|
|
expect(widthPx).toBeGreaterThan(120);
|
|
expect(widthPx).toBeLessThan(260);
|
|
});
|