에디터 정리 및 버그 수정
This commit is contained in:
@@ -9,6 +9,7 @@ import type { Block, ProjectConfig } from "@/features/editor/state/editorStore";
|
||||
// - 로컬스토리지에 없으면 /api/projects/[slug] 를 호출해 서버에서 프로젝트를 불러와 replaceBlocks 해야 한다.
|
||||
|
||||
let mockState: any;
|
||||
let searchParamsSlug: string | null = null;
|
||||
|
||||
beforeEach(() => {
|
||||
const baseProjectConfig: ProjectConfig = {
|
||||
@@ -32,14 +33,17 @@ beforeEach(() => {
|
||||
projectConfig: baseProjectConfig,
|
||||
selectedBlockId: null as string | null,
|
||||
selectedListItemId: null as string | null,
|
||||
// EditorPage 가 참조하는 액션들: 기본적으로 no-op mock 으로 채운다.
|
||||
// EditorPage 가 참조하는 액션들: 기본적으로 mock 으로 채우되,
|
||||
// replaceBlocks 는 실제 store.blocks 도 업데이트하도록 구현한다.
|
||||
undo: vi.fn(),
|
||||
redo: vi.fn(),
|
||||
removeBlock: vi.fn(),
|
||||
duplicateBlock: vi.fn(),
|
||||
selectBlock: vi.fn(),
|
||||
selectListItem: vi.fn(),
|
||||
replaceBlocks: vi.fn(),
|
||||
replaceBlocks: vi.fn((nextBlocks: Block[]) => {
|
||||
mockState.blocks = nextBlocks;
|
||||
}),
|
||||
reorderBlocks: vi.fn(),
|
||||
moveBlock: vi.fn(),
|
||||
addTextBlock: vi.fn(),
|
||||
@@ -69,9 +73,11 @@ beforeEach(() => {
|
||||
...partial,
|
||||
} as ProjectConfig;
|
||||
}),
|
||||
resetHistory: vi.fn(),
|
||||
};
|
||||
|
||||
window.localStorage.clear();
|
||||
searchParamsSlug = null;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -102,6 +108,9 @@ vi.mock("next/navigation", () => {
|
||||
useRouter: () => ({
|
||||
push: vi.fn(),
|
||||
}),
|
||||
useSearchParams: () => ({
|
||||
get: (key: string) => (key === "slug" ? searchParamsSlug : null),
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -277,6 +286,12 @@ describe("EditorPage - 프로젝트 저장/불러오기", () => {
|
||||
|
||||
expect(mockState.replaceBlocks).toHaveBeenCalledWith(savedBlocks as any);
|
||||
|
||||
expect(
|
||||
(mockState.updateProjectConfig as any).mock.calls.some(
|
||||
([arg]: any[]) => arg && arg.title === "로컬 프로젝트" && arg.slug === slug,
|
||||
),
|
||||
).toBe(true);
|
||||
|
||||
const serverCalls = fetchMock.mock.calls.filter(
|
||||
([url]: any[]) => typeof url === "string" && url.startsWith("/api/projects"),
|
||||
);
|
||||
@@ -304,7 +319,7 @@ describe("EditorPage - 프로젝트 저장/불러오기", () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ slug, contentJson: serverBlocks }),
|
||||
json: async () => ({ slug, title: "서버 프로젝트", contentJson: serverBlocks }),
|
||||
} as any);
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
@@ -341,10 +356,145 @@ describe("EditorPage - 프로젝트 저장/불러오기", () => {
|
||||
expect(mockState.replaceBlocks).toHaveBeenCalledWith(serverBlocks as any);
|
||||
});
|
||||
|
||||
expect(
|
||||
(mockState.updateProjectConfig as any).mock.calls.some(
|
||||
([arg]: any[]) => arg && arg.title === "서버 프로젝트" && arg.slug === slug,
|
||||
),
|
||||
).toBe(true);
|
||||
|
||||
const message = await screen.findByText(`프로젝트를 불러왔습니다: ${slug}`);
|
||||
expect(message).toBeTruthy();
|
||||
});
|
||||
|
||||
it("서버에서 복잡한 프로젝트 contentJson 을 불러오면 모든 블록 속성과 스타일이 그대로 복원되어야 한다", async () => {
|
||||
const slug = "roundtrip-slug";
|
||||
|
||||
const complexBlocks: Block[] = [
|
||||
{
|
||||
id: "text_1",
|
||||
type: "text",
|
||||
props: {
|
||||
text: "복잡한 텍스트",
|
||||
align: "center",
|
||||
size: "xl",
|
||||
colorCustom: "#ff0000",
|
||||
backgroundColorCustom: "#123456",
|
||||
},
|
||||
} as any,
|
||||
{
|
||||
id: "button_1",
|
||||
type: "button",
|
||||
props: {
|
||||
label: "CTA 버튼",
|
||||
href: "/pricing",
|
||||
variant: "outline",
|
||||
size: "lg",
|
||||
align: "right",
|
||||
widthMode: "fixed",
|
||||
widthPx: 320,
|
||||
borderRadius: "full",
|
||||
colorPalette: "secondary",
|
||||
textColorCustom: "#111111",
|
||||
fillColorCustom: "#222222",
|
||||
strokeColorCustom: "#333333",
|
||||
},
|
||||
} as any,
|
||||
{
|
||||
id: "section_1",
|
||||
type: "section",
|
||||
props: {
|
||||
paddingY: "lg",
|
||||
background: "image",
|
||||
backgroundImageUrl: "https://example.com/bg.png",
|
||||
columns: [
|
||||
{ id: "col_1", span: 8 },
|
||||
{ id: "col_2", span: 4 },
|
||||
],
|
||||
},
|
||||
} as any,
|
||||
{
|
||||
id: "form_input_1",
|
||||
type: "formInput",
|
||||
props: {
|
||||
label: "이메일",
|
||||
inputType: "email",
|
||||
required: true,
|
||||
labelMode: "text",
|
||||
labelLayout: "stacked",
|
||||
labelGapPx: 12,
|
||||
widthMode: "custom",
|
||||
widthPx: 360,
|
||||
borderRadius: "lg",
|
||||
paddingX: 16,
|
||||
paddingY: 12,
|
||||
textColorCustom: "#fafafa",
|
||||
fillColorCustom: "#0f172a",
|
||||
strokeColorCustom: "#1e293b",
|
||||
formFieldName: "email-1",
|
||||
},
|
||||
} as any,
|
||||
];
|
||||
|
||||
const fetchMock = vi.fn().mockImplementation((input: any, init?: any) => {
|
||||
const url = typeof input === "string" ? input : input.url;
|
||||
const method = init?.method ?? "GET";
|
||||
|
||||
if (url === "/api/auth/me" && method === "GET") {
|
||||
return Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({ id: "user-1", email: "user@example.com", tokenVersion: 1 }),
|
||||
{
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (url === `/api/projects/${slug}` && method === "GET") {
|
||||
return Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({ slug, title: "복잡한 프로젝트", contentJson: complexBlocks }),
|
||||
{
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.resolve(new Response(null, { status: 200 }));
|
||||
});
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock as any);
|
||||
|
||||
mockState.projectConfig.slug = slug;
|
||||
|
||||
render(<EditorPage />);
|
||||
|
||||
const menuButton = screen.getByText("메뉴");
|
||||
fireEvent.click(menuButton);
|
||||
|
||||
const projectMenuItem = screen.getByText("프로젝트 저장/불러오기");
|
||||
fireEvent.click(projectMenuItem);
|
||||
|
||||
const loadButton = screen.getByText("불러오기");
|
||||
fireEvent.click(loadButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockState.replaceBlocks).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
const [loadedBlocks] = (mockState.replaceBlocks as any).mock.calls[0];
|
||||
expect(loadedBlocks).toEqual(complexBlocks as any);
|
||||
|
||||
expect(
|
||||
(mockState.updateProjectConfig as any).mock.calls.some(
|
||||
([arg]: any[]) => arg && arg.title === "복잡한 프로젝트" && arg.slug === slug,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("에디터 메뉴에서 프로젝트 삭제를 선택하면 서버에 DELETE 요청을 보내고 로컬 저장/자동저장 상태를 정리한 뒤 목록으로 이동해야 한다", async () => {
|
||||
const slug = "delete-test-slug";
|
||||
mockState.projectConfig.slug = slug;
|
||||
@@ -391,4 +541,75 @@ describe("EditorPage - 프로젝트 저장/불러오기", () => {
|
||||
|
||||
confirmSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("URL 쿼리의 slug 가 있으면 해당 slug 프로젝트를 자동으로 서버에서 불러와야 한다", async () => {
|
||||
const slug = "auto-load-slug";
|
||||
|
||||
const serverBlocks: Block[] = [
|
||||
{
|
||||
id: "server_1",
|
||||
type: "text",
|
||||
props: {
|
||||
text: "쿼리 로드 블록",
|
||||
align: "left",
|
||||
size: "base",
|
||||
},
|
||||
} as any,
|
||||
];
|
||||
|
||||
const fetchMock = vi.fn().mockImplementation((input: any, init?: any) => {
|
||||
const url = typeof input === "string" ? input : input.url;
|
||||
const method = init?.method ?? "GET";
|
||||
|
||||
if (url === "/api/auth/me" && method === "GET") {
|
||||
return Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({ id: "user-1", email: "user@example.com", tokenVersion: 1 }),
|
||||
{
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (url === `/api/projects/${slug}` && method === "GET") {
|
||||
return Promise.resolve(
|
||||
new Response(
|
||||
JSON.stringify({ slug, title: "쿼리 로드 프로젝트", contentJson: serverBlocks }),
|
||||
{
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Promise.resolve(new Response(null, { status: 200 }));
|
||||
});
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock as any);
|
||||
|
||||
searchParamsSlug = slug;
|
||||
|
||||
render(<EditorPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
fetchMock.mock.calls.some(
|
||||
([url, options]: any[]) => url === `/api/projects/${slug}` && (options?.method ?? "GET") === "GET",
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockState.replaceBlocks).toHaveBeenCalledWith(serverBlocks as any);
|
||||
});
|
||||
|
||||
expect(
|
||||
(mockState.updateProjectConfig as any).mock.calls.some(
|
||||
([arg]: any[]) => arg && arg.title === "쿼리 로드 프로젝트" && arg.slug === slug,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user