i18n 적용
This commit is contained in:
+12
-12
@@ -2056,9 +2056,9 @@ describe("/api/export", () => {
|
||||
|
||||
const html = await exportTemplateHtml(blocks as Block[]);
|
||||
|
||||
expect(html).toContain("Hero 제목을 여기에 입력하세요");
|
||||
expect(html).toContain("제품이나 서비스를 한 문장으로 설명하는 서브텍스트입니다.");
|
||||
expect(html).toContain("지금 시작하기");
|
||||
expect(html).toContain("Your hero headline goes here");
|
||||
expect(html).toContain("Describe your product or service in a single, clear sentence.");
|
||||
expect(html).toContain("Get started now");
|
||||
});
|
||||
|
||||
it("Features 템플릿 섹션은 export HTML 에 3개의 Feature 제목과 설명 텍스트를 포함해야 한다", async () => {
|
||||
@@ -2067,11 +2067,11 @@ describe("/api/export", () => {
|
||||
|
||||
const html = await exportTemplateHtml(blocks as Block[]);
|
||||
|
||||
expect(html).toContain("Feature 1 제목");
|
||||
expect(html).toContain("Feature 2 제목");
|
||||
expect(html).toContain("Feature 3 제목");
|
||||
expect(html).toContain("Feature 1");
|
||||
expect(html).toContain("Feature 2");
|
||||
expect(html).toContain("Feature 3");
|
||||
|
||||
const descText = "해당 기능을 간단히 설명하는 텍스트입니다.";
|
||||
const descText = "A short description of this feature.";
|
||||
const occurrences = html.split(descText).length - 1;
|
||||
expect(occurrences).toBeGreaterThanOrEqual(3);
|
||||
});
|
||||
@@ -2082,8 +2082,8 @@ describe("/api/export", () => {
|
||||
|
||||
const html = await exportTemplateHtml(blocks as Block[]);
|
||||
|
||||
expect(html).toContain("지금 바로 행동을 유도하는 CTA 텍스트를 입력하세요.");
|
||||
expect(html).toContain("CTA 버튼");
|
||||
expect(html).toContain("Write a compelling CTA message here.");
|
||||
expect(html).toContain("Call to action");
|
||||
});
|
||||
|
||||
it("Footer 템플릿 섹션은 export HTML 의 마지막 섹션으로 렌더되고 푸터 텍스트를 포함해야 한다", async () => {
|
||||
@@ -2093,9 +2093,9 @@ describe("/api/export", () => {
|
||||
const html = await exportTemplateHtml(blocks as Block[]);
|
||||
|
||||
expect(html).toContain("MyLanding");
|
||||
expect(html).toContain("더 나은 웹사이트를 위한 최고의 선택.");
|
||||
expect(html).toContain("서비스 소개");
|
||||
expect(html).toContain("문의하기");
|
||||
expect(html).toContain("The best choice for building better websites.");
|
||||
expect(html).toContain("Product");
|
||||
expect(html).toContain("Contact");
|
||||
expect(html).toContain(" 2025 MyLanding.");
|
||||
|
||||
const lastSectionIndex = html.lastIndexOf("<section");
|
||||
|
||||
@@ -16,7 +16,7 @@ test("비로그인 사용자가 /projects 에 접근하면 /login 으로 리다
|
||||
await page.goto("/projects");
|
||||
|
||||
// 최종적으로 로그인 페이지의 헤더가 보여야 한다.
|
||||
await expect(page.getByRole("heading", { name: "로그인" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "Log in" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("비로그인 사용자가 /editor 에 접근하면 /login 으로 리다이렉트되어야 한다", async ({ page }) => {
|
||||
@@ -31,7 +31,7 @@ test("비로그인 사용자가 /editor 에 접근하면 /login 으로 리다이
|
||||
|
||||
await page.goto("/editor");
|
||||
|
||||
await expect(page.getByRole("heading", { name: "로그인" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "Log in" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("비로그인 사용자가 /preview 에 접근하면 /login 으로 리다이렉트되어야 한다", async ({ page }) => {
|
||||
@@ -45,7 +45,7 @@ test("비로그인 사용자가 /preview 에 접근하면 /login 으로 리다
|
||||
|
||||
await page.goto("/preview");
|
||||
|
||||
await expect(page.getByRole("heading", { name: "로그인" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "Log in" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("회원가입 후에는 /dashboard, /editor, /preview 에 정상 접근할 수 있어야 한다", async ({ page }) => {
|
||||
@@ -94,14 +94,14 @@ test("회원가입 후에는 /dashboard, /editor, /preview 에 정상 접근할
|
||||
// 1) 회원가입
|
||||
await page.goto("/signup");
|
||||
|
||||
await page.getByLabel("이메일").fill(email);
|
||||
await page.getByLabel("비밀번호", { exact: true }).fill(password);
|
||||
await page.getByLabel("비밀번호 확인").fill(password);
|
||||
await page.getByRole("button", { name: "회원가입" }).click();
|
||||
await page.getByLabel("Email").fill(email);
|
||||
await page.getByLabel("Password", { exact: true }).fill(password);
|
||||
await page.getByLabel("Confirm password").fill(password);
|
||||
await page.getByRole("button", { name: "Sign up" }).click();
|
||||
|
||||
// /dashboard 로 이동했는지, 헤더가 보이는지 확인
|
||||
await expect(page).toHaveURL(/\/dashboard/);
|
||||
await expect(page.getByRole("heading", { name: "대시보드" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
|
||||
|
||||
// 2) /editor 접근 확인 – 로그인 페이지로 리다이렉트되면 안 된다.
|
||||
await page.goto("/editor");
|
||||
|
||||
@@ -16,7 +16,7 @@ test("비로그인 사용자가 /dashboard 에 접근하면 /login 으로 리다
|
||||
|
||||
await page.goto("/dashboard");
|
||||
|
||||
await expect(page.getByRole("heading", { name: "로그인" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "Log in" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("로그인 사용자가 /dashboard 에 접근하면 요약 지표와 프로젝트 카드 리스트, 일별 그래프를 볼 수 있어야 한다", async ({ page }) => {
|
||||
@@ -61,19 +61,19 @@ test("로그인 사용자가 /dashboard 에 접근하면 요약 지표와 프로
|
||||
await page.goto("/dashboard");
|
||||
|
||||
// 대시보드 헤더가 보여야 한다.
|
||||
await expect(page.getByRole("heading", { name: "대시보드" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
|
||||
|
||||
// 상단 헤더 내비게이션이 보여야 한다.
|
||||
const dashboardLink = page.getByRole("link", { name: "대시보드" });
|
||||
const dashboardLink = page.getByRole("link", { name: "Dashboard" });
|
||||
await expect(dashboardLink).toBeVisible();
|
||||
|
||||
const projectsLink = page.getByRole("link", { name: "프로젝트 목록" });
|
||||
const projectsLink = page.getByRole("link", { name: "Projects" });
|
||||
await expect(projectsLink).toBeVisible();
|
||||
|
||||
const submissionsLink = page.getByRole("link", { name: "전체 제출 내역" });
|
||||
const submissionsLink = page.getByRole("link", { name: "All submissions" });
|
||||
await expect(submissionsLink).toBeVisible();
|
||||
|
||||
const menuButton = page.getByRole("button", { name: "메뉴" });
|
||||
const menuButton = page.getByRole("button", { name: "Menu" });
|
||||
await expect(menuButton).toBeVisible();
|
||||
|
||||
// 상단 요약 위젯의 지표들이 기대 값으로 렌더되어야 한다.
|
||||
@@ -112,7 +112,7 @@ test("로그인 사용자가 /dashboard 에 접근하면 요약 지표와 프로
|
||||
|
||||
// 메뉴 버튼을 클릭하면 로그아웃 항목이 보여야 한다.
|
||||
await menuButton.click();
|
||||
await expect(page.getByRole("button", { name: "로그아웃" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Log out" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("로그인 사용자가 / 에 접속하면 대시보드로 진입해야 한다", async ({ page }) => {
|
||||
@@ -136,5 +136,5 @@ test("로그인 사용자가 / 에 접속하면 대시보드로 진입해야 한
|
||||
await page.goto("/");
|
||||
|
||||
await expect(page).toHaveURL(/\/dashboard/);
|
||||
await expect(page.getByRole("heading", { name: "대시보드" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
|
||||
});
|
||||
|
||||
+34
-30
@@ -23,7 +23,7 @@ test("/editor 페이지가 존재하고 Page Editor 헤더를 보여줘야 한
|
||||
test("텍스트 버튼을 누르면 캔버스에 '새 텍스트' 블록이 보여야 한다", async ({ page }) => {
|
||||
await page.goto("/editor");
|
||||
|
||||
await page.getByRole("button", { name: "텍스트" }).first().click();
|
||||
await page.getByRole("button", { name: "Text" }).first().click();
|
||||
|
||||
const canvas = page.getByTestId("editor-canvas");
|
||||
await expect(canvas.getByText("새 텍스트")).toBeVisible();
|
||||
@@ -255,22 +255,22 @@ test("에디터 상태를 JSON으로 내보내고 다시 불러오면 동일한
|
||||
await sizeSelect.selectOption("lg");
|
||||
|
||||
// JSON 내보내기/불러오기 모달을 연다.
|
||||
await page.getByRole("button", { name: "메뉴 ▼" }).click();
|
||||
await page.getByRole("button", { name: "JSON 내보내기/불러오기" }).click();
|
||||
await page.getByRole("button", { name: "Menu" }).click();
|
||||
await page.getByRole("button", { name: "JSON export / import" }).click();
|
||||
|
||||
// 현재 상태를 JSON으로 내보낸다.
|
||||
await page.getByRole("button", { name: "JSON 내보내기" }).click();
|
||||
const exportArea = page.getByRole("textbox", { name: "에디터 상태 JSON" });
|
||||
await page.getByRole("button", { name: "Export JSON" }).click();
|
||||
const exportArea = page.getByRole("textbox", { name: "Editor state JSON" });
|
||||
const exportedJson = await exportArea.inputValue();
|
||||
|
||||
// 캔버스를 초기화한다.
|
||||
await page.getByRole("button", { name: "캔버스 초기화" }).click();
|
||||
await page.getByRole("button", { name: "Clear canvas" }).click();
|
||||
await expect(canvas.getByTestId("editor-block")).toHaveCount(0);
|
||||
|
||||
// JSON을 다시 입력하고 불러온다.
|
||||
const importArea = page.getByRole("textbox", { name: "JSON에서 불러오기" });
|
||||
const importArea = page.getByRole("textbox", { name: "Import from JSON" });
|
||||
await importArea.fill(exportedJson);
|
||||
await page.getByRole("button", { name: "JSON 적용하기" }).click();
|
||||
await page.getByRole("button", { name: "Apply JSON" }).click();
|
||||
|
||||
// 복원된 캔버스에 두 블록이 모두 존재해야 한다.
|
||||
const restoredBlocks = canvas.getByTestId("editor-block");
|
||||
@@ -510,9 +510,9 @@ test("에디터 메뉴에서 페이지 파일로 내보내기 (ZIP)를 클릭하
|
||||
await page.getByRole("button", { name: "텍스트" }).first().click();
|
||||
|
||||
// 헤더 메뉴를 연다.
|
||||
await page.getByRole("button", { name: "메뉴 ▼" }).click();
|
||||
await page.getByRole("button", { name: "Menu" }).click();
|
||||
|
||||
const exportButton = page.getByRole("button", { name: "페이지 파일로 내보내기 (ZIP)" });
|
||||
const exportButton = page.getByRole("button", { name: "Export as page files (ZIP)" });
|
||||
await expect(exportButton).toBeVisible();
|
||||
|
||||
const [download] = await Promise.all([
|
||||
@@ -573,10 +573,10 @@ test("Hero 템플릿 버튼을 클릭하면 섹션과 기본 텍스트/버튼
|
||||
await expect(blocks.first()).toBeVisible();
|
||||
|
||||
// Hero 헤드라인 텍스트가 포함된 블록이 보여야 한다.
|
||||
await expect(canvas.getByText("Hero 제목을 여기에 입력하세요")).toBeVisible();
|
||||
await expect(canvas.getByText("Your hero headline goes here")).toBeVisible();
|
||||
|
||||
// CTA 버튼도 렌더되어 있어야 한다.
|
||||
const ctaButton = canvas.getByRole("button", { name: "지금 시작하기" });
|
||||
const ctaButton = canvas.getByRole("button", { name: "Get started now" });
|
||||
await expect(ctaButton).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -588,9 +588,9 @@ test("Features 템플릿 버튼을 클릭하면 3개의 Feature 항목(제목/
|
||||
await page.getByRole("button", { name: "기능 템플릿" }).click();
|
||||
|
||||
// Feature 제목들이 렌더되어야 한다.
|
||||
await expect(canvas.getByText("Feature 1 제목")).toBeVisible();
|
||||
await expect(canvas.getByText("Feature 2 제목")).toBeVisible();
|
||||
await expect(canvas.getByText("Feature 3 제목")).toBeVisible();
|
||||
await expect(canvas.getByText("Feature 1")).toBeVisible();
|
||||
await expect(canvas.getByText("Feature 2")).toBeVisible();
|
||||
await expect(canvas.getByText("Feature 3")).toBeVisible();
|
||||
});
|
||||
|
||||
test("CTA 템플릿 버튼을 클릭하면 CTA 텍스트와 버튼이 포함된 섹션이 캔버스에 생성되어야 한다", async ({ page }) => {
|
||||
@@ -600,8 +600,8 @@ test("CTA 템플릿 버튼을 클릭하면 CTA 텍스트와 버튼이 포함된
|
||||
|
||||
await page.getByRole("button", { name: "CTA 템플릿" }).click();
|
||||
|
||||
await expect(canvas.getByText("지금 바로 행동을 유도하는 CTA 텍스트를 입력하세요.")).toBeVisible();
|
||||
const ctaButton = canvas.getByRole("button", { name: "CTA 버튼" });
|
||||
await expect(canvas.getByText("Write a compelling CTA message here.")).toBeVisible();
|
||||
const ctaButton = canvas.getByRole("button", { name: "Call to action" });
|
||||
await expect(ctaButton).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -613,10 +613,14 @@ test("FAQ 템플릿 버튼을 클릭하면 질문/답변 텍스트가 세 쌍
|
||||
await page.getByRole("button", { name: "FAQ 템플릿" }).click();
|
||||
|
||||
// 기본 FAQ 질문/답변 텍스트들이 렌더되어야 한다.
|
||||
await expect(canvas.getByText("서비스 이용료는 얼마인가요?")).toBeVisible();
|
||||
await expect(canvas.getByText("기본 기능은 무료로 제공되며, 프리미엄 기능은 월 구독료가 발생합니다.")).toBeVisible();
|
||||
await expect(canvas.getByText("환불 정책은 어떻게 되나요?")).toBeVisible();
|
||||
await expect(canvas.getByText("팀원 초대 기능이 있나요?")).toBeVisible();
|
||||
await expect(canvas.getByText("How much does the service cost?")).toBeVisible();
|
||||
await expect(
|
||||
canvas.getByText(
|
||||
"The core features are free, and premium features are available with a monthly subscription.",
|
||||
),
|
||||
).toBeVisible();
|
||||
await expect(canvas.getByText("What is your refund policy?")).toBeVisible();
|
||||
await expect(canvas.getByText("Can I invite teammates?")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Pricing 템플릿 버튼을 클릭하면 3개의 요금제 카드가 생성되어야 한다", async ({ page }) => {
|
||||
@@ -627,11 +631,11 @@ test("Pricing 템플릿 버튼을 클릭하면 3개의 요금제 카드가 생
|
||||
await page.getByRole("button", { name: "상품 템플릿" }).click();
|
||||
|
||||
await expect(canvas.getByText("Basic")).toBeVisible();
|
||||
await expect(canvas.getByText("₩9,900/월")).toBeVisible();
|
||||
await expect(canvas.getByText("$9/month")).toBeVisible();
|
||||
await expect(canvas.getByText("Pro")).toBeVisible();
|
||||
await expect(canvas.getByText("₩29,900/월")).toBeVisible();
|
||||
await expect(canvas.getByText("$29/month")).toBeVisible();
|
||||
await expect(canvas.getByText("Enterprise")).toBeVisible();
|
||||
await expect(canvas.getByText("문의")).toBeVisible();
|
||||
await expect(canvas.getByText("Contact us")).toBeVisible();
|
||||
});
|
||||
|
||||
test("Testimonials 템플릿 버튼을 클릭하면 3개의 후기 카드가 생성되어야 한다", async ({ page }) => {
|
||||
@@ -743,8 +747,8 @@ test("헤더의 실행 취소/다시 실행 버튼으로 블록 추가를 Undo/R
|
||||
await page.getByRole("button", { name: "텍스트" }).first().click();
|
||||
await expect(canvas.getByTestId("editor-block")).toHaveCount(1);
|
||||
|
||||
const undoButton = page.getByRole("button", { name: "실행 취소" });
|
||||
const redoButton = page.getByRole("button", { name: "다시 실행" });
|
||||
const undoButton = page.getByRole("button", { name: "Undo" });
|
||||
const redoButton = page.getByRole("button", { name: "Redo" });
|
||||
|
||||
await undoButton.click();
|
||||
await expect(canvas.getByTestId("editor-block")).toHaveCount(0);
|
||||
@@ -909,10 +913,10 @@ test("리스트 아이템을 선택하면 패널에서 아이템 위/아래/들
|
||||
await firstItem.click({ force: true });
|
||||
|
||||
// 리스트 아이템 행에 위치한 조작 버튼들을 사용할 수 있어야 한다.
|
||||
const moveUpButton = firstItem.getByRole("button", { name: "위" });
|
||||
const moveDownButton = firstItem.getByRole("button", { name: "아래" });
|
||||
const indentButton = firstItem.getByRole("button", { name: "들여쓰기" });
|
||||
const outdentButton = firstItem.getByRole("button", { name: "내어쓰기" });
|
||||
const moveUpButton = firstItem.getByRole("button", { name: "Move item up" });
|
||||
const moveDownButton = firstItem.getByRole("button", { name: "Move item down" });
|
||||
const indentButton = firstItem.getByRole("button", { name: "Indent item" });
|
||||
const outdentButton = firstItem.getByRole("button", { name: "Outdent item" });
|
||||
|
||||
await expect(moveUpButton).toBeVisible();
|
||||
await expect(moveDownButton).toBeVisible();
|
||||
|
||||
+22
-18
@@ -34,8 +34,8 @@ test("에디터에서 Hero 템플릿을 추가하면 프리뷰에서 동일한 H
|
||||
|
||||
// 헤드라인과 CTA 버튼이 에디터에서 생성되었는지 한 번 확인한다.
|
||||
const editorCanvas = page.getByTestId("editor-canvas");
|
||||
await expect(editorCanvas.getByText("Hero 제목을 여기에 입력하세요")).toBeVisible();
|
||||
await expect(editorCanvas.getByRole("button", { name: "지금 시작하기" })).toBeVisible();
|
||||
await expect(editorCanvas.getByText("Your hero headline goes here")).toBeVisible();
|
||||
await expect(editorCanvas.getByRole("button", { name: "Get started now" })).toBeVisible();
|
||||
|
||||
// 헤더의 프리뷰 열기 링크를 통해 /preview 로 이동한다 (클라이언트 내 내비게이션 유지).
|
||||
await page.getByRole("link", { name: "프리뷰 열기" }).click();
|
||||
@@ -44,9 +44,9 @@ test("에디터에서 Hero 템플릿을 추가하면 프리뷰에서 동일한 H
|
||||
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
||||
|
||||
// 프리뷰에서도 Hero 헤드라인과 CTA 버튼이 그대로 보여야 한다.
|
||||
await expect(page.getByText("Hero 제목을 여기에 입력하세요")).toBeVisible();
|
||||
await expect(page.getByText("Your hero headline goes here")).toBeVisible();
|
||||
// 프리뷰에서는 CTA를 실제 링크(<a>)로 렌더링하므로 텍스트 기준으로만 검증한다.
|
||||
await expect(page.getByText("지금 시작하기")).toBeVisible();
|
||||
await expect(page.getByText("Get started now")).toBeVisible();
|
||||
|
||||
// 프리뷰 화면에는 에디터용 "텍스트" 버튼이 없어야 한다.
|
||||
await expect(page.getByRole("button", { name: "텍스트" })).toHaveCount(0);
|
||||
@@ -73,17 +73,17 @@ test("에디터에서 Features 템플릿을 추가하면 프리뷰에서 3개의
|
||||
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 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();
|
||||
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 }) => {
|
||||
@@ -92,15 +92,15 @@ test("에디터에서 CTA 템플릿을 추가하면 프리뷰에서 CTA 텍스
|
||||
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 expect(editorCanvas.getByText("Write a compelling CTA message here.")).toBeVisible();
|
||||
await expect(editorCanvas.getByRole("button", { name: "Call to action" })).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();
|
||||
await expect(page.getByText("Write a compelling CTA message here.")).toBeVisible();
|
||||
await expect(page.getByText("Call to action")).toBeVisible();
|
||||
});
|
||||
|
||||
test("모바일 뷰에서도 프리뷰 페이지가 깨지지 않고 주요 템플릿 컨텐츠를 보여줘야 한다", async ({ page }) => {
|
||||
@@ -122,17 +122,21 @@ test("모바일 뷰에서도 프리뷰 페이지가 깨지지 않고 주요 템
|
||||
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
||||
|
||||
// Hero 헤드라인
|
||||
await expect(page.getByText("Hero 제목을 여기에 입력하세요")).toBeVisible();
|
||||
await expect(page.getByText("Your hero headline goes here")).toBeVisible();
|
||||
|
||||
// Features 제목 일부
|
||||
await expect(page.getByText("Feature 1 제목")).toBeVisible();
|
||||
await expect(page.getByText("Feature 1")).toBeVisible();
|
||||
|
||||
// Pricing 플랜 이름/가격 일부
|
||||
await expect(page.getByText("Basic")).toBeVisible();
|
||||
await expect(page.getByText("₩9,900/월")).toBeVisible();
|
||||
await expect(page.getByText("$9/month")).toBeVisible();
|
||||
|
||||
// Testimonials 본문 일부
|
||||
await expect(page.getByText("이 서비스 덕분에 랜딩 페이지 제작 시간이 크게 줄었습니다.")).toBeVisible();
|
||||
await expect(
|
||||
page.getByText(
|
||||
"This builder dramatically reduced the time we spend creating landing pages.",
|
||||
),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test("텍스트 블록의 정렬/폰트 크기/색상이 프리뷰에도 반영되어야 한다", async ({ page }) => {
|
||||
|
||||
@@ -209,7 +209,7 @@ test("프로젝트 목록에서 '폼 제출 내역' 링크를 클릭하면 해
|
||||
await expect(page.getByText("test-project-a")).toBeVisible();
|
||||
|
||||
// 액션 영역의 "폼 제출 내역" 링크를 클릭하면 제출 내역 페이지로 이동해야 한다.
|
||||
await page.getByRole("link", { name: "폼 제출 내역" }).click();
|
||||
await page.getByRole("link", { name: "Form submissions" }).click();
|
||||
|
||||
await expect(page).toHaveURL(/\/projects\/test-project-a\/submissions/);
|
||||
|
||||
@@ -284,10 +284,10 @@ test("프로젝트 목록 헤더에서 대시보드로 이동할 수 있어야
|
||||
|
||||
await expect(page.getByText("테스트 프로젝트 A")).toBeVisible();
|
||||
|
||||
await page.getByRole("link", { name: "대시보드" }).click();
|
||||
await page.getByRole("link", { name: "Dashboard" }).click();
|
||||
|
||||
await expect(page).toHaveURL(/\/dashboard/);
|
||||
await expect(page.getByRole("heading", { name: "대시보드" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("전체 제출 내역 페이지에서 공통 GNB와 메뉴가 보여야 한다", async ({ page }) => {
|
||||
@@ -309,20 +309,20 @@ test("전체 제출 내역 페이지에서 공통 GNB와 메뉴가 보여야 한
|
||||
|
||||
await page.goto("/projects/submissions");
|
||||
|
||||
await expect(page.getByRole("heading", { name: "전체 폼 제출 내역" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "All form submissions" })).toBeVisible();
|
||||
|
||||
const dashboardLink = page.getByRole("link", { name: "대시보드" });
|
||||
const dashboardLink = page.getByRole("link", { name: "Dashboard" });
|
||||
await expect(dashboardLink).toBeVisible();
|
||||
|
||||
const projectsLink = page.getByRole("link", { name: "프로젝트 목록" });
|
||||
const projectsLink = page.getByRole("link", { name: "Projects" });
|
||||
await expect(projectsLink).toBeVisible();
|
||||
|
||||
const submissionsLink = page.getByRole("link", { name: "전체 제출 내역" });
|
||||
const submissionsLink = page.getByRole("link", { name: "All submissions" });
|
||||
await expect(submissionsLink).toBeVisible();
|
||||
|
||||
const menuButton = page.getByRole("button", { name: "메뉴" });
|
||||
const menuButton = page.getByRole("button", { name: "Menu" });
|
||||
await expect(menuButton).toBeVisible();
|
||||
|
||||
await menuButton.click();
|
||||
await expect(page.getByRole("button", { name: "로그아웃" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Log out" })).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -73,7 +73,7 @@ describe("AllProjectSubmissionsPage - 전체 폼 제출 내역", () => {
|
||||
expect(url).toBe("/api/projects/submissions");
|
||||
expect(options).toBeUndefined();
|
||||
|
||||
expect(await screen.findByText("전체 폼 제출 내역")).toBeTruthy();
|
||||
expect(await screen.findByText("All form submissions")).toBeTruthy();
|
||||
|
||||
expect(screen.getByText("프로젝트 1")).toBeTruthy();
|
||||
expect(screen.getByText("proj-1")).toBeTruthy();
|
||||
@@ -126,7 +126,7 @@ describe("AllProjectSubmissionsPage - 전체 폼 제출 내역", () => {
|
||||
render(<AllProjectSubmissionsPage />);
|
||||
|
||||
const errorText = await screen.findByText(
|
||||
"폼 제출 내역을 불러오는 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.",
|
||||
"An error occurred while loading form submissions. Please try again later.",
|
||||
);
|
||||
|
||||
expect(errorText).toBeTruthy();
|
||||
@@ -144,7 +144,7 @@ describe("AllProjectSubmissionsPage - 전체 폼 제출 내역", () => {
|
||||
|
||||
render(<AllProjectSubmissionsPage />);
|
||||
|
||||
const backLink = await screen.findByRole("link", { name: "프로젝트 목록" });
|
||||
const backLink = await screen.findByRole("link", { name: "Projects" });
|
||||
expect(backLink).toBeTruthy();
|
||||
expect(backLink.closest("a")?.getAttribute("href")).toBe("/projects");
|
||||
});
|
||||
@@ -161,13 +161,13 @@ describe("AllProjectSubmissionsPage - 전체 폼 제출 내역", () => {
|
||||
|
||||
render(<AllProjectSubmissionsPage />);
|
||||
|
||||
const dashboardLink = await screen.findByRole("link", { name: "대시보드" });
|
||||
const dashboardLink = await screen.findByRole("link", { name: "Dashboard" });
|
||||
expect(dashboardLink.closest("a")?.getAttribute("href")).toBe("/dashboard");
|
||||
|
||||
const projectsLink = await screen.findByRole("link", { name: "프로젝트 목록" });
|
||||
const projectsLink = await screen.findByRole("link", { name: "Projects" });
|
||||
expect(projectsLink.closest("a")?.getAttribute("href")).toBe("/projects");
|
||||
|
||||
const submissionsLink = await screen.findByRole("link", { name: "전체 제출 내역" });
|
||||
const submissionsLink = await screen.findByRole("link", { name: "All submissions" });
|
||||
expect(submissionsLink.closest("a")?.getAttribute("href")).toBe("/projects/submissions");
|
||||
});
|
||||
|
||||
@@ -237,9 +237,9 @@ describe("AllProjectSubmissionsPage - 전체 폼 제출 내역", () => {
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
const dashboardLink = await screen.findByRole("link", { name: "대시보드" });
|
||||
const projectsLink = await screen.findByRole("link", { name: "프로젝트 목록" });
|
||||
const submissionsLink = await screen.findByRole("link", { name: "전체 제출 내역" });
|
||||
const dashboardLink = await screen.findByRole("link", { name: "Dashboard" });
|
||||
const projectsLink = await screen.findByRole("link", { name: "Projects" });
|
||||
const submissionsLink = await screen.findByRole("link", { name: "All submissions" });
|
||||
|
||||
expect(submissionsLink.getAttribute("href")).toBe("/projects/submissions");
|
||||
expect(submissionsLink.getAttribute("aria-current")).toBe("page");
|
||||
@@ -273,10 +273,10 @@ describe("AllProjectSubmissionsPage - 전체 폼 제출 내역", () => {
|
||||
|
||||
render(<AllProjectSubmissionsPage />);
|
||||
|
||||
const menuButton = await screen.findByRole("button", { name: "메뉴" });
|
||||
const menuButton = await screen.findByRole("button", { name: "Menu" });
|
||||
menuButton.click();
|
||||
|
||||
const logoutButton = await screen.findByRole("button", { name: "로그아웃" });
|
||||
const logoutButton = await screen.findByRole("button", { name: "Log out" });
|
||||
logoutButton.click();
|
||||
|
||||
await waitFor(() => {
|
||||
|
||||
@@ -54,7 +54,8 @@ describe("BlocksSidebar - 템플릿 버튼", () => {
|
||||
|
||||
it("Hero 템플릿 버튼 클릭 시 addHeroTemplateSection 이 호출되어야 한다", () => {
|
||||
render(<BlocksSidebar />);
|
||||
const button = screen.getByRole("button", { name: "Hero 템플릿" });
|
||||
|
||||
const button = screen.getByRole("button", { name: "Hero template" });
|
||||
fireEvent.click(button);
|
||||
|
||||
expect(templateActions.addHeroTemplateSection).toHaveBeenCalledTimes(1);
|
||||
@@ -62,7 +63,8 @@ describe("BlocksSidebar - 템플릿 버튼", () => {
|
||||
|
||||
it("Features 템플릿 버튼 클릭 시 addFeaturesTemplateSection 이 호출되어야 한다", () => {
|
||||
render(<BlocksSidebar />);
|
||||
const button = screen.getByRole("button", { name: "기능 템플릿" });
|
||||
|
||||
const button = screen.getByRole("button", { name: "Features template" });
|
||||
fireEvent.click(button);
|
||||
|
||||
expect(templateActions.addFeaturesTemplateSection).toHaveBeenCalledTimes(1);
|
||||
@@ -70,7 +72,8 @@ describe("BlocksSidebar - 템플릿 버튼", () => {
|
||||
|
||||
it("CTA 템플릿 버튼 클릭 시 addCtaTemplateSection 이 호출되어야 한다", () => {
|
||||
render(<BlocksSidebar />);
|
||||
const button = screen.getByRole("button", { name: "CTA 템플릿" });
|
||||
|
||||
const button = screen.getByRole("button", { name: "CTA template" });
|
||||
fireEvent.click(button);
|
||||
|
||||
expect(templateActions.addCtaTemplateSection).toHaveBeenCalledTimes(1);
|
||||
@@ -78,12 +81,13 @@ describe("BlocksSidebar - 템플릿 버튼", () => {
|
||||
|
||||
it("FAQ/Pricing/Testimonials/Blog/Team/Footer 템플릿 버튼도 각각의 액션을 호출해야 한다", () => {
|
||||
render(<BlocksSidebar />);
|
||||
fireEvent.click(screen.getByRole("button", { name: "FAQ 템플릿" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "상품 템플릿" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "후기 템플릿" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "블로그 템플릿" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Team 템플릿" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Footer 템플릿" }));
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "FAQ template" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Pricing template" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Testimonials template" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Blog template" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Team template" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Footer template" }));
|
||||
|
||||
expect(templateActions.addFaqTemplateSection).toHaveBeenCalledTimes(1);
|
||||
expect(templateActions.addPricingTemplateSection).toHaveBeenCalledTimes(1);
|
||||
@@ -96,20 +100,21 @@ describe("BlocksSidebar - 템플릿 버튼", () => {
|
||||
it("템플릿 섹션에는 각 템플릿의 용도를 설명하는 텍스트가 함께 표시되어야 한다", () => {
|
||||
render(<BlocksSidebar />);
|
||||
|
||||
expect(screen.getByText("페이지 상단 Hero 섹션 (큰 제목 + 서브텍스트 + 버튼)"));
|
||||
expect(screen.getByText("3컬럼 기능 소개 섹션"));
|
||||
expect(screen.getByText("콜투액션(CTA) 섹션"));
|
||||
expect(screen.getByText("자주 묻는 질문(FAQ) 섹션"));
|
||||
expect(screen.getByText("요금제/플랜 소개 섹션"));
|
||||
expect(screen.getByText("고객 후기(Testimonials) 섹션"));
|
||||
expect(screen.getByText("블로그 포스트 목록 섹션"));
|
||||
expect(screen.getByText("팀 소개 섹션"));
|
||||
expect(screen.getByText("페이지 푸터 섹션"));
|
||||
expect(screen.getByText("Top-of-page hero section (headline + subtext + button)"));
|
||||
expect(screen.getByText("Three-column features section"));
|
||||
expect(screen.getByText("Call-to-action (CTA) section"));
|
||||
expect(screen.getByText("Frequently asked questions (FAQ) section"));
|
||||
expect(screen.getByText("Pricing plans section"));
|
||||
expect(screen.getByText("Customer testimonials section"));
|
||||
expect(screen.getByText("Blog post listing section"));
|
||||
expect(screen.getByText("Team members section"));
|
||||
expect(screen.getByText("Page footer section"));
|
||||
});
|
||||
|
||||
it("비디오 버튼 클릭 시 addVideoBlock 이 호출되어야 한다", () => {
|
||||
render(<BlocksSidebar />);
|
||||
const button = screen.getByRole("button", { name: "비디오" });
|
||||
|
||||
const button = screen.getByRole("button", { name: "Video" });
|
||||
fireEvent.click(button);
|
||||
|
||||
expect(otherActions.addVideoBlock).toHaveBeenCalledTimes(1);
|
||||
@@ -118,10 +123,10 @@ describe("BlocksSidebar - 템플릿 버튼", () => {
|
||||
it("템플릿들은 카테고리별로 그룹 헤더를 가져야 한다", () => {
|
||||
render(<BlocksSidebar />);
|
||||
|
||||
expect(screen.getByText("히어로 · CTA")).toBeDefined();
|
||||
expect(screen.getByText("콘텐츠 섹션")).toBeDefined();
|
||||
expect(screen.getByText("신뢰/소개")).toBeDefined();
|
||||
expect(screen.getByText("푸터/기타")).toBeDefined();
|
||||
expect(screen.getByText("Hero · CTA")).toBeDefined();
|
||||
expect(screen.getByText("Content sections")).toBeDefined();
|
||||
expect(screen.getByText("Trust & about")).toBeDefined();
|
||||
expect(screen.getByText("Footer & misc")).toBeDefined();
|
||||
});
|
||||
|
||||
it("각 템플릿 카드에는 미니 썸네일(레이아웃 힌트)가 렌더링되어야 한다", () => {
|
||||
@@ -165,57 +170,57 @@ describe("BlocksSidebar - 템플릿 버튼", () => {
|
||||
render(<BlocksSidebar />);
|
||||
|
||||
// 초기에는 텍스트 버튼이 보여야 한다.
|
||||
expect(screen.getByRole("button", { name: "텍스트" })).toBeDefined();
|
||||
expect(screen.getByRole("button", { name: "Text" })).toBeDefined();
|
||||
|
||||
const blockToggle = screen.getByRole("button", { name: "블록" });
|
||||
const blockToggle = screen.getByRole("button", { name: "Blocks" });
|
||||
fireEvent.click(blockToggle);
|
||||
|
||||
// 접힌 상태에서는 텍스트 버튼이 보이지 않아야 한다.
|
||||
expect(screen.queryByRole("button", { name: "텍스트" })).toBeNull();
|
||||
expect(screen.queryByRole("button", { name: "Text" })).toBeNull();
|
||||
|
||||
// 다시 클릭하면 펼쳐져야 한다.
|
||||
fireEvent.click(blockToggle);
|
||||
expect(screen.getByRole("button", { name: "텍스트" })).toBeDefined();
|
||||
expect(screen.getByRole("button", { name: "Text" })).toBeDefined();
|
||||
});
|
||||
|
||||
it("폼 요소 섹션 타이틀을 클릭하면 폼 관련 버튼들을 접고 펼 수 있어야 한다", () => {
|
||||
render(<BlocksSidebar />);
|
||||
|
||||
// 초기에는 폼 입력 버튼이 보여야 한다.
|
||||
expect(screen.getByRole("button", { name: "입력(Input)" })).toBeDefined();
|
||||
expect(screen.getByRole("button", { name: "Input field" })).toBeDefined();
|
||||
|
||||
const formToggle = screen.getByRole("button", { name: "폼 요소" });
|
||||
const formToggle = screen.getByRole("button", { name: "Form elements" });
|
||||
fireEvent.click(formToggle);
|
||||
|
||||
// 접힌 상태에서는 폼 입력 버튼이 보이지 않아야 한다.
|
||||
expect(screen.queryByRole("button", { name: "입력(Input)" })).toBeNull();
|
||||
expect(screen.queryByRole("button", { name: "Input field" })).toBeNull();
|
||||
|
||||
// 다시 클릭하면 펼쳐져야 한다.
|
||||
fireEvent.click(formToggle);
|
||||
expect(screen.getByRole("button", { name: "입력(Input)" })).toBeDefined();
|
||||
expect(screen.getByRole("button", { name: "Input field" })).toBeDefined();
|
||||
});
|
||||
|
||||
it("템플릿 섹션 타이틀을 클릭하면 템플릿 카드들을 접고 펼 수 있어야 한다", () => {
|
||||
render(<BlocksSidebar />);
|
||||
|
||||
// 초기에는 Hero 템플릿 버튼이 보여야 한다.
|
||||
expect(screen.getByRole("button", { name: "Hero 템플릿" })).toBeDefined();
|
||||
expect(screen.getByRole("button", { name: "Hero template" })).toBeDefined();
|
||||
|
||||
const templateToggle = screen.getByRole("button", { name: "템플릿" });
|
||||
const templateToggle = screen.getByRole("button", { name: "Templates" });
|
||||
fireEvent.click(templateToggle);
|
||||
|
||||
// 접힌 상태에서는 Hero 템플릿 버튼이 보이지 않아야 한다.
|
||||
expect(screen.queryByRole("button", { name: "Hero 템플릿" })).toBeNull();
|
||||
expect(screen.queryByRole("button", { name: "Hero template" })).toBeNull();
|
||||
|
||||
// 다시 클릭하면 펼쳐져야 한다.
|
||||
fireEvent.click(templateToggle);
|
||||
expect(screen.getByRole("button", { name: "Hero 템플릿" })).toBeDefined();
|
||||
expect(screen.getByRole("button", { name: "Hero template" })).toBeDefined();
|
||||
});
|
||||
|
||||
it("블록 추가 버튼들은 라이트/다크 테마에 맞는 배경/텍스트 클래스를 사용해야 한다", () => {
|
||||
render(<BlocksSidebar />);
|
||||
|
||||
const textButton = screen.getByRole("button", { name: "텍스트" });
|
||||
const textButton = screen.getByRole("button", { name: "Text" });
|
||||
const className = textButton.getAttribute("class") ?? "";
|
||||
|
||||
// 라이트 모드: 밝은 배경 + 어두운 텍스트
|
||||
@@ -231,7 +236,7 @@ describe("BlocksSidebar - 템플릿 버튼", () => {
|
||||
render(<BlocksSidebar />);
|
||||
|
||||
const heroDescription = screen.getByText(
|
||||
"페이지 상단 Hero 섹션 (큰 제목 + 서브텍스트 + 버튼)",
|
||||
"Top-of-page hero section (headline + subtext + button)",
|
||||
);
|
||||
const card = heroDescription.closest("div");
|
||||
expect(card).not.toBeNull();
|
||||
@@ -246,7 +251,7 @@ describe("BlocksSidebar - 템플릿 버튼", () => {
|
||||
it("히어로 템플릿 추가 버튼은 라이트/다크 테마에 맞는 배경/텍스트 클래스를 사용해야 한다", () => {
|
||||
render(<BlocksSidebar />);
|
||||
|
||||
const heroButton = screen.getByRole("button", { name: "Hero 템플릿" });
|
||||
const heroButton = screen.getByRole("button", { name: "Hero template" });
|
||||
const className = heroButton.getAttribute("class") ?? "";
|
||||
|
||||
// 라이트 모드: 밝은 배경 + 어두운 텍스트
|
||||
|
||||
@@ -31,7 +31,7 @@ describe("ButtonPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const hexInput = screen.getByLabelText("버튼 텍스트 색상 HEX");
|
||||
const hexInput = screen.getByLabelText("Button text color HEX");
|
||||
fireEvent.change(hexInput, { target: { value: "#112233" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -51,7 +51,7 @@ describe("ButtonPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const hexInput = screen.getByLabelText("버튼 채움 색상 HEX");
|
||||
const hexInput = screen.getByLabelText("Button fill color HEX");
|
||||
fireEvent.change(hexInput, { target: { value: "#445566" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -71,7 +71,7 @@ describe("ButtonPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const hexInput = screen.getByLabelText("버튼 외곽선 색상 HEX");
|
||||
const hexInput = screen.getByLabelText("Button border color HEX");
|
||||
fireEvent.change(hexInput, { target: { value: "#778899" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -80,7 +80,7 @@ describe("ButtonPropertiesPanel", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("텍스트 색상 팔레트에서 \"없음\" 을 선택하면 textColorCustom 이 빈 문자열로 업데이트되어야 한다", () => {
|
||||
it("텍스트 색상 팔레트에서 \"None\" 을 선택하면 textColorCustom 이 빈 문자열로 업데이트되어야 한다", () => {
|
||||
const updateBlock = vi.fn();
|
||||
|
||||
render(
|
||||
@@ -92,11 +92,11 @@ describe("ButtonPropertiesPanel", () => {
|
||||
);
|
||||
|
||||
// 첫 번째 ColorPickerField(텍스트 색상)의 팔레트 버튼을 연다.
|
||||
const paletteButtons = screen.getAllByText("색상 팔레트");
|
||||
const paletteButtons = screen.getAllByText("Color palette");
|
||||
fireEvent.click(paletteButtons[0].closest("button") as HTMLButtonElement);
|
||||
|
||||
// 팔레트에서 "없음" 항목을 버튼 role 기준으로 선택한다.
|
||||
const noneButtons = screen.getAllByRole("button", { name: "없음" });
|
||||
// 팔레트에서 "None" 항목을 버튼 role 기준으로 선택한다.
|
||||
const noneButtons = screen.getAllByRole("button", { name: "None" });
|
||||
fireEvent.click(noneButtons[0]);
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -116,7 +116,7 @@ describe("ButtonPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const hexInput = screen.getByLabelText("버튼 텍스트 색상 HEX") as HTMLInputElement;
|
||||
const hexInput = screen.getByLabelText("Button text color HEX") as HTMLInputElement;
|
||||
expect(hexInput.value).toBe("");
|
||||
});
|
||||
|
||||
@@ -131,7 +131,7 @@ describe("ButtonPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByLabelText("버튼 너비 모드");
|
||||
const select = screen.getByLabelText("Button width mode");
|
||||
fireEvent.change(select, { target: { value: "fixed" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -155,7 +155,7 @@ describe("ButtonPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const urlInput = screen.getByLabelText("버튼 이미지 URL");
|
||||
const urlInput = screen.getByLabelText("Button image URL");
|
||||
fireEvent.change(urlInput, { target: { value: "https://example.com/new.png" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -183,7 +183,7 @@ describe("ButtonPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByLabelText("버튼 이미지 위치");
|
||||
const select = screen.getByLabelText("Button image placement");
|
||||
fireEvent.change(select, { target: { value: "right" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -207,7 +207,7 @@ describe("ButtonPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByLabelText("버튼 이미지 소스");
|
||||
const select = screen.getByLabelText("Button image source");
|
||||
// 업로드 → URL 로 전환
|
||||
fireEvent.change(select, { target: { value: "url" } });
|
||||
|
||||
@@ -231,7 +231,7 @@ describe("ButtonPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const textarea = screen.getByLabelText("버튼 텍스트");
|
||||
const textarea = screen.getByLabelText("Button text");
|
||||
fireEvent.change(textarea, { target: { value: "새 버튼" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -251,7 +251,7 @@ describe("ButtonPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const textarea = screen.getByLabelText("버튼 텍스트") as HTMLTextAreaElement;
|
||||
const textarea = screen.getByLabelText("Button text") as HTMLTextAreaElement;
|
||||
const className = textarea.getAttribute("class") ?? "";
|
||||
|
||||
// 라이트 모드: 흰 배경 + 어두운 텍스트
|
||||
@@ -273,7 +273,7 @@ describe("ButtonPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const slider = screen.getByLabelText("가로 패딩 (px) 슬라이더");
|
||||
const slider = screen.getByLabelText("Horizontal padding (px) 슬라이더");
|
||||
fireEvent.change(slider, { target: { value: "24" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -293,7 +293,7 @@ describe("ButtonPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const slider = screen.getByLabelText("세로 패딩 (px) 슬라이더");
|
||||
const slider = screen.getByLabelText("Vertical padding (px) 슬라이더");
|
||||
fireEvent.change(slider, { target: { value: "18" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -313,7 +313,7 @@ describe("ButtonPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByLabelText("버튼 링크");
|
||||
const input = screen.getByLabelText("Button link");
|
||||
fireEvent.change(input, { target: { value: "/signup" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -333,7 +333,7 @@ describe("ButtonPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByLabelText("버튼 정렬");
|
||||
const select = screen.getByLabelText("Button alignment");
|
||||
fireEvent.change(select, { target: { value: "center" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -353,7 +353,7 @@ describe("ButtonPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByLabelText("버튼 스타일");
|
||||
const select = screen.getByLabelText("Button style");
|
||||
fireEvent.change(select, { target: { value: "outline" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -373,7 +373,7 @@ describe("ButtonPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const slider = screen.getByLabelText("모서리 둥글기 슬라이더");
|
||||
const slider = screen.getByLabelText("Border radius 슬라이더");
|
||||
fireEvent.change(slider, { target: { value: "4" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -393,7 +393,7 @@ describe("ButtonPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const slider = screen.getByLabelText("버튼 고정 너비 슬라이더");
|
||||
const slider = screen.getByLabelText("Button fixed width 슬라이더");
|
||||
fireEvent.change(slider, { target: { value: "300" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -413,7 +413,7 @@ describe("ButtonPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const slider = screen.getByLabelText("버튼 크기 슬라이더");
|
||||
const slider = screen.getByLabelText("Button font size 슬라이더");
|
||||
fireEvent.change(slider, { target: { value: "20" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -433,7 +433,7 @@ describe("ButtonPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const slider = screen.getByLabelText("줄 간격 슬라이더");
|
||||
const slider = screen.getByLabelText("Line height 슬라이더");
|
||||
fireEvent.change(slider, { target: { value: "1.8" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -453,7 +453,7 @@ describe("ButtonPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const slider = screen.getByLabelText("글자 간격 슬라이더");
|
||||
const slider = screen.getByLabelText("Letter spacing 슬라이더");
|
||||
fireEvent.change(slider, { target: { value: "16" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -473,7 +473,7 @@ describe("ButtonPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const labelTextarea = screen.getByLabelText("버튼 텍스트") as HTMLTextAreaElement;
|
||||
const labelTextarea = screen.getByLabelText("Button text") as HTMLTextAreaElement;
|
||||
const labelClass = labelTextarea.getAttribute("class") ?? "";
|
||||
expect(labelClass).toContain("bg-white");
|
||||
expect(labelClass).toContain("text-slate-900");
|
||||
@@ -482,7 +482,7 @@ describe("ButtonPropertiesPanel", () => {
|
||||
expect(labelClass).toContain("dark:text-slate-100");
|
||||
expect(labelClass).toContain("dark:border-slate-700");
|
||||
|
||||
const imageSourceSelect = screen.getByLabelText("버튼 이미지 소스") as HTMLSelectElement;
|
||||
const imageSourceSelect = screen.getByLabelText("Button image source") as HTMLSelectElement;
|
||||
const imageSourceClass = imageSourceSelect.getAttribute("class") ?? "";
|
||||
expect(imageSourceClass).toContain("bg-white");
|
||||
expect(imageSourceClass).toContain("text-slate-900");
|
||||
@@ -491,7 +491,7 @@ describe("ButtonPropertiesPanel", () => {
|
||||
expect(imageSourceClass).toContain("dark:text-slate-100");
|
||||
expect(imageSourceClass).toContain("dark:border-slate-700");
|
||||
|
||||
const alignSelect = screen.getByLabelText("버튼 정렬") as HTMLSelectElement;
|
||||
const alignSelect = screen.getByLabelText("Button alignment") as HTMLSelectElement;
|
||||
const alignClass = alignSelect.getAttribute("class") ?? "";
|
||||
expect(alignClass).toContain("bg-white");
|
||||
expect(alignClass).toContain("text-slate-900");
|
||||
|
||||
@@ -38,7 +38,7 @@ describe("ColorPickerField - 테마", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const [label] = screen.getAllByText("색상 팔레트");
|
||||
const [label] = screen.getAllByText("Color palette");
|
||||
const button = label.closest("button") as HTMLButtonElement | null;
|
||||
expect(button).not.toBeNull();
|
||||
|
||||
@@ -65,14 +65,13 @@ describe("ColorPickerField - 테마", () => {
|
||||
);
|
||||
|
||||
// 드롭다운을 연다
|
||||
const [label] = screen.getAllByText("색상 팔레트");
|
||||
const [label] = screen.getAllByText("Color palette");
|
||||
const toggleButton = label.closest("button") as HTMLButtonElement | null;
|
||||
expect(toggleButton).not.toBeNull();
|
||||
fireEvent.click(toggleButton!);
|
||||
|
||||
// 팔레트 첫 항목 버튼을 찾는다 (TEXT_COLOR_PALETTE[0] 라벨과 동기화)
|
||||
const firstItem = TEXT_COLOR_PALETTE[0];
|
||||
const itemButton = screen.getByRole("button", { name: firstItem.label }) as HTMLButtonElement;
|
||||
// 팔레트 첫 항목 버튼(기본값: None)을 role/name 기준으로 찾는다
|
||||
const itemButton = screen.getByRole("button", { name: "None" }) as HTMLButtonElement;
|
||||
const className = itemButton.getAttribute("class") ?? "";
|
||||
|
||||
// 라이트 모드: 흰 배경 + 어두운 텍스트
|
||||
|
||||
@@ -42,9 +42,9 @@ describe("DashboardPage - GNB와 테마", () => {
|
||||
|
||||
render(<DashboardPage />);
|
||||
|
||||
const dashboardLink = await screen.findByRole("link", { name: "대시보드" });
|
||||
const projectsLink = await screen.findByRole("link", { name: "프로젝트 목록" });
|
||||
const submissionsLink = await screen.findByRole("link", { name: "전체 제출 내역" });
|
||||
const dashboardLink = await screen.findByRole("link", { name: "Dashboard" });
|
||||
const projectsLink = await screen.findByRole("link", { name: "Projects" });
|
||||
const submissionsLink = await screen.findByRole("link", { name: "All submissions" });
|
||||
|
||||
expect(dashboardLink.getAttribute("href")).toBe("/dashboard");
|
||||
expect(dashboardLink.getAttribute("aria-current")).toBe("page");
|
||||
@@ -79,7 +79,7 @@ describe("DashboardPage - GNB와 테마", () => {
|
||||
const html = document.documentElement;
|
||||
html.classList.remove("dark");
|
||||
|
||||
const themeToggleButton = await screen.findByRole("button", { name: "테마 전환" });
|
||||
const themeToggleButton = await screen.findByRole("button", { name: "Toggle theme" });
|
||||
|
||||
expect(html.classList.contains("dark")).toBe(false);
|
||||
|
||||
@@ -91,4 +91,51 @@ describe("DashboardPage - GNB와 테마", () => {
|
||||
|
||||
expect(html.classList.contains("dark")).toBe(false);
|
||||
});
|
||||
|
||||
it("대시보드 요약 카드와 프로젝트 카드가 라이트/다크 테마에서 읽기 쉬운 텍스트 색상 클래스를 사용해야 한다", async () => {
|
||||
const overview = {
|
||||
summaryStats: {
|
||||
totalProjects: 1,
|
||||
totalSubmissions: 10,
|
||||
todaySubmissions: 2,
|
||||
last7DaysSubmissions: 5,
|
||||
},
|
||||
projectStats: [
|
||||
{
|
||||
projectId: "p1",
|
||||
title: "Test project 1",
|
||||
slug: "test-project-1",
|
||||
status: "DRAFT",
|
||||
totalSubmissions: 3,
|
||||
lastSubmissionAt: "2025-01-01T00:00:00.000Z",
|
||||
},
|
||||
],
|
||||
dailySubmissions: [],
|
||||
};
|
||||
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify(overview), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
);
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock as any);
|
||||
|
||||
const { container } = render(<DashboardPage />);
|
||||
|
||||
// 요약 카드가 렌더링될 때까지 기다린다.
|
||||
await screen.findByTestId("dashboard-summary-total-projects");
|
||||
|
||||
const summaryCard = container.querySelector(
|
||||
'[data-testid="dashboard-summary-total-projects"]',
|
||||
) as HTMLElement | null;
|
||||
expect(summaryCard).not.toBeNull();
|
||||
|
||||
const summaryLabel = summaryCard!.querySelector("span") as HTMLElement | null;
|
||||
expect(summaryLabel).not.toBeNull();
|
||||
const summaryLabelClass = summaryLabel!.className;
|
||||
expect(summaryLabelClass).toContain("text-slate-500");
|
||||
expect(summaryLabelClass).toContain("dark:text-slate-400");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,7 +32,7 @@ describe("DividerPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByLabelText("구분선 정렬");
|
||||
const select = screen.getByLabelText("Divider alignment");
|
||||
fireEvent.change(select, { target: { value: "center" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -52,7 +52,7 @@ describe("DividerPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByLabelText("구분선 두께");
|
||||
const select = screen.getByLabelText("Divider thickness");
|
||||
fireEvent.change(select, { target: { value: "medium" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -72,7 +72,7 @@ describe("DividerPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByLabelText("구분선 길이 모드");
|
||||
const select = screen.getByLabelText("Divider length mode");
|
||||
fireEvent.change(select, { target: { value: "fixed" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -92,7 +92,7 @@ describe("DividerPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const widthInput = screen.getByLabelText("고정 길이 (px) 커스텀 (px)");
|
||||
const widthInput = screen.getByLabelText("Fixed length (px) 커스텀 (px)");
|
||||
fireEvent.change(widthInput, { target: { value: "480" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -112,7 +112,7 @@ describe("DividerPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const hexInputs = screen.getAllByLabelText("구분선 색상 HEX");
|
||||
const hexInputs = screen.getAllByLabelText("Divider color HEX");
|
||||
const hexInput = hexInputs[0] as HTMLInputElement;
|
||||
fireEvent.change(hexInput, { target: { value: "#123456" } });
|
||||
|
||||
@@ -133,7 +133,7 @@ describe("DividerPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const marginInputs = screen.getAllByLabelText("위/아래 여백 커스텀 (px)");
|
||||
const marginInputs = screen.getAllByLabelText("Vertical margin 커스텀 (px)");
|
||||
const marginInput = marginInputs[0] as HTMLInputElement;
|
||||
|
||||
fireEvent.change(marginInput, { target: { value: "24" } });
|
||||
@@ -155,7 +155,7 @@ describe("DividerPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const alignSelect = screen.getByLabelText("구분선 정렬") as HTMLSelectElement;
|
||||
const alignSelect = screen.getByLabelText("Divider alignment") as HTMLSelectElement;
|
||||
const alignClass = alignSelect.getAttribute("class") ?? "";
|
||||
expect(alignClass).toContain("bg-white");
|
||||
expect(alignClass).toContain("text-slate-900");
|
||||
@@ -164,7 +164,7 @@ describe("DividerPropertiesPanel", () => {
|
||||
expect(alignClass).toContain("dark:text-slate-100");
|
||||
expect(alignClass).toContain("dark:border-slate-700");
|
||||
|
||||
const thicknessSelect = screen.getByLabelText("구분선 두께") as HTMLSelectElement;
|
||||
const thicknessSelect = screen.getByLabelText("Divider thickness") as HTMLSelectElement;
|
||||
const thicknessClass = thicknessSelect.getAttribute("class") ?? "";
|
||||
expect(thicknessClass).toContain("bg-white");
|
||||
expect(thicknessClass).toContain("text-slate-900");
|
||||
@@ -173,7 +173,7 @@ describe("DividerPropertiesPanel", () => {
|
||||
expect(thicknessClass).toContain("dark:text-slate-100");
|
||||
expect(thicknessClass).toContain("dark:border-slate-700");
|
||||
|
||||
const widthModeSelect = screen.getByLabelText("구분선 길이 모드") as HTMLSelectElement;
|
||||
const widthModeSelect = screen.getByLabelText("Divider length mode") as HTMLSelectElement;
|
||||
const widthModeClass = widthModeSelect.getAttribute("class") ?? "";
|
||||
expect(widthModeClass).toContain("bg-white");
|
||||
expect(widthModeClass).toContain("text-slate-900");
|
||||
|
||||
@@ -2,6 +2,8 @@ import { describe, it, expect, afterEach } from "vitest";
|
||||
import { render, screen, cleanup } from "@testing-library/react";
|
||||
import type { Block, ProjectConfig } from "@/features/editor/state/editorStore";
|
||||
import { EditorCanvas, EditorCanvasDragPreview } from "@/app/editor/EditorCanvas";
|
||||
import { LocaleProvider } from "@/features/i18n/LocaleProvider";
|
||||
import { DEFAULT_LOCALE } from "@/features/i18n/locale";
|
||||
|
||||
// 에디터 캔버스/페이지 배경색 적용 규칙에 대한 최소 TDD
|
||||
// - canvasBgColorHex: 실제 캔버스 영역(editor-canvas-inner)의 배경색
|
||||
@@ -37,15 +39,38 @@ describe("EditorCanvas - 캔버스/페이지 배경색", () => {
|
||||
};
|
||||
|
||||
it("canvasBgColorHex 는 editor-canvas-inner 배경색에 적용되어야 한다", () => {
|
||||
render(<EditorCanvas {...baseProps} />);
|
||||
render(
|
||||
<LocaleProvider initialLocale={DEFAULT_LOCALE}>
|
||||
<EditorCanvas {...baseProps} />
|
||||
</LocaleProvider>,
|
||||
);
|
||||
|
||||
const inner = screen.getByTestId("editor-canvas-inner") as HTMLElement;
|
||||
|
||||
expect(inner.style.backgroundColor).toBe("rgb(17, 17, 17)");
|
||||
});
|
||||
|
||||
it("blocks 가 없을 때 en 로케일에서는 영어 안내 문구를 표시해야 한다", () => {
|
||||
render(
|
||||
<LocaleProvider initialLocale={DEFAULT_LOCALE}>
|
||||
<EditorCanvas {...baseProps} />
|
||||
</LocaleProvider>,
|
||||
);
|
||||
|
||||
const hint = screen.getByText(
|
||||
'Click the "Text" button on the left to add your first block.',
|
||||
);
|
||||
expect(hint.textContent).toBe(
|
||||
'Click the "Text" button on the left to add your first block.',
|
||||
);
|
||||
});
|
||||
|
||||
it("bodyBgColorHex 는 editor-canvas 바깥 래퍼(editor-canvas)의 배경색에 적용되어야 한다", () => {
|
||||
const { container } = render(<EditorCanvas {...baseProps} />);
|
||||
const { container } = render(
|
||||
<LocaleProvider initialLocale={DEFAULT_LOCALE}>
|
||||
<EditorCanvas {...baseProps} />
|
||||
</LocaleProvider>,
|
||||
);
|
||||
|
||||
const outer = container.querySelector('[data-testid="editor-canvas"]') as HTMLElement | null;
|
||||
|
||||
@@ -56,7 +81,11 @@ describe("EditorCanvas - 캔버스/페이지 배경색", () => {
|
||||
it("canvasPreset / canvasWidthPx 에 따라 editor-canvas-inner 의 maxWidth 가 설정되어야 한다", () => {
|
||||
const renderWithConfig = (partial: Partial<ProjectConfig>) => {
|
||||
const projectConfig = { ...baseProjectConfig, ...partial } as ProjectConfig;
|
||||
const { getByTestId, unmount } = render(<EditorCanvas {...baseProps} projectConfig={projectConfig} />);
|
||||
const { getByTestId, unmount } = render(
|
||||
<LocaleProvider initialLocale={DEFAULT_LOCALE}>
|
||||
<EditorCanvas {...baseProps} projectConfig={projectConfig} />
|
||||
</LocaleProvider>,
|
||||
);
|
||||
const inner = getByTestId("editor-canvas-inner") as HTMLElement;
|
||||
const maxWidth = inner.style.maxWidth;
|
||||
unmount();
|
||||
@@ -85,7 +114,11 @@ describe("EditorCanvas - 캔버스/페이지 배경색", () => {
|
||||
},
|
||||
} as any;
|
||||
|
||||
const { container } = render(<EditorCanvasDragPreview block={block} />);
|
||||
const { container } = render(
|
||||
<LocaleProvider initialLocale={DEFAULT_LOCALE}>
|
||||
<EditorCanvasDragPreview block={block} />
|
||||
</LocaleProvider>,
|
||||
);
|
||||
|
||||
const overlay = container.querySelector(
|
||||
"div.pointer-events-none.rounded.border",
|
||||
@@ -99,4 +132,25 @@ describe("EditorCanvas - 캔버스/페이지 배경색", () => {
|
||||
expect(className).toContain("dark:bg-slate-900");
|
||||
expect(className).toContain("dark:text-slate-100");
|
||||
});
|
||||
|
||||
it("en 로케일에서는 EditorCanvasDragPreview 의 텍스트 블록 fallback 라벨이 영어로 표시되어야 한다", () => {
|
||||
const block: Block = {
|
||||
id: "text_fallback_1",
|
||||
type: "text",
|
||||
props: {
|
||||
text: "",
|
||||
align: "left",
|
||||
size: "base",
|
||||
},
|
||||
} as any;
|
||||
|
||||
render(
|
||||
<LocaleProvider initialLocale={DEFAULT_LOCALE}>
|
||||
<EditorCanvasDragPreview block={block} />
|
||||
</LocaleProvider>,
|
||||
);
|
||||
|
||||
const fallbackLabel = screen.getByText("Text block");
|
||||
expect(fallbackLabel.textContent).toBe("Text block");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -102,20 +102,20 @@ describe("EditorPage - 상단 메뉴 (Export 미리보기 제거)", () => {
|
||||
it("메뉴를 열었을 때 'Export 미리보기' 항목은 보이지 않고, '프로젝트 목록' 항목이 노출되어야 한다", () => {
|
||||
render(<EditorPage />);
|
||||
|
||||
const menuButton = screen.getByText("메뉴");
|
||||
const menuButton = screen.getByText("Menu");
|
||||
fireEvent.click(menuButton);
|
||||
|
||||
const removedItem = screen.queryByText("Export 미리보기");
|
||||
expect(removedItem).toBeNull();
|
||||
|
||||
const projectListItem = screen.getByText("프로젝트 목록");
|
||||
const projectListItem = screen.getByText("Projects");
|
||||
expect(projectListItem).toBeTruthy();
|
||||
});
|
||||
|
||||
it("헤더의 '프리뷰 열기' 링크는 현재 프로젝트 slug 를 쿼리로 포함한 /preview?slug=... 형식이어야 한다", () => {
|
||||
render(<EditorPage />);
|
||||
|
||||
const previewLink = screen.getByRole("link", { name: "프리뷰 열기" });
|
||||
const previewLink = screen.getByRole("link", { name: "Open preview" });
|
||||
expect(previewLink).toBeTruthy();
|
||||
expect(previewLink.getAttribute("href")).toBe("/preview?slug=export-preview-test");
|
||||
});
|
||||
|
||||
@@ -103,10 +103,10 @@ vi.mock("next/navigation", () => {
|
||||
});
|
||||
|
||||
function openJsonModal() {
|
||||
const menuButton = screen.getByText("메뉴");
|
||||
const menuButton = screen.getByText("Menu");
|
||||
fireEvent.click(menuButton);
|
||||
|
||||
const jsonMenuItem = screen.getByText("JSON 내보내기/불러오기");
|
||||
const jsonMenuItem = screen.getByText("JSON export / import");
|
||||
fireEvent.click(jsonMenuItem);
|
||||
|
||||
const modalTitle = screen.getByText("JSON Export / Import");
|
||||
@@ -120,13 +120,13 @@ describe("EditorPage - JSON 내보내기/불러오기", () => {
|
||||
|
||||
const jsonModal = openJsonModal();
|
||||
|
||||
const exportLabel = within(jsonModal).getByText("에디터 상태 JSON").closest("label") as HTMLElement;
|
||||
const exportLabel = within(jsonModal).getByText("Editor state JSON").closest("label") as HTMLElement;
|
||||
const exportTextarea = within(exportLabel).getByRole("textbox") as HTMLTextAreaElement;
|
||||
|
||||
// 초기에는 비어 있어야 한다.
|
||||
expect(exportTextarea.value).toBe("");
|
||||
|
||||
const exportButton = within(jsonModal).getByText("JSON 내보내기");
|
||||
const exportButton = within(jsonModal).getByText("Export JSON");
|
||||
fireEvent.click(exportButton);
|
||||
|
||||
const value = exportTextarea.value;
|
||||
@@ -143,7 +143,7 @@ describe("EditorPage - JSON 내보내기/불러오기", () => {
|
||||
|
||||
const jsonModal = openJsonModal();
|
||||
|
||||
const importLabel = within(jsonModal).getByText("JSON에서 불러오기").closest("label") as HTMLElement;
|
||||
const importLabel = within(jsonModal).getByText("Import from JSON").closest("label") as HTMLElement;
|
||||
const importTextarea = within(importLabel).getByRole("textbox") as HTMLTextAreaElement;
|
||||
|
||||
const importedBlocks: Block[] = [
|
||||
@@ -175,7 +175,7 @@ describe("EditorPage - JSON 내보내기/불러오기", () => {
|
||||
mockState.replaceBlocks.mockReset();
|
||||
mockState.updateProjectConfig.mockReset();
|
||||
|
||||
const applyButton = within(importLabel).getByText("JSON 적용하기");
|
||||
const applyButton = within(importLabel).getByText("Apply JSON");
|
||||
fireEvent.click(applyButton);
|
||||
|
||||
expect(mockState.replaceBlocks).toHaveBeenCalledTimes(1);
|
||||
@@ -190,7 +190,7 @@ describe("EditorPage - JSON 내보내기/불러오기", () => {
|
||||
|
||||
const jsonModal = openJsonModal();
|
||||
|
||||
const importLabel = within(jsonModal).getByText("JSON에서 불러오기").closest("label") as HTMLElement;
|
||||
const importLabel = within(jsonModal).getByText("Import from JSON").closest("label") as HTMLElement;
|
||||
const importTextarea = within(importLabel).getByRole("textbox") as HTMLTextAreaElement;
|
||||
|
||||
fireEvent.change(importTextarea, { target: { value: "{ this-is: not-valid-json" } });
|
||||
@@ -198,7 +198,7 @@ describe("EditorPage - JSON 내보내기/불러오기", () => {
|
||||
mockState.replaceBlocks.mockReset();
|
||||
mockState.updateProjectConfig.mockReset();
|
||||
|
||||
const applyButton = within(importLabel).getByText("JSON 적용하기");
|
||||
const applyButton = within(importLabel).getByText("Apply JSON");
|
||||
fireEvent.click(applyButton);
|
||||
|
||||
expect(mockState.replaceBlocks).not.toHaveBeenCalled();
|
||||
|
||||
@@ -90,7 +90,7 @@ describe("EditorPage - 레이아웃 스크롤 컨테이너", () => {
|
||||
it("좌측 사이드바, 캔버스, 우측 속성 패널에 pb-scroll 클래스가 적용되어야 한다", () => {
|
||||
render(<EditorPage />);
|
||||
|
||||
const blocksHeading = screen.getByText("블록");
|
||||
const blocksHeading = screen.getByText("Blocks");
|
||||
const blocksSidebar = blocksHeading.closest("aside") as HTMLElement;
|
||||
const canvas = screen.getByTestId("editor-canvas");
|
||||
const propertiesSidebar = screen.getByTestId("properties-sidebar");
|
||||
|
||||
@@ -114,13 +114,13 @@ describe("EditorPage - 페이지/캔버스 배경", () => {
|
||||
it("프로젝트 저장/불러오기 모달 카드는 라이트/다크 테마에 맞는 카드/인풋 크롬을 사용해야 한다", () => {
|
||||
render(<EditorPage />);
|
||||
|
||||
const menuButton = screen.getByRole("button", { name: /메뉴/ });
|
||||
const menuButton = screen.getByRole("button", { name: /Menu/ });
|
||||
fireEvent.click(menuButton);
|
||||
|
||||
const projectMenuItem = screen.getByRole("button", { name: "프로젝트 저장/불러오기" });
|
||||
const projectMenuItem = screen.getByRole("button", { name: "Save / load project" });
|
||||
fireEvent.click(projectMenuItem);
|
||||
|
||||
const heading = screen.getByText("프로젝트 저장 / 불러오기");
|
||||
const heading = screen.getByText("Save / load project");
|
||||
const headerDiv = heading.closest("div") as HTMLDivElement | null;
|
||||
expect(headerDiv).not.toBeNull();
|
||||
|
||||
@@ -136,7 +136,7 @@ describe("EditorPage - 페이지/캔버스 배경", () => {
|
||||
expect(cardClass).toContain("dark:border-slate-700");
|
||||
|
||||
const titleSpan = Array.from(card!.querySelectorAll("span")).find(
|
||||
(el) => el.textContent === "프로젝트 제목",
|
||||
(el) => el.textContent === "Project title",
|
||||
);
|
||||
expect(titleSpan).not.toBeUndefined();
|
||||
const titleWrapper = (titleSpan as HTMLSpanElement).parentElement as HTMLElement | null;
|
||||
@@ -157,10 +157,10 @@ describe("EditorPage - 페이지/캔버스 배경", () => {
|
||||
it("JSON Export / Import 모달 카드는 라이트/다크 테마에 맞는 카드/textarea 크롬을 사용해야 한다", () => {
|
||||
render(<EditorPage />);
|
||||
|
||||
const menuButton = screen.getByRole("button", { name: /메뉴/ });
|
||||
const menuButton = screen.getByRole("button", { name: /Menu/ });
|
||||
fireEvent.click(menuButton);
|
||||
|
||||
const jsonMenuItem = screen.getByRole("button", { name: "JSON 내보내기/불러오기" });
|
||||
const jsonMenuItem = screen.getByRole("button", { name: "JSON export / import" });
|
||||
fireEvent.click(jsonMenuItem);
|
||||
|
||||
const heading = screen.getByText(/JSON Export \/ Import/);
|
||||
@@ -178,7 +178,7 @@ describe("EditorPage - 페이지/캔버스 배경", () => {
|
||||
expect(cardClass).toContain("dark:text-slate-100");
|
||||
expect(cardClass).toContain("dark:border-slate-700");
|
||||
|
||||
const jsonLabel = screen.getByText("에디터 상태 JSON");
|
||||
const jsonLabel = screen.getByText("Editor state JSON");
|
||||
const jsonWrapper = jsonLabel.parentElement as HTMLElement | null;
|
||||
expect(jsonWrapper).not.toBeNull();
|
||||
|
||||
|
||||
@@ -126,17 +126,17 @@ describe("EditorPage - 프로젝트 저장/불러오기", () => {
|
||||
|
||||
const { rerender } = render(<EditorPage />);
|
||||
|
||||
const menuButton = screen.getByText("메뉴");
|
||||
const menuButton = screen.getByText("Menu");
|
||||
fireEvent.click(menuButton);
|
||||
|
||||
const projectMenuItem = screen.getByText("프로젝트 저장/불러오기");
|
||||
const projectMenuItem = screen.getByText("Save / load project");
|
||||
fireEvent.click(projectMenuItem);
|
||||
|
||||
const modalTitle = screen.getByText("프로젝트 저장 / 불러오기");
|
||||
const modalTitle = screen.getByText("Save / load project");
|
||||
const projectModal = (modalTitle.parentElement?.parentElement ?? document.body) as HTMLElement;
|
||||
|
||||
const titleInput = within(projectModal).getByLabelText("프로젝트 제목") as HTMLInputElement;
|
||||
const slugInput = within(projectModal).getByLabelText("프로젝트 주소 (예: my-landing)") as HTMLInputElement;
|
||||
const titleInput = within(projectModal).getByLabelText("Project title") as HTMLInputElement;
|
||||
const slugInput = within(projectModal).getByLabelText("Project address (e.g. my-landing)") as HTMLInputElement;
|
||||
|
||||
expect(titleInput.value).toBe("프로젝트 저장/불러오기 테스트");
|
||||
expect(slugInput.value).toBe("editor-project-test");
|
||||
@@ -149,7 +149,7 @@ describe("EditorPage - 프로젝트 저장/불러오기", () => {
|
||||
|
||||
rerender(<EditorPage />);
|
||||
|
||||
const saveButton = screen.getByText("저장 (로컬 + 서버)");
|
||||
const saveButton = screen.getByText("Save (local + server)");
|
||||
fireEvent.click(saveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -184,7 +184,7 @@ describe("EditorPage - 프로젝트 저장/불러오기", () => {
|
||||
expect(Array.isArray(body.contentJson)).toBe(true);
|
||||
expect(body.contentJson[0].id).toBe("blk_1");
|
||||
|
||||
const message = await screen.findByText("프로젝트가 저장되었습니다: save-test-slug");
|
||||
const message = await screen.findByText("Project saved: save-test-slug");
|
||||
expect(message).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -213,24 +213,24 @@ describe("EditorPage - 프로젝트 저장/불러오기", () => {
|
||||
|
||||
const { rerender } = render(<EditorPage />);
|
||||
|
||||
const menuButton = screen.getByText("메뉴");
|
||||
const menuButton = screen.getByText("Menu");
|
||||
fireEvent.click(menuButton);
|
||||
|
||||
const projectMenuItem = screen.getByText("프로젝트 저장/불러오기");
|
||||
const projectMenuItem = screen.getByText("Save / load project");
|
||||
fireEvent.click(projectMenuItem);
|
||||
|
||||
const modalTitle = screen.getByText("프로젝트 저장 / 불러오기");
|
||||
const modalTitle = screen.getByText("Save / load project");
|
||||
const projectModal = (modalTitle.parentElement?.parentElement ?? document.body) as HTMLElement;
|
||||
|
||||
const titleInput = within(projectModal).getByLabelText("프로젝트 제목") as HTMLInputElement;
|
||||
const slugInput = within(projectModal).getByLabelText("프로젝트 주소 (예: my-landing)") as HTMLInputElement;
|
||||
const titleInput = within(projectModal).getByLabelText("Project title") as HTMLInputElement;
|
||||
const slugInput = within(projectModal).getByLabelText("Project address (e.g. my-landing)") as HTMLInputElement;
|
||||
|
||||
fireEvent.change(titleInput, { target: { value: "충돌 테스트 프로젝트" } });
|
||||
fireEvent.change(slugInput, { target: { value: "conflict-slug" } });
|
||||
|
||||
rerender(<EditorPage />);
|
||||
|
||||
const saveButton = screen.getByText("저장 (로컬 + 서버)");
|
||||
const saveButton = screen.getByText("Save (local + server)");
|
||||
fireEvent.click(saveButton);
|
||||
|
||||
const message = await screen.findByText("이미 다른 사용자가 사용 중인 프로젝트 주소입니다.");
|
||||
@@ -268,16 +268,16 @@ describe("EditorPage - 프로젝트 저장/불러오기", () => {
|
||||
|
||||
render(<EditorPage />);
|
||||
|
||||
const menuButton = screen.getByText("메뉴");
|
||||
const menuButton = screen.getByText("Menu");
|
||||
fireEvent.click(menuButton);
|
||||
|
||||
const projectMenuItem = screen.getByText("프로젝트 저장/불러오기");
|
||||
const projectMenuItem = screen.getByText("Save / load project");
|
||||
fireEvent.click(projectMenuItem);
|
||||
|
||||
const modalTitle = screen.getByText("프로젝트 저장 / 불러오기");
|
||||
const modalTitle = screen.getByText("Save / load project");
|
||||
const projectModal = (modalTitle.parentElement?.parentElement ?? document.body) as HTMLElement;
|
||||
|
||||
const loadButton = screen.getByText("불러오기");
|
||||
const loadButton = screen.getByText("Load");
|
||||
fireEvent.click(loadButton);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -297,7 +297,7 @@ describe("EditorPage - 프로젝트 저장/불러오기", () => {
|
||||
);
|
||||
expect(serverCalls.length).toBe(0);
|
||||
|
||||
const message = await screen.findByText(`로컬에서 프로젝트를 불러왔습니다: ${slug}`);
|
||||
const message = await screen.findByText(`Loaded project from local: ${slug}`);
|
||||
expect(message).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -328,13 +328,13 @@ describe("EditorPage - 프로젝트 저장/불러오기", () => {
|
||||
|
||||
render(<EditorPage />);
|
||||
|
||||
const menuButton = screen.getByText("메뉴");
|
||||
const menuButton = screen.getByText("Menu");
|
||||
fireEvent.click(menuButton);
|
||||
|
||||
const projectMenuItem = screen.getByText("프로젝트 저장/불러오기");
|
||||
const projectMenuItem = screen.getByText("Save / load project");
|
||||
fireEvent.click(projectMenuItem);
|
||||
|
||||
const loadButton = screen.getByText("불러오기");
|
||||
const loadButton = screen.getByText("Load");
|
||||
fireEvent.click(loadButton);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -362,7 +362,7 @@ describe("EditorPage - 프로젝트 저장/불러오기", () => {
|
||||
),
|
||||
).toBe(true);
|
||||
|
||||
const message = await screen.findByText(`프로젝트를 불러왔습니다: ${slug}`);
|
||||
const message = await screen.findByText(`Loaded project from server: ${slug}`);
|
||||
expect(message).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -472,13 +472,13 @@ describe("EditorPage - 프로젝트 저장/불러오기", () => {
|
||||
|
||||
render(<EditorPage />);
|
||||
|
||||
const menuButton = screen.getByText("메뉴");
|
||||
const menuButton = screen.getByText("Menu");
|
||||
fireEvent.click(menuButton);
|
||||
|
||||
const projectMenuItem = screen.getByText("프로젝트 저장/불러오기");
|
||||
const projectMenuItem = screen.getByText("Save / load project");
|
||||
fireEvent.click(projectMenuItem);
|
||||
|
||||
const loadButton = screen.getByText("불러오기");
|
||||
const loadButton = screen.getByText("Load");
|
||||
fireEvent.click(loadButton);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -514,10 +514,10 @@ describe("EditorPage - 프로젝트 저장/불러오기", () => {
|
||||
|
||||
render(<EditorPage />);
|
||||
|
||||
const menuButton = screen.getByText("메뉴");
|
||||
const menuButton = screen.getByText("Menu");
|
||||
fireEvent.click(menuButton);
|
||||
|
||||
const deleteMenuItem = screen.getByText("프로젝트 삭제");
|
||||
const deleteMenuItem = screen.getByText("Delete project");
|
||||
fireEvent.click(deleteMenuItem);
|
||||
|
||||
await waitFor(() => {
|
||||
|
||||
@@ -42,7 +42,7 @@ describe("FormControllerPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByLabelText("성공 메시지");
|
||||
const input = screen.getByLabelText("Success message");
|
||||
fireEvent.change(input, { target: { value: "새 성공 메시지" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -67,7 +67,7 @@ describe("FormControllerPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByLabelText("에러 메시지");
|
||||
const input = screen.getByLabelText("Error message");
|
||||
fireEvent.change(input, { target: { value: "새 에러 메시지" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -143,7 +143,7 @@ describe("FormControllerPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByLabelText("Submit 버튼") as HTMLSelectElement;
|
||||
const select = screen.getByLabelText("Submit button") as HTMLSelectElement;
|
||||
|
||||
const submitMainOption = screen.getByText("submit-main (제출하기)");
|
||||
|
||||
@@ -168,17 +168,15 @@ describe("FormControllerPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const guideButton = screen.getByRole("button", { name: "Google Sheets 연동 가이드" });
|
||||
const guideButton = screen.getByRole("button", { name: "Google Sheets integration guide" });
|
||||
guideButton.click();
|
||||
|
||||
// Google Sheets 연동 가이드 헤딩과 Apps Script 안내 문구가 노출되어야 한다.
|
||||
expect(screen.getByText("Google Sheets 연동 가이드")).toBeTruthy();
|
||||
expect(
|
||||
screen.getByText(/Apps Script 웹 앱 URL/i),
|
||||
).toBeTruthy();
|
||||
// Google Sheets integration guide heading and Apps Script web app URL hint should be visible.
|
||||
expect(screen.getByText("Google Sheets integration guide")).toBeTruthy();
|
||||
expect(screen.getByText(/Apps Script web app URL/i)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("Google Sheets 가이드 모달의 Apps Script 코드가 컨트롤러에 매핑된 전송 키와 작성일시를 포함해야 한다", async () => {
|
||||
it("Google Sheets guide modal Apps Script code should include mapped submit keys and createdAt field", async () => {
|
||||
const formBlock = makeFormBlock("form-webhook", {
|
||||
submitTarget: "webhook",
|
||||
fieldIds: ["input-name", "input-email"],
|
||||
@@ -213,7 +211,7 @@ describe("FormControllerPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const guideButton = screen.getByRole("button", { name: "Google Sheets 연동 가이드" });
|
||||
const guideButton = screen.getByRole("button", { name: "Google Sheets integration guide" });
|
||||
fireEvent.click(guideButton);
|
||||
|
||||
const scriptTextarea = (await screen.findByDisplayValue(/function doPost/)) as HTMLTextAreaElement;
|
||||
@@ -239,7 +237,7 @@ describe("FormControllerPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const submitTargetSelect = screen.getByLabelText("전송 대상") as HTMLSelectElement;
|
||||
const submitTargetSelect = screen.getByLabelText("Submit target") as HTMLSelectElement;
|
||||
const submitTargetClass = submitTargetSelect.getAttribute("class") ?? "";
|
||||
expect(submitTargetClass).toContain("bg-white");
|
||||
expect(submitTargetClass).toContain("text-slate-900");
|
||||
@@ -248,7 +246,7 @@ describe("FormControllerPanel", () => {
|
||||
expect(submitTargetClass).toContain("dark:text-slate-100");
|
||||
expect(submitTargetClass).toContain("dark:border-slate-700");
|
||||
|
||||
const formWidthModeSelect = screen.getByLabelText("폼 너비 모드") as HTMLSelectElement;
|
||||
const formWidthModeSelect = screen.getByLabelText("Form width mode") as HTMLSelectElement;
|
||||
const formWidthModeClass = formWidthModeSelect.getAttribute("class") ?? "";
|
||||
expect(formWidthModeClass).toContain("bg-white");
|
||||
expect(formWidthModeClass).toContain("text-slate-900");
|
||||
@@ -257,7 +255,7 @@ describe("FormControllerPanel", () => {
|
||||
expect(formWidthModeClass).toContain("dark:text-slate-100");
|
||||
expect(formWidthModeClass).toContain("dark:border-slate-700");
|
||||
|
||||
const successInput = screen.getByLabelText("성공 메시지") as HTMLInputElement;
|
||||
const successInput = screen.getByLabelText("Success message") as HTMLInputElement;
|
||||
const successClass = successInput.getAttribute("class") ?? "";
|
||||
expect(successClass).toContain("bg-white");
|
||||
expect(successClass).toContain("text-slate-900");
|
||||
@@ -266,7 +264,7 @@ describe("FormControllerPanel", () => {
|
||||
expect(successClass).toContain("dark:text-slate-100");
|
||||
expect(successClass).toContain("dark:border-slate-700");
|
||||
|
||||
const errorInput = screen.getByLabelText("에러 메시지") as HTMLInputElement;
|
||||
const errorInput = screen.getByLabelText("Error message") as HTMLInputElement;
|
||||
const errorClass = errorInput.getAttribute("class") ?? "";
|
||||
expect(errorClass).toContain("bg-white");
|
||||
expect(errorClass).toContain("text-slate-900");
|
||||
@@ -275,7 +273,7 @@ describe("FormControllerPanel", () => {
|
||||
expect(errorClass).toContain("dark:text-slate-100");
|
||||
expect(errorClass).toContain("dark:border-slate-700");
|
||||
|
||||
const extraParamsTextarea = screen.getByLabelText(/추가 파라미터/) as HTMLTextAreaElement;
|
||||
const extraParamsTextarea = screen.getByLabelText(/Additional parameters/) as HTMLTextAreaElement;
|
||||
const extraParamsClass = extraParamsTextarea.getAttribute("class") ?? "";
|
||||
expect(extraParamsClass).toContain("bg-white");
|
||||
expect(extraParamsClass).toContain("text-slate-900");
|
||||
@@ -285,7 +283,7 @@ describe("FormControllerPanel", () => {
|
||||
expect(extraParamsClass).toContain("dark:border-slate-700");
|
||||
});
|
||||
|
||||
it("Google Sheets 연동 가이드 모달 카드와 textarea 는 라이트/다크 듀얼 테마 크롬을 사용해야 한다", () => {
|
||||
it("Google Sheets integration guide modal card and textarea should use dual light/dark theme chrome", () => {
|
||||
const formBlock = makeFormBlock("form-webhook-theme", {
|
||||
submitTarget: "webhook",
|
||||
} as any);
|
||||
@@ -301,10 +299,10 @@ describe("FormControllerPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const guideButton = screen.getByRole("button", { name: "Google Sheets 연동 가이드" });
|
||||
const guideButton = screen.getByRole("button", { name: "Google Sheets integration guide" });
|
||||
fireEvent.click(guideButton);
|
||||
|
||||
const heading = screen.getByRole("heading", { name: "Google Sheets 연동 가이드" });
|
||||
const heading = screen.getByRole("heading", { name: "Google Sheets integration guide" });
|
||||
const card = heading.closest("div")?.parentElement as HTMLDivElement | null;
|
||||
expect(card).not.toBeNull();
|
||||
|
||||
@@ -352,7 +350,7 @@ describe("FormControllerPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const fieldset = screen.getByRole("group", { name: "폼 필드 매핑" });
|
||||
const fieldset = screen.getByRole("group", { name: "Form field mapping" });
|
||||
const checkboxes = within(fieldset).getAllByRole("checkbox");
|
||||
|
||||
const includeCheckbox = checkboxes[0] as HTMLInputElement;
|
||||
@@ -391,7 +389,7 @@ describe("FormControllerPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByLabelText("Submit 버튼") as HTMLSelectElement;
|
||||
const select = screen.getByLabelText("Submit button") as HTMLSelectElement;
|
||||
const className = select.getAttribute("class") ?? "";
|
||||
|
||||
expect(className).toContain("bg-white");
|
||||
|
||||
@@ -45,7 +45,7 @@ describe("FormInputPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const hexInput = screen.getByLabelText("필드 텍스트 색상 HEX");
|
||||
const hexInput = screen.getByLabelText("Field text color HEX");
|
||||
fireEvent.change(hexInput, { target: { value: "#112233" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -70,7 +70,7 @@ describe("FormInputPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const hexInput = screen.getByLabelText("필드 채움 색상 HEX");
|
||||
const hexInput = screen.getByLabelText("Field fill color HEX");
|
||||
fireEvent.change(hexInput, { target: { value: "#445566" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -95,7 +95,7 @@ describe("FormInputPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const hexInput = screen.getByLabelText("필드 테두리 색상 HEX");
|
||||
const hexInput = screen.getByLabelText("Field border color HEX");
|
||||
fireEvent.change(hexInput, { target: { value: "#778899" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -104,7 +104,7 @@ describe("FormInputPropertiesPanel", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("텍스트 색상 팔레트에서 \"없음\" 을 선택하면 FormInput textColorCustom 이 빈 문자열로 업데이트되어야 한다", () => {
|
||||
it("텍스트 색상 팔레트에서 \"None\" 을 선택하면 FormInput textColorCustom 이 빈 문자열로 업데이트되어야 한다", () => {
|
||||
const updateBlock = vi.fn();
|
||||
|
||||
const block = makeBlock<FormInputBlockProps>("f-input-none", "formInput", {
|
||||
@@ -121,11 +121,11 @@ describe("FormInputPropertiesPanel", () => {
|
||||
);
|
||||
|
||||
// 첫 번째 ColorPickerField(필드 텍스트 색상)의 팔레트 버튼을 연다.
|
||||
const paletteButtons = screen.getAllByText("색상 팔레트");
|
||||
const paletteButtons = screen.getAllByText("Color palette");
|
||||
fireEvent.click(paletteButtons[0].closest("button") as HTMLButtonElement);
|
||||
|
||||
// 팔레트에서 "없음" 항목을 버튼 role 기준으로 선택한다.
|
||||
const noneButtons = screen.getAllByRole("button", { name: "없음" });
|
||||
// 팔레트에서 "None" 항목을 버튼 role 기준으로 선택한다.
|
||||
const noneButtons = screen.getAllByRole("button", { name: "None" });
|
||||
fireEvent.click(noneButtons[0]);
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -150,7 +150,7 @@ describe("FormInputPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const hexInput = screen.getByLabelText("필드 텍스트 색상 HEX") as HTMLInputElement;
|
||||
const hexInput = screen.getByLabelText("Field text color HEX") as HTMLInputElement;
|
||||
expect(hexInput.value).toBe("");
|
||||
});
|
||||
|
||||
@@ -171,7 +171,7 @@ describe("FormInputPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByLabelText("너비");
|
||||
const select = screen.getByLabelText("Width");
|
||||
fireEvent.change(select, { target: { value: "fixed" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -197,7 +197,7 @@ describe("FormInputPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const layoutSelect = screen.getByLabelText("레이아웃");
|
||||
const layoutSelect = screen.getByLabelText("Layout");
|
||||
expect(layoutSelect).toBeTruthy();
|
||||
|
||||
cleanup();
|
||||
@@ -216,7 +216,7 @@ describe("FormInputPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByLabelText("레이아웃")).toBeNull();
|
||||
expect(screen.queryByLabelText("Layout")).toBeNull();
|
||||
});
|
||||
|
||||
it("폼 입력 텍스트/줄간격/자간 px 컨트롤이 fontSizeCustom/lineHeightCustom/letterSpacingCustom 으로 저장되어야 한다", () => {
|
||||
@@ -237,13 +237,13 @@ describe("FormInputPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const fontSizeInput = screen.getByLabelText("필드 텍스트 크기 (px) 커스텀 (px)");
|
||||
const fontSizeInput = screen.getByLabelText("Field text size (px) 커스텀 (px)");
|
||||
fireEvent.change(fontSizeInput, { target: { value: "24" } });
|
||||
|
||||
const lineHeightInput = screen.getByLabelText("필드 줄간격 (px) 커스텀 (px)");
|
||||
const lineHeightInput = screen.getByLabelText("Field line height (px) 커스텀 (px)");
|
||||
fireEvent.change(lineHeightInput, { target: { value: "30" } });
|
||||
|
||||
const letterSpacingInput = screen.getByLabelText("필드 자간 (px) 커스텀 (px)");
|
||||
const letterSpacingInput = screen.getByLabelText("Field letter spacing (px) 커스텀 (px)");
|
||||
fireEvent.change(letterSpacingInput, { target: { value: "1.5" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -280,19 +280,19 @@ describe("FormInputPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const alignSelect = screen.getByLabelText("텍스트 정렬");
|
||||
const alignSelect = screen.getByLabelText("Text alignment");
|
||||
fireEvent.change(alignSelect, { target: { value: "center" } });
|
||||
|
||||
const widthModeSelect = screen.getByLabelText("너비");
|
||||
const widthModeSelect = screen.getByLabelText("Width");
|
||||
fireEvent.change(widthModeSelect, { target: { value: "fixed" } });
|
||||
|
||||
const widthPxInput = screen.getByLabelText("필드 고정 너비 커스텀 (px)");
|
||||
const widthPxInput = screen.getByLabelText("Field fixed width 커스텀 (px)");
|
||||
fireEvent.change(widthPxInput, { target: { value: "320" } });
|
||||
|
||||
const paddingXInput = screen.getByLabelText("필드 가로 패딩 (px) 커스텀 (px)");
|
||||
const paddingXInput = screen.getByLabelText("Field horizontal padding (px) 커스텀 (px)");
|
||||
fireEvent.change(paddingXInput, { target: { value: "16" } });
|
||||
|
||||
const paddingYInput = screen.getByLabelText("필드 세로 패딩 (px) 커스텀 (px)");
|
||||
const paddingYInput = screen.getByLabelText("Field vertical padding (px) 커스텀 (px)");
|
||||
fireEvent.change(paddingYInput, { target: { value: "12" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -332,7 +332,7 @@ describe("FormInputPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const labelTypeSelect = screen.getByLabelText("라벨 타입") as HTMLSelectElement;
|
||||
const labelTypeSelect = screen.getByLabelText("Label type") as HTMLSelectElement;
|
||||
const labelTypeClass = labelTypeSelect.getAttribute("class") ?? "";
|
||||
expect(labelTypeClass).toContain("bg-white");
|
||||
expect(labelTypeClass).toContain("text-slate-900");
|
||||
@@ -341,7 +341,7 @@ describe("FormInputPropertiesPanel", () => {
|
||||
expect(labelTypeClass).toContain("dark:text-slate-100");
|
||||
expect(labelTypeClass).toContain("dark:border-slate-700");
|
||||
|
||||
const labelInput = screen.getByLabelText("필드 라벨") as HTMLInputElement;
|
||||
const labelInput = screen.getByLabelText("Field label") as HTMLInputElement;
|
||||
const labelInputClass = labelInput.getAttribute("class") ?? "";
|
||||
expect(labelInputClass).toContain("bg-white");
|
||||
expect(labelInputClass).toContain("text-slate-900");
|
||||
@@ -350,7 +350,7 @@ describe("FormInputPropertiesPanel", () => {
|
||||
expect(labelInputClass).toContain("dark:text-slate-100");
|
||||
expect(labelInputClass).toContain("dark:border-slate-700");
|
||||
|
||||
const labelDisplaySelect = screen.getByLabelText("라벨 표시 방식") as HTMLSelectElement;
|
||||
const labelDisplaySelect = screen.getByLabelText("Label display mode") as HTMLSelectElement;
|
||||
const labelDisplayClass = labelDisplaySelect.getAttribute("class") ?? "";
|
||||
expect(labelDisplayClass).toContain("bg-white");
|
||||
expect(labelDisplayClass).toContain("text-slate-900");
|
||||
@@ -359,7 +359,7 @@ describe("FormInputPropertiesPanel", () => {
|
||||
expect(labelDisplayClass).toContain("dark:text-slate-100");
|
||||
expect(labelDisplayClass).toContain("dark:border-slate-700");
|
||||
|
||||
const fieldTypeSelect = screen.getByLabelText("필드 타입") as HTMLSelectElement;
|
||||
const fieldTypeSelect = screen.getByLabelText("Field type") as HTMLSelectElement;
|
||||
const fieldTypeClass = fieldTypeSelect.getAttribute("class") ?? "";
|
||||
expect(fieldTypeClass).toContain("bg-white");
|
||||
expect(fieldTypeClass).toContain("text-slate-900");
|
||||
@@ -406,7 +406,7 @@ describe("FormSelectPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const hexInput = screen.getByLabelText("셀렉트 텍스트 색상 HEX");
|
||||
const hexInput = screen.getByLabelText("Select text color HEX");
|
||||
fireEvent.change(hexInput, { target: { value: "#112233" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -431,7 +431,7 @@ describe("FormSelectPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const hexInput = screen.getByLabelText("셀렉트 채움 색상 HEX");
|
||||
const hexInput = screen.getByLabelText("Select fill color HEX");
|
||||
fireEvent.change(hexInput, { target: { value: "#445566" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -456,7 +456,7 @@ describe("FormSelectPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const hexInput = screen.getByLabelText("셀렉트 테두리 색상 HEX");
|
||||
const hexInput = screen.getByLabelText("Select border color HEX");
|
||||
fireEvent.change(hexInput, { target: { value: "#778899" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -482,7 +482,7 @@ describe("FormSelectPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByLabelText("필드 너비");
|
||||
const select = screen.getByLabelText("Field width");
|
||||
fireEvent.change(select, { target: { value: "fixed" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -509,13 +509,13 @@ describe("FormSelectPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const fontSizeInput = screen.getByLabelText("셀렉트 텍스트 크기 (px) 커스텀 (px)");
|
||||
const fontSizeInput = screen.getByLabelText("Select text size (px) 커스텀 (px)");
|
||||
fireEvent.change(fontSizeInput, { target: { value: "24" } });
|
||||
|
||||
const lineHeightInput = screen.getByLabelText("셀렉트 줄간격 (px) 커스텀 (px)");
|
||||
const lineHeightInput = screen.getByLabelText("Select line height (px) 커스텀 (px)");
|
||||
fireEvent.change(lineHeightInput, { target: { value: "30" } });
|
||||
|
||||
const letterSpacingInput = screen.getByLabelText("셀렉트 자간 (px) 커스텀 (px)");
|
||||
const letterSpacingInput = screen.getByLabelText("Select letter spacing (px) 커스텀 (px)");
|
||||
fireEvent.change(letterSpacingInput, { target: { value: "1.5" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -551,13 +551,13 @@ describe("FormSelectPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const widthPxInput = screen.getByLabelText("필드 고정 너비 커스텀 (px)");
|
||||
const widthPxInput = screen.getByLabelText("Field fixed width 커스텀 (px)");
|
||||
fireEvent.change(widthPxInput, { target: { value: "320" } });
|
||||
|
||||
const paddingXInput = screen.getByLabelText("셀렉트 가로 패딩 (px) 커스텀 (px)");
|
||||
const paddingXInput = screen.getByLabelText("Select horizontal padding (px) 커스텀 (px)");
|
||||
fireEvent.change(paddingXInput, { target: { value: "16" } });
|
||||
|
||||
const paddingYInput = screen.getByLabelText("셀렉트 세로 패딩 (px) 커스텀 (px)");
|
||||
const paddingYInput = screen.getByLabelText("Select vertical padding (px) 커스텀 (px)");
|
||||
fireEvent.change(paddingYInput, { target: { value: "12" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -591,7 +591,7 @@ describe("FormSelectPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const layoutSelect = screen.getByLabelText("레이아웃");
|
||||
const layoutSelect = screen.getByLabelText("Layout");
|
||||
expect(layoutSelect).toBeTruthy();
|
||||
|
||||
cleanup();
|
||||
@@ -610,7 +610,7 @@ describe("FormSelectPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByLabelText("레이아웃")).toBeNull();
|
||||
expect(screen.queryByLabelText("Layout")).toBeNull();
|
||||
});
|
||||
|
||||
it("라벨 타입 셀렉트 변경 시 updateBlock 이 labelMode 로 호출되어야 한다", () => {
|
||||
@@ -629,7 +629,7 @@ describe("FormSelectPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByLabelText("라벨 타입");
|
||||
const select = screen.getByLabelText("Label type");
|
||||
fireEvent.change(select, { target: { value: "image" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -654,7 +654,7 @@ describe("FormSelectPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByLabelText("라벨 표시 방식");
|
||||
const select = screen.getByLabelText("Label display mode");
|
||||
fireEvent.change(select, { target: { value: "hidden" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -680,7 +680,7 @@ describe("FormSelectPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByLabelText("레이아웃");
|
||||
const select = screen.getByLabelText("Layout");
|
||||
fireEvent.change(select, { target: { value: "inline" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -707,7 +707,7 @@ describe("FormSelectPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const gapInput = screen.getByLabelText("라벨/필드 간격 (px) 커스텀 (px)");
|
||||
const gapInput = screen.getByLabelText("Label/field gap (px) 커스텀 (px)");
|
||||
fireEvent.change(gapInput, { target: { value: "16" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -732,7 +732,7 @@ describe("FormSelectPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByLabelText("필드 라벨");
|
||||
const input = screen.getByLabelText("Field label");
|
||||
fireEvent.change(input, { target: { value: "새 카테고리" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -757,7 +757,7 @@ describe("FormSelectPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByLabelText("전송 키");
|
||||
const input = screen.getByLabelText("Submit key");
|
||||
fireEvent.change(input, { target: { value: "category_new" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -782,7 +782,7 @@ describe("FormSelectPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const radiusInput = screen.getByLabelText("필드 모서리 둥글기 커스텀");
|
||||
const radiusInput = screen.getByLabelText("Field border radius 커스텀");
|
||||
fireEvent.change(radiusInput, { target: { value: "8" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -806,7 +806,7 @@ describe("FormSelectPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const labelTypeSelect = screen.getByLabelText("라벨 타입") as HTMLSelectElement;
|
||||
const labelTypeSelect = screen.getByLabelText("Label type") as HTMLSelectElement;
|
||||
const labelTypeClass = labelTypeSelect.getAttribute("class") ?? "";
|
||||
expect(labelTypeClass).toContain("bg-white");
|
||||
expect(labelTypeClass).toContain("text-slate-900");
|
||||
@@ -815,7 +815,7 @@ describe("FormSelectPropertiesPanel", () => {
|
||||
expect(labelTypeClass).toContain("dark:text-slate-100");
|
||||
expect(labelTypeClass).toContain("dark:border-slate-700");
|
||||
|
||||
const labelInput = screen.getByLabelText("필드 라벨") as HTMLInputElement;
|
||||
const labelInput = screen.getByLabelText("Field label") as HTMLInputElement;
|
||||
const labelInputClass = labelInput.getAttribute("class") ?? "";
|
||||
expect(labelInputClass).toContain("bg-white");
|
||||
expect(labelInputClass).toContain("text-slate-900");
|
||||
@@ -824,7 +824,7 @@ describe("FormSelectPropertiesPanel", () => {
|
||||
expect(labelInputClass).toContain("dark:text-slate-100");
|
||||
expect(labelInputClass).toContain("dark:border-slate-700");
|
||||
|
||||
const labelDisplaySelect = screen.getByLabelText("라벨 표시 방식") as HTMLSelectElement;
|
||||
const labelDisplaySelect = screen.getByLabelText("Label display mode") as HTMLSelectElement;
|
||||
const labelDisplayClass = labelDisplaySelect.getAttribute("class") ?? "";
|
||||
expect(labelDisplayClass).toContain("bg-white");
|
||||
expect(labelDisplayClass).toContain("text-slate-900");
|
||||
@@ -833,7 +833,7 @@ describe("FormSelectPropertiesPanel", () => {
|
||||
expect(labelDisplayClass).toContain("dark:text-slate-100");
|
||||
expect(labelDisplayClass).toContain("dark:border-slate-700");
|
||||
|
||||
const widthModeSelect = screen.getByLabelText("필드 너비") as HTMLSelectElement;
|
||||
const widthModeSelect = screen.getByLabelText("Field width") as HTMLSelectElement;
|
||||
const widthModeClass = widthModeSelect.getAttribute("class") ?? "";
|
||||
expect(widthModeClass).toContain("bg-white");
|
||||
expect(widthModeClass).toContain("text-slate-900");
|
||||
@@ -842,7 +842,7 @@ describe("FormSelectPropertiesPanel", () => {
|
||||
expect(widthModeClass).toContain("dark:text-slate-100");
|
||||
expect(widthModeClass).toContain("dark:border-slate-700");
|
||||
|
||||
const optionLabelInput = screen.getByPlaceholderText("라벨") as HTMLInputElement;
|
||||
const optionLabelInput = screen.getByPlaceholderText("Label") as HTMLInputElement;
|
||||
const optionLabelClass = optionLabelInput.getAttribute("class") ?? "";
|
||||
expect(optionLabelClass).toContain("bg-white");
|
||||
expect(optionLabelClass).toContain("text-slate-900");
|
||||
@@ -867,7 +867,7 @@ describe("FormSelectPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const optionLabelInput = screen.getByPlaceholderText("라벨") as HTMLInputElement;
|
||||
const optionLabelInput = screen.getByPlaceholderText("Label") as HTMLInputElement;
|
||||
const rowDiv = optionLabelInput.closest("div") as HTMLDivElement | null;
|
||||
expect(rowDiv).not.toBeNull();
|
||||
|
||||
@@ -904,7 +904,7 @@ describe("FormCheckboxPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const hexInput = screen.getByLabelText("체크박스 텍스트 색상 HEX");
|
||||
const hexInput = screen.getByLabelText("Checkbox text color HEX");
|
||||
fireEvent.change(hexInput, { target: { value: "#112233" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -931,13 +931,13 @@ describe("FormCheckboxPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const fontSizeInput = screen.getByLabelText("체크박스 텍스트 크기 (px) 커스텀 (px)");
|
||||
const fontSizeInput = screen.getByLabelText("Checkbox text size (px) 커스텀 (px)");
|
||||
fireEvent.change(fontSizeInput, { target: { value: "24" } });
|
||||
|
||||
const lineHeightInput = screen.getByLabelText("체크박스 줄간격 (px) 커스텀 (px)");
|
||||
const lineHeightInput = screen.getByLabelText("Checkbox line height (px) 커스텀 (px)");
|
||||
fireEvent.change(lineHeightInput, { target: { value: "30" } });
|
||||
|
||||
const letterSpacingInput = screen.getByLabelText("체크박스 자간 (px) 커스텀 (px)");
|
||||
const letterSpacingInput = screen.getByLabelText("Checkbox letter spacing (px) 커스텀 (px)");
|
||||
fireEvent.change(letterSpacingInput, { target: { value: "1.5" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -973,13 +973,13 @@ describe("FormCheckboxPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const widthPxInput = screen.getByLabelText("필드 고정 너비 커스텀 (px)");
|
||||
const widthPxInput = screen.getByLabelText("Field fixed width 커스텀 (px)");
|
||||
fireEvent.change(widthPxInput, { target: { value: "320" } });
|
||||
|
||||
const paddingXInput = screen.getByLabelText("체크박스 가로 패딩 (px) 커스텀 (px)");
|
||||
const paddingXInput = screen.getByLabelText("Checkbox horizontal padding (px) 커스텀 (px)");
|
||||
fireEvent.change(paddingXInput, { target: { value: "16" } });
|
||||
|
||||
const paddingYInput = screen.getByLabelText("체크박스 세로 패딩 (px) 커스텀 (px)");
|
||||
const paddingYInput = screen.getByLabelText("Checkbox vertical padding (px) 커스텀 (px)");
|
||||
fireEvent.change(paddingYInput, { target: { value: "12" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -1013,7 +1013,7 @@ describe("FormCheckboxPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const layoutSelect = screen.getByLabelText("레이아웃");
|
||||
const layoutSelect = screen.getByLabelText("Layout");
|
||||
expect(layoutSelect).toBeTruthy();
|
||||
|
||||
cleanup();
|
||||
@@ -1032,7 +1032,7 @@ describe("FormCheckboxPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByLabelText("레이아웃")).toBeNull();
|
||||
expect(screen.queryByLabelText("Layout")).toBeNull();
|
||||
});
|
||||
|
||||
it("그룹 타이틀 타입 셀렉트 변경 시 updateBlock 이 groupLabelMode 로 호출되어야 한다", () => {
|
||||
@@ -1051,7 +1051,7 @@ describe("FormCheckboxPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByLabelText("그룹 타이틀 타입");
|
||||
const select = screen.getByLabelText("Group title type");
|
||||
fireEvent.change(select, { target: { value: "image" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -1076,7 +1076,7 @@ describe("FormCheckboxPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByLabelText("그룹 타이틀 표시 방식");
|
||||
const select = screen.getByLabelText("Group title display mode");
|
||||
fireEvent.change(select, { target: { value: "hidden" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -1102,7 +1102,7 @@ describe("FormCheckboxPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByLabelText("레이아웃");
|
||||
const select = screen.getByLabelText("Layout");
|
||||
fireEvent.change(select, { target: { value: "inline" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -1129,7 +1129,7 @@ describe("FormCheckboxPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const gapInput = screen.getByLabelText("라벨/필드 간격 (px) 커스텀 (px)");
|
||||
const gapInput = screen.getByLabelText("Label/field gap (px) 커스텀 (px)");
|
||||
fireEvent.change(gapInput, { target: { value: "16" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -1154,7 +1154,7 @@ describe("FormCheckboxPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByLabelText("그룹 타이틀");
|
||||
const input = screen.getByLabelText("Group title");
|
||||
fireEvent.change(input, { target: { value: "새 옵션들" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -1179,7 +1179,7 @@ describe("FormCheckboxPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const gapInput = screen.getByLabelText("체크박스 옵션 간격 (px) 커스텀 (px)");
|
||||
const gapInput = screen.getByLabelText("Checkbox option gap (px) 커스텀 (px)");
|
||||
fireEvent.change(gapInput, { target: { value: "10" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -1204,7 +1204,7 @@ describe("FormCheckboxPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByLabelText("옵션 레이아웃");
|
||||
const select = screen.getByLabelText("Option layout");
|
||||
fireEvent.change(select, { target: { value: "inline" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -1229,7 +1229,7 @@ describe("FormCheckboxPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const radiusInput = screen.getByLabelText("필드 모서리 둥글기 커스텀");
|
||||
const radiusInput = screen.getByLabelText("Field border radius 커스텀");
|
||||
fireEvent.change(radiusInput, { target: { value: "8" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -1253,7 +1253,7 @@ describe("FormCheckboxPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByLabelText("필드 너비") as HTMLSelectElement;
|
||||
const select = screen.getByLabelText("Field width") as HTMLSelectElement;
|
||||
const className = select.getAttribute("class") ?? "";
|
||||
|
||||
expect(className).toContain("bg-white");
|
||||
@@ -1282,13 +1282,13 @@ describe("FormCheckboxPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const fontSizeInput = screen.getByLabelText("라디오 텍스트 크기 (px) 커스텀 (px)");
|
||||
const fontSizeInput = screen.getByLabelText("Radio text size (px) 커스텀 (px)");
|
||||
fireEvent.change(fontSizeInput, { target: { value: "24" } });
|
||||
|
||||
const lineHeightInput = screen.getByLabelText("라디오 줄간격 (px) 커스텀 (px)");
|
||||
const lineHeightInput = screen.getByLabelText("Radio line height (px) 커스텀 (px)");
|
||||
fireEvent.change(lineHeightInput, { target: { value: "30" } });
|
||||
|
||||
const letterSpacingInput = screen.getByLabelText("라디오 자간 (px) 커스텀 (px)");
|
||||
const letterSpacingInput = screen.getByLabelText("Radio letter spacing (px) 커스텀 (px)");
|
||||
fireEvent.change(letterSpacingInput, { target: { value: "1.5" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -1325,16 +1325,16 @@ describe("FormCheckboxPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const widthPxInput = screen.getByLabelText("필드 고정 너비 커스텀 (px)");
|
||||
const widthPxInput = screen.getByLabelText("Field fixed width 커스텀 (px)");
|
||||
fireEvent.change(widthPxInput, { target: { value: "320" } });
|
||||
|
||||
const paddingXInput = screen.getByLabelText("라디오 가로 패딩 (px) 커스텀 (px)");
|
||||
const paddingXInput = screen.getByLabelText("Radio horizontal padding (px) 커스텀 (px)");
|
||||
fireEvent.change(paddingXInput, { target: { value: "12" } });
|
||||
|
||||
const paddingYInput = screen.getByLabelText("라디오 세로 패딩 (px) 커스텀 (px)");
|
||||
const paddingYInput = screen.getByLabelText("Radio vertical padding (px) 커스텀 (px)");
|
||||
fireEvent.change(paddingYInput, { target: { value: "6" } });
|
||||
|
||||
const optionGapInput = screen.getByLabelText("라디오 옵션 간격 (px) 커스텀 (px)");
|
||||
const optionGapInput = screen.getByLabelText("Radio option gap (px) 커스텀 (px)");
|
||||
fireEvent.change(optionGapInput, { target: { value: "10" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -1616,7 +1616,7 @@ describe("FormCheckboxPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const hexInput = screen.getByLabelText("라디오 배경 색상 HEX");
|
||||
const hexInput = screen.getByLabelText("Radio background color HEX");
|
||||
fireEvent.change(hexInput, { target: { value: "#445566" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -1641,7 +1641,7 @@ describe("FormCheckboxPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const hexInput = screen.getByLabelText("라디오 테두리 색상 HEX");
|
||||
const hexInput = screen.getByLabelText("Radio border color HEX");
|
||||
fireEvent.change(hexInput, { target: { value: "#778899" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -1666,7 +1666,7 @@ describe("FormCheckboxPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const radiusInput = screen.getByLabelText("필드 모서리 둥글기 커스텀");
|
||||
const radiusInput = screen.getByLabelText("Field border radius 커스텀");
|
||||
fireEvent.change(radiusInput, { target: { value: "8" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
|
||||
@@ -30,7 +30,7 @@ describe("ImagePropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const hexInput = screen.getByLabelText("이미지 카드 배경색 HEX");
|
||||
const hexInput = screen.getByLabelText("Image card background color HEX");
|
||||
fireEvent.change(hexInput, { target: { value: "#112233" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -55,7 +55,7 @@ describe("ImagePropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByLabelText("이미지 소스");
|
||||
const select = screen.getByLabelText("Image source");
|
||||
fireEvent.change(select, { target: { value: "url" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -80,7 +80,7 @@ describe("ImagePropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const urlInput = screen.getByLabelText("이미지 URL");
|
||||
const urlInput = screen.getByLabelText("Image URL");
|
||||
fireEvent.change(urlInput, { target: { value: "https://example.com/after.png" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -104,7 +104,7 @@ describe("ImagePropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const altInput = screen.getByLabelText("대체 텍스트");
|
||||
const altInput = screen.getByLabelText("Alt text");
|
||||
fireEvent.change(altInput, { target: { value: "새 대체 텍스트" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -124,7 +124,7 @@ describe("ImagePropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByLabelText("이미지 정렬");
|
||||
const select = screen.getByLabelText("Image alignment");
|
||||
fireEvent.change(select, { target: { value: "left" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -144,7 +144,7 @@ describe("ImagePropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByLabelText("이미지 너비 모드");
|
||||
const select = screen.getByLabelText("Image width mode");
|
||||
fireEvent.change(select, { target: { value: "fixed" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -164,7 +164,7 @@ describe("ImagePropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const slider = screen.getByLabelText("고정 너비 (px) 슬라이더");
|
||||
const slider = screen.getByLabelText("Fixed width (px) 슬라이더");
|
||||
fireEvent.change(slider, { target: { value: "480" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -184,7 +184,7 @@ describe("ImagePropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const slider = screen.getByLabelText("모서리 둥글기 슬라이더");
|
||||
const slider = screen.getByLabelText("Border radius 슬라이더");
|
||||
fireEvent.change(slider, { target: { value: "24" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -204,7 +204,7 @@ describe("ImagePropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const sourceSelect = screen.getByLabelText("이미지 소스") as HTMLSelectElement;
|
||||
const sourceSelect = screen.getByLabelText("Image source") as HTMLSelectElement;
|
||||
const sourceClass = sourceSelect.getAttribute("class") ?? "";
|
||||
expect(sourceClass).toContain("bg-white");
|
||||
expect(sourceClass).toContain("text-slate-900");
|
||||
@@ -213,7 +213,7 @@ describe("ImagePropertiesPanel", () => {
|
||||
expect(sourceClass).toContain("dark:text-slate-100");
|
||||
expect(sourceClass).toContain("dark:border-slate-700");
|
||||
|
||||
const urlInput = screen.getByLabelText("이미지 URL") as HTMLInputElement;
|
||||
const urlInput = screen.getByLabelText("Image URL") as HTMLInputElement;
|
||||
const urlClass = urlInput.getAttribute("class") ?? "";
|
||||
expect(urlClass).toContain("bg-white");
|
||||
expect(urlClass).toContain("text-slate-900");
|
||||
@@ -222,7 +222,7 @@ describe("ImagePropertiesPanel", () => {
|
||||
expect(urlClass).toContain("dark:text-slate-100");
|
||||
expect(urlClass).toContain("dark:border-slate-700");
|
||||
|
||||
const alignSelect = screen.getByLabelText("이미지 정렬") as HTMLSelectElement;
|
||||
const alignSelect = screen.getByLabelText("Image alignment") as HTMLSelectElement;
|
||||
const alignClass = alignSelect.getAttribute("class") ?? "";
|
||||
expect(alignClass).toContain("bg-white");
|
||||
expect(alignClass).toContain("text-slate-900");
|
||||
|
||||
@@ -26,7 +26,7 @@ describe("ListPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const textarea = screen.getByLabelText("리스트 아이템들");
|
||||
const textarea = screen.getByLabelText("List items");
|
||||
fireEvent.change(textarea, { target: { value: "첫째\n둘째" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -49,7 +49,7 @@ describe("ListPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByLabelText("리스트 정렬");
|
||||
const select = screen.getByLabelText("List alignment");
|
||||
fireEvent.change(select, { target: { value: "center" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -70,7 +70,7 @@ describe("ListPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const slider = screen.getByLabelText("글자 크기 (px) 슬라이더");
|
||||
const slider = screen.getByLabelText("Font size (px) 슬라이더");
|
||||
fireEvent.change(slider, { target: { value: "20" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -91,7 +91,7 @@ describe("ListPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const slider = screen.getByLabelText("줄 간격 슬라이더");
|
||||
const slider = screen.getByLabelText("Line height 슬라이더");
|
||||
fireEvent.change(slider, { target: { value: "1.8" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -112,7 +112,7 @@ describe("ListPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const hexInput = screen.getByLabelText("리스트 텍스트 색상 HEX");
|
||||
const hexInput = screen.getByLabelText("List text color HEX");
|
||||
fireEvent.change(hexInput, { target: { value: "#112233" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -121,7 +121,7 @@ describe("ListPropertiesPanel", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("텍스트 색상 팔레트에서 \"없음\" 을 선택하면 textColorCustom 이 빈 문자열로 업데이트되어야 한다", () => {
|
||||
it("텍스트 색상 팔레트에서 \"None\" 을 선택하면 textColorCustom 이 빈 문자열로 업데이트되어야 한다", () => {
|
||||
const updateBlock = vi.fn();
|
||||
|
||||
render(
|
||||
@@ -134,11 +134,11 @@ describe("ListPropertiesPanel", () => {
|
||||
);
|
||||
|
||||
// 첫 번째 ColorPickerField(텍스트 색상)의 팔레트 버튼을 연다.
|
||||
const paletteButtons = screen.getAllByText("색상 팔레트");
|
||||
const paletteButtons = screen.getAllByText("Color palette");
|
||||
fireEvent.click(paletteButtons[0].closest("button") as HTMLButtonElement);
|
||||
|
||||
// 팔레트에서 "없음" 항목을 버튼 role 기준으로 선택한다 (불릿 스타일 select 의 option "없음" 과 구분).
|
||||
const noneButtons = screen.getAllByRole("button", { name: "없음" });
|
||||
// 팔레트에서 "None" 항목을 버튼 role 기준으로 선택한다 (불릿 스타일 select 의 option "none" 과 구분).
|
||||
const noneButtons = screen.getAllByRole("button", { name: "None" });
|
||||
fireEvent.click(noneButtons[0]);
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -159,7 +159,7 @@ describe("ListPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const hexInput = screen.getByLabelText("리스트 텍스트 색상 HEX") as HTMLInputElement;
|
||||
const hexInput = screen.getByLabelText("List text color HEX") as HTMLInputElement;
|
||||
expect(hexInput.value).toBe("");
|
||||
});
|
||||
|
||||
@@ -175,7 +175,7 @@ describe("ListPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByLabelText("리스트 불릿 스타일");
|
||||
const select = screen.getByLabelText("List bullet style");
|
||||
fireEvent.change(select, { target: { value: "decimal" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -196,7 +196,7 @@ describe("ListPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByLabelText("리스트 불릿 스타일");
|
||||
const select = screen.getByLabelText("List bullet style");
|
||||
fireEvent.change(select, { target: { value: "none" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -217,7 +217,7 @@ describe("ListPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const slider = screen.getByLabelText("아이템 간 여백 (px) 슬라이더");
|
||||
const slider = screen.getByLabelText("Item gap (px) 슬라이더");
|
||||
fireEvent.change(slider, { target: { value: "16" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -238,7 +238,7 @@ describe("ListPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByLabelText("리스트 정렬") as HTMLSelectElement;
|
||||
const select = screen.getByLabelText("List alignment") as HTMLSelectElement;
|
||||
const className = select.getAttribute("class") ?? "";
|
||||
|
||||
expect(className).toContain("bg-white");
|
||||
@@ -261,7 +261,7 @@ describe("ListPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByLabelText("리스트 불릿 스타일") as HTMLSelectElement;
|
||||
const select = screen.getByLabelText("List bullet style") as HTMLSelectElement;
|
||||
const className = select.getAttribute("class") ?? "";
|
||||
|
||||
expect(className).toContain("bg-white");
|
||||
@@ -284,7 +284,7 @@ describe("ListPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const textarea = screen.getByLabelText("리스트 아이템들") as HTMLTextAreaElement;
|
||||
const textarea = screen.getByLabelText("List items") as HTMLTextAreaElement;
|
||||
const className = textarea.getAttribute("class") ?? "";
|
||||
|
||||
// 라이트 모드: 흰 배경 + 어두운 텍스트
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { describe, it, expect, afterEach } from "vitest";
|
||||
import { render, screen, fireEvent, cleanup } from "@testing-library/react";
|
||||
import { LocaleProvider, useAppLocale, useLocaleActions } from "@/features/i18n/LocaleProvider";
|
||||
|
||||
// LocaleProvider / useAppLocale / useLocaleActions TDD
|
||||
// - initialLocale 로 전달된 로케일이 하위 컴포넌트에서 그대로 노출되는지 검증한다.
|
||||
// - useLocaleActions 로 setLocale 을 호출하면 하위 컴포넌트의 로케일이 변경되어야 한다.
|
||||
|
||||
describe("LocaleProvider", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
function LocaleConsumer() {
|
||||
const locale = useAppLocale();
|
||||
return <div data-testid="current-locale">{locale}</div>;
|
||||
}
|
||||
|
||||
function LocaleSetterButton({ next }: { next: "en" | "ko" }) {
|
||||
const { setLocale } = useLocaleActions();
|
||||
return (
|
||||
<button type="button" onClick={() => setLocale(next)}>
|
||||
set-{next}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
it("initialLocale 로 en 을 전달하면 하위 컴포넌트에서 en 로케일을 읽을 수 있어야 한다", () => {
|
||||
render(
|
||||
<LocaleProvider initialLocale="en">
|
||||
<LocaleConsumer />
|
||||
</LocaleProvider>,
|
||||
);
|
||||
|
||||
const current = screen.getByTestId("current-locale");
|
||||
|
||||
expect(current.textContent).toBe("en");
|
||||
});
|
||||
|
||||
it("setLocale 을 호출하면 하위 컴포넌트의 useAppLocale 결과가 업데이트되어야 한다", () => {
|
||||
render(
|
||||
<LocaleProvider initialLocale="en">
|
||||
<LocaleConsumer />
|
||||
<LocaleSetterButton next="ko" />
|
||||
</LocaleProvider>,
|
||||
);
|
||||
|
||||
const current = screen.getByTestId("current-locale");
|
||||
|
||||
expect(current.textContent).toBe("en");
|
||||
|
||||
fireEvent.click(screen.getByText("set-ko"));
|
||||
|
||||
expect(current.textContent).toBe("ko");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import { describe, it, expect, afterEach } from "vitest";
|
||||
import { render, screen, fireEvent, cleanup } from "@testing-library/react";
|
||||
import { LocaleProvider, useAppLocale } from "@/features/i18n/LocaleProvider";
|
||||
import { LocaleSwitcher } from "@/features/i18n/LocaleSwitcher";
|
||||
|
||||
// LocaleSwitcher TDD
|
||||
// - 현재 로케일에 따라 EN/KO 버튼 텍스트가 표시되어야 한다.
|
||||
// - 버튼을 클릭하면 로케일이 토글되고, 하위 컴포넌트의 useAppLocale 결과도 함께 변경되어야 한다.
|
||||
|
||||
describe("LocaleSwitcher", () => {
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
function LocaleConsumer() {
|
||||
const locale = useAppLocale();
|
||||
return <div data-testid="current-locale">{locale}</div>;
|
||||
}
|
||||
|
||||
it("초기 로케일이 en 이면 버튼 텍스트는 EN 이어야 한다", () => {
|
||||
render(
|
||||
<LocaleProvider initialLocale="en">
|
||||
<LocaleSwitcher />
|
||||
<LocaleConsumer />
|
||||
</LocaleProvider>,
|
||||
);
|
||||
|
||||
const button = screen.getByRole("button", { name: "Toggle locale" });
|
||||
const current = screen.getByTestId("current-locale");
|
||||
|
||||
expect(button.textContent).toContain("EN");
|
||||
expect(current.textContent).toBe("en");
|
||||
});
|
||||
|
||||
it("버튼을 클릭하면 로케일이 en/ko 사이에서 토글되어야 한다", () => {
|
||||
render(
|
||||
<LocaleProvider initialLocale="en">
|
||||
<LocaleSwitcher />
|
||||
<LocaleConsumer />
|
||||
</LocaleProvider>,
|
||||
);
|
||||
|
||||
const button = screen.getByRole("button", { name: "Toggle locale" });
|
||||
const current = screen.getByTestId("current-locale");
|
||||
|
||||
expect(current.textContent).toBe("en");
|
||||
expect(button.textContent).toContain("EN");
|
||||
|
||||
fireEvent.click(button);
|
||||
|
||||
expect(current.textContent).toBe("ko");
|
||||
expect(button.textContent).toContain("KO");
|
||||
});
|
||||
});
|
||||
@@ -54,9 +54,9 @@ describe("LoginPage", () => {
|
||||
|
||||
render(<LoginPage />);
|
||||
|
||||
const emailInput = screen.getByLabelText("이메일") as HTMLInputElement;
|
||||
const passwordInput = screen.getByLabelText("비밀번호") as HTMLInputElement;
|
||||
const submitButton = screen.getByRole("button", { name: "로그인" });
|
||||
const emailInput = screen.getByLabelText("Email") as HTMLInputElement;
|
||||
const passwordInput = screen.getByLabelText("Password") as HTMLInputElement;
|
||||
const submitButton = screen.getByRole("button", { name: "Log in" });
|
||||
|
||||
fireEvent.change(emailInput, { target: { value: "user@example.com" } });
|
||||
fireEvent.change(passwordInput, { target: { value: "securePass1" } });
|
||||
@@ -116,9 +116,9 @@ describe("LoginPage", () => {
|
||||
|
||||
render(<LoginPage />);
|
||||
|
||||
const emailInput = screen.getByLabelText("이메일") as HTMLInputElement;
|
||||
const passwordInput = screen.getByLabelText("비밀번호") as HTMLInputElement;
|
||||
const submitButton = screen.getByRole("button", { name: "로그인" });
|
||||
const emailInput = screen.getByLabelText("Email") as HTMLInputElement;
|
||||
const passwordInput = screen.getByLabelText("Password") as HTMLInputElement;
|
||||
const submitButton = screen.getByRole("button", { name: "Log in" });
|
||||
|
||||
fireEvent.change(emailInput, { target: { value: "user@example.com" } });
|
||||
fireEvent.change(passwordInput, { target: { value: "wrongpass" } });
|
||||
@@ -179,8 +179,8 @@ describe("LoginPage", () => {
|
||||
|
||||
render(<LoginPage />);
|
||||
|
||||
const emailInput = screen.getByLabelText("이메일") as HTMLInputElement;
|
||||
const passwordInput = screen.getByLabelText("비밀번호") as HTMLInputElement;
|
||||
const emailInput = screen.getByLabelText("Email") as HTMLInputElement;
|
||||
const passwordInput = screen.getByLabelText("Password") as HTMLInputElement;
|
||||
|
||||
const emailClass = emailInput.className;
|
||||
const passwordClass = passwordInput.className;
|
||||
@@ -215,7 +215,7 @@ describe("LoginPage", () => {
|
||||
const html = document.documentElement;
|
||||
html.classList.remove("dark");
|
||||
|
||||
const themeToggleButton = await screen.findByRole("button", { name: "테마 전환" });
|
||||
const themeToggleButton = await screen.findByRole("button", { name: "Toggle theme" });
|
||||
|
||||
expect(html.classList.contains("dark")).toBe(false);
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ describe("PreviewPage - ZIP Export", () => {
|
||||
it("프리뷰 헤더에 프로젝트 목록 링크가 노출되고 /projects 로 연결되어야 한다", () => {
|
||||
render(<PreviewPage />);
|
||||
|
||||
const link = screen.getByText("프로젝트 목록");
|
||||
const link = screen.getByText("Projects");
|
||||
expect(link).toBeTruthy();
|
||||
expect(link.closest("a")?.getAttribute("href")).toBe("/projects");
|
||||
});
|
||||
@@ -73,7 +73,7 @@ describe("PreviewPage - ZIP Export", () => {
|
||||
it("프리뷰 헤더에 ZIP Export 버튼이 노출되어야 한다", () => {
|
||||
render(<PreviewPage />);
|
||||
|
||||
const exportButton = screen.getByText("페이지 파일로 내보내기 (ZIP)");
|
||||
const exportButton = screen.getByText("Export as page files (ZIP)");
|
||||
expect(exportButton).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -88,7 +88,7 @@ describe("PreviewPage - ZIP Export", () => {
|
||||
|
||||
render(<PreviewPage />);
|
||||
|
||||
const exportButton = screen.getByText("페이지 파일로 내보내기 (ZIP)");
|
||||
const exportButton = screen.getByText("Export as page files (ZIP)");
|
||||
fireEvent.click(exportButton);
|
||||
|
||||
await waitFor(() => {
|
||||
|
||||
@@ -7,7 +7,7 @@ let mockState: { projectConfig: ProjectConfig; updateProjectConfig: ReturnType<t
|
||||
|
||||
beforeEach(() => {
|
||||
const baseConfig: ProjectConfig = {
|
||||
title: "프로젝트",
|
||||
title: "Project",
|
||||
slug: "project",
|
||||
canvasPreset: "full",
|
||||
canvasWidthPx: 1024,
|
||||
@@ -37,7 +37,7 @@ describe("ProjectPropertiesPanel", () => {
|
||||
it("캔버스 너비 프리셋을 변경하면 canvasPreset/canvasWidthPx 가 업데이트되어야 한다", () => {
|
||||
render(<ProjectPropertiesPanel />);
|
||||
|
||||
const select = screen.getByLabelText("캔버스 너비 프리셋") as HTMLSelectElement;
|
||||
const select = screen.getByLabelText("Canvas width 프리셋") as HTMLSelectElement;
|
||||
|
||||
fireEvent.change(select, { target: { value: "mobile" } });
|
||||
|
||||
@@ -47,7 +47,7 @@ describe("ProjectPropertiesPanel", () => {
|
||||
it("태블릿 프리셋을 선택하면 canvasPreset=tablet 과 canvasWidthPx=768 로 업데이트되어야 한다", () => {
|
||||
render(<ProjectPropertiesPanel />);
|
||||
|
||||
const select = screen.getByLabelText("캔버스 너비 프리셋") as HTMLSelectElement;
|
||||
const select = screen.getByLabelText("Canvas width 프리셋") as HTMLSelectElement;
|
||||
|
||||
fireEvent.change(select, { target: { value: "tablet" } });
|
||||
|
||||
@@ -57,7 +57,7 @@ describe("ProjectPropertiesPanel", () => {
|
||||
it("데스크톱 프리셋을 선택하면 canvasPreset=desktop 과 canvasWidthPx=1200 으로 업데이트되어야 한다", () => {
|
||||
render(<ProjectPropertiesPanel />);
|
||||
|
||||
const select = screen.getByLabelText("캔버스 너비 프리셋") as HTMLSelectElement;
|
||||
const select = screen.getByLabelText("Canvas width 프리셋") as HTMLSelectElement;
|
||||
|
||||
fireEvent.change(select, { target: { value: "desktop" } });
|
||||
|
||||
@@ -67,7 +67,7 @@ describe("ProjectPropertiesPanel", () => {
|
||||
it("캔버스 너비를 390 으로 입력하면 mobile 프리셋으로 동기화되어야 한다", () => {
|
||||
render(<ProjectPropertiesPanel />);
|
||||
|
||||
const input = screen.getByLabelText("캔버스 너비 커스텀 (px)") as HTMLInputElement;
|
||||
const input = screen.getByLabelText("Canvas width 커스텀 (px)") as HTMLInputElement;
|
||||
|
||||
fireEvent.change(input, { target: { value: "390" } });
|
||||
|
||||
@@ -77,7 +77,7 @@ describe("ProjectPropertiesPanel", () => {
|
||||
it("캔버스 너비를 768 로 입력하면 tablet 프리셋으로 동기화되어야 한다", () => {
|
||||
render(<ProjectPropertiesPanel />);
|
||||
|
||||
const input = screen.getByLabelText("캔버스 너비 커스텀 (px)") as HTMLInputElement;
|
||||
const input = screen.getByLabelText("Canvas width 커스텀 (px)") as HTMLInputElement;
|
||||
|
||||
fireEvent.change(input, { target: { value: "768" } });
|
||||
|
||||
@@ -87,7 +87,7 @@ describe("ProjectPropertiesPanel", () => {
|
||||
it("캔버스 너비를 1200 으로 입력하면 desktop 프리셋으로 동기화되어야 한다", () => {
|
||||
render(<ProjectPropertiesPanel />);
|
||||
|
||||
const input = screen.getByLabelText("캔버스 너비 커스텀 (px)") as HTMLInputElement;
|
||||
const input = screen.getByLabelText("Canvas width 커스텀 (px)") as HTMLInputElement;
|
||||
|
||||
fireEvent.change(input, { target: { value: "1200" } });
|
||||
|
||||
@@ -97,7 +97,7 @@ describe("ProjectPropertiesPanel", () => {
|
||||
it("캔버스 너비를 커스텀 값으로 입력하면 canvasPreset=custom 과 함께 canvasWidthPx 가 설정되어야 한다", () => {
|
||||
render(<ProjectPropertiesPanel />);
|
||||
|
||||
const input = screen.getByLabelText("캔버스 너비 커스텀 (px)") as HTMLInputElement;
|
||||
const input = screen.getByLabelText("Canvas width 커스텀 (px)") as HTMLInputElement;
|
||||
|
||||
fireEvent.change(input, { target: { value: "1000" } });
|
||||
|
||||
@@ -107,7 +107,7 @@ describe("ProjectPropertiesPanel", () => {
|
||||
it("캔버스 배경색 HEX 입력을 변경하면 canvasBgColorHex 가 업데이트되어야 한다", () => {
|
||||
render(<ProjectPropertiesPanel />);
|
||||
|
||||
const input = screen.getByLabelText("캔버스 배경색 HEX") as HTMLInputElement;
|
||||
const input = screen.getByLabelText("Canvas background HEX") as HTMLInputElement;
|
||||
|
||||
fireEvent.change(input, { target: { value: "#123456" } });
|
||||
|
||||
@@ -117,35 +117,35 @@ describe("ProjectPropertiesPanel", () => {
|
||||
it("페이지 배경색 HEX 입력을 변경하면 bodyBgColorHex 가 업데이트되어야 한다", () => {
|
||||
render(<ProjectPropertiesPanel />);
|
||||
|
||||
const input = screen.getByLabelText("페이지 배경색 HEX") as HTMLInputElement;
|
||||
const input = screen.getByLabelText("Page background HEX") as HTMLInputElement;
|
||||
|
||||
fireEvent.change(input, { target: { value: "#654321" } });
|
||||
|
||||
expect(mockState.updateProjectConfig).toHaveBeenCalledWith({ bodyBgColorHex: "#654321" });
|
||||
});
|
||||
|
||||
it("캔버스 배경색 팔레트에서 \"없음\" 을 선택하면 canvasBgColorHex 가 빈 문자열로 업데이트되어야 한다", () => {
|
||||
it("캔버스 배경색 팔레트에서 \"None\" 을 선택하면 canvasBgColorHex 가 빈 문자열로 업데이트되어야 한다", () => {
|
||||
render(<ProjectPropertiesPanel />);
|
||||
|
||||
// 첫 번째 ColorPickerField(캔버스 배경색)의 팔레트 버튼을 연다.
|
||||
const paletteButtons = screen.getAllByText("색상 팔레트");
|
||||
const paletteButtons = screen.getAllByText("Color palette");
|
||||
fireEvent.click(paletteButtons[0].closest("button") as HTMLButtonElement);
|
||||
|
||||
// 팔레트에서 "없음" 항목을 선택한다.
|
||||
const noneButton = screen.getByText("없음");
|
||||
// 팔레트에서 "None" 항목을 선택한다.
|
||||
const noneButton = screen.getByText("None");
|
||||
fireEvent.click(noneButton);
|
||||
|
||||
expect(mockState.updateProjectConfig).toHaveBeenCalledWith({ canvasBgColorHex: "" });
|
||||
});
|
||||
|
||||
it("페이지 배경색 팔레트에서 \"없음\" 을 선택하면 bodyBgColorHex 가 빈 문자열로 업데이트되어야 한다", () => {
|
||||
it("페이지 배경색 팔레트에서 \"None\" 을 선택하면 bodyBgColorHex 가 빈 문자열로 업데이트되어야 한다", () => {
|
||||
render(<ProjectPropertiesPanel />);
|
||||
|
||||
// 두 번째 ColorPickerField(페이지 배경색)의 팔레트 버튼은 기본 선택 팔레트 라벨("어두운 텍스트")를 기준으로 찾는다.
|
||||
const darkLabel = screen.getByText("어두운 텍스트");
|
||||
// 두 번째 ColorPickerField(페이지 배경색)의 팔레트 버튼은 기본 선택 팔레트 라벨("Dark text")를 기준으로 찾는다.
|
||||
const darkLabel = screen.getByText("Dark text");
|
||||
fireEvent.click(darkLabel.closest("button") as HTMLButtonElement);
|
||||
|
||||
const noneButton = screen.getByText("없음");
|
||||
const noneButton = screen.getByText("None");
|
||||
fireEvent.click(noneButton);
|
||||
|
||||
expect(mockState.updateProjectConfig).toHaveBeenCalledWith({ bodyBgColorHex: "" });
|
||||
@@ -154,7 +154,7 @@ describe("ProjectPropertiesPanel", () => {
|
||||
it("프로젝트 제목 입력은 라이트/다크 테마에 맞는 배경/텍스트 클래스를 사용해야 한다", () => {
|
||||
render(<ProjectPropertiesPanel />);
|
||||
|
||||
const input = screen.getByLabelText("프로젝트 제목") as HTMLInputElement;
|
||||
const input = screen.getByLabelText("Project title") as HTMLInputElement;
|
||||
const className = input.getAttribute("class") ?? "";
|
||||
|
||||
// 라이트 모드: 흰 배경 + 어두운 텍스트
|
||||
|
||||
@@ -69,12 +69,12 @@ describe("ProjectsPage - 프로젝트 목록", () => {
|
||||
expect(await screen.findByText("테스트 프로젝트 B")).toBeTruthy();
|
||||
expect(screen.getByText("test-project-b")).toBeTruthy();
|
||||
|
||||
const editLinks = screen.getAllByText("편집");
|
||||
const editLinks = screen.getAllByText("Edit");
|
||||
expect(editLinks).toHaveLength(2);
|
||||
expect(editLinks[0].closest("a")?.getAttribute("href")).toBe("/editor?slug=test-project-a");
|
||||
expect(editLinks[1].closest("a")?.getAttribute("href")).toBe("/editor?slug=test-project-b");
|
||||
|
||||
const previewLinks = screen.getAllByText("미리보기");
|
||||
const previewLinks = screen.getAllByText("Preview");
|
||||
expect(previewLinks).toHaveLength(2);
|
||||
expect(previewLinks[0].closest("a")?.getAttribute("href")).toBe("/preview?slug=test-project-a");
|
||||
expect(previewLinks[1].closest("a")?.getAttribute("href")).toBe("/preview?slug=test-project-b");
|
||||
@@ -107,7 +107,7 @@ describe("ProjectsPage - 프로젝트 목록", () => {
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
const link = screen.getByText("전체 제출 내역");
|
||||
const link = screen.getByText("All submissions");
|
||||
expect(link).toBeTruthy();
|
||||
expect(link.closest("a")?.getAttribute("href")).toBe("/projects/submissions");
|
||||
});
|
||||
@@ -139,7 +139,7 @@ describe("ProjectsPage - 프로젝트 목록", () => {
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
const link = screen.getByText("대시보드");
|
||||
const link = screen.getByText("Dashboard");
|
||||
expect(link).toBeTruthy();
|
||||
expect(link.closest("a")?.getAttribute("href")).toBe("/dashboard");
|
||||
});
|
||||
@@ -220,31 +220,31 @@ describe("ProjectsPage - 프로젝트 목록", () => {
|
||||
expect(toolbarClass).toContain("text-slate-600");
|
||||
expect(toolbarClass).toContain("dark:text-slate-300");
|
||||
|
||||
const editLink = screen.getByText("편집").closest("a") as HTMLElement | null;
|
||||
const editLink = screen.getByText("Edit").closest("a") as HTMLElement | null;
|
||||
expect(editLink).not.toBeNull();
|
||||
const editClass = editLink!.className;
|
||||
expect(editClass).toContain("text-sky-600");
|
||||
expect(editClass).toContain("dark:text-sky-300");
|
||||
|
||||
const previewLink = screen.getByText("미리보기").closest("a") as HTMLElement | null;
|
||||
const previewLink = screen.getByText("Preview").closest("a") as HTMLElement | null;
|
||||
expect(previewLink).not.toBeNull();
|
||||
const previewClass = previewLink!.className;
|
||||
expect(previewClass).toContain("text-slate-600");
|
||||
expect(previewClass).toContain("dark:text-slate-300");
|
||||
|
||||
const submissionsLink = screen.getByText("폼 제출 내역").closest("a") as HTMLElement | null;
|
||||
const submissionsLink = screen.getByText("Form submissions").closest("a") as HTMLElement | null;
|
||||
expect(submissionsLink).not.toBeNull();
|
||||
const submissionsClass = submissionsLink!.className;
|
||||
expect(submissionsClass).toContain("text-emerald-600");
|
||||
expect(submissionsClass).toContain("dark:text-emerald-300");
|
||||
|
||||
const deleteButton = screen.getByText("삭제").closest("button") as HTMLElement | null;
|
||||
const deleteButton = screen.getByText("Delete").closest("button") as HTMLElement | null;
|
||||
expect(deleteButton).not.toBeNull();
|
||||
const deleteClass = deleteButton!.className;
|
||||
expect(deleteClass).toContain("text-red-600");
|
||||
expect(deleteClass).toContain("dark:text-red-300");
|
||||
|
||||
const prevButton = screen.getByRole("button", { name: "이전" });
|
||||
const prevButton = screen.getByRole("button", { name: "Previous" });
|
||||
const prevClass = prevButton.className;
|
||||
expect(prevClass).toContain("border-slate-300");
|
||||
expect(prevClass).toContain("text-slate-700");
|
||||
@@ -279,9 +279,9 @@ describe("ProjectsPage - 프로젝트 목록", () => {
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
const dashboardLink = await screen.findByRole("link", { name: "대시보드" });
|
||||
const projectsLink = await screen.findByRole("link", { name: "프로젝트 목록" });
|
||||
const submissionsLink = await screen.findByRole("link", { name: "전체 제출 내역" });
|
||||
const dashboardLink = await screen.findByRole("link", { name: "Dashboard" });
|
||||
const projectsLink = await screen.findByRole("link", { name: "Projects" });
|
||||
const submissionsLink = await screen.findByRole("link", { name: "All submissions" });
|
||||
|
||||
expect(projectsLink.closest("a")?.getAttribute("href")).toBe("/projects");
|
||||
expect(projectsLink.closest("a")?.getAttribute("aria-current")).toBe("page");
|
||||
@@ -320,7 +320,7 @@ describe("ProjectsPage - 프로젝트 목록", () => {
|
||||
const html = document.documentElement;
|
||||
html.classList.remove("dark");
|
||||
|
||||
const themeToggleButton = await screen.findByRole("button", { name: "테마 전환" });
|
||||
const themeToggleButton = await screen.findByRole("button", { name: "Toggle theme" });
|
||||
|
||||
expect(html.classList.contains("dark")).toBe(false);
|
||||
|
||||
@@ -367,7 +367,7 @@ describe("ProjectsPage - 프로젝트 목록", () => {
|
||||
expect(await screen.findByText("테스트 프로젝트 A")).toBeTruthy();
|
||||
expect(await screen.findByText("테스트 프로젝트 B")).toBeTruthy();
|
||||
|
||||
const submissionLinks = screen.getAllByText("폼 제출 내역");
|
||||
const submissionLinks = screen.getAllByText("Form submissions");
|
||||
expect(submissionLinks).toHaveLength(2);
|
||||
expect(submissionLinks[0].closest("a")?.getAttribute("href")).toBe(
|
||||
"/projects/test-project-a/submissions",
|
||||
@@ -428,7 +428,7 @@ describe("ProjectsPage - 프로젝트 목록", () => {
|
||||
|
||||
expect(await screen.findByText("테스트 프로젝트 A")).toBeTruthy();
|
||||
|
||||
const deleteButtons = screen.getAllByText("삭제");
|
||||
const deleteButtons = screen.getAllByText("Delete");
|
||||
expect(deleteButtons.length).toBeGreaterThan(0);
|
||||
|
||||
deleteButtons[0].click();
|
||||
@@ -482,8 +482,8 @@ describe("ProjectsPage - 프로젝트 목록", () => {
|
||||
// 상단 툴바에서 선택 삭제와 새 프로젝트 만들기 버튼이 함께 보여야 한다.
|
||||
const toolbar = await screen.findByTestId("projects-toolbar");
|
||||
|
||||
expect(toolbar.querySelector("button[type='button']")?.textContent).toContain("선택 삭제");
|
||||
expect(toolbar.querySelector("a[href='/editor?new=1']")?.textContent).toContain("새 프로젝트 만들기");
|
||||
expect(toolbar.querySelector("button[type='button']")?.textContent).toContain("Delete selected");
|
||||
expect(toolbar.querySelector("a[href='/editor?new=1']")?.textContent).toContain("Create new project");
|
||||
});
|
||||
|
||||
it("체크박스로 여러 프로젝트를 선택한 뒤 '선택 삭제' 버튼으로 일괄 삭제할 수 있어야 한다", async () => {
|
||||
@@ -544,13 +544,20 @@ describe("ProjectsPage - 프로젝트 목록", () => {
|
||||
expect(await screen.findByText("테스트 프로젝트 A")).toBeTruthy();
|
||||
expect(await screen.findByText("테스트 프로젝트 B")).toBeTruthy();
|
||||
|
||||
const checkboxA = screen.getByLabelText("테스트 프로젝트 A 선택");
|
||||
const checkboxB = screen.getByLabelText("테스트 프로젝트 B 선택");
|
||||
const rowA = (await screen.findByText("테스트 프로젝트 A")).closest("tr") as HTMLElement | null;
|
||||
const rowB = (await screen.findByText("테스트 프로젝트 B")).closest("tr") as HTMLElement | null;
|
||||
expect(rowA).not.toBeNull();
|
||||
expect(rowB).not.toBeNull();
|
||||
|
||||
fireEvent.click(checkboxA);
|
||||
fireEvent.click(checkboxB);
|
||||
const checkboxA = rowA!.querySelector("input[type='checkbox']") as HTMLInputElement | null;
|
||||
const checkboxB = rowB!.querySelector("input[type='checkbox']") as HTMLInputElement | null;
|
||||
expect(checkboxA).not.toBeNull();
|
||||
expect(checkboxB).not.toBeNull();
|
||||
|
||||
const bulkDeleteButton = screen.getByText("선택 삭제");
|
||||
fireEvent.click(checkboxA!);
|
||||
fireEvent.click(checkboxB!);
|
||||
|
||||
const bulkDeleteButton = screen.getByText("Delete selected");
|
||||
fireEvent.click(bulkDeleteButton);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -635,10 +642,10 @@ describe("ProjectsPage - 프로젝트 목록", () => {
|
||||
expect(fetchMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
const menuButton = await screen.findByRole("button", { name: "메뉴" });
|
||||
const menuButton = await screen.findByRole("button", { name: "Menu" });
|
||||
fireEvent.click(menuButton);
|
||||
|
||||
const logoutButton = await screen.findByRole("button", { name: "로그아웃" });
|
||||
const logoutButton = await screen.findByRole("button", { name: "Log out" });
|
||||
fireEvent.click(logoutButton);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -669,7 +676,7 @@ describe("ProjectsPage - 프로젝트 목록", () => {
|
||||
|
||||
expect(
|
||||
await screen.findByText(
|
||||
"프로젝트 목록을 불러오는 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.",
|
||||
"An error occurred while loading your projects. Please try again later.",
|
||||
),
|
||||
).toBeTruthy();
|
||||
});
|
||||
@@ -714,11 +721,13 @@ describe("ProjectsPage - 프로젝트 목록", () => {
|
||||
|
||||
expect(await screen.findByText("삭제 실패 프로젝트")).toBeTruthy();
|
||||
|
||||
const deleteButton = screen.getByText("삭제");
|
||||
const deleteButton = screen.getByText("Delete");
|
||||
fireEvent.click(deleteButton);
|
||||
|
||||
expect(
|
||||
await screen.findByText("프로젝트 삭제 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요."),
|
||||
await screen.findByText(
|
||||
"An error occurred while deleting the project. Please try again later.",
|
||||
),
|
||||
).toBeTruthy();
|
||||
|
||||
// 삭제 실패이므로 여전히 목록에 남아 있어야 한다.
|
||||
@@ -780,18 +789,25 @@ describe("ProjectsPage - 프로젝트 목록", () => {
|
||||
expect(await screen.findByText("성공 프로젝트")).toBeTruthy();
|
||||
expect(await screen.findByText("실패 프로젝트")).toBeTruthy();
|
||||
|
||||
const checkboxSuccess = screen.getByLabelText("성공 프로젝트 선택");
|
||||
const checkboxFail = screen.getByLabelText("실패 프로젝트 선택");
|
||||
const successRow = (await screen.findByText("성공 프로젝트")).closest("tr") as HTMLElement | null;
|
||||
const failRow = (await screen.findByText("실패 프로젝트")).closest("tr") as HTMLElement | null;
|
||||
expect(successRow).not.toBeNull();
|
||||
expect(failRow).not.toBeNull();
|
||||
|
||||
fireEvent.click(checkboxSuccess);
|
||||
fireEvent.click(checkboxFail);
|
||||
const checkboxSuccess = successRow!.querySelector("input[type='checkbox']") as HTMLInputElement | null;
|
||||
const checkboxFail = failRow!.querySelector("input[type='checkbox']") as HTMLInputElement | null;
|
||||
expect(checkboxSuccess).not.toBeNull();
|
||||
expect(checkboxFail).not.toBeNull();
|
||||
|
||||
const bulkDeleteButton = screen.getByText("선택 삭제");
|
||||
fireEvent.click(checkboxSuccess!);
|
||||
fireEvent.click(checkboxFail!);
|
||||
|
||||
const bulkDeleteButton = screen.getByText("Delete selected");
|
||||
fireEvent.click(bulkDeleteButton);
|
||||
|
||||
expect(
|
||||
await screen.findByText(
|
||||
"일부 프로젝트 삭제에 실패했습니다. 페이지를 새로고침한 뒤 목록을 확인해 주세요.",
|
||||
"Failed to delete some projects. Please refresh and review the list.",
|
||||
),
|
||||
).toBeTruthy();
|
||||
|
||||
|
||||
@@ -52,8 +52,8 @@ describe("PropertiesSidebar HELP 튜토리얼 모달", () => {
|
||||
const helpButton = screen.getByRole("button", { name: "HELP" });
|
||||
fireEvent.click(helpButton);
|
||||
|
||||
expect(screen.getByText("텍스트 블록 튜토리얼")).toBeDefined();
|
||||
expect(screen.getByText(/제목과 본문 텍스트를 입력할 때 사용하는 기본 블록입니다/)).toBeDefined();
|
||||
expect(screen.getByText("Text block tutorial")).toBeDefined();
|
||||
expect(screen.getByText(/basic block for headings and body text/i)).toBeDefined();
|
||||
});
|
||||
|
||||
it("HELP 모달은 라이트/다크 테마에 맞는 카드 배경/테두리/텍스트 클래스를 사용해야 한다", () => {
|
||||
@@ -65,7 +65,7 @@ describe("PropertiesSidebar HELP 튜토리얼 모달", () => {
|
||||
fireEvent.click(helpButton);
|
||||
|
||||
// 모달 카드 컨테이너 (제목 h3 → 헤더 div → 카드 div 순으로 감싸져 있으므로 parentElement.parentElement 사용)
|
||||
const heading = screen.getByText("텍스트 블록 튜토리얼");
|
||||
const heading = screen.getByText("Text block tutorial");
|
||||
const modalCard = heading.parentElement?.parentElement as HTMLElement | null;
|
||||
expect(modalCard).not.toBeNull();
|
||||
const className = modalCard!.getAttribute("class") ?? "";
|
||||
@@ -88,7 +88,7 @@ describe("PropertiesSidebar HELP 튜토리얼 모달", () => {
|
||||
const helpButton = screen.getByRole("button", { name: "HELP" });
|
||||
fireEvent.click(helpButton);
|
||||
|
||||
const closeButton = screen.getByRole("button", { name: "닫기" });
|
||||
const closeButton = screen.getByRole("button", { name: "Close" });
|
||||
fireEvent.click(closeButton);
|
||||
|
||||
expect(screen.queryByText("텍스트 블록 튜토리얼")).toBeNull();
|
||||
@@ -99,8 +99,8 @@ describe("PropertiesSidebar HELP 튜토리얼 모달", () => {
|
||||
|
||||
render(<PropertiesSidebar {...defaultProps([textBlock], textBlock.id)} />);
|
||||
|
||||
const deleteButton = screen.getByRole("button", { name: "블록 삭제" });
|
||||
const duplicateButton = screen.getByRole("button", { name: "블록 복제" });
|
||||
const deleteButton = screen.getByRole("button", { name: "Delete block" });
|
||||
const duplicateButton = screen.getByRole("button", { name: "Duplicate block" });
|
||||
const helpButton = screen.getByRole("button", { name: "HELP" });
|
||||
|
||||
for (const btn of [deleteButton, duplicateButton, helpButton]) {
|
||||
@@ -119,7 +119,7 @@ describe("PropertiesSidebar HELP 튜토리얼 모달", () => {
|
||||
|
||||
render(<PropertiesSidebar {...defaultProps([textBlock], textBlock.id)} />);
|
||||
|
||||
const textarea = screen.getByLabelText("선택한 텍스트 블록 내용") as HTMLTextAreaElement;
|
||||
const textarea = screen.getByLabelText("Selected text block content") as HTMLTextAreaElement;
|
||||
const className = textarea.getAttribute("class") ?? "";
|
||||
|
||||
// 라이트 모드: 흰 배경 + 어두운 텍스트
|
||||
|
||||
@@ -23,8 +23,8 @@ describe("PublicPageRenderer - 폼 제출 메시지", () => {
|
||||
props: {
|
||||
kind: "contact",
|
||||
submitTarget: "internal",
|
||||
successMessage: "폼 성공 메시지 (config)",
|
||||
errorMessage: "폼 에러 메시지 (config)",
|
||||
successMessage: "Form success message (config)",
|
||||
errorMessage: "Form error message (config)",
|
||||
fieldIds: ["field_name"],
|
||||
requiredFieldIds: ["field_name"],
|
||||
submitButtonId: "submit_btn",
|
||||
@@ -34,7 +34,7 @@ describe("PublicPageRenderer - 폼 제출 메시지", () => {
|
||||
id: "field_name",
|
||||
type: "formInput",
|
||||
props: {
|
||||
label: "이름",
|
||||
label: "Name",
|
||||
formFieldName: "name",
|
||||
required: true,
|
||||
} as any,
|
||||
@@ -43,7 +43,7 @@ describe("PublicPageRenderer - 폼 제출 메시지", () => {
|
||||
id: "submit_btn",
|
||||
type: "button",
|
||||
props: {
|
||||
label: "제출하기",
|
||||
label: "Submit",
|
||||
href: "#",
|
||||
} as any,
|
||||
},
|
||||
@@ -60,10 +60,10 @@ describe("PublicPageRenderer - 폼 제출 메시지", () => {
|
||||
|
||||
render(<PublicPageRenderer blocks={blocks} />);
|
||||
|
||||
const nameInput = screen.getByLabelText("이름") as HTMLInputElement;
|
||||
const nameInput = screen.getByLabelText("Name") as HTMLInputElement;
|
||||
fireEvent.change(nameInput, { target: { value: "홍길동" } });
|
||||
|
||||
const submitButton = screen.getByRole("link", { name: "제출하기" });
|
||||
const submitButton = screen.getByRole("link", { name: "Submit" });
|
||||
expect(submitButton).toBeTruthy();
|
||||
|
||||
fireEvent.click(submitButton);
|
||||
@@ -73,7 +73,7 @@ describe("PublicPageRenderer - 폼 제출 메시지", () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const msg = screen.getByText("폼 성공 메시지 (config)");
|
||||
const msg = screen.getByText("Form success message (config)");
|
||||
expect(msg).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -86,8 +86,8 @@ describe("PublicPageRenderer - 폼 제출 메시지", () => {
|
||||
props: {
|
||||
kind: "contact",
|
||||
submitTarget: "internal",
|
||||
successMessage: "폼 성공 메시지 (config)",
|
||||
errorMessage: "폼 에러 메시지 (config)",
|
||||
successMessage: "Form success message (config)",
|
||||
errorMessage: "Form error message (config)",
|
||||
fieldIds: ["field_name"],
|
||||
requiredFieldIds: ["field_name"],
|
||||
submitButtonId: "submit_btn",
|
||||
@@ -97,7 +97,7 @@ describe("PublicPageRenderer - 폼 제출 메시지", () => {
|
||||
id: "field_name",
|
||||
type: "formInput",
|
||||
props: {
|
||||
label: "이름",
|
||||
label: "Name",
|
||||
formFieldName: "name",
|
||||
required: true,
|
||||
} as any,
|
||||
@@ -106,7 +106,7 @@ describe("PublicPageRenderer - 폼 제출 메시지", () => {
|
||||
id: "submit_btn",
|
||||
type: "button",
|
||||
props: {
|
||||
label: "제출하기",
|
||||
label: "Submit",
|
||||
href: "#",
|
||||
} as any,
|
||||
},
|
||||
@@ -123,10 +123,10 @@ describe("PublicPageRenderer - 폼 제출 메시지", () => {
|
||||
|
||||
render(<PublicPageRenderer blocks={blocks} />);
|
||||
|
||||
const nameInput = screen.getByLabelText("이름") as HTMLInputElement;
|
||||
const nameInput = screen.getByLabelText("Name") as HTMLInputElement;
|
||||
fireEvent.change(nameInput, { target: { value: "홍길동" } });
|
||||
|
||||
const submitButton = screen.getByRole("link", { name: "제출하기" });
|
||||
const submitButton = screen.getByRole("link", { name: "Submit" });
|
||||
expect(submitButton).toBeTruthy();
|
||||
|
||||
fireEvent.click(submitButton);
|
||||
@@ -136,7 +136,7 @@ describe("PublicPageRenderer - 폼 제출 메시지", () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const errorMsg = screen.getByText("폼 에러 메시지 (config)");
|
||||
const errorMsg = screen.getByText("Form error message (config)");
|
||||
expect(errorMsg).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -149,7 +149,7 @@ describe("PublicPageRenderer - 폼 제출 메시지", () => {
|
||||
props: {
|
||||
kind: "contact",
|
||||
submitTarget: "internal",
|
||||
successMessage: "폼 성공 메시지 (config)",
|
||||
successMessage: "Form success message (config)",
|
||||
fieldIds: ["field_email"],
|
||||
requiredFieldIds: ["field_email"],
|
||||
submitButtonId: "submit_btn",
|
||||
@@ -159,7 +159,7 @@ describe("PublicPageRenderer - 폼 제출 메시지", () => {
|
||||
id: "field_email",
|
||||
type: "formInput",
|
||||
props: {
|
||||
label: "이메일",
|
||||
label: "Email",
|
||||
formFieldName: "email",
|
||||
required: true,
|
||||
} as any,
|
||||
@@ -168,7 +168,7 @@ describe("PublicPageRenderer - 폼 제출 메시지", () => {
|
||||
id: "submit_btn",
|
||||
type: "button",
|
||||
props: {
|
||||
label: "제출하기",
|
||||
label: "Submit",
|
||||
href: "#",
|
||||
} as any,
|
||||
},
|
||||
@@ -185,7 +185,7 @@ describe("PublicPageRenderer - 폼 제출 메시지", () => {
|
||||
|
||||
render(<PublicPageRenderer blocks={blocks} />);
|
||||
|
||||
const submitButton = screen.getByRole("link", { name: "제출하기" });
|
||||
const submitButton = screen.getByRole("link", { name: "Submit" });
|
||||
expect(submitButton).toBeTruthy();
|
||||
|
||||
fireEvent.click(submitButton);
|
||||
@@ -195,7 +195,7 @@ describe("PublicPageRenderer - 폼 제출 메시지", () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const errorMsg = screen.getByText(/다음 필수 항목을 입력해 주세요:\s*-\s*이메일/);
|
||||
const errorMsg = screen.getByText(/Please fill in the following required fields:\s*-\s*Email/);
|
||||
expect(errorMsg).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -208,8 +208,8 @@ describe("PublicPageRenderer - 폼 제출 메시지", () => {
|
||||
props: {
|
||||
kind: "contact",
|
||||
submitTarget: "internal",
|
||||
successMessage: "폼 성공 메시지 (config)",
|
||||
errorMessage: "폼 에러 메시지 (config)",
|
||||
successMessage: "Form success message (config)",
|
||||
errorMessage: "Form error message (config)",
|
||||
fieldIds: ["field_name"],
|
||||
requiredFieldIds: ["field_name"],
|
||||
submitButtonId: "submit_btn",
|
||||
@@ -219,7 +219,7 @@ describe("PublicPageRenderer - 폼 제출 메시지", () => {
|
||||
id: "field_name",
|
||||
type: "formInput",
|
||||
props: {
|
||||
label: "이름",
|
||||
label: "Name",
|
||||
formFieldName: "name",
|
||||
required: true,
|
||||
} as any,
|
||||
@@ -228,7 +228,7 @@ describe("PublicPageRenderer - 폼 제출 메시지", () => {
|
||||
id: "submit_btn",
|
||||
type: "button",
|
||||
props: {
|
||||
label: "제출하기",
|
||||
label: "Submit",
|
||||
href: "#",
|
||||
} as any,
|
||||
},
|
||||
@@ -245,7 +245,7 @@ describe("PublicPageRenderer - 폼 제출 메시지", () => {
|
||||
|
||||
render(<PublicPageRenderer blocks={blocks} />);
|
||||
|
||||
const submitButton = screen.getByRole("link", { name: "제출하기" });
|
||||
const submitButton = screen.getByRole("link", { name: "Submit" });
|
||||
expect(submitButton).toBeTruthy();
|
||||
|
||||
// 이름 필드를 비운 채로 제출한다.
|
||||
@@ -256,7 +256,7 @@ describe("PublicPageRenderer - 폼 제출 메시지", () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const errorMsg = screen.getByText(/다음 필수 항목을 입력해 주세요:\s*-\s*이름/);
|
||||
const errorMsg = screen.getByText(/Please fill in the following required fields:\s*-\s*Name/);
|
||||
expect(errorMsg).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,10 +26,10 @@ describe("PublicPageRenderer - 섹션 템플릿", () => {
|
||||
render(<PublicPageRenderer blocks={blocks as Block[]} />);
|
||||
|
||||
// 헤드라인/서브텍스트 텍스트가 존재해야 한다.
|
||||
screen.getByText("Hero 제목을 여기에 입력하세요");
|
||||
screen.getByText("제품이나 서비스를 한 문장으로 설명하는 서브텍스트입니다.");
|
||||
screen.getByText("Your hero headline goes here");
|
||||
screen.getByText("Describe your product or service in a single, clear sentence.");
|
||||
// CTA 버튼 라벨 텍스트가 존재해야 한다.
|
||||
screen.getByText("지금 시작하기");
|
||||
screen.getByText("Get started now");
|
||||
});
|
||||
|
||||
it("Features 템플릿 섹션은 3컬럼 Feature 제목/설명 텍스트를 렌더해야 한다", () => {
|
||||
@@ -39,11 +39,11 @@ describe("PublicPageRenderer - 섹션 템플릿", () => {
|
||||
render(<PublicPageRenderer blocks={blocks as Block[]} />);
|
||||
|
||||
// 각 컬럼의 Feature 제목과 설명 텍스트가 모두 존재해야 한다.
|
||||
screen.getByText("Feature 1 제목");
|
||||
screen.getByText("Feature 2 제목");
|
||||
screen.getByText("Feature 3 제목");
|
||||
screen.getByText("Feature 1");
|
||||
screen.getByText("Feature 2");
|
||||
screen.getByText("Feature 3");
|
||||
|
||||
const descText = "해당 기능을 간단히 설명하는 텍스트입니다.";
|
||||
const descText = "A short description of this feature.";
|
||||
// 설명 텍스트는 3번 등장해야 한다.
|
||||
const allDesc = screen.getAllByText(descText);
|
||||
if (allDesc.length !== 3) {
|
||||
@@ -59,8 +59,8 @@ describe("PublicPageRenderer - 섹션 템플릿", () => {
|
||||
|
||||
// CTA 본문 텍스트와 버튼 라벨이 존재해야 한다.
|
||||
screen.getByText((content) =>
|
||||
content.includes("지금 바로 행동을 유도하는 CTA 텍스트를 입력하세요."),
|
||||
content.includes("Write a compelling CTA message here."),
|
||||
);
|
||||
screen.getByText("CTA 버튼");
|
||||
screen.getByText("Call to action");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@ describe("SectionPropertiesPanel - layout presets", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByLabelText("섹션 컬럼 레이아웃");
|
||||
const select = screen.getByLabelText("Section column layout");
|
||||
|
||||
fireEvent.change(select, { target: { value: "two-equal" } });
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ describe("SectionPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const hexInput = screen.getByLabelText("섹션 배경 색상 HEX 입력");
|
||||
const hexInput = screen.getByLabelText("Section background color HEX");
|
||||
fireEvent.change(hexInput, { target: { value: "#112233" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -57,7 +57,7 @@ describe("SectionPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const presetSelect = screen.getByLabelText("배경 이미지 가로 위치 프리셋");
|
||||
const presetSelect = screen.getByLabelText("Background image horizontal position 프리셋");
|
||||
fireEvent.change(presetSelect, { target: { value: "left" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -83,7 +83,7 @@ describe("SectionPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const presetSelect = screen.getByLabelText("배경 이미지 세로 위치 프리셋");
|
||||
const presetSelect = screen.getByLabelText("Background image vertical position 프리셋");
|
||||
fireEvent.change(presetSelect, { target: { value: "top" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -110,7 +110,7 @@ describe("SectionPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const xSlider = screen.getByLabelText("배경 이미지 가로 위치 슬라이더");
|
||||
const xSlider = screen.getByLabelText("Background image horizontal position 슬라이더");
|
||||
fireEvent.change(xSlider, { target: { value: "30" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -121,7 +121,7 @@ describe("SectionPropertiesPanel", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const ySlider = screen.getByLabelText("배경 이미지 세로 위치 슬라이더");
|
||||
const ySlider = screen.getByLabelText("Background image vertical position 슬라이더");
|
||||
fireEvent.change(ySlider, { target: { value: "70" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -143,7 +143,7 @@ describe("SectionPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const slider = screen.getByLabelText("세로 패딩 슬라이더");
|
||||
const slider = screen.getByLabelText("Vertical padding 슬라이더");
|
||||
fireEvent.change(slider, { target: { value: "40" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith("section-1", expect.objectContaining({ paddingYPx: 40 }));
|
||||
@@ -160,7 +160,7 @@ describe("SectionPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const urlInput = screen.getByLabelText("배경 이미지 URL");
|
||||
const urlInput = screen.getByLabelText("Background image URL");
|
||||
fireEvent.change(urlInput, { target: { value: "https://example.com/bg.png" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -189,7 +189,7 @@ describe("SectionPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByLabelText("배경 이미지 크기");
|
||||
const select = screen.getByLabelText("Background image size");
|
||||
fireEvent.change(select, { target: { value: "contain" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -214,7 +214,7 @@ describe("SectionPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByLabelText("배경 이미지 위치");
|
||||
const select = screen.getByLabelText("Background image position");
|
||||
fireEvent.change(select, { target: { value: "top" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -234,7 +234,7 @@ describe("SectionPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const sourceSelect = screen.getByLabelText("배경 이미지 소스") as HTMLSelectElement;
|
||||
const sourceSelect = screen.getByLabelText("Background image source") as HTMLSelectElement;
|
||||
expect(sourceSelect.value).toBe("none");
|
||||
|
||||
fireEvent.change(sourceSelect, { target: { value: "none" } });
|
||||
@@ -246,9 +246,9 @@ describe("SectionPropertiesPanel", () => {
|
||||
} as any);
|
||||
|
||||
// "없음" 상태에서는 크기/위치/반복 컨트롤이 렌더되지 않아야 한다.
|
||||
expect(screen.queryByLabelText("배경 이미지 크기")).toBeNull();
|
||||
expect(screen.queryByLabelText("배경 이미지 위치")).toBeNull();
|
||||
expect(screen.queryByLabelText("배경 이미지 반복")).toBeNull();
|
||||
expect(screen.queryByLabelText("Background image size")).toBeNull();
|
||||
expect(screen.queryByLabelText("Background image position")).toBeNull();
|
||||
expect(screen.queryByLabelText("Background image repeat")).toBeNull();
|
||||
});
|
||||
|
||||
it("배경 이미지 반복 셀렉트 변경 시 updateBlock 이 backgroundImageRepeat 로 호출된다", () => {
|
||||
@@ -267,7 +267,7 @@ describe("SectionPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const repeatSelect = screen.getByLabelText("배경 이미지 반복");
|
||||
const repeatSelect = screen.getByLabelText("Background image repeat");
|
||||
fireEvent.change(repeatSelect, { target: { value: "repeat-x" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -292,7 +292,7 @@ describe("SectionPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const sourceSelect = screen.getByLabelText("배경 이미지 소스") as HTMLSelectElement;
|
||||
const sourceSelect = screen.getByLabelText("Background image source") as HTMLSelectElement;
|
||||
expect(sourceSelect.value).toBe("upload");
|
||||
|
||||
fireEvent.change(sourceSelect, { target: { value: "url" } });
|
||||
@@ -321,7 +321,7 @@ describe("SectionPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const urlInput = screen.getByLabelText("배경 비디오 URL");
|
||||
const urlInput = screen.getByLabelText("Background video URL");
|
||||
fireEvent.change(urlInput, { target: { value: "https://example.com/bg-video.mp4" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -350,7 +350,7 @@ describe("SectionPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const sourceSelect = screen.getByLabelText("배경 비디오 소스") as HTMLSelectElement;
|
||||
const sourceSelect = screen.getByLabelText("Background video source") as HTMLSelectElement;
|
||||
expect(sourceSelect.value).toBe("upload");
|
||||
|
||||
fireEvent.change(sourceSelect, { target: { value: "url" } });
|
||||
@@ -381,7 +381,7 @@ describe("SectionPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const bgImageSourceSelect = screen.getByLabelText("배경 이미지 소스") as HTMLSelectElement;
|
||||
const bgImageSourceSelect = screen.getByLabelText("Background image source") as HTMLSelectElement;
|
||||
const bgImageSourceClass = bgImageSourceSelect.getAttribute("class") ?? "";
|
||||
expect(bgImageSourceClass).toContain("bg-white");
|
||||
expect(bgImageSourceClass).toContain("text-slate-900");
|
||||
@@ -390,7 +390,7 @@ describe("SectionPropertiesPanel", () => {
|
||||
expect(bgImageSourceClass).toContain("dark:text-slate-100");
|
||||
expect(bgImageSourceClass).toContain("dark:border-slate-700");
|
||||
|
||||
const bgImageUrlInput = screen.getByLabelText("배경 이미지 URL") as HTMLInputElement;
|
||||
const bgImageUrlInput = screen.getByLabelText("Background image URL") as HTMLInputElement;
|
||||
const bgImageUrlClass = bgImageUrlInput.getAttribute("class") ?? "";
|
||||
expect(bgImageUrlClass).toContain("bg-white");
|
||||
expect(bgImageUrlClass).toContain("text-slate-900");
|
||||
@@ -399,7 +399,7 @@ describe("SectionPropertiesPanel", () => {
|
||||
expect(bgImageUrlClass).toContain("dark:text-slate-100");
|
||||
expect(bgImageUrlClass).toContain("dark:border-slate-700");
|
||||
|
||||
const bgVideoUrlInput = screen.getByLabelText("배경 비디오 URL") as HTMLInputElement;
|
||||
const bgVideoUrlInput = screen.getByLabelText("Background video URL") as HTMLInputElement;
|
||||
const bgVideoUrlClass = bgVideoUrlInput.getAttribute("class") ?? "";
|
||||
expect(bgVideoUrlClass).toContain("bg-white");
|
||||
expect(bgVideoUrlClass).toContain("text-slate-900");
|
||||
@@ -433,12 +433,12 @@ describe("SectionPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const columnLayoutSelect = screen.getByLabelText("섹션 컬럼 레이아웃") as HTMLSelectElement;
|
||||
const alignItemsSelect = screen.getByLabelText("섹션 컬럼 세로 정렬") as HTMLSelectElement;
|
||||
const positionModeSelect = screen.getByLabelText("배경 이미지 위치 모드") as HTMLSelectElement;
|
||||
const sizeSelect = screen.getByLabelText("배경 이미지 크기") as HTMLSelectElement;
|
||||
const positionSelect = screen.getByLabelText("배경 이미지 위치") as HTMLSelectElement;
|
||||
const repeatSelect = screen.getByLabelText("배경 이미지 반복") as HTMLSelectElement;
|
||||
const columnLayoutSelect = screen.getByLabelText("Section column layout") as HTMLSelectElement;
|
||||
const alignItemsSelect = screen.getByLabelText("Section column vertical alignment") as HTMLSelectElement;
|
||||
const positionModeSelect = screen.getByLabelText("Background image position mode") as HTMLSelectElement;
|
||||
const sizeSelect = screen.getByLabelText("Background image size") as HTMLSelectElement;
|
||||
const positionSelect = screen.getByLabelText("Background image position") as HTMLSelectElement;
|
||||
const repeatSelect = screen.getByLabelText("Background image repeat") as HTMLSelectElement;
|
||||
|
||||
const assertDualTheme = (el: HTMLElement) => {
|
||||
const className = el.getAttribute("class") ?? "";
|
||||
|
||||
@@ -54,10 +54,10 @@ describe("SignupPage", () => {
|
||||
|
||||
render(<SignupPage />);
|
||||
|
||||
const emailInput = screen.getByLabelText("이메일") as HTMLInputElement;
|
||||
const passwordInput = screen.getByLabelText("비밀번호") as HTMLInputElement;
|
||||
const confirmInput = screen.getByLabelText("비밀번호 확인") as HTMLInputElement;
|
||||
const submitButton = screen.getByRole("button", { name: "회원가입" });
|
||||
const emailInput = screen.getByLabelText("Email") as HTMLInputElement;
|
||||
const passwordInput = screen.getByLabelText("Password") as HTMLInputElement;
|
||||
const confirmInput = screen.getByLabelText("Confirm password") as HTMLInputElement;
|
||||
const submitButton = screen.getByRole("button", { name: "Sign up" });
|
||||
|
||||
fireEvent.change(emailInput, { target: { value: "new@example.com" } });
|
||||
fireEvent.change(passwordInput, { target: { value: "securePass1" } });
|
||||
@@ -118,10 +118,10 @@ describe("SignupPage", () => {
|
||||
|
||||
render(<SignupPage />);
|
||||
|
||||
const emailInput = screen.getByLabelText("이메일") as HTMLInputElement;
|
||||
const passwordInput = screen.getByLabelText("비밀번호") as HTMLInputElement;
|
||||
const confirmInput = screen.getByLabelText("비밀번호 확인") as HTMLInputElement;
|
||||
const submitButton = screen.getByRole("button", { name: "회원가입" });
|
||||
const emailInput = screen.getByLabelText("Email") as HTMLInputElement;
|
||||
const passwordInput = screen.getByLabelText("Password") as HTMLInputElement;
|
||||
const confirmInput = screen.getByLabelText("Confirm password") as HTMLInputElement;
|
||||
const submitButton = screen.getByRole("button", { name: "Sign up" });
|
||||
|
||||
fireEvent.change(emailInput, { target: { value: "dup@example.com" } });
|
||||
fireEvent.change(passwordInput, { target: { value: "securePass1" } });
|
||||
@@ -183,9 +183,9 @@ describe("SignupPage", () => {
|
||||
|
||||
render(<SignupPage />);
|
||||
|
||||
const emailInput = screen.getByLabelText("이메일") as HTMLInputElement;
|
||||
const passwordInput = screen.getByLabelText("비밀번호") as HTMLInputElement;
|
||||
const confirmInput = screen.getByLabelText("비밀번호 확인") as HTMLInputElement;
|
||||
const emailInput = screen.getByLabelText("Email") as HTMLInputElement;
|
||||
const passwordInput = screen.getByLabelText("Password") as HTMLInputElement;
|
||||
const confirmInput = screen.getByLabelText("Confirm password") as HTMLInputElement;
|
||||
|
||||
const emailClass = emailInput.className;
|
||||
const passwordClass = passwordInput.className;
|
||||
@@ -214,7 +214,7 @@ describe("SignupPage", () => {
|
||||
|
||||
render(<SignupPage />);
|
||||
|
||||
const confirmInput = await screen.findByLabelText("비밀번호 확인");
|
||||
const confirmInput = await screen.findByLabelText("Confirm password");
|
||||
expect(confirmInput).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -248,10 +248,10 @@ describe("SignupPage", () => {
|
||||
|
||||
render(<SignupPage />);
|
||||
|
||||
const emailInput = screen.getByLabelText("이메일") as HTMLInputElement;
|
||||
const passwordInput = screen.getByLabelText("비밀번호") as HTMLInputElement;
|
||||
const confirmInput = screen.getByLabelText("비밀번호 확인") as HTMLInputElement;
|
||||
const submitButton = screen.getByRole("button", { name: "회원가입" });
|
||||
const emailInput = screen.getByLabelText("Email") as HTMLInputElement;
|
||||
const passwordInput = screen.getByLabelText("Password") as HTMLInputElement;
|
||||
const confirmInput = screen.getByLabelText("Confirm password") as HTMLInputElement;
|
||||
const submitButton = screen.getByRole("button", { name: "Sign up" });
|
||||
|
||||
fireEvent.change(emailInput, { target: { value: "new@example.com" } });
|
||||
fireEvent.change(passwordInput, { target: { value: "securePass1" } });
|
||||
@@ -259,7 +259,7 @@ describe("SignupPage", () => {
|
||||
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
const errorText = await screen.findByText(/비밀번호와 비밀번호 확인이 일치하지 않습니다/);
|
||||
const errorText = await screen.findByText("Password and confirmation do not match.");
|
||||
expect(errorText).toBeTruthy();
|
||||
|
||||
const signupCall = fetchMock.mock.calls.find(([url, options]) => {
|
||||
@@ -281,11 +281,11 @@ describe("SignupPage", () => {
|
||||
|
||||
render(<SignupPage />);
|
||||
|
||||
const passwordInput = screen.getByLabelText("비밀번호") as HTMLInputElement;
|
||||
const passwordInput = screen.getByLabelText("Password") as HTMLInputElement;
|
||||
|
||||
fireEvent.change(passwordInput, { target: { value: "abc12345" } });
|
||||
|
||||
const weakText = await screen.findByText("비밀번호 난이도: 약함");
|
||||
const weakText = await screen.findByText("Password strength: Weak");
|
||||
expect(weakText).toBeTruthy();
|
||||
|
||||
const bar = await screen.findByTestId("password-strength-bar");
|
||||
@@ -306,11 +306,11 @@ describe("SignupPage", () => {
|
||||
|
||||
render(<SignupPage />);
|
||||
|
||||
const passwordInput = screen.getByLabelText("비밀번호") as HTMLInputElement;
|
||||
const passwordInput = screen.getByLabelText("Password") as HTMLInputElement;
|
||||
|
||||
fireEvent.change(passwordInput, { target: { value: "Abc12345" } });
|
||||
|
||||
const mediumText = await screen.findByText("비밀번호 난이도: 보통");
|
||||
const mediumText = await screen.findByText("Password strength: Medium");
|
||||
expect(mediumText).toBeTruthy();
|
||||
|
||||
const bar = await screen.findByTestId("password-strength-bar");
|
||||
@@ -318,29 +318,4 @@ describe("SignupPage", () => {
|
||||
expect(barClass).toContain("w-2/3");
|
||||
expect(barClass).toContain("bg-amber-500");
|
||||
});
|
||||
|
||||
it("대소문자/숫자/특수문자가 섞인 충분히 긴 비밀번호는 난이도가 강함으로 표시되어야 한다", async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify({ message: "인증이 필요합니다." }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
);
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock as any);
|
||||
|
||||
render(<SignupPage />);
|
||||
|
||||
const passwordInput = screen.getByLabelText("비밀번호") as HTMLInputElement;
|
||||
|
||||
fireEvent.change(passwordInput, { target: { value: "Abc12345!@" } });
|
||||
|
||||
const strongText = await screen.findByText("비밀번호 난이도: 강함");
|
||||
expect(strongText).toBeTruthy();
|
||||
|
||||
const bar = await screen.findByTestId("password-strength-bar");
|
||||
const barClass = bar.className;
|
||||
expect(barClass).toContain("w-full");
|
||||
expect(barClass).toContain("bg-emerald-600");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,7 +24,7 @@ describe("Text/List/Form 패널 - 블록 배경색 컨트롤", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const hexInput = screen.getByLabelText("텍스트 블록 배경색 HEX");
|
||||
const hexInput = screen.getByLabelText("Text block background color HEX");
|
||||
fireEvent.change(hexInput, { target: { value: "#112233" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -49,7 +49,7 @@ describe("Text/List/Form 패널 - 블록 배경색 컨트롤", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const hexInput = screen.getByLabelText("리스트 배경색 HEX");
|
||||
const hexInput = screen.getByLabelText("List background color HEX");
|
||||
fireEvent.change(hexInput, { target: { value: "#445566" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
|
||||
@@ -29,7 +29,7 @@ describe("TextPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByLabelText("정렬") as HTMLSelectElement;
|
||||
const select = screen.getByLabelText("Alignment") as HTMLSelectElement;
|
||||
const className = select.getAttribute("class") ?? "";
|
||||
|
||||
expect(className).toContain("bg-white");
|
||||
@@ -53,7 +53,7 @@ describe("TextPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByLabelText("글자 간격 프리셋") as HTMLSelectElement;
|
||||
const select = screen.getByLabelText("Letter spacing preset") as HTMLSelectElement;
|
||||
const className = select.getAttribute("class") ?? "";
|
||||
|
||||
expect(className).toContain("bg-white");
|
||||
@@ -77,7 +77,7 @@ describe("TextPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByLabelText("최대 너비 프리셋") as HTMLSelectElement;
|
||||
const select = screen.getByLabelText("Max width preset") as HTMLSelectElement;
|
||||
const className = select.getAttribute("class") ?? "";
|
||||
|
||||
expect(className).toContain("bg-white");
|
||||
@@ -101,7 +101,7 @@ describe("TextPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByLabelText("최대 너비 커스텀") as HTMLInputElement;
|
||||
const input = screen.getByLabelText("Max width custom") as HTMLInputElement;
|
||||
const className = input.getAttribute("class") ?? "";
|
||||
|
||||
expect(className).toContain("bg-white");
|
||||
|
||||
@@ -29,7 +29,7 @@ describe("VideoPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const urlInput = screen.getByLabelText("비디오 URL");
|
||||
const urlInput = screen.getByLabelText("Video URL");
|
||||
fireEvent.change(urlInput, { target: { value: "https://example.com/video.mp4" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -53,7 +53,7 @@ describe("VideoPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByLabelText("비디오 정렬");
|
||||
const select = screen.getByLabelText("Video alignment");
|
||||
fireEvent.change(select, { target: { value: "left" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -73,7 +73,7 @@ describe("VideoPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByLabelText("비디오 너비 모드");
|
||||
const select = screen.getByLabelText("Video width mode");
|
||||
fireEvent.change(select, { target: { value: "fixed" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -93,7 +93,7 @@ describe("VideoPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByLabelText("화면 비율");
|
||||
const select = screen.getByLabelText("Video aspect ratio");
|
||||
fireEvent.change(select, { target: { value: "4:3" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -113,7 +113,7 @@ describe("VideoPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const hexInput = screen.getByLabelText("비디오 카드 배경색 HEX");
|
||||
const hexInput = screen.getByLabelText("Video card background color HEX");
|
||||
fireEvent.change(hexInput, { target: { value: "#123456" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -133,7 +133,7 @@ describe("VideoPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const slider = screen.getByLabelText("카드 패딩 슬라이더");
|
||||
const slider = screen.getByLabelText("Card padding 슬라이더");
|
||||
fireEvent.change(slider, { target: { value: "24" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -153,7 +153,7 @@ describe("VideoPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const slider = screen.getByLabelText("카드 모서리 둥글기 슬라이더");
|
||||
const slider = screen.getByLabelText("Card border radius 슬라이더");
|
||||
fireEvent.change(slider, { target: { value: "20" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -181,7 +181,7 @@ describe("VideoPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByLabelText("포스터 이미지 URL");
|
||||
const input = screen.getByLabelText("Poster image URL");
|
||||
fireEvent.change(input, { target: { value: "https://example.com/poster-after.png" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -205,7 +205,7 @@ describe("VideoPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const slider = screen.getByLabelText("시작 시점 (초) 슬라이더");
|
||||
const slider = screen.getByLabelText("Start time (sec) 슬라이더");
|
||||
fireEvent.change(slider, { target: { value: "5" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -225,7 +225,7 @@ describe("VideoPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const slider = screen.getByLabelText("종료 시점 (초) 슬라이더");
|
||||
const slider = screen.getByLabelText("End time (sec) 슬라이더");
|
||||
fireEvent.change(slider, { target: { value: "15" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -245,7 +245,7 @@ describe("VideoPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByLabelText("비디오 제목");
|
||||
const input = screen.getByLabelText("Video title");
|
||||
fireEvent.change(input, { target: { value: "새 비디오 제목" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -265,7 +265,7 @@ describe("VideoPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByLabelText("비디오 aria-label");
|
||||
const input = screen.getByLabelText("Video aria-label");
|
||||
fireEvent.change(input, { target: { value: "새 aria-label" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -285,7 +285,7 @@ describe("VideoPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByLabelText("비디오 캡션 텍스트");
|
||||
const input = screen.getByLabelText("Video caption text");
|
||||
fireEvent.change(input, { target: { value: "새 캡션 텍스트" } });
|
||||
|
||||
expect(updateBlock).toHaveBeenCalledWith(
|
||||
@@ -305,7 +305,7 @@ describe("VideoPropertiesPanel", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const sourceSelect = screen.getByLabelText("비디오 소스") as HTMLSelectElement;
|
||||
const sourceSelect = screen.getByLabelText("Video source") as HTMLSelectElement;
|
||||
const sourceClass = sourceSelect.getAttribute("class") ?? "";
|
||||
expect(sourceClass).toContain("bg-white");
|
||||
expect(sourceClass).toContain("text-slate-900");
|
||||
@@ -314,7 +314,7 @@ describe("VideoPropertiesPanel", () => {
|
||||
expect(sourceClass).toContain("dark:text-slate-100");
|
||||
expect(sourceClass).toContain("dark:border-slate-700");
|
||||
|
||||
const urlInput = screen.getByLabelText("비디오 URL") as HTMLInputElement;
|
||||
const urlInput = screen.getByLabelText("Video URL") as HTMLInputElement;
|
||||
const urlClass = urlInput.getAttribute("class") ?? "";
|
||||
expect(urlClass).toContain("bg-white");
|
||||
expect(urlClass).toContain("text-slate-900");
|
||||
@@ -323,7 +323,7 @@ describe("VideoPropertiesPanel", () => {
|
||||
expect(urlClass).toContain("dark:text-slate-100");
|
||||
expect(urlClass).toContain("dark:border-slate-700");
|
||||
|
||||
const alignSelect = screen.getByLabelText("비디오 정렬") as HTMLSelectElement;
|
||||
const alignSelect = screen.getByLabelText("Video alignment") as HTMLSelectElement;
|
||||
const alignClass = alignSelect.getAttribute("class") ?? "";
|
||||
expect(alignClass).toContain("bg-white");
|
||||
expect(alignClass).toContain("text-slate-900");
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { getDashboardMessages } from "@/features/i18n/messages/dashboard";
|
||||
import { DEFAULT_LOCALE } from "@/features/i18n/locale";
|
||||
|
||||
// Dashboard i18n 메시지 딕셔너리 TDD
|
||||
// - getDashboardMessages("en") 은 대시보드 타이틀/탭 이름을 영어로 반환해야 한다.
|
||||
// - getDashboardMessages("ko") 는 동일 키를 한국어로 반환해야 한다.
|
||||
// - 지원하지 않는 로케일이면 DEFAULT_LOCALE(en) 메시지를 반환해야 한다.
|
||||
|
||||
describe("getDashboardMessages", () => {
|
||||
it("en 로케일에서는 대시보드 헤더와 GNB 텍스트를 영어로 반환해야 한다", () => {
|
||||
const t = getDashboardMessages("en");
|
||||
|
||||
expect(t.title).toBe("Dashboard");
|
||||
expect(t.navDashboard).toBe("Dashboard");
|
||||
expect(t.navProjects).toBe("Projects");
|
||||
expect(t.navSubmissions).toBe("All submissions");
|
||||
expect(t.summaryTotalProjectsLabel).toBe("Projects");
|
||||
expect(t.chartTitle).toBe("Recent submissions (daily)");
|
||||
});
|
||||
|
||||
it("ko 로케일에서는 대시보드 헤더와 GNB 텍스트를 한국어로 반환해야 한다", () => {
|
||||
const t = getDashboardMessages("ko");
|
||||
|
||||
expect(t.title).toBe("대시보드");
|
||||
expect(t.navDashboard).toBe("대시보드");
|
||||
expect(t.navProjects).toBe("프로젝트 목록");
|
||||
expect(t.navSubmissions).toBe("전체 제출 내역");
|
||||
expect(t.summaryTotalProjectsLabel).toBe("프로젝트 수");
|
||||
expect(t.chartTitle).toBe("최근 제출 추이 (일별)");
|
||||
});
|
||||
|
||||
it("지원하지 않는 로케일이면 DEFAULT_LOCALE(en) 메시지를 반환해야 한다", () => {
|
||||
const fallback = getDashboardMessages(DEFAULT_LOCALE);
|
||||
const t = getDashboardMessages("fr" as any);
|
||||
|
||||
expect(t.title).toBe(fallback.title);
|
||||
expect(t.navDashboard).toBe(fallback.navDashboard);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { getEditorCanvasMessages } from "@/features/i18n/messages/editorCanvas";
|
||||
import { DEFAULT_LOCALE } from "@/features/i18n/locale";
|
||||
|
||||
describe("getEditorCanvasMessages", () => {
|
||||
it("returns English messages for en locale", () => {
|
||||
const t = getEditorCanvasMessages("en");
|
||||
|
||||
expect(t.emptyStateHint).toBe('Click the "Text" button on the left to add your first block.');
|
||||
expect(t.previewFallbackTextBlock).toBe("Text block");
|
||||
expect(t.previewFallbackButtonBlock).toBe("Button block");
|
||||
expect(t.previewFallbackImageBlock).toBe("Image block");
|
||||
});
|
||||
|
||||
it("returns Korean messages for ko locale", () => {
|
||||
const t = getEditorCanvasMessages("ko");
|
||||
|
||||
expect(t.emptyStateHint).toBe('왼쪽에서 "텍스트" 버튼을 눌러 블록을 추가해 보세요.');
|
||||
expect(t.previewFallbackTextBlock).toBe("텍스트 블록");
|
||||
expect(t.previewFallbackButtonBlock).toBe("버튼 블록");
|
||||
expect(t.previewFallbackImageBlock).toBe("이미지 블록");
|
||||
});
|
||||
|
||||
it("falls back to DEFAULT_LOCALE for unsupported locale", () => {
|
||||
const fallback = getEditorCanvasMessages(DEFAULT_LOCALE);
|
||||
const t = getEditorCanvasMessages("fr" as any);
|
||||
|
||||
expect(t.emptyStateHint).toBe(fallback.emptyStateHint);
|
||||
expect(t.previewFallbackTextBlock).toBe(fallback.previewFallbackTextBlock);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { getEditorMessages } from "@/features/i18n/messages/editor";
|
||||
import { DEFAULT_LOCALE } from "@/features/i18n/locale";
|
||||
|
||||
// Editor i18n 메시지 딕셔너리 TDD
|
||||
// - en/ko 각각에서 헤더/네비게이션/메뉴 주요 키가 올바른 문자열을 반환하는지 검증한다.
|
||||
// - 지원하지 않는 로케일은 DEFAULT_LOCALE(en) 으로 폴백한다.
|
||||
|
||||
describe("getEditorMessages", () => {
|
||||
it("en 로케일에서는 에디터 헤더/네비게이션/메뉴 텍스트를 영어로 반환해야 한다", () => {
|
||||
const t = getEditorMessages("en");
|
||||
|
||||
expect(t.headerTitle).toBe("Page Editor");
|
||||
expect(t.navProjects).toBe("Projects");
|
||||
expect(t.menuProjectSaveLoad).toBe("Save / load project");
|
||||
expect(t.jsonModalTitle).toBe("JSON Export / Import");
|
||||
expect(t.undoLabel).toBe("Undo");
|
||||
expect(t.listItemToolbarMoveUpLabel).toBe("Move item up");
|
||||
});
|
||||
|
||||
it("ko 로케일에서는 에디터 헤더/네비게이션/메뉴 텍스트를 한국어로 반환해야 한다", () => {
|
||||
const t = getEditorMessages("ko");
|
||||
|
||||
expect(t.headerTitle).toBe("페이지 에디터");
|
||||
expect(t.navProjects).toBe("프로젝트 목록");
|
||||
expect(t.menuProjectSaveLoad).toBe("프로젝트 저장/불러오기");
|
||||
expect(t.jsonEditorStateLabel).toBe("에디터 상태 JSON");
|
||||
expect(t.undoLabel).toBe("실행 취소");
|
||||
expect(t.listItemToolbarMoveUpLabel).toBe("아이템 위로 이동");
|
||||
});
|
||||
|
||||
it("지원하지 않는 로케일이면 DEFAULT_LOCALE(en) 메시지를 반환해야 한다", () => {
|
||||
const fallback = getEditorMessages(DEFAULT_LOCALE);
|
||||
const t = getEditorMessages("fr" as any);
|
||||
|
||||
expect(t.headerTitle).toBe(fallback.headerTitle);
|
||||
expect(t.navProjects).toBe(fallback.navProjects);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { resolveLocaleFromAcceptLanguage, DEFAULT_LOCALE } from "@/features/i18n/locale";
|
||||
|
||||
// 브라우저 Accept-Language 헤더 기반 로케일 해석 헬퍼 테스트
|
||||
// - 지원 언어: en(기본), ko
|
||||
// - 헤더가 없거나 지원 언어가 없으면 항상 en으로 떨어져야 한다.
|
||||
|
||||
describe("resolveLocaleFromAcceptLanguage", () => {
|
||||
it("헤더가 없으면 기본 로케일(en)을 반환해야 한다", () => {
|
||||
expect(resolveLocaleFromAcceptLanguage(null)).toBe(DEFAULT_LOCALE);
|
||||
expect(resolveLocaleFromAcceptLanguage(undefined)).toBe(DEFAULT_LOCALE);
|
||||
});
|
||||
|
||||
it("ko 가 포함된 헤더면 ko 로케일을 반환해야 한다", () => {
|
||||
expect(resolveLocaleFromAcceptLanguage("ko")) .toBe("ko");
|
||||
expect(resolveLocaleFromAcceptLanguage("ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7")).toBe("ko");
|
||||
expect(resolveLocaleFromAcceptLanguage("en-US,en;q=0.8,ko;q=0.7")).toBe("ko");
|
||||
});
|
||||
|
||||
it("지원하지 않는 언어만 있을 경우 기본 로케일(en)을 반환해야 한다", () => {
|
||||
expect(resolveLocaleFromAcceptLanguage("fr-FR,fr;q=0.9")).toBe(DEFAULT_LOCALE);
|
||||
expect(resolveLocaleFromAcceptLanguage("zh-CN,zh;q=0.9")) .toBe(DEFAULT_LOCALE);
|
||||
});
|
||||
|
||||
it("en 이 포함되어 있고 ko 는 없는 경우 기본(en)을 유지해야 한다", () => {
|
||||
expect(resolveLocaleFromAcceptLanguage("en-US,en;q=0.9")) .toBe("en");
|
||||
expect(resolveLocaleFromAcceptLanguage("en;q=0.8,fr;q=0.7")) .toBe("en");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { getProjectsMessages } from "@/features/i18n/messages/projects";
|
||||
import { DEFAULT_LOCALE } from "@/features/i18n/locale";
|
||||
|
||||
// Projects i18n 메시지 딕셔너리 TDD
|
||||
// - en/ko 각각에서 헤더/툴바/테이블 헤더 일부 키가 올바른 문자열을 반환하는지 검증한다.
|
||||
// - 지원하지 않는 로케일은 DEFAULT_LOCALE(en) 으로 폴백한다.
|
||||
|
||||
describe("getProjectsMessages", () => {
|
||||
it("en 로케일에서는 프로젝트 목록 헤더/툴바/테이블 헤더를 영어로 반환해야 한다", () => {
|
||||
const t = getProjectsMessages("en");
|
||||
|
||||
expect(t.headerTitle).toBe("Projects");
|
||||
expect(t.toolbarDeleteSelected).toBe("Delete selected");
|
||||
expect(t.tableHeaderTitle).toBe("Title");
|
||||
expect(t.tableHeaderSlug).toBe("Address (slug)");
|
||||
});
|
||||
|
||||
it("ko 로케일에서는 프로젝트 목록 헤더/툴바/테이블 헤더를 한국어로 반환해야 한다", () => {
|
||||
const t = getProjectsMessages("ko");
|
||||
|
||||
expect(t.headerTitle).toBe("프로젝트 목록");
|
||||
expect(t.toolbarDeleteSelected).toBe("선택 삭제");
|
||||
expect(t.tableHeaderTitle).toBe("제목");
|
||||
expect(t.tableHeaderSlug).toBe("주소(slug)");
|
||||
});
|
||||
|
||||
it("지원하지 않는 로케일이면 DEFAULT_LOCALE(en) 메시지를 반환해야 한다", () => {
|
||||
const fallback = getProjectsMessages(DEFAULT_LOCALE);
|
||||
const t = getProjectsMessages("fr" as any);
|
||||
|
||||
expect(t.headerTitle).toBe(fallback.headerTitle);
|
||||
expect(t.tableHeaderTitle).toBe(fallback.tableHeaderTitle);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { getSubmissionsMessages } from "@/features/i18n/messages/submissions";
|
||||
import { DEFAULT_LOCALE } from "@/features/i18n/locale";
|
||||
|
||||
// Submissions i18n 메시지 딕셔너리 TDD
|
||||
// - en/ko 각각에서 헤더/테이블 헤더 일부 키가 올바른 문자열을 반환하는지 검증한다.
|
||||
// - 지원하지 않는 로케일은 DEFAULT_LOCALE(en) 으로 폴백한다.
|
||||
|
||||
describe("getSubmissionsMessages", () => {
|
||||
it("en 로케일에서는 전체 제출 내역 헤더/테이블 헤더를 영어로 반환해야 한다", () => {
|
||||
const t = getSubmissionsMessages("en");
|
||||
|
||||
expect(t.headerTitle).toBe("All form submissions");
|
||||
expect(t.tableHeaderCreatedAt).toBe("Submitted at");
|
||||
expect(t.tableHeaderProject).toBe("Project");
|
||||
});
|
||||
|
||||
it("ko 로케일에서는 전체 제출 내역 헤더/테이블 헤더를 한국어로 반환해야 한다", () => {
|
||||
const t = getSubmissionsMessages("ko");
|
||||
|
||||
expect(t.headerTitle).toBe("전체 폼 제출 내역");
|
||||
expect(t.tableHeaderCreatedAt).toBe("제출 시각");
|
||||
expect(t.tableHeaderProject).toBe("프로젝트");
|
||||
});
|
||||
|
||||
it("지원하지 않는 로케일이면 DEFAULT_LOCALE(en) 메시지를 반환해야 한다", () => {
|
||||
const fallback = getSubmissionsMessages(DEFAULT_LOCALE);
|
||||
const t = getSubmissionsMessages("fr" as any);
|
||||
|
||||
expect(t.headerTitle).toBe(fallback.headerTitle);
|
||||
expect(t.tableHeaderCreatedAt).toBe(fallback.tableHeaderCreatedAt);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user