403 lines
18 KiB
TypeScript
403 lines
18 KiB
TypeScript
import { test, expect } from "@playwright/test";
|
|
|
|
// 프리뷰/뷰 모드 TDD: 먼저 실패하는 E2E부터 작성한다.
|
|
|
|
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("에디터에서 Features 템플릿을 추가하면 프리뷰에서 3개의 Feature 섹션이 보여야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
await page.getByRole("button", { name: "Features 템플릿 추가" }).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: "Features 템플릿 추가" }).click();
|
|
await page.getByRole("button", { name: "Pricing 템플릿 추가" }).click();
|
|
await page.getByRole("button", { name: "Testimonials 템플릿 추가" }).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: "구분선 블록 추가" }).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();
|
|
|
|
// 정렬: 가운데 정렬
|
|
await page.getByLabel("리스트 정렬").selectOption("center");
|
|
|
|
// 글자 크기: 20px 로 설정
|
|
const fontSizeSlider = page.getByLabel("글자 크기");
|
|
await fontSizeSlider.fill("20");
|
|
|
|
// 줄 간격: 2.0 으로 설정
|
|
const lineHeightSlider = page.getByLabel("줄 간격");
|
|
await lineHeightSlider.fill("2");
|
|
|
|
// 텍스트 색상: #ff0000
|
|
const textColorHexInput = page.getByLabel("리스트 텍스트 색상 HEX");
|
|
await textColorHexInput.fill("#ff0000");
|
|
|
|
// 불릿 스타일: 숫자 (1.)
|
|
await page.getByLabel("리스트 불릿 스타일").selectOption("decimal");
|
|
|
|
// 아이템 간 여백: 24px
|
|
const gapSlider = page.getByLabel("아이템 간 여백");
|
|
await gapSlider.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 근처 (마지막 li 가 아니면 margin-bottom 이 설정된다. 첫 번째 li 기준으로 startsWith 로 비교)
|
|
expect(styles.marginBottom.startsWith("24")).toBeTruthy();
|
|
});
|
|
|
|
test("폼 입력/셀렉트 블록도 FormBlock 없이 프리뷰에서 보여야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
// 폼 입력/셀렉트 블록을 각각 하나씩 추가한다.
|
|
await page.getByRole("button", { name: "폼 입력 추가" }).click();
|
|
await page.getByRole("button", { name: "폼 셀렉트 추가" }).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("폼 입력 필드 스타일 속성이 프리뷰에도 반영되어야 한다", async ({ page }) => {
|
|
await page.goto("/editor");
|
|
|
|
// 폼 입력 블록을 추가한다.
|
|
await page.getByRole("button", { name: "폼 입력 추가" }).click();
|
|
|
|
// 우측 속성 패널에서 폼 입력 스타일을 설정한다.
|
|
// 라벨/필드 텍스트 등 기본 값은 그대로 두고, 스타일 속성만 눈에 띄게 변경한다.
|
|
|
|
// 정렬: 가운데 정렬
|
|
await page.getByLabel("텍스트 정렬").selectOption("center");
|
|
|
|
// 레이아웃: 인라인
|
|
await page.getByLabel("레이아웃").selectOption("inline");
|
|
|
|
// 너비: 고정 320px
|
|
await page.getByLabel("필드 너비").selectOption("fixed");
|
|
await page.getByLabel("필드 고정 너비").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 근처 (브라우저에 따라 소수점이 붙을 수 있어 startsWith 로 비교)
|
|
expect(styles.width.startsWith("320")).toBeTruthy();
|
|
|
|
// 텍스트/배경/테두리 색: 설정한 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("폼 컨트롤러에 매핑한 폼 요소와 버튼이 프리뷰에서 함께 동작해야 한다", async ({ page }) => {
|
|
// 1) 에디터에서 폼 요소, 버튼, 폼 블록을 추가한다.
|
|
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();
|
|
|
|
// 방금 추가된 폼 블록을 선택한다.
|
|
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 input.fill("테스트 값");
|
|
|
|
await page.getByRole("button", { name: "버튼" }).click();
|
|
|
|
await expect(page.getByText("성공적으로 전송되었습니다.")).toBeVisible();
|
|
});
|
|
|
|
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: "폼 입력 추가" }).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: "폼 입력 추가" }).click();
|
|
await page.getByRole("button", { name: "버튼 블록 추가" }).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);
|
|
|
|
// Form A 입력값을 채우고 첫 번째 버튼 클릭 → 성공 메시지 기대
|
|
await firstInput.fill("A 폼 값");
|
|
await page.getByRole("button", { name: "버튼" }).first().click();
|
|
// 첫 번째 폼에 대한 성공 메시지가 최소 하나는 보여야 한다.
|
|
await expect(page.getByText("성공적으로 전송되었습니다.").first()).toBeVisible();
|
|
|
|
// Form B 입력값을 채우고 두 번째 버튼 클릭 → 다시 성공 메시지 기대
|
|
await secondInput.fill("B 폼 값");
|
|
await page.getByRole("button", { name: "버튼" }).nth(1).click();
|
|
// 두 번째 폼에 대한 성공 메시지도 두 번째 요소로 표시되었는지 확인한다.
|
|
const successMessages = page.getByText("성공적으로 전송되었습니다.");
|
|
await expect(successMessages.nth(1)).toBeVisible();
|
|
});
|