스타일 수정
This commit is contained in:
@@ -33,6 +33,15 @@ describe("PublicPageRenderer - 버튼 블록 스타일", () => {
|
||||
|
||||
const link = container.querySelector("a") as HTMLAnchorElement | null;
|
||||
expect(link).not.toBeNull();
|
||||
|
||||
const wrapper = link!.parentElement as HTMLElement | null;
|
||||
expect(wrapper).not.toBeNull();
|
||||
// 정렬/버튼 베이스는 pb-* 토큰 클래스로 렌더된다.
|
||||
expect(wrapper!.className).toContain("pb-text-center");
|
||||
expect(link!.className).toContain("pb-btn-base");
|
||||
expect(link!.className).toContain("pb-btn-size-md");
|
||||
expect(link!.className).toContain("pb-btn-variant-solid-primary");
|
||||
expect(link!.className).toContain("pb-btn-radius-md");
|
||||
// 배경색: #123456 → rgb(18, 52, 86)
|
||||
expect(link!.style.backgroundColor).toBe("rgb(18, 52, 86)");
|
||||
// 외곽선 색상: #ff0000 → rgb(255, 0, 0)
|
||||
|
||||
@@ -56,7 +56,7 @@ describe("PublicPageRenderer - 텍스트 블록 스타일", () => {
|
||||
// 정렬 클래스
|
||||
expect(el.className).toContain("text-center");
|
||||
// 커스텀 폰트 크기: sizeClass 는 비워지고, 인라인 스타일로 설정된다.
|
||||
expect(el.style.fontSize).toBe("24px");
|
||||
expect(el.style.fontSize).toBe("1.5em");
|
||||
// 커스텀 색상/배경색
|
||||
expect(el.style.color).toBe("rgb(255, 0, 0)");
|
||||
expect(el.style.backgroundColor).toBe("rgb(18, 52, 86)");
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { expectNoPxInStyleParts } from "./styleInvariants";
|
||||
import {
|
||||
computeButtonPbTokens,
|
||||
computeButtonPublicTokens,
|
||||
@@ -53,15 +54,31 @@ describe("buttonHelpers.computeButtonPbTokens", () => {
|
||||
});
|
||||
|
||||
expect(tokens.alignClass).toBe("pb-text-right");
|
||||
expect(tokens.inlineStyles).toContain("width:200px");
|
||||
expect(tokens.inlineStyles).toContain("padding-left:16px");
|
||||
expect(tokens.inlineStyles).toContain("padding-right:16px");
|
||||
expect(tokens.inlineStyles).toContain("padding-top:8px");
|
||||
expect(tokens.inlineStyles).toContain("padding-bottom:8px");
|
||||
expect(tokens.inlineStyles).toContain("width:12.5em");
|
||||
expect(tokens.inlineStyles).toContain("padding-left:1em");
|
||||
expect(tokens.inlineStyles).toContain("padding-right:1em");
|
||||
expect(tokens.inlineStyles).toContain("padding-top:0.5em");
|
||||
expect(tokens.inlineStyles).toContain("padding-bottom:0.5em");
|
||||
expect(tokens.inlineStyles).toContain("background-color:#111111");
|
||||
expect(tokens.inlineStyles).toContain("border-color:#222222");
|
||||
expect(tokens.inlineStyles).toContain("color:#ffffff");
|
||||
});
|
||||
|
||||
it("Button Pb inlineStyles 에는 px 단위 값이 포함되지 않아야 한다", () => {
|
||||
const tokens = computeButtonPbTokens({
|
||||
align: "right",
|
||||
size: "sm",
|
||||
widthMode: "fixed",
|
||||
widthPx: 200,
|
||||
paddingX: 16,
|
||||
paddingY: 8,
|
||||
fillColorCustom: "#111111",
|
||||
strokeColorCustom: "#222222",
|
||||
textColorCustom: "#ffffff",
|
||||
});
|
||||
|
||||
expectNoPxInStyleParts(tokens.inlineStyles);
|
||||
});
|
||||
});
|
||||
|
||||
describe("buttonHelpers.computeButtonPublicTokens", () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { expectNoPxInStyleObject, expectNoPxInStyleString } from "./styleInvariants";
|
||||
import {
|
||||
computeDividerExportTokens,
|
||||
computeDividerEditorTokens,
|
||||
@@ -16,9 +17,9 @@ const baseProps: DividerBlockProps = {
|
||||
describe("dividerHelpers.computeDividerExportTokens", () => {
|
||||
const escapers = { escapeAttr: (v: string) => v };
|
||||
|
||||
it("기본 값은 1px 두께, 기본 색상(#475569), marginY=16px 이어야 한다", () => {
|
||||
it("기본 값은 1px 두께, 기본 색상(#475569), marginY=1em 이어야 한다", () => {
|
||||
const tokens = computeDividerExportTokens(baseProps, escapers);
|
||||
expect(tokens.style).toBe("border:0;border-bottom:1px solid #475569;margin:16px 0;");
|
||||
expect(tokens.style).toBe("border:0;border-bottom:1px solid #475569;margin:1em 0;");
|
||||
});
|
||||
|
||||
it("thickness=\"medium\" 인 경우 2px 두께로 렌더링되어야 한다", () => {
|
||||
@@ -33,12 +34,25 @@ describe("dividerHelpers.computeDividerExportTokens", () => {
|
||||
|
||||
it("marginYPx > 0 이면 해당 값을 margin Y 로 사용해야 한다", () => {
|
||||
const tokens = computeDividerExportTokens({ ...baseProps, marginYPx: 24 }, escapers);
|
||||
expect(tokens.style).toContain("margin:24px 0;");
|
||||
expect(tokens.style).toContain("margin:1.5em 0;");
|
||||
});
|
||||
|
||||
it("marginYPx 가 0 이하이거나 숫자가 아니면 기본 16px 을 사용해야 한다", () => {
|
||||
it("marginYPx 가 0 이하이거나 숫자가 아니면 기본 1em 을 사용해야 한다", () => {
|
||||
const zeroTokens = computeDividerExportTokens({ ...baseProps, marginYPx: 0 }, escapers);
|
||||
expect(zeroTokens.style).toContain("margin:16px 0;");
|
||||
expect(zeroTokens.style).toContain("margin:1em 0;");
|
||||
});
|
||||
|
||||
it("Export 구분선 style 에서는 border-bottom 두께를 제외하고 px 단위가 없어야 한다", () => {
|
||||
const tokens = computeDividerExportTokens({
|
||||
...baseProps,
|
||||
thickness: "medium",
|
||||
colorHex: "#123456",
|
||||
marginYPx: 24,
|
||||
}, escapers);
|
||||
|
||||
expectNoPxInStyleString(tokens.style, {
|
||||
allowPatterns: [/border-bottom:\d+px/],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -136,4 +150,18 @@ describe("dividerHelpers.computeDividerPublicTokens", () => {
|
||||
expect(tokens.innerStyle.width).toBe("auto");
|
||||
expect(tokens.lineStyle.width).toBe("30em");
|
||||
});
|
||||
|
||||
it("퍼블릭 구분선 스타일에 px 단위 값이 포함되지 않아야 한다", () => {
|
||||
const tokens = computeDividerPublicTokens({
|
||||
align: "center",
|
||||
thickness: "medium",
|
||||
colorHex: "#123456",
|
||||
marginYPx: 32,
|
||||
widthMode: "fixed",
|
||||
widthPx: 480,
|
||||
} as DividerBlockProps);
|
||||
|
||||
expectNoPxInStyleObject(tokens.innerStyle);
|
||||
expectNoPxInStyleObject(tokens.lineStyle);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { expectNoPxInStyleObject, expectNoPxInStyleString } from "./styleInvariants";
|
||||
import {
|
||||
computeFormInputExportTokens,
|
||||
computeFormSelectExportTokens,
|
||||
@@ -64,12 +65,17 @@ describe("formHelpers.computeFormInputExportTokens", () => {
|
||||
expect(tokens.inputStyleAttr).toContain("color:#ff0000");
|
||||
expect(tokens.inputStyleAttr).toContain("background-color:#111827");
|
||||
expect(tokens.inputStyleAttr).toContain("border-color:#4b5563");
|
||||
expect(tokens.inputStyleAttr).toContain("padding-left:8px");
|
||||
expect(tokens.inputStyleAttr).toContain("padding-right:8px");
|
||||
expect(tokens.inputStyleAttr).toContain("padding-top:4px");
|
||||
expect(tokens.inputStyleAttr).toContain("padding-bottom:4px");
|
||||
expect(tokens.inputStyleAttr).toContain("width:300px");
|
||||
expect(tokens.inputStyleAttr).toContain("padding-left:0.5em");
|
||||
expect(tokens.inputStyleAttr).toContain("padding-right:0.5em");
|
||||
expect(tokens.inputStyleAttr).toContain("padding-top:0.25em");
|
||||
expect(tokens.inputStyleAttr).toContain("padding-bottom:0.25em");
|
||||
expect(tokens.inputStyleAttr).toContain("width:18.75em");
|
||||
expect(tokens.inputStyleAttr).toContain("border-radius:6px");
|
||||
|
||||
// Export 인풋 스타일에는 border-radius 등의 예외를 제외하고 px 단위가 포함되지 않아야 한다.
|
||||
expectNoPxInStyleString(tokens.inputStyleAttr, {
|
||||
allowPatterns: [/border-radius:\d+px/, /border-width:\d+px/],
|
||||
});
|
||||
});
|
||||
|
||||
it("스타일 값이 없으면 style 속성이 비어 있어야 한다", () => {
|
||||
@@ -97,12 +103,17 @@ describe("formHelpers.computeFormSelectExportTokens", () => {
|
||||
expect(tokens.selectStyleAttr).toContain("color:#00ff00");
|
||||
expect(tokens.selectStyleAttr).toContain("background-color:#020617");
|
||||
expect(tokens.selectStyleAttr).toContain("border-color:#64748b");
|
||||
expect(tokens.selectStyleAttr).toContain("padding-left:12px");
|
||||
expect(tokens.selectStyleAttr).toContain("padding-right:12px");
|
||||
expect(tokens.selectStyleAttr).toContain("padding-top:6px");
|
||||
expect(tokens.selectStyleAttr).toContain("padding-bottom:6px");
|
||||
expect(tokens.selectStyleAttr).toContain("width:240px");
|
||||
expect(tokens.selectStyleAttr).toContain("padding-left:0.75em");
|
||||
expect(tokens.selectStyleAttr).toContain("padding-right:0.75em");
|
||||
expect(tokens.selectStyleAttr).toContain("padding-top:0.375em");
|
||||
expect(tokens.selectStyleAttr).toContain("padding-bottom:0.375em");
|
||||
expect(tokens.selectStyleAttr).toContain("width:15em");
|
||||
expect(tokens.selectStyleAttr).toContain("border-radius:9999px");
|
||||
|
||||
// Export 셀렉트 스타일에는 border-radius 등의 예외를 제외하고 px 단위가 포함되지 않아야 한다.
|
||||
expectNoPxInStyleString(tokens.selectStyleAttr, {
|
||||
allowPatterns: [/border-radius:\d+px/, /border-width:\d+px/],
|
||||
});
|
||||
});
|
||||
|
||||
it("스타일 값이 없으면 style 속성이 비어 있어야 한다", () => {
|
||||
@@ -130,11 +141,16 @@ describe("formHelpers.computeFormCheckboxExportTokens", () => {
|
||||
expect(tokens.optionStyleAttr).toContain("border-color:#e5e7eb");
|
||||
expect(tokens.optionStyleAttr).toContain("border-width:1px");
|
||||
expect(tokens.optionStyleAttr).toContain("border-style:solid");
|
||||
expect(tokens.optionStyleAttr).toContain("padding-left:10px");
|
||||
expect(tokens.optionStyleAttr).toContain("padding-right:10px");
|
||||
expect(tokens.optionStyleAttr).toContain("padding-top:5px");
|
||||
expect(tokens.optionStyleAttr).toContain("padding-bottom:5px");
|
||||
expect(tokens.optionStyleAttr).toContain("padding-left:0.625em");
|
||||
expect(tokens.optionStyleAttr).toContain("padding-right:0.625em");
|
||||
expect(tokens.optionStyleAttr).toContain("padding-top:0.3125em");
|
||||
expect(tokens.optionStyleAttr).toContain("padding-bottom:0.3125em");
|
||||
expect(tokens.optionStyleAttr).toContain("border-radius:2px");
|
||||
|
||||
// Export 체크박스 옵션 스타일에는 border/border-radius 를 제외하고 px 단위가 포함되지 않아야 한다.
|
||||
expectNoPxInStyleString(tokens.optionStyleAttr, {
|
||||
allowPatterns: [/border-radius:\d+px/, /border-width:\d+px/],
|
||||
});
|
||||
});
|
||||
|
||||
it("스타일 값이 없으면 style 속성이 비어 있어야 한다", () => {
|
||||
@@ -162,11 +178,16 @@ describe("formHelpers.computeFormRadioExportTokens", () => {
|
||||
expect(tokens.optionStyleAttr).toContain("border-color:#facc15");
|
||||
expect(tokens.optionStyleAttr).toContain("border-width:1px");
|
||||
expect(tokens.optionStyleAttr).toContain("border-style:solid");
|
||||
expect(tokens.optionStyleAttr).toContain("padding-left:4px");
|
||||
expect(tokens.optionStyleAttr).toContain("padding-right:4px");
|
||||
expect(tokens.optionStyleAttr).toContain("padding-top:2px");
|
||||
expect(tokens.optionStyleAttr).toContain("padding-bottom:2px");
|
||||
expect(tokens.optionStyleAttr).toContain("padding-left:0.25em");
|
||||
expect(tokens.optionStyleAttr).toContain("padding-right:0.25em");
|
||||
expect(tokens.optionStyleAttr).toContain("padding-top:0.125em");
|
||||
expect(tokens.optionStyleAttr).toContain("padding-bottom:0.125em");
|
||||
expect(tokens.optionStyleAttr).toContain("border-radius:4px");
|
||||
|
||||
// Export 라디오 옵션 스타일에는 border/border-radius 를 제외하고 px 단위가 포함되지 않아야 한다.
|
||||
expectNoPxInStyleString(tokens.optionStyleAttr, {
|
||||
allowPatterns: [/border-radius:\d+px/, /border-width:\d+px/],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -253,6 +274,10 @@ describe("formHelpers - public tokens", () => {
|
||||
expect(tokens.inputStyle.backgroundColor).toBe("#123456");
|
||||
expect(tokens.inputStyle.borderColor).toBe("#ff0000");
|
||||
expect(tokens.inputStyle.borderRadius).toBe("6px");
|
||||
|
||||
// 퍼블릭 인풋 토큰 스타일에는 borderRadius/borderWidth 를 제외하고 px 단위가 없어야 한다.
|
||||
expectNoPxInStyleObject(tokens.wrapperStyle);
|
||||
expectNoPxInStyleObject(tokens.inputStyle, { allowKeys: ["borderRadius", "borderWidth"] });
|
||||
});
|
||||
|
||||
it("computeFormSelectPublicTokens: 퍼블릭 셀렉트 필드 스타일을 계산해야 한다", () => {
|
||||
@@ -276,6 +301,10 @@ describe("formHelpers - public tokens", () => {
|
||||
expect(tokens.selectStyle.backgroundColor).toBe("#123456");
|
||||
expect(tokens.selectStyle.borderColor).toBe("#ff0000");
|
||||
expect(tokens.selectStyle.borderRadius).toBe("6px");
|
||||
|
||||
// 퍼블릭 셀렉트 토큰 스타일에는 borderRadius/borderWidth 를 제외하고 px 단위가 없어야 한다.
|
||||
expectNoPxInStyleObject(tokens.wrapperStyle);
|
||||
expectNoPxInStyleObject(tokens.selectStyle, { allowKeys: ["borderRadius", "borderWidth"] });
|
||||
});
|
||||
|
||||
it("computeFormCheckboxPublicTokens: 퍼블릭 체크박스 그룹 스타일을 계산해야 한다", () => {
|
||||
@@ -304,6 +333,13 @@ describe("formHelpers - public tokens", () => {
|
||||
expect(tokens.optionContainerStyle.borderRadius).toBe("6px");
|
||||
expect(tokens.groupTextStyle.color).toBe("#ffffff");
|
||||
expect(tokens.optionTextStyle.color).toBe("#ffffff");
|
||||
|
||||
// 퍼블릭 체크박스 토큰 스타일에는 borderRadius/borderWidth 를 제외하고 px 단위가 없어야 한다.
|
||||
expectNoPxInStyleObject(tokens.groupStyle);
|
||||
expectNoPxInStyleObject(tokens.optionsStyle);
|
||||
expectNoPxInStyleObject(tokens.optionContainerStyle, { allowKeys: ["borderRadius", "borderWidth"] });
|
||||
expectNoPxInStyleObject(tokens.groupTextStyle);
|
||||
expectNoPxInStyleObject(tokens.optionTextStyle);
|
||||
});
|
||||
|
||||
it("computeFormRadioPublicTokens: 퍼블릭 라디오 그룹 스타일을 계산해야 한다", () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { expectNoPxInStyleParts } from "./styleInvariants";
|
||||
import {
|
||||
computeImageExportStyles,
|
||||
computeImageEditorTokens,
|
||||
@@ -28,7 +29,7 @@ describe("imageHelpers.computeImageExportStyles", () => {
|
||||
widthPx: 320,
|
||||
});
|
||||
|
||||
expect(imgStyleParts).toContain("width:320px");
|
||||
expect(imgStyleParts).toContain("width:20em");
|
||||
});
|
||||
|
||||
it("borderRadius 토큰이 full 이면 border-radius 9999px 를 사용해야 한다", () => {
|
||||
@@ -47,6 +48,18 @@ describe("imageHelpers.computeImageExportStyles", () => {
|
||||
|
||||
expect(imgStyleParts).toContain("border-radius:20px");
|
||||
});
|
||||
|
||||
it("Export 이미지 스타일에는 border-radius 를 제외하고 px 단위 값이 포함되지 않아야 한다", () => {
|
||||
const { wrapperStyleParts, imgStyleParts } = computeImageExportStyles({
|
||||
backgroundColorCustom: "#123456",
|
||||
widthMode: "fixed",
|
||||
widthPx: 320,
|
||||
borderRadius: "lg",
|
||||
});
|
||||
|
||||
expectNoPxInStyleParts(wrapperStyleParts);
|
||||
expectNoPxInStyleParts(imgStyleParts, { allowPatterns: [/border-radius:\d+px/] });
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeImageEditorTokens", () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { expectNoPxInStyleObject, expectNoPxInStyleParts } from "./styleInvariants";
|
||||
import {
|
||||
computeListExportTokens,
|
||||
computeListEditorTokens,
|
||||
@@ -81,6 +82,17 @@ describe("listHelpers.computeListExportTokens", () => {
|
||||
const tokens = computeListExportTokens({ ...baseProps });
|
||||
expect(tokens.items).toEqual([]);
|
||||
});
|
||||
|
||||
it("Export 리스트 listStyleParts 에는 px 단위 값이 포함되지 않아야 한다", () => {
|
||||
const tokens = computeListExportTokens({
|
||||
...baseProps,
|
||||
align: "center",
|
||||
items: ["a"],
|
||||
backgroundColorCustom: "#123456",
|
||||
});
|
||||
|
||||
expectNoPxInStyleParts(tokens.listStyleParts);
|
||||
});
|
||||
});
|
||||
|
||||
describe("listHelpers.computeListEditorTokens", () => {
|
||||
@@ -158,17 +170,28 @@ describe("listHelpers.computeListPublicTokens", () => {
|
||||
expect(byTokenLg.gapEm).toBe(16 / 16);
|
||||
});
|
||||
|
||||
it("fontSizeCustom 이 px 인 경우 em 단위로 변환되어야 하고, backgroundColorCustom 은 trim 되어야 한다", () => {
|
||||
it("fontSizeCustom / lineHeightCustom 이 px 인 경우 em 단위로 변환되어야 하고, backgroundColorCustom 은 trim 되어야 한다", () => {
|
||||
const tokens = computeListPublicTokens({
|
||||
...baseProps,
|
||||
fontSizeCustom: "32px",
|
||||
lineHeightCustom: "24px",
|
||||
backgroundColorCustom: " #123456 ",
|
||||
});
|
||||
|
||||
expect(tokens.listStyle.fontSize).toBe("2em");
|
||||
expect(tokens.listStyle.lineHeight).toBe("1.5em");
|
||||
expect(tokens.listStyle.backgroundColor).toBe("#123456");
|
||||
});
|
||||
|
||||
it("lineHeightCustom 이 px 인 경우 em 단위로 변환되어야 한다", () => {
|
||||
const tokens = computeListPublicTokens({
|
||||
...baseProps,
|
||||
lineHeightCustom: "24px",
|
||||
});
|
||||
|
||||
expect(tokens.listStyle.lineHeight).toBe("1.5em");
|
||||
});
|
||||
|
||||
it("bulletStyle 은 bulletStyle 지정값 또는 ordered 여부에 따라 결정되어야 한다", () => {
|
||||
const defaultDisc = computeListPublicTokens({ ...baseProps, ordered: false, bulletStyle: undefined });
|
||||
const defaultDecimal = computeListPublicTokens({ ...baseProps, ordered: true, bulletStyle: undefined });
|
||||
@@ -178,4 +201,16 @@ describe("listHelpers.computeListPublicTokens", () => {
|
||||
expect(defaultDecimal.bulletStyle).toBe("decimal");
|
||||
expect(none.bulletStyle).toBe("none");
|
||||
});
|
||||
|
||||
it("퍼블릭 리스트 style 에 px 단위 값이 포함되지 않아야 한다", () => {
|
||||
const tokens = computeListPublicTokens({
|
||||
...baseProps,
|
||||
fontSizeCustom: "32px",
|
||||
lineHeightCustom: "24px",
|
||||
textColorCustom: "#ff0000",
|
||||
backgroundColorCustom: "#123456",
|
||||
});
|
||||
|
||||
expectNoPxInStyleObject(tokens.listStyle);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -192,4 +192,74 @@ describe("프리뷰와 정적 내보내기 일치 (고수준)", () => {
|
||||
expect(staticHtml).toContain(text);
|
||||
}
|
||||
});
|
||||
|
||||
it(
|
||||
"리스트+폼+구분선 조합에서 리스트 컨테이너와 구분선이 프리뷰/정적 HTML 모두에 존재하고, 리스트는 pb-list 클래스를 사용하며 li 에 margin-bottom 인라인 스타일을 사용하지 않아야 한다",
|
||||
() => {
|
||||
const blocks: Block[] = [
|
||||
{
|
||||
id: "divider_combo",
|
||||
type: "divider",
|
||||
props: {
|
||||
align: "center",
|
||||
thickness: "medium",
|
||||
widthMode: "full",
|
||||
},
|
||||
} as any,
|
||||
{
|
||||
id: "list_combo",
|
||||
type: "list",
|
||||
props: {
|
||||
items: ["리스트 아이템 1", "리스트 아이템 2"],
|
||||
ordered: false,
|
||||
align: "left",
|
||||
},
|
||||
} as any,
|
||||
{
|
||||
id: "form_combo",
|
||||
type: "form",
|
||||
props: {
|
||||
kind: "contact",
|
||||
submitTarget: "internal",
|
||||
// v1 fallback 필드를 사용해 양쪽 렌더러가 동일한 폼 필드를 가지도록 한다.
|
||||
fields: [
|
||||
{ id: "f_name", name: "name", label: "이름", type: "text", required: true },
|
||||
],
|
||||
fieldIds: [],
|
||||
submitButtonId: null,
|
||||
formWidthMode: "full",
|
||||
},
|
||||
} as any,
|
||||
];
|
||||
|
||||
const projectConfig: ProjectConfig = {
|
||||
title: "리스트+폼+구분선 레이아웃 테스트",
|
||||
slug: "list-form-divider-layout",
|
||||
canvasPreset: "full",
|
||||
};
|
||||
|
||||
const previewHtml = ReactDOMServer.renderToString(
|
||||
React.createElement(PublicPageRenderer, { blocks }),
|
||||
);
|
||||
|
||||
const staticHtml = buildStaticHtml(blocks, projectConfig);
|
||||
|
||||
// 리스트 텍스트는 양쪽에 존재해야 한다.
|
||||
expect(previewHtml).toContain("리스트 아이템 1");
|
||||
expect(staticHtml).toContain("리스트 아이템 1");
|
||||
|
||||
// 구분선은 프리뷰/Export 모두에 존재해야 한다.
|
||||
expect(previewHtml).toContain('data-testid="preview-divider"');
|
||||
expect(staticHtml).toContain('<hr class="pb-divider"');
|
||||
|
||||
// 리스트 컨테이너는 pb-list 클래스를 사용해야 한다.
|
||||
expect(previewHtml).toContain('class="pb-list');
|
||||
expect(staticHtml).toContain('class="pb-list"');
|
||||
|
||||
// 리스트 아이템(li)은 margin-bottom 인라인 스타일을 사용하지 않아야 한다.
|
||||
// divider 등 다른 요소에서 margin-bottom 을 사용하는 것은 허용한다.
|
||||
expect(previewHtml).not.toMatch(/<li[^>]*style=\"[^\"]*margin-bottom:/);
|
||||
expect(staticHtml).not.toMatch(/<li[^>]*style=\"[^\"]*margin-bottom:/);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { expectNoPxInStyleObject, expectNoPxInStyleParts } from "./styleInvariants";
|
||||
import {
|
||||
computeSectionExportTokens,
|
||||
computeSectionPublicTokens,
|
||||
@@ -73,6 +74,21 @@ describe("sectionHelpers.computeSectionExportTokens", () => {
|
||||
expect(tokens.sectionStyleParts).toContain("position:relative");
|
||||
expect(tokens.sectionStyleParts).toContain("overflow:hidden");
|
||||
});
|
||||
|
||||
it("Export 섹션 sectionStyleParts 에는 px 단위 값이 포함되지 않아야 한다", () => {
|
||||
const tokens = computeSectionExportTokens({
|
||||
...baseProps,
|
||||
backgroundColorCustom: "#123456",
|
||||
backgroundImageSrc: "https://example.com/bg.png",
|
||||
backgroundImageSize: "cover",
|
||||
backgroundImagePositionMode: "preset",
|
||||
backgroundImagePosition: "top",
|
||||
backgroundImageRepeat: "repeat-x",
|
||||
backgroundVideoSrc: "https://example.com/bg.mp4",
|
||||
} as SectionBlockProps);
|
||||
|
||||
expectNoPxInStyleParts(tokens.sectionStyleParts);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sectionHelpers.computeSectionPublicTokens", () => {
|
||||
@@ -90,6 +106,11 @@ describe("sectionHelpers.computeSectionPublicTokens", () => {
|
||||
expect(tokens.sectionStyle.paddingBottom).toBe("2em");
|
||||
expect(tokens.innerWrapperStyle.maxWidth).toBe("50em");
|
||||
expect(tokens.columnsContainerStyle.columnGap).toBe("1.5em");
|
||||
|
||||
// 퍼블릭 섹션 토큰 스타일에는 px 단위 값이 포함되지 않아야 한다.
|
||||
expectNoPxInStyleObject(tokens.sectionStyle);
|
||||
expectNoPxInStyleObject(tokens.innerWrapperStyle);
|
||||
expectNoPxInStyleObject(tokens.columnsContainerStyle);
|
||||
});
|
||||
|
||||
it("backgroundImage* 와 backgroundVideoSrc 를 올바르게 반영해야 한다", () => {
|
||||
|
||||
@@ -16,7 +16,7 @@ describe("getSectionLayoutConfig", () => {
|
||||
const cfg = getSectionLayoutConfig(baseProps);
|
||||
|
||||
expect(cfg.backgroundClass).toBe("bg-slate-950");
|
||||
expect(cfg.paddingYClass).toBe("py-12");
|
||||
expect(cfg.paddingYClass).toBe("py-6");
|
||||
expect(cfg.maxWidthClass).toBe("max-w-5xl");
|
||||
expect(cfg.gapXClass).toBe("gap-8");
|
||||
expect(cfg.alignItemsClass).toBe("items-start");
|
||||
@@ -49,7 +49,7 @@ describe("getSectionLayoutConfig", () => {
|
||||
expect(narrowCfg.maxWidthClass).toBe("max-w-3xl");
|
||||
expect(narrowCfg.gapXClass).toBe("gap-4");
|
||||
expect(narrowCfg.alignItemsClass).toBe("items-end");
|
||||
expect(narrowCfg.paddingYClass).toBe("py-8");
|
||||
expect(narrowCfg.paddingYClass).toBe("py-4");
|
||||
});
|
||||
|
||||
it("primary 배경과 큰 패딩에 대해 올바른 클래스를 반환해야 한다", () => {
|
||||
@@ -61,6 +61,6 @@ describe("getSectionLayoutConfig", () => {
|
||||
|
||||
const cfg = getSectionLayoutConfig(primaryLarge);
|
||||
expect(cfg.backgroundClass).toBe("bg-sky-900");
|
||||
expect(cfg.paddingYClass).toBe("py-20");
|
||||
expect(cfg.paddingYClass).toBe("py-10");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { expect } from "vitest";
|
||||
|
||||
// 스타일 관련 인변언트(전역 규칙)를 검증하기 위한 공통 테스트 유틸리티.
|
||||
// - 퍼블릭/익스포트 토큰에서 px 단위 값 사용을 금지하거나,
|
||||
// - border-width 등 꼭 필요한 px 사용만 예외로 허용할 때 사용한다.
|
||||
export type StyleObject = Record<string, unknown>;
|
||||
|
||||
export function expectNoPxInStyleObject(style: unknown, options?: { allowKeys?: string[] }) {
|
||||
if (!style || typeof style !== "object") return;
|
||||
const allow = new Set(options?.allowKeys ?? []);
|
||||
|
||||
for (const [key, value] of Object.entries(style as StyleObject)) {
|
||||
if (value == null) continue;
|
||||
if (allow.has(key)) continue;
|
||||
if (typeof value === "object") continue;
|
||||
|
||||
const str = String(value);
|
||||
expect(str).not.toContain("px");
|
||||
}
|
||||
}
|
||||
|
||||
export function expectNoPxInStyleString(style: string | undefined | null, options?: { allowPatterns?: RegExp[] }) {
|
||||
if (!style) return;
|
||||
const allowPatterns = options?.allowPatterns ?? [];
|
||||
if (allowPatterns.some((re) => re.test(style))) return;
|
||||
|
||||
expect(style).not.toContain("px");
|
||||
}
|
||||
|
||||
export function expectNoPxInStyleParts(parts: string[] | undefined | null, options?: { allowPatterns?: RegExp[] }) {
|
||||
if (!parts || parts.length === 0) return;
|
||||
const combined = parts.join(";");
|
||||
expectNoPxInStyleString(combined, options);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { expectNoPxInStyleObject, expectNoPxInStyleParts } from "./styleInvariants";
|
||||
import {
|
||||
computeTextPbTokens,
|
||||
computeTextEditorTokens,
|
||||
@@ -68,6 +69,17 @@ describe("textHelpers.computeTextPbTokens", () => {
|
||||
expect(prose.maxWidthClass).toBe("pb-text-maxw-prose");
|
||||
expect(narrow.maxWidthClass).toBe("pb-text-maxw-narrow");
|
||||
});
|
||||
|
||||
it("텍스트 Pb inlineStyles 에는 px 단위 값이 포함되지 않아야 한다", () => {
|
||||
const tokens = computeTextPbTokens({
|
||||
...baseProps,
|
||||
colorMode: "custom",
|
||||
colorCustom: "#ff0000",
|
||||
backgroundColorCustom: "#123456",
|
||||
});
|
||||
|
||||
expectNoPxInStyleParts(tokens.inlineStyles);
|
||||
});
|
||||
});
|
||||
|
||||
describe("textHelpers.computeTextEditorTokens", () => {
|
||||
@@ -128,7 +140,7 @@ describe("textHelpers.computeTextPublicTokens", () => {
|
||||
} as TextBlockProps);
|
||||
|
||||
expect(tokens.sizeClass).toBe("");
|
||||
expect(tokens.styleOverrides.fontSize).toBe("24px");
|
||||
expect(tokens.styleOverrides.fontSize).toBe("1.5em");
|
||||
});
|
||||
|
||||
it("colorCustom / backgroundColorCustom 이 styleOverrides 에 반영되어야 한다", () => {
|
||||
@@ -141,4 +153,29 @@ describe("textHelpers.computeTextPublicTokens", () => {
|
||||
expect(tokens.styleOverrides.color).toBe("#ff0000");
|
||||
expect(tokens.styleOverrides.backgroundColor).toBe("#123456");
|
||||
});
|
||||
|
||||
it("lineHeightCustom / letterSpacingCustom \uc774 px \uc778 \uacbd\uc6b0 em \ub2e8\uc704 \uc2a4\ud0c0\uc77c\ub85c \ubcc0\ud658\ub418\uc5b4\uc57c \ud55c\ub2e4", () => {
|
||||
const tokens = computeTextPublicTokens({
|
||||
...baseBlockProps,
|
||||
lineHeightCustom: "24px",
|
||||
letterSpacingCustom: "2px",
|
||||
} as TextBlockProps);
|
||||
|
||||
expect(tokens.styleOverrides.lineHeight).toBe("1.5em");
|
||||
expect(tokens.styleOverrides.letterSpacing).toBe("0.125em");
|
||||
});
|
||||
|
||||
it("퍼블릭 텍스트 styleOverrides 에 px 단위 값이 포함되지 않아야 한다", () => {
|
||||
const tokens = computeTextPublicTokens({
|
||||
...baseBlockProps,
|
||||
fontSizeMode: "custom",
|
||||
fontSizeCustom: "24px",
|
||||
lineHeightCustom: "24px",
|
||||
letterSpacingCustom: "2px",
|
||||
colorCustom: "#ff0000",
|
||||
backgroundColorCustom: "#123456",
|
||||
} as TextBlockProps);
|
||||
|
||||
expectNoPxInStyleObject(tokens.styleOverrides);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { expectNoPxInStyleParts } from "./styleInvariants";
|
||||
import {
|
||||
normalizeVideoSourceUrl,
|
||||
resolveVideoPlatform,
|
||||
@@ -124,7 +125,7 @@ describe("videoHelpers.computeVideoExportTokens", () => {
|
||||
|
||||
const escapers = { escapeAttr: (value: string) => value };
|
||||
|
||||
it("fixed width / 카드 스타일을 px 단위 스타일 파트로 계산해야 한다", () => {
|
||||
it("fixed width / 카드 스타일을 em 단위 스타일 파트로 계산해야 한다", () => {
|
||||
const tokens = computeVideoExportTokens(
|
||||
{
|
||||
...baseProps,
|
||||
@@ -139,15 +140,33 @@ describe("videoHelpers.computeVideoExportTokens", () => {
|
||||
);
|
||||
|
||||
expect(tokens.aspectClass).toBe("");
|
||||
expect(tokens.wrapperStyleParts).toContain("width:640px");
|
||||
expect(tokens.videoStyleParts).toContain("width:640px");
|
||||
expect(tokens.wrapperStyleParts).toContain("width:40em");
|
||||
expect(tokens.videoStyleParts).toContain("width:40em");
|
||||
expect(tokens.wrapperStyleParts).toContain("background-color:#123456");
|
||||
expect(tokens.wrapperStyleParts).toContain("padding:24px");
|
||||
expect(tokens.wrapperStyleParts).toContain("border-radius:16px");
|
||||
expect(tokens.wrapperStyleParts).toContain("padding:1.5em");
|
||||
expect(tokens.wrapperStyleParts).toContain("border-radius:1em");
|
||||
expect(tokens.outerStyleParts).toContain("display:flex");
|
||||
expect(tokens.outerStyleParts).toContain("justify-content:flex-start");
|
||||
});
|
||||
|
||||
it("Export 비디오 wrapper/video 스타일 파트에는 px 단위 값이 포함되지 않아야 한다", () => {
|
||||
const tokens = computeVideoExportTokens(
|
||||
{
|
||||
...baseProps,
|
||||
align: "left",
|
||||
widthMode: "fixed",
|
||||
widthPx: 640,
|
||||
backgroundColorCustom: "#123456",
|
||||
cardPaddingPx: 24,
|
||||
borderRadiusPx: 16,
|
||||
} as any,
|
||||
escapers,
|
||||
);
|
||||
|
||||
expectNoPxInStyleParts(tokens.wrapperStyleParts);
|
||||
expectNoPxInStyleParts(tokens.videoStyleParts);
|
||||
});
|
||||
|
||||
it("widthMode 가 full 이면 width:100% 로 설정하고 align 에 따라 justify-content 를 계산해야 한다", () => {
|
||||
const right = computeVideoExportTokens(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user