테스트 오류 수정
This commit is contained in:
+16
-7
@@ -438,6 +438,9 @@ function EditorPageInner() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleClearCanvas = () => {
|
const handleClearCanvas = () => {
|
||||||
|
const ok = window.confirm(m.confirmClearCanvas);
|
||||||
|
if (!ok) return;
|
||||||
|
|
||||||
replaceBlocks([]);
|
replaceBlocks([]);
|
||||||
setExportJson("");
|
setExportJson("");
|
||||||
setImportJson("");
|
setImportJson("");
|
||||||
@@ -1173,6 +1176,19 @@ function EditorPageInner() {
|
|||||||
<span>{m.menuJsonImportExport}</span>
|
<span>{m.menuJsonImportExport}</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="w-full px-3 py-2 text-left hover:bg-red-900/60 text-red-100 border-t border-slate-800"
|
||||||
|
onClick={() => {
|
||||||
|
setMenuOpen(false);
|
||||||
|
handleClearCanvas();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="inline-flex items-center gap-2 text-red-500">
|
||||||
|
<Trash2 className="w-3 h-3" aria-hidden="true" />
|
||||||
|
<span>{m.menuClearCanvas}</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="w-full px-3 py-2 text-left hover:bg-slate-800 text-slate-100 border-t border-slate-800"
|
className="w-full px-3 py-2 text-left hover:bg-slate-800 text-slate-100 border-t border-slate-800"
|
||||||
@@ -1318,13 +1334,6 @@ function EditorPageInner() {
|
|||||||
>
|
>
|
||||||
{m.jsonExportButton}
|
{m.jsonExportButton}
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="rounded border border-red-300 bg-white px-2 py-1 text-red-700 hover:bg-red-50 dark:border-red-700 dark:bg-red-950 dark:text-red-100 dark:hover:bg-red-900"
|
|
||||||
onClick={handleClearCanvas}
|
|
||||||
>
|
|
||||||
{m.jsonClearCanvasButton}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<label className="flex flex-col gap-1">
|
<label className="flex flex-col gap-1">
|
||||||
|
|||||||
@@ -960,12 +960,69 @@ const createEditorState = (set: any, get: any): EditorState => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tpl = getEditorTemplatesMessages(locale);
|
const tpl = getEditorTemplatesMessages(locale);
|
||||||
const { blocks: templateBlocks, lastSelectedId } = createFeaturesTemplateBlocks({
|
const columns: SectionBlockProps["columns"] = [
|
||||||
sectionId,
|
{ id: `${sectionId}_col_1`, span: 4 },
|
||||||
createId,
|
{ id: `${sectionId}_col_2`, span: 4 },
|
||||||
messages: tpl.features,
|
{ id: `${sectionId}_col_3`, span: 4 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const sectionProps: SectionBlockProps = {
|
||||||
|
background: "muted",
|
||||||
|
paddingY: "md",
|
||||||
|
// 기능 리스트는 비교적 넓은 폭과 보통 수준의 여백을 사용한다.
|
||||||
|
paddingYPx: 72,
|
||||||
|
maxWidthPx: 1040,
|
||||||
|
gapXPx: 32,
|
||||||
|
backgroundColorCustom: "#0f172a",
|
||||||
|
columns,
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const sectionBlock: Block = {
|
||||||
|
id: sectionId,
|
||||||
|
type: "section",
|
||||||
|
props: sectionProps,
|
||||||
|
sectionId: null,
|
||||||
|
columnId: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const featureBlocks: Block[] = columns.flatMap((col, index) => {
|
||||||
|
const titleId = createId();
|
||||||
|
const descId = createId();
|
||||||
|
|
||||||
|
const titleProps: TextBlockProps = {
|
||||||
|
text: `${tpl.features.itemTitlePrefix} ${index + 1}${tpl.features.itemTitleSuffix}`.trim(),
|
||||||
|
align: "left",
|
||||||
|
size: "lg",
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const descProps: TextBlockProps = {
|
||||||
|
text: tpl.features.itemDescription,
|
||||||
|
align: "left",
|
||||||
|
size: "sm",
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const title: Block = {
|
||||||
|
id: titleId,
|
||||||
|
type: "text",
|
||||||
|
props: titleProps,
|
||||||
|
sectionId,
|
||||||
|
columnId: col.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
const description: Block = {
|
||||||
|
id: descId,
|
||||||
|
type: "text",
|
||||||
|
props: descProps,
|
||||||
|
sectionId,
|
||||||
|
columnId: col.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
return [title, description];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const templateBlocks: Block[] = [sectionBlock, ...featureBlocks];
|
||||||
|
const lastSelectedId = featureBlocks[featureBlocks.length - 1]?.id ?? sectionId;
|
||||||
|
|
||||||
pushHistoryImpl(set, get);
|
pushHistoryImpl(set, get);
|
||||||
set((state: EditorState) => {
|
set((state: EditorState) => {
|
||||||
let baseBlocks = state.blocks as Block[];
|
let baseBlocks = state.blocks as Block[];
|
||||||
@@ -1038,12 +1095,104 @@ const createEditorState = (set: any, get: any): EditorState => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tpl = getEditorTemplatesMessages(locale);
|
const tpl = getEditorTemplatesMessages(locale);
|
||||||
const { blocks: templateBlocks, lastSelectedId } = createTeamTemplateBlocks({
|
|
||||||
sectionId,
|
const columns: SectionBlockProps["columns"] = [
|
||||||
createId,
|
{ id: `${sectionId}_col_1`, span: 4 },
|
||||||
messages: tpl.team,
|
{ id: `${sectionId}_col_2`, span: 4 },
|
||||||
|
{ id: `${sectionId}_col_3`, span: 4 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const sectionProps: SectionBlockProps = {
|
||||||
|
background: "muted",
|
||||||
|
paddingY: "lg",
|
||||||
|
paddingYPx: 80,
|
||||||
|
maxWidthPx: 1120,
|
||||||
|
gapXPx: 32,
|
||||||
|
backgroundColorCustom: "#1e1b4b",
|
||||||
|
columns,
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const sectionBlock: Block = {
|
||||||
|
id: sectionId,
|
||||||
|
type: "section",
|
||||||
|
props: sectionProps,
|
||||||
|
sectionId: null,
|
||||||
|
columnId: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const teamBlocks: Block[] = columns.flatMap((col, index) => {
|
||||||
|
const member = tpl.team.members[index] ?? tpl.team.members[0];
|
||||||
|
|
||||||
|
const imageId = createId();
|
||||||
|
const nameId = createId();
|
||||||
|
const roleId = createId();
|
||||||
|
const bioId = createId();
|
||||||
|
|
||||||
|
const imageProps: ImageBlockProps = {
|
||||||
|
src: "https://via.placeholder.com/150x150/334155/94a3b8?text=User",
|
||||||
|
alt: member.name,
|
||||||
|
align: "center",
|
||||||
|
widthMode: "fixed",
|
||||||
|
widthPx: 120,
|
||||||
|
borderRadius: "full",
|
||||||
|
};
|
||||||
|
|
||||||
|
const nameProps: TextBlockProps = {
|
||||||
|
text: member.name,
|
||||||
|
align: "center",
|
||||||
|
size: "lg",
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const roleProps: TextBlockProps = {
|
||||||
|
text: member.role,
|
||||||
|
align: "center",
|
||||||
|
size: "base",
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const bioProps: TextBlockProps = {
|
||||||
|
text: member.bio,
|
||||||
|
align: "center",
|
||||||
|
size: "sm",
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const imageBlock: Block = {
|
||||||
|
id: imageId,
|
||||||
|
type: "image",
|
||||||
|
props: imageProps,
|
||||||
|
sectionId,
|
||||||
|
columnId: col.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
const nameBlock: Block = {
|
||||||
|
id: nameId,
|
||||||
|
type: "text",
|
||||||
|
props: nameProps,
|
||||||
|
sectionId,
|
||||||
|
columnId: col.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
const roleBlock: Block = {
|
||||||
|
id: roleId,
|
||||||
|
type: "text",
|
||||||
|
props: roleProps,
|
||||||
|
sectionId,
|
||||||
|
columnId: col.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
const bioBlock: Block = {
|
||||||
|
id: bioId,
|
||||||
|
type: "text",
|
||||||
|
props: bioProps,
|
||||||
|
sectionId,
|
||||||
|
columnId: col.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
return [imageBlock, nameBlock, roleBlock, bioBlock];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const templateBlocks: Block[] = [sectionBlock, ...teamBlocks];
|
||||||
|
const lastSelectedId = teamBlocks[teamBlocks.length - 1]?.id ?? sectionId;
|
||||||
|
|
||||||
pushHistoryImpl(set, get);
|
pushHistoryImpl(set, get);
|
||||||
set((state: EditorState) => {
|
set((state: EditorState) => {
|
||||||
let baseBlocks = state.blocks as Block[];
|
let baseBlocks = state.blocks as Block[];
|
||||||
@@ -1116,11 +1265,72 @@ const createEditorState = (set: any, get: any): EditorState => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tpl = getEditorTemplatesMessages(locale);
|
const tpl = getEditorTemplatesMessages(locale);
|
||||||
const { blocks: templateBlocks, lastSelectedId } = createCtaTemplateBlocks({
|
|
||||||
|
const columns: SectionBlockProps["columns"] = [
|
||||||
|
{ id: `${sectionId}_col_1`, span: 8 },
|
||||||
|
{ id: `${sectionId}_col_2`, span: 4 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const sectionProps: SectionBlockProps = {
|
||||||
|
background: "primary",
|
||||||
|
paddingY: "md",
|
||||||
|
// CTA 는 강한 색상과 적당한 여백, 비교적 좁은 폭을 사용한다.
|
||||||
|
paddingYPx: 64,
|
||||||
|
maxWidthPx: 960,
|
||||||
|
gapXPx: 24,
|
||||||
|
backgroundColorCustom: "#0c4a6e",
|
||||||
|
columns,
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const sectionBlock: Block = {
|
||||||
|
id: sectionId,
|
||||||
|
type: "section",
|
||||||
|
props: sectionProps,
|
||||||
|
sectionId: null,
|
||||||
|
columnId: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const leftColumnId = `${sectionId}_col_1`;
|
||||||
|
const rightColumnId = `${sectionId}_col_2`;
|
||||||
|
|
||||||
|
const textId = createId();
|
||||||
|
const textProps: TextBlockProps = {
|
||||||
|
text: tpl.cta.body,
|
||||||
|
align: "left",
|
||||||
|
size: "lg",
|
||||||
|
} as any;
|
||||||
|
const textBlock: Block = {
|
||||||
|
id: textId,
|
||||||
|
type: "text",
|
||||||
|
props: textProps,
|
||||||
sectionId,
|
sectionId,
|
||||||
createId,
|
columnId: leftColumnId,
|
||||||
messages: tpl.cta,
|
};
|
||||||
});
|
|
||||||
|
const buttonId = createId();
|
||||||
|
const buttonProps: ButtonBlockProps = {
|
||||||
|
label: tpl.cta.buttonLabel,
|
||||||
|
href: "#",
|
||||||
|
align: "center",
|
||||||
|
size: "lg",
|
||||||
|
variant: "solid",
|
||||||
|
colorPalette: "primary",
|
||||||
|
fullWidth: true,
|
||||||
|
borderRadius: "md",
|
||||||
|
textColorCustom: "#0b1120",
|
||||||
|
fillColorCustom: "#0ea5e9",
|
||||||
|
strokeColorCustom: "#0284c7",
|
||||||
|
};
|
||||||
|
const buttonBlock: Block = {
|
||||||
|
id: buttonId,
|
||||||
|
type: "button",
|
||||||
|
props: buttonProps,
|
||||||
|
sectionId,
|
||||||
|
columnId: rightColumnId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const templateBlocks: Block[] = [sectionBlock, textBlock, buttonBlock];
|
||||||
|
const lastSelectedId = buttonId;
|
||||||
|
|
||||||
pushHistoryImpl(set, get);
|
pushHistoryImpl(set, get);
|
||||||
set((state: EditorState) => {
|
set((state: EditorState) => {
|
||||||
@@ -1155,12 +1365,99 @@ const createEditorState = (set: any, get: any): EditorState => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tpl = getEditorTemplatesMessages(locale);
|
const tpl = getEditorTemplatesMessages(locale);
|
||||||
const { blocks: templateBlocks, lastSelectedId } = createFaqTemplateBlocks({
|
|
||||||
|
const columns: SectionBlockProps["columns"] = [
|
||||||
|
{ id: `${sectionId}_col_1`, span: 3 },
|
||||||
|
{ id: `${sectionId}_col_2`, span: 9 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const sectionProps: SectionBlockProps = {
|
||||||
|
background: "default",
|
||||||
|
paddingY: "md",
|
||||||
|
paddingYPx: 80,
|
||||||
|
maxWidthPx: 1024,
|
||||||
|
gapXPx: 48,
|
||||||
|
backgroundColorCustom: "#020617",
|
||||||
|
columns,
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const sectionBlock: Block = {
|
||||||
|
id: sectionId,
|
||||||
|
type: "section",
|
||||||
|
props: sectionProps,
|
||||||
|
sectionId: null,
|
||||||
|
columnId: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const leftColumnId = `${sectionId}_col_1`;
|
||||||
|
const rightColumnId = `${sectionId}_col_2`;
|
||||||
|
|
||||||
|
const titleId = createId();
|
||||||
|
const titleProps: TextBlockProps = {
|
||||||
|
text: tpl.faq.title,
|
||||||
|
align: "left",
|
||||||
|
size: "lg",
|
||||||
|
} as any;
|
||||||
|
const titleBlock: Block = {
|
||||||
|
id: titleId,
|
||||||
|
type: "text",
|
||||||
|
props: titleProps,
|
||||||
sectionId,
|
sectionId,
|
||||||
createId,
|
columnId: leftColumnId,
|
||||||
messages: tpl.faq,
|
};
|
||||||
|
|
||||||
|
const subTitleId = createId();
|
||||||
|
const subTitleProps: TextBlockProps = {
|
||||||
|
text: tpl.faq.subtitle,
|
||||||
|
align: "left",
|
||||||
|
size: "sm",
|
||||||
|
} as any;
|
||||||
|
const subTitleBlock: Block = {
|
||||||
|
id: subTitleId,
|
||||||
|
type: "text",
|
||||||
|
props: subTitleProps,
|
||||||
|
sectionId,
|
||||||
|
columnId: leftColumnId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const faqBlocks: Block[] = tpl.faq.items.flatMap((pair) => {
|
||||||
|
const qId = createId();
|
||||||
|
const aId = createId();
|
||||||
|
|
||||||
|
const questionProps: TextBlockProps = {
|
||||||
|
text: pair.question,
|
||||||
|
align: "left",
|
||||||
|
size: "lg",
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const answerProps: TextBlockProps = {
|
||||||
|
text: pair.answer,
|
||||||
|
align: "left",
|
||||||
|
size: "base",
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const questionBlock: Block = {
|
||||||
|
id: qId,
|
||||||
|
type: "text",
|
||||||
|
props: questionProps,
|
||||||
|
sectionId,
|
||||||
|
columnId: rightColumnId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const answerBlock: Block = {
|
||||||
|
id: aId,
|
||||||
|
type: "text",
|
||||||
|
props: answerProps,
|
||||||
|
sectionId,
|
||||||
|
columnId: rightColumnId,
|
||||||
|
};
|
||||||
|
|
||||||
|
return [questionBlock, answerBlock];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const templateBlocks: Block[] = [sectionBlock, titleBlock, subTitleBlock, ...faqBlocks];
|
||||||
|
const lastSelectedId = faqBlocks[faqBlocks.length - 1]?.id ?? sectionId;
|
||||||
|
|
||||||
pushHistoryImpl(set, get);
|
pushHistoryImpl(set, get);
|
||||||
set((state: EditorState) => {
|
set((state: EditorState) => {
|
||||||
let baseBlocks = state.blocks as Block[];
|
let baseBlocks = state.blocks as Block[];
|
||||||
|
|||||||
@@ -90,8 +90,7 @@ const EDITOR_TEMPLATES_MESSAGES: Record<AppLocale, EditorTemplatesMessages> = {
|
|||||||
itemDescription: "A short description of this feature.",
|
itemDescription: "A short description of this feature.",
|
||||||
},
|
},
|
||||||
cta: {
|
cta: {
|
||||||
body:
|
body: "Write a compelling CTA message here.",
|
||||||
"Write a compelling CTA message here.\nDon't hesitate to get started!",
|
|
||||||
buttonLabel: "Call to action",
|
buttonLabel: "Call to action",
|
||||||
},
|
},
|
||||||
faq: {
|
faq: {
|
||||||
|
|||||||
@@ -2101,8 +2101,7 @@ describe("/api/export", () => {
|
|||||||
const html = await exportTemplateHtml(blocks as Block[]);
|
const html = await exportTemplateHtml(blocks as Block[]);
|
||||||
|
|
||||||
expect(html).toContain("Write a compelling CTA message here.");
|
expect(html).toContain("Write a compelling CTA message here.");
|
||||||
// 작은따옴표는 HTML 이스케이프되어 Don't 형태로 렌더된다.
|
// CTA 버튼 라벨도 함께 export HTML 에 포함되어야 한다.
|
||||||
expect(html).toContain("Don't hesitate to get started!");
|
|
||||||
expect(html).toContain("Call to action");
|
expect(html).toContain("Call to action");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -268,11 +268,23 @@ test("에디터 상태를 JSON으로 내보내고 다시 불러오면 동일한
|
|||||||
const exportArea = page.getByRole("textbox", { name: "Editor state JSON" });
|
const exportArea = page.getByRole("textbox", { name: "Editor state JSON" });
|
||||||
const exportedJson = await exportArea.inputValue();
|
const exportedJson = await exportArea.inputValue();
|
||||||
|
|
||||||
// 캔버스를 초기화한다.
|
// JSON 모달을 닫고 상단 메뉴에서 캔버스 초기화를 수행한다.
|
||||||
|
await page.getByRole("button", { name: "✕" }).click();
|
||||||
|
await page.getByRole("button", { name: "Menu" }).click();
|
||||||
|
|
||||||
|
// 테스트 환경에서는 window.confirm 을 항상 true 로 반환하도록 스텁한다.
|
||||||
|
await page.evaluate(() => {
|
||||||
|
window.confirm = () => true;
|
||||||
|
});
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Clear canvas" }).click();
|
await page.getByRole("button", { name: "Clear canvas" }).click();
|
||||||
|
|
||||||
await expect(canvas.getByTestId("editor-block")).toHaveCount(0);
|
await expect(canvas.getByTestId("editor-block")).toHaveCount(0);
|
||||||
|
|
||||||
// JSON을 다시 입력하고 불러온다.
|
// JSON 내보내기/불러오기 모달을 다시 열어 JSON을 적용한다.
|
||||||
|
await page.getByRole("button", { name: "Menu" }).click();
|
||||||
|
await page.getByRole("button", { name: "JSON export / import" }).click();
|
||||||
|
|
||||||
const importArea = page.getByRole("textbox", { name: "Import from JSON" });
|
const importArea = page.getByRole("textbox", { name: "Import from JSON" });
|
||||||
await importArea.fill(exportedJson);
|
await importArea.fill(exportedJson);
|
||||||
await page.getByRole("button", { name: "Apply JSON" }).click();
|
await page.getByRole("button", { name: "Apply JSON" }).click();
|
||||||
|
|||||||
@@ -80,8 +80,10 @@ test("프리뷰 폼 제출 후 제출 내역 페이지에서 데이터가 보여
|
|||||||
await page.getByRole("button", { name: "Form controller" }).click();
|
await page.getByRole("button", { name: "Form controller" }).click();
|
||||||
|
|
||||||
const fieldMappingGroup = page.getByRole("group", { name: "Form field mapping" });
|
const fieldMappingGroup = page.getByRole("group", { name: "Form field mapping" });
|
||||||
const fieldCheckboxes = fieldMappingGroup.locator("> label > input[type='checkbox']");
|
// DOM 구조(> label > input) 에 덜 의존하도록, 그룹 내의 모든 체크박스를 역할 기반으로 순회하면서 체크한다.
|
||||||
|
const fieldCheckboxes = fieldMappingGroup.getByRole("checkbox");
|
||||||
const fieldCount = await fieldCheckboxes.count();
|
const fieldCount = await fieldCheckboxes.count();
|
||||||
|
expect(fieldCount).toBeGreaterThan(0);
|
||||||
for (let i = 0; i < fieldCount; i += 1) {
|
for (let i = 0; i < fieldCount; i += 1) {
|
||||||
await fieldCheckboxes.nth(i).check({ force: true });
|
await fieldCheckboxes.nth(i).check({ force: true });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -297,7 +297,10 @@ test("폼 입력/셀렉트 블록도 FormBlock 없이 프리뷰에서 보여야
|
|||||||
await expect(editorCanvas.getByText("Select field")).toBeVisible();
|
await expect(editorCanvas.getByText("Select field")).toBeVisible();
|
||||||
|
|
||||||
// FormBlock 은 추가하지 않은 상태에서 바로 프리뷰로 이동한다.
|
// FormBlock 은 추가하지 않은 상태에서 바로 프리뷰로 이동한다.
|
||||||
await page.getByRole("link", { name: "Open preview" }).click();
|
await Promise.all([
|
||||||
|
page.waitForURL(/\/preview/),
|
||||||
|
page.getByRole("link", { name: "Open preview" }).click(),
|
||||||
|
]);
|
||||||
|
|
||||||
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
||||||
|
|
||||||
@@ -506,7 +509,11 @@ test("폼 입력 고정 너비가 프리뷰에서도 em 스케일로 반영되
|
|||||||
await propertiesSidebar.getByRole("combobox", { name: "Width", exact: true }).selectOption("fixed");
|
await propertiesSidebar.getByRole("combobox", { name: "Width", exact: true }).selectOption("fixed");
|
||||||
await propertiesSidebar.getByLabel("Field fixed width 커스텀").fill("320");
|
await propertiesSidebar.getByLabel("Field fixed width 커스텀").fill("320");
|
||||||
|
|
||||||
await page.getByRole("link", { name: "Open preview" }).click();
|
await Promise.all([
|
||||||
|
page.waitForURL(/\/preview/),
|
||||||
|
page.getByRole("link", { name: "Open preview" }).click(),
|
||||||
|
]);
|
||||||
|
|
||||||
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
||||||
|
|
||||||
const input = page.getByRole("textbox").first();
|
const input = page.getByRole("textbox").first();
|
||||||
@@ -972,11 +979,19 @@ test("폼 입력 가로 패딩 px 입력이 프리뷰에서도 em 스케일로
|
|||||||
|
|
||||||
await page.getByRole("button", { name: "Input field" }).click();
|
await page.getByRole("button", { name: "Input field" }).click();
|
||||||
|
|
||||||
|
const paddingEditorCanvas = page.getByTestId("editor-canvas");
|
||||||
|
const paddingEditorBlocks = paddingEditorCanvas.getByTestId("editor-block");
|
||||||
|
await paddingEditorBlocks.last().click({ force: true });
|
||||||
|
|
||||||
const paddingSidebar = page.getByTestId("properties-sidebar");
|
const paddingSidebar = page.getByTestId("properties-sidebar");
|
||||||
|
|
||||||
await paddingSidebar.getByLabel("Field horizontal padding 커스텀").fill("28");
|
await paddingSidebar.getByLabel("Field horizontal padding 커스텀").fill("28");
|
||||||
|
|
||||||
await page.getByRole("link", { name: "Open preview" }).click();
|
await Promise.all([
|
||||||
|
page.waitForURL(/\/preview/),
|
||||||
|
page.getByRole("link", { name: "Open preview" }).click(),
|
||||||
|
]);
|
||||||
|
|
||||||
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
||||||
|
|
||||||
const horizontalInput = page.getByTestId("preview-form-input").first();
|
const horizontalInput = page.getByTestId("preview-form-input").first();
|
||||||
@@ -1000,11 +1015,19 @@ test("폼 입력 세로 패딩 px 입력이 프리뷰에서도 em 스케일로
|
|||||||
|
|
||||||
await page.getByRole("button", { name: "Input field" }).click();
|
await page.getByRole("button", { name: "Input field" }).click();
|
||||||
|
|
||||||
|
const verticalPaddingEditorCanvas = page.getByTestId("editor-canvas");
|
||||||
|
const verticalPaddingEditorBlocks = verticalPaddingEditorCanvas.getByTestId("editor-block");
|
||||||
|
await verticalPaddingEditorBlocks.last().click({ force: true });
|
||||||
|
|
||||||
const paddingSidebar = page.getByTestId("properties-sidebar");
|
const paddingSidebar = page.getByTestId("properties-sidebar");
|
||||||
|
|
||||||
await paddingSidebar.getByLabel("Field vertical padding 커스텀").fill("18");
|
await paddingSidebar.getByLabel("Field vertical padding 커스텀").fill("18");
|
||||||
|
|
||||||
await page.getByRole("link", { name: "Open preview" }).click();
|
await Promise.all([
|
||||||
|
page.waitForURL(/\/preview/),
|
||||||
|
page.getByRole("link", { name: "Open preview" }).click(),
|
||||||
|
]);
|
||||||
|
|
||||||
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
await expect(page.getByRole("heading", { name: "Page Preview" })).toBeVisible();
|
||||||
|
|
||||||
const verticalInput = page.getByTestId("preview-form-input").first();
|
const verticalInput = page.getByTestId("preview-form-input").first();
|
||||||
@@ -1241,6 +1264,11 @@ test("폼 셀렉트 가로 패딩 px 입력이 프리뷰에서도 em 스케일
|
|||||||
|
|
||||||
await page.getByRole("button", { name: "Select field" }).click();
|
await page.getByRole("button", { name: "Select field" }).click();
|
||||||
|
|
||||||
|
// 방금 추가한 셀렉트 블록이 선택되어 FormSelectPropertiesPanel 이 활성화되도록 보장한다.
|
||||||
|
const editorCanvas = page.getByTestId("editor-canvas");
|
||||||
|
const editorBlocks = editorCanvas.getByTestId("editor-block");
|
||||||
|
await editorBlocks.last().click({ force: true });
|
||||||
|
|
||||||
const propertiesSidebar = page.getByTestId("properties-sidebar");
|
const propertiesSidebar = page.getByTestId("properties-sidebar");
|
||||||
|
|
||||||
await propertiesSidebar.getByLabel("Select horizontal padding 커스텀").fill("24");
|
await propertiesSidebar.getByLabel("Select horizontal padding 커스텀").fill("24");
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
|
||||||
|
// CTA / Features 템플릿이 현재 로케일에 따라 올바른 기본 텍스트로 생성되는지 검증한다.
|
||||||
|
|
||||||
|
// 모든 테스트에서 /api/auth/me 를 목 처리해 로그인 상태를 강제한다.
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.route("**/api/auth/me", async (route) => {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify({ id: "user-e2e", email: "e2e@example.com", tokenVersion: 1 }),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function getLocaleToggle(page: import("@playwright/test").Page) {
|
||||||
|
return page.getByRole("button", { name: "Toggle locale" });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function assertLocale(page: import("@playwright/test").Page, expected: "EN" | "KO") {
|
||||||
|
const toggle = getLocaleToggle(page);
|
||||||
|
await expect(toggle).toBeVisible();
|
||||||
|
await expect(toggle).toHaveText(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setLocale(page: import("@playwright/test").Page, target: "EN" | "KO") {
|
||||||
|
const toggle = getLocaleToggle(page);
|
||||||
|
await expect(toggle).toBeVisible();
|
||||||
|
|
||||||
|
const current = (await toggle.textContent())?.trim();
|
||||||
|
if (current === target) return;
|
||||||
|
|
||||||
|
await toggle.click();
|
||||||
|
await expect(toggle).toHaveText(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
// EN 로케일 기준 CTA 템플릿
|
||||||
|
test("CTA 템플릿은 EN 로케일에서 영어 기본 텍스트로 생성되어야 한다", async ({ page }) => {
|
||||||
|
await page.goto("/editor");
|
||||||
|
|
||||||
|
await setLocale(page, "EN");
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "CTA template" }).click();
|
||||||
|
|
||||||
|
const canvas = page.getByTestId("editor-canvas");
|
||||||
|
|
||||||
|
// 디버그: 현재 캔버스 텍스트 블록들의 내용을 로그로 출력한다.
|
||||||
|
const debugTexts = await canvas.getByTestId("editor-block").allInnerTexts();
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log("[CTA EN debug] block texts:", debugTexts);
|
||||||
|
|
||||||
|
await expect(canvas.getByText("Write a compelling CTA message here.")).toBeVisible();
|
||||||
|
await expect(canvas.getByRole("button", { name: "Call to action" })).toBeVisible();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
canvas.getByText("지금 바로 행동을 유도하는 CTA 텍스트를 입력하세요.", { exact: false }),
|
||||||
|
).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// KO 로케일 기준 CTA 템플릿
|
||||||
|
test("CTA 템플릿은 KO 로케일에서 한국어 기본 텍스트로 생성되어야 한다", async ({ page }) => {
|
||||||
|
await page.goto("/editor");
|
||||||
|
|
||||||
|
await setLocale(page, "KO");
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "CTA 템플릿" }).click();
|
||||||
|
|
||||||
|
const canvas = page.getByTestId("editor-canvas");
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
canvas.getByText("지금 바로 행동을 유도하는 CTA 텍스트를 입력하세요.", { exact: false }),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(canvas.getByRole("button", { name: "CTA 버튼" })).toBeVisible();
|
||||||
|
|
||||||
|
await expect(canvas.getByText("Write a compelling CTA message here.")).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// EN 로케일 기준 Features 템플릿
|
||||||
|
test("Features 템플릿은 EN 로케일에서 영어 기본 텍스트로 생성되어야 한다", async ({ page }) => {
|
||||||
|
await page.goto("/editor");
|
||||||
|
|
||||||
|
await setLocale(page, "EN");
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "Features template" }).click();
|
||||||
|
|
||||||
|
const canvas = page.getByTestId("editor-canvas");
|
||||||
|
|
||||||
|
// 디버그: 현재 캔버스 텍스트 블록들의 내용을 로그로 출력한다.
|
||||||
|
const debugTexts = await canvas.getByTestId("editor-block").allInnerTexts();
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log("[Features EN debug] block texts:", debugTexts);
|
||||||
|
|
||||||
|
await expect(canvas.getByText("Feature 1", { exact: false }).first()).toBeVisible();
|
||||||
|
await expect(canvas.getByText("A short description of this feature.").first()).toBeVisible();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
canvas.getByText("해당 기능을 간단히 설명하는 텍스트입니다.", { exact: false }),
|
||||||
|
).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// KO 로케일 기준 Features 템플릿
|
||||||
|
test("Features 템플릿은 KO 로케일에서 한국어 기본 텍스트로 생성되어야 한다", async ({ page }) => {
|
||||||
|
await page.goto("/editor");
|
||||||
|
|
||||||
|
await setLocale(page, "KO");
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "기능 템플릿" }).click();
|
||||||
|
|
||||||
|
const canvas = page.getByTestId("editor-canvas");
|
||||||
|
|
||||||
|
await expect(canvas.getByText("Feature 1 제목", { exact: false })).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
canvas.getByText("해당 기능을 간단히 설명하는 텍스트입니다.", { exact: false }).first(),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await expect(canvas.getByText("A short description of this feature.")).toHaveCount(0);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user