프로젝트 저장 및 목록
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import { render, cleanup } from "@testing-library/react";
|
||||
import EditorPage from "@/app/editor/page";
|
||||
import type { Block, ProjectConfig } from "@/features/editor/state/editorStore";
|
||||
|
||||
// EditorPage 자동저장 TDD
|
||||
// - blocks + projectConfig 를 localStorage 의 pb:autosave:<slug> 키로 자동 저장해야 한다.
|
||||
// - localStorage 에 autosave 스냅샷이 있으면 초기 렌더에서 replaceBlocks + updateProjectConfig 로 복원해야 한다.
|
||||
|
||||
let mockState: any;
|
||||
|
||||
beforeEach(() => {
|
||||
const baseProjectConfig: ProjectConfig = {
|
||||
title: "자동저장 테스트 페이지",
|
||||
slug: "autosave-test",
|
||||
canvasPreset: "full",
|
||||
} as ProjectConfig;
|
||||
|
||||
mockState = {
|
||||
blocks: [
|
||||
{
|
||||
id: "blk_1",
|
||||
type: "text",
|
||||
props: {
|
||||
text: "자동저장 테스트 블록",
|
||||
align: "left",
|
||||
size: "base",
|
||||
},
|
||||
},
|
||||
] as Block[],
|
||||
projectConfig: baseProjectConfig,
|
||||
selectedBlockId: null as string | null,
|
||||
selectedListItemId: null as string | null,
|
||||
// EditorPage 가 참조하는 액션들: 기본적으로 no-op mock 으로 채운다.
|
||||
undo: vi.fn(),
|
||||
redo: vi.fn(),
|
||||
removeBlock: vi.fn(),
|
||||
duplicateBlock: vi.fn(),
|
||||
selectBlock: vi.fn(),
|
||||
selectListItem: vi.fn(),
|
||||
replaceBlocks: vi.fn(),
|
||||
reorderBlocks: vi.fn(),
|
||||
moveBlock: vi.fn(),
|
||||
addTextBlock: vi.fn(),
|
||||
addButtonBlock: vi.fn(),
|
||||
addImageBlock: vi.fn(),
|
||||
addDividerBlock: vi.fn(),
|
||||
addListBlock: vi.fn(),
|
||||
addSectionBlock: vi.fn(),
|
||||
addFormBlock: vi.fn(),
|
||||
addFormInputBlock: vi.fn(),
|
||||
addFormSelectBlock: vi.fn(),
|
||||
addFormCheckboxBlock: vi.fn(),
|
||||
addFormRadioBlock: vi.fn(),
|
||||
addHeroTemplateSection: vi.fn(),
|
||||
addFeaturesTemplateSection: vi.fn(),
|
||||
addCtaTemplateSection: vi.fn(),
|
||||
addFaqTemplateSection: vi.fn(),
|
||||
addPricingTemplateSection: vi.fn(),
|
||||
addTestimonialsTemplateSection: vi.fn(),
|
||||
addBlogTemplateSection: vi.fn(),
|
||||
addTeamTemplateSection: vi.fn(),
|
||||
addFooterTemplateSection: vi.fn(),
|
||||
updateBlock: vi.fn(),
|
||||
updateProjectConfig: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
vi.mock("@/features/editor/state/editorStore", () => {
|
||||
const useEditorStore = (selector: (state: any) => any) => selector(mockState);
|
||||
(useEditorStore as any).getState = () => mockState;
|
||||
|
||||
return {
|
||||
__esModule: true,
|
||||
useEditorStore,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("next/link", () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
default: ({ href, children }: any) => <a href={href}>{children}</a>,
|
||||
};
|
||||
});
|
||||
|
||||
describe("EditorPage - 자동저장", () => {
|
||||
it("blocks 와 projectConfig 를 localStorage 의 pb:autosave:<slug> 키로 자동 저장해야 한다", () => {
|
||||
const storageProto = Object.getPrototypeOf(window.localStorage);
|
||||
const setItemSpy = vi.spyOn(storageProto, "setItem");
|
||||
|
||||
render(<EditorPage />);
|
||||
|
||||
expect(setItemSpy).toHaveBeenCalled();
|
||||
const [key, value] = setItemSpy.mock.calls[0] as [string, string];
|
||||
expect(key).toBe("pb:autosave:autosave-test");
|
||||
|
||||
const parsed = JSON.parse(value);
|
||||
expect(Array.isArray(parsed.blocks)).toBe(true);
|
||||
expect(parsed.blocks[0].id).toBe("blk_1");
|
||||
expect(parsed.projectConfig.slug).toBe("autosave-test");
|
||||
});
|
||||
|
||||
it("localStorage 에 autosave 스냅샷이 있으면 초기 렌더에서 replaceBlocks 와 updateProjectConfig 로 복원해야 한다", () => {
|
||||
const savedBlocks: Block[] = [
|
||||
{
|
||||
id: "saved_1",
|
||||
type: "text",
|
||||
props: {
|
||||
text: "저장된 블록",
|
||||
align: "left",
|
||||
size: "base",
|
||||
},
|
||||
} as any,
|
||||
];
|
||||
|
||||
const savedConfig: ProjectConfig = {
|
||||
title: "저장된 프로젝트",
|
||||
slug: "autosave-test",
|
||||
canvasPreset: "full",
|
||||
} as ProjectConfig;
|
||||
|
||||
const payload = {
|
||||
blocks: savedBlocks,
|
||||
projectConfig: savedConfig,
|
||||
};
|
||||
|
||||
window.localStorage.setItem("pb:autosave:autosave-test", JSON.stringify(payload));
|
||||
|
||||
render(<EditorPage />);
|
||||
|
||||
expect(mockState.replaceBlocks).toHaveBeenCalledTimes(1);
|
||||
expect(mockState.replaceBlocks).toHaveBeenCalledWith(savedBlocks);
|
||||
|
||||
expect(mockState.updateProjectConfig).toHaveBeenCalledTimes(1);
|
||||
expect(mockState.updateProjectConfig).toHaveBeenCalledWith(savedConfig);
|
||||
});
|
||||
});
|
||||
@@ -1,13 +1,11 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import { render, screen, cleanup, fireEvent, waitFor } from "@testing-library/react";
|
||||
import { render, screen, cleanup, fireEvent } from "@testing-library/react";
|
||||
import type { Block, ProjectConfig } from "@/features/editor/state/editorStore";
|
||||
import EditorPage from "@/app/editor/page";
|
||||
|
||||
// EditorPage Export 미리보기 UX TDD
|
||||
// - 상단 메뉴에 "Export 미리보기" 항목이 노출되는지 검증한다.
|
||||
// - Export 미리보기 모달에서 "미리보기 새로고침" 클릭 시
|
||||
// - /api/export/preview 엔드포인트를 호출하고,
|
||||
// - 반환된 HTML 을 iframe srcdoc 으로 렌더하는지 확인한다.
|
||||
// EditorPage 상단 메뉴 UX TDD
|
||||
// - Export 미리보기 기능 제거 후, 상단 메뉴에 더 이상 "Export 미리보기" 항목이 노출되지 않아야 한다.
|
||||
// - 대신 프로젝트 저장/불러오기와 연계된 "프로젝트 목록" 항목이 메뉴에 노출되어야 한다.
|
||||
|
||||
let mockState: any;
|
||||
|
||||
@@ -88,61 +86,17 @@ vi.mock("next/link", () => {
|
||||
};
|
||||
});
|
||||
|
||||
describe("EditorPage - Export 미리보기", () => {
|
||||
it("메뉴에서 Export 미리보기 항목을 클릭하면 Export 미리보기 모달이 열려야 한다", () => {
|
||||
describe("EditorPage - 상단 메뉴 (Export 미리보기 제거)", () => {
|
||||
it("메뉴를 열었을 때 'Export 미리보기' 항목은 보이지 않고, '프로젝트 목록' 항목이 노출되어야 한다", () => {
|
||||
render(<EditorPage />);
|
||||
|
||||
const menuButton = screen.getByText("메뉴");
|
||||
fireEvent.click(menuButton);
|
||||
|
||||
const previewMenuItem = screen.getByText("Export 미리보기");
|
||||
fireEvent.click(previewMenuItem);
|
||||
const removedItem = screen.queryByText("Export 미리보기");
|
||||
expect(removedItem).toBeNull();
|
||||
|
||||
const modalTitle = screen.getByText("Export 미리보기");
|
||||
expect(modalTitle).toBeTruthy();
|
||||
});
|
||||
|
||||
it("Export 미리보기 새로고침 시 /api/export/preview 를 호출하고 iframe 에 HTML 을 렌더해야 한다", async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(
|
||||
"<!DOCTYPE html><html><head><title>Export 미리보기 테스트</title></head><body>Export 미리보기 본문</body></html>",
|
||||
{
|
||||
status: 200,
|
||||
headers: { "Content-Type": "text/html; charset=utf-8" },
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
render(<EditorPage />);
|
||||
|
||||
const menuButton = screen.getByText("메뉴");
|
||||
fireEvent.click(menuButton);
|
||||
|
||||
const previewMenuItem = screen.getByText("Export 미리보기");
|
||||
fireEvent.click(previewMenuItem);
|
||||
|
||||
const refreshButton = screen.getByText("미리보기 새로고침");
|
||||
fireEvent.click(refreshButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
const [url, options] = fetchMock.mock.calls[0] as any;
|
||||
expect(url).toBe("/api/export/preview");
|
||||
expect(options.method).toBe("POST");
|
||||
expect(options.headers["Content-Type"]).toBe("application/json");
|
||||
|
||||
const parsed = JSON.parse(options.body);
|
||||
expect(Array.isArray(parsed.blocks)).toBe(true);
|
||||
expect(parsed.blocks[0].id).toBe("blk_text_1");
|
||||
expect(parsed.projectConfig.slug).toBe("export-preview-test");
|
||||
|
||||
const iframe = await screen.findByTestId("export-preview-frame");
|
||||
const srcDoc = iframe.getAttribute("srcdoc") ?? "";
|
||||
expect(srcDoc).toContain("Export 미리보기 테스트");
|
||||
expect(srcDoc).toContain("Export 미리보기 본문");
|
||||
const projectListItem = screen.getByText("프로젝트 목록");
|
||||
expect(projectListItem).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import { render, screen, fireEvent, cleanup, within } from "@testing-library/react";
|
||||
import EditorPage from "@/app/editor/page";
|
||||
import type { Block, ProjectConfig } from "@/features/editor/state/editorStore";
|
||||
|
||||
// EditorPage JSON 내보내기/불러오기 TDD
|
||||
// - "JSON 내보내기" 클릭 시 blocks + projectConfig 를 포함한 JSON 을 에디터 상태 영역에 출력해야 한다.
|
||||
// - "JSON 적용하기" 클릭 시 blocks + projectConfig 형태의 JSON 을 파싱해 replaceBlocks + updateProjectConfig 로 반영해야 한다.
|
||||
// - 잘못된 JSON 이거나 구조가 맞지 않으면 replaceBlocks / updateProjectConfig 를 호출하지 않아야 한다.
|
||||
|
||||
let mockState: any;
|
||||
|
||||
beforeEach(() => {
|
||||
const baseProjectConfig: ProjectConfig = {
|
||||
title: "JSON Export/Import 테스트",
|
||||
slug: "json-export-import-test",
|
||||
canvasPreset: "full",
|
||||
} as ProjectConfig;
|
||||
|
||||
mockState = {
|
||||
blocks: [
|
||||
{
|
||||
id: "blk_1",
|
||||
type: "text",
|
||||
props: {
|
||||
text: "JSON 테스트 블록",
|
||||
align: "left",
|
||||
size: "base",
|
||||
},
|
||||
},
|
||||
] as Block[],
|
||||
projectConfig: baseProjectConfig,
|
||||
selectedBlockId: null as string | null,
|
||||
selectedListItemId: null as string | null,
|
||||
// EditorPage 가 참조하는 액션들: 기본적으로 no-op mock 으로 채운다.
|
||||
undo: vi.fn(),
|
||||
redo: vi.fn(),
|
||||
removeBlock: vi.fn(),
|
||||
duplicateBlock: vi.fn(),
|
||||
selectBlock: vi.fn(),
|
||||
selectListItem: vi.fn(),
|
||||
replaceBlocks: vi.fn(),
|
||||
reorderBlocks: vi.fn(),
|
||||
moveBlock: vi.fn(),
|
||||
addTextBlock: vi.fn(),
|
||||
addButtonBlock: vi.fn(),
|
||||
addImageBlock: vi.fn(),
|
||||
addDividerBlock: vi.fn(),
|
||||
addListBlock: vi.fn(),
|
||||
addSectionBlock: vi.fn(),
|
||||
addFormBlock: vi.fn(),
|
||||
addFormInputBlock: vi.fn(),
|
||||
addFormSelectBlock: vi.fn(),
|
||||
addFormCheckboxBlock: vi.fn(),
|
||||
addFormRadioBlock: vi.fn(),
|
||||
addHeroTemplateSection: vi.fn(),
|
||||
addFeaturesTemplateSection: vi.fn(),
|
||||
addCtaTemplateSection: vi.fn(),
|
||||
addFaqTemplateSection: vi.fn(),
|
||||
addPricingTemplateSection: vi.fn(),
|
||||
addTestimonialsTemplateSection: vi.fn(),
|
||||
addBlogTemplateSection: vi.fn(),
|
||||
addTeamTemplateSection: vi.fn(),
|
||||
addFooterTemplateSection: vi.fn(),
|
||||
updateBlock: vi.fn(),
|
||||
updateProjectConfig: vi.fn(),
|
||||
};
|
||||
|
||||
window.localStorage.clear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
vi.mock("@/features/editor/state/editorStore", () => {
|
||||
const useEditorStore = (selector: (state: any) => any) => selector(mockState);
|
||||
(useEditorStore as any).getState = () => mockState;
|
||||
|
||||
return {
|
||||
__esModule: true,
|
||||
useEditorStore,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("next/link", () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
default: ({ href, children }: any) => <a href={href}>{children}</a>,
|
||||
};
|
||||
});
|
||||
|
||||
function openJsonModal() {
|
||||
const menuButton = screen.getByText("메뉴");
|
||||
fireEvent.click(menuButton);
|
||||
|
||||
const jsonMenuItem = screen.getByText("JSON 내보내기/불러오기");
|
||||
fireEvent.click(jsonMenuItem);
|
||||
|
||||
const modalTitle = screen.getByText("JSON Export / Import");
|
||||
const jsonModal = (modalTitle.parentElement?.parentElement ?? document.body) as HTMLElement;
|
||||
return jsonModal;
|
||||
}
|
||||
|
||||
describe("EditorPage - JSON 내보내기/불러오기", () => {
|
||||
it("JSON 내보내기 클릭 시 blocks + projectConfig 구조의 JSON 이 에디터 상태 영역에 출력되어야 한다", () => {
|
||||
render(<EditorPage />);
|
||||
|
||||
const jsonModal = openJsonModal();
|
||||
|
||||
const exportLabel = within(jsonModal).getByText("에디터 상태 JSON").closest("label") as HTMLElement;
|
||||
const exportTextarea = within(exportLabel).getByRole("textbox") as HTMLTextAreaElement;
|
||||
|
||||
// 초기에는 비어 있어야 한다.
|
||||
expect(exportTextarea.value).toBe("");
|
||||
|
||||
const exportButton = within(jsonModal).getByText("JSON 내보내기");
|
||||
fireEvent.click(exportButton);
|
||||
|
||||
const value = exportTextarea.value;
|
||||
expect(value.trim().length).toBeGreaterThan(0);
|
||||
|
||||
const parsed = JSON.parse(value);
|
||||
expect(Array.isArray(parsed.blocks)).toBe(true);
|
||||
expect(parsed.blocks[0].id).toBe("blk_1");
|
||||
expect(parsed.projectConfig.slug).toBe("json-export-import-test");
|
||||
});
|
||||
|
||||
it("JSON 적용하기 클릭 시 blocks + projectConfig 구조의 JSON 으로 replaceBlocks 와 updateProjectConfig 를 호출해야 한다", () => {
|
||||
render(<EditorPage />);
|
||||
|
||||
const jsonModal = openJsonModal();
|
||||
|
||||
const importLabel = within(jsonModal).getByText("JSON에서 불러오기").closest("label") as HTMLElement;
|
||||
const importTextarea = within(importLabel).getByRole("textbox") as HTMLTextAreaElement;
|
||||
|
||||
const importedBlocks: Block[] = [
|
||||
{
|
||||
id: "imported_1",
|
||||
type: "text",
|
||||
props: {
|
||||
text: "가져온 블록",
|
||||
align: "center",
|
||||
size: "lg",
|
||||
},
|
||||
} as any,
|
||||
];
|
||||
|
||||
const importedConfig: ProjectConfig = {
|
||||
title: "가져온 프로젝트",
|
||||
slug: "imported-slug",
|
||||
canvasPreset: "full",
|
||||
} as ProjectConfig;
|
||||
|
||||
const payload = {
|
||||
blocks: importedBlocks,
|
||||
projectConfig: importedConfig,
|
||||
};
|
||||
|
||||
fireEvent.change(importTextarea, { target: { value: JSON.stringify(payload) } });
|
||||
|
||||
// 초기 마운트 시 호출됐을 수 있는 기록은 무시한다.
|
||||
mockState.replaceBlocks.mockReset();
|
||||
mockState.updateProjectConfig.mockReset();
|
||||
|
||||
const applyButton = within(importLabel).getByText("JSON 적용하기");
|
||||
fireEvent.click(applyButton);
|
||||
|
||||
expect(mockState.replaceBlocks).toHaveBeenCalledTimes(1);
|
||||
expect(mockState.replaceBlocks).toHaveBeenCalledWith(importedBlocks as any);
|
||||
|
||||
expect(mockState.updateProjectConfig).toHaveBeenCalledTimes(1);
|
||||
expect(mockState.updateProjectConfig).toHaveBeenCalledWith(importedConfig as ProjectConfig);
|
||||
});
|
||||
|
||||
it("잘못된 JSON 은 무시되어야 하며 replaceBlocks/updateProjectConfig 를 호출하면 안 된다", () => {
|
||||
render(<EditorPage />);
|
||||
|
||||
const jsonModal = openJsonModal();
|
||||
|
||||
const importLabel = within(jsonModal).getByText("JSON에서 불러오기").closest("label") as HTMLElement;
|
||||
const importTextarea = within(importLabel).getByRole("textbox") as HTMLTextAreaElement;
|
||||
|
||||
fireEvent.change(importTextarea, { target: { value: "{ this-is: not-valid-json" } });
|
||||
|
||||
mockState.replaceBlocks.mockReset();
|
||||
mockState.updateProjectConfig.mockReset();
|
||||
|
||||
const applyButton = within(importLabel).getByText("JSON 적용하기");
|
||||
fireEvent.click(applyButton);
|
||||
|
||||
expect(mockState.replaceBlocks).not.toHaveBeenCalled();
|
||||
expect(mockState.updateProjectConfig).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,308 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import { render, screen, fireEvent, cleanup, waitFor, within } from "@testing-library/react";
|
||||
import EditorPage from "@/app/editor/page";
|
||||
import type { Block, ProjectConfig } from "@/features/editor/state/editorStore";
|
||||
|
||||
// EditorPage 프로젝트 저장/불러오기 UX TDD
|
||||
// - 프로젝트 저장 버튼 클릭 시 현재 blocks 를 로컬스토리지(pb:project:<slug>)와 서버(/api/projects) 모두에 저장해야 한다.
|
||||
// - 불러오기 버튼 클릭 시 로컬스토리지에 프로젝트가 있으면 서버를 호출하지 않고 로컬 데이터로 replaceBlocks 해야 한다.
|
||||
// - 로컬스토리지에 없으면 /api/projects/[slug] 를 호출해 서버에서 프로젝트를 불러와 replaceBlocks 해야 한다.
|
||||
|
||||
let mockState: any;
|
||||
|
||||
beforeEach(() => {
|
||||
const baseProjectConfig: ProjectConfig = {
|
||||
title: "프로젝트 저장/불러오기 테스트",
|
||||
slug: "editor-project-test",
|
||||
canvasPreset: "full",
|
||||
} as ProjectConfig;
|
||||
|
||||
mockState = {
|
||||
blocks: [
|
||||
{
|
||||
id: "blk_1",
|
||||
type: "text",
|
||||
props: {
|
||||
text: "프로젝트 저장/불러오기 테스트 블록",
|
||||
align: "left",
|
||||
size: "base",
|
||||
},
|
||||
},
|
||||
] as Block[],
|
||||
projectConfig: baseProjectConfig,
|
||||
selectedBlockId: null as string | null,
|
||||
selectedListItemId: null as string | null,
|
||||
// EditorPage 가 참조하는 액션들: 기본적으로 no-op mock 으로 채운다.
|
||||
undo: vi.fn(),
|
||||
redo: vi.fn(),
|
||||
removeBlock: vi.fn(),
|
||||
duplicateBlock: vi.fn(),
|
||||
selectBlock: vi.fn(),
|
||||
selectListItem: vi.fn(),
|
||||
replaceBlocks: vi.fn(),
|
||||
reorderBlocks: vi.fn(),
|
||||
moveBlock: vi.fn(),
|
||||
addTextBlock: vi.fn(),
|
||||
addButtonBlock: vi.fn(),
|
||||
addImageBlock: vi.fn(),
|
||||
addDividerBlock: vi.fn(),
|
||||
addListBlock: vi.fn(),
|
||||
addSectionBlock: vi.fn(),
|
||||
addFormBlock: vi.fn(),
|
||||
addFormInputBlock: vi.fn(),
|
||||
addFormSelectBlock: vi.fn(),
|
||||
addFormCheckboxBlock: vi.fn(),
|
||||
addFormRadioBlock: vi.fn(),
|
||||
addHeroTemplateSection: vi.fn(),
|
||||
addFeaturesTemplateSection: vi.fn(),
|
||||
addCtaTemplateSection: vi.fn(),
|
||||
addFaqTemplateSection: vi.fn(),
|
||||
addPricingTemplateSection: vi.fn(),
|
||||
addTestimonialsTemplateSection: vi.fn(),
|
||||
addBlogTemplateSection: vi.fn(),
|
||||
addTeamTemplateSection: vi.fn(),
|
||||
addFooterTemplateSection: vi.fn(),
|
||||
updateBlock: vi.fn(),
|
||||
updateProjectConfig: vi.fn((partial: Partial<ProjectConfig>) => {
|
||||
mockState.projectConfig = {
|
||||
...(mockState.projectConfig as ProjectConfig),
|
||||
...partial,
|
||||
} as ProjectConfig;
|
||||
}),
|
||||
};
|
||||
|
||||
window.localStorage.clear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
vi.mock("@/features/editor/state/editorStore", () => {
|
||||
const useEditorStore = (selector: (state: any) => any) => selector(mockState);
|
||||
(useEditorStore as any).getState = () => mockState;
|
||||
|
||||
return {
|
||||
__esModule: true,
|
||||
useEditorStore,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("next/link", () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
default: ({ href, children }: any) => <a href={href}>{children}</a>,
|
||||
};
|
||||
});
|
||||
|
||||
describe("EditorPage - 프로젝트 저장/불러오기", () => {
|
||||
it("프로젝트 저장 버튼 클릭 시 로컬스토리지와 서버에 상태를 저장해야 하고, projectConfig 와 제목/주소 입력이 동기화되어야 한다", async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
status: 201,
|
||||
json: async () => ({ slug: "save-test-slug" }),
|
||||
} as any);
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const { rerender } = render(<EditorPage />);
|
||||
|
||||
const menuButton = screen.getByText("메뉴");
|
||||
fireEvent.click(menuButton);
|
||||
|
||||
const projectMenuItem = screen.getByText("프로젝트 저장/불러오기");
|
||||
fireEvent.click(projectMenuItem);
|
||||
|
||||
const modalTitle = screen.getByText("프로젝트 저장 / 불러오기");
|
||||
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;
|
||||
|
||||
expect(titleInput.value).toBe("프로젝트 저장/불러오기 테스트");
|
||||
expect(slugInput.value).toBe("editor-project-test");
|
||||
|
||||
fireEvent.change(titleInput, { target: { value: "저장 테스트 프로젝트" } });
|
||||
fireEvent.change(slugInput, { target: { value: "save-test-slug" } });
|
||||
|
||||
expect(mockState.projectConfig.title).toBe("저장 테스트 프로젝트");
|
||||
expect(mockState.projectConfig.slug).toBe("save-test-slug");
|
||||
|
||||
rerender(<EditorPage />);
|
||||
|
||||
const saveButton = screen.getByText("저장 (로컬 + 서버)");
|
||||
fireEvent.click(saveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
const localKey = "pb:project:save-test-slug";
|
||||
const raw = window.localStorage.getItem(localKey);
|
||||
expect(raw).not.toBeNull();
|
||||
|
||||
const parsed = JSON.parse(raw as string);
|
||||
expect(parsed.title).toBe("저장 테스트 프로젝트");
|
||||
expect(Array.isArray(parsed.contentJson)).toBe(true);
|
||||
expect(parsed.contentJson[0].id).toBe("blk_1");
|
||||
|
||||
const [url, options] = fetchMock.mock.calls[0] as any;
|
||||
expect(url).toBe("/api/projects");
|
||||
expect(options.method).toBe("POST");
|
||||
expect(options.headers["Content-Type"]).toBe("application/json");
|
||||
|
||||
const body = JSON.parse(options.body);
|
||||
expect(body.slug).toBe("save-test-slug");
|
||||
expect(body.title).toBe("저장 테스트 프로젝트");
|
||||
expect(Array.isArray(body.contentJson)).toBe(true);
|
||||
expect(body.contentJson[0].id).toBe("blk_1");
|
||||
|
||||
const message = await screen.findByText("프로젝트가 저장되었습니다: save-test-slug");
|
||||
expect(message).toBeTruthy();
|
||||
});
|
||||
|
||||
it("로컬스토리지에 프로젝트가 있으면 서버를 호출하지 않고 로컬 데이터로 불러와야 한다", async () => {
|
||||
const slug = "local-test-slug";
|
||||
const localKey = `pb:project:${slug}`;
|
||||
|
||||
const savedBlocks: Block[] = [
|
||||
{
|
||||
id: "saved_1",
|
||||
type: "text",
|
||||
props: {
|
||||
text: "로컬 저장된 블록",
|
||||
align: "left",
|
||||
size: "base",
|
||||
},
|
||||
} as any,
|
||||
];
|
||||
|
||||
window.localStorage.setItem(
|
||||
localKey,
|
||||
JSON.stringify({
|
||||
title: "로컬 프로젝트",
|
||||
contentJson: savedBlocks,
|
||||
}),
|
||||
);
|
||||
|
||||
const fetchMock = vi.fn();
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
mockState.projectConfig.slug = slug;
|
||||
|
||||
render(<EditorPage />);
|
||||
|
||||
const menuButton = screen.getByText("메뉴");
|
||||
fireEvent.click(menuButton);
|
||||
|
||||
const projectMenuItem = screen.getByText("프로젝트 저장/불러오기");
|
||||
fireEvent.click(projectMenuItem);
|
||||
|
||||
const modalTitle = screen.getByText("프로젝트 저장 / 불러오기");
|
||||
const projectModal = (modalTitle.parentElement?.parentElement ?? document.body) as HTMLElement;
|
||||
|
||||
const loadButton = screen.getByText("불러오기");
|
||||
fireEvent.click(loadButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockState.replaceBlocks).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
expect(mockState.replaceBlocks).toHaveBeenCalledWith(savedBlocks as any);
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
|
||||
const message = await screen.findByText(`로컬에서 프로젝트를 불러왔습니다: ${slug}`);
|
||||
expect(message).toBeTruthy();
|
||||
});
|
||||
|
||||
it("로컬스토리지에 없으면 서버에서 프로젝트를 불러와야 한다", async () => {
|
||||
const slug = "server-test-slug";
|
||||
|
||||
const serverBlocks: Block[] = [
|
||||
{
|
||||
id: "server_1",
|
||||
type: "text",
|
||||
props: {
|
||||
text: "서버에서 불러온 블록",
|
||||
align: "left",
|
||||
size: "base",
|
||||
},
|
||||
} as any,
|
||||
];
|
||||
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ slug, contentJson: serverBlocks }),
|
||||
} as any);
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
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(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
const [url] = fetchMock.mock.calls[0] as any;
|
||||
expect(url).toBe(`/api/projects/${slug}`);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockState.replaceBlocks).toHaveBeenCalledWith(serverBlocks as any);
|
||||
});
|
||||
|
||||
const message = await screen.findByText(`프로젝트를 불러왔습니다: ${slug}`);
|
||||
expect(message).toBeTruthy();
|
||||
});
|
||||
|
||||
it("에디터 메뉴에서 프로젝트 삭제를 선택하면 서버에 DELETE 요청을 보내고 로컬 저장/자동저장 상태를 정리한 뒤 목록으로 이동해야 한다", async () => {
|
||||
const slug = "delete-test-slug";
|
||||
mockState.projectConfig.slug = slug;
|
||||
|
||||
// 로컬 저장/자동저장 스냅샷이 존재하는 상황을 준비한다.
|
||||
window.localStorage.setItem(`pb:project:${slug}`, "stored-project");
|
||||
window.localStorage.setItem(`pb:autosave:${slug}`, "stored-autosave");
|
||||
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
} as any);
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(true as any);
|
||||
|
||||
render(<EditorPage />);
|
||||
|
||||
const menuButton = screen.getByText("메뉴");
|
||||
fireEvent.click(menuButton);
|
||||
|
||||
const deleteMenuItem = screen.getByText("프로젝트 삭제");
|
||||
fireEvent.click(deleteMenuItem);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
const [url, options] = fetchMock.mock.calls[0] as any;
|
||||
expect(url).toBe(`/api/projects/${slug}`);
|
||||
expect(options.method).toBe("DELETE");
|
||||
|
||||
expect(window.localStorage.getItem(`pb:project:${slug}`)).toBeNull();
|
||||
expect(window.localStorage.getItem(`pb:autosave:${slug}`)).toBeNull();
|
||||
|
||||
confirmSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import { render, cleanup } from "@testing-library/react";
|
||||
import PreviewPage from "@/app/preview/page";
|
||||
import type { Block, ProjectConfig } from "@/features/editor/state/editorStore";
|
||||
|
||||
// PreviewPage 자동 복원 TDD
|
||||
// - localStorage 의 pb:autosave:<slug> 스냅샷을 기반으로 초기 렌더에서 replaceBlocks + updateProjectConfig 로 상태를 복원해야 한다.
|
||||
|
||||
let mockState: any;
|
||||
|
||||
beforeEach(() => {
|
||||
const baseProjectConfig: ProjectConfig = {
|
||||
title: "프리뷰 자동복원 테스트",
|
||||
slug: "preview-autosave-test",
|
||||
canvasPreset: "full",
|
||||
} as ProjectConfig;
|
||||
|
||||
mockState = {
|
||||
blocks: [
|
||||
{
|
||||
id: "blk_1",
|
||||
type: "text",
|
||||
props: {
|
||||
text: "프리뷰 기본 블록",
|
||||
align: "left",
|
||||
size: "base",
|
||||
},
|
||||
},
|
||||
] as Block[],
|
||||
projectConfig: baseProjectConfig,
|
||||
replaceBlocks: vi.fn(),
|
||||
updateProjectConfig: vi.fn(),
|
||||
};
|
||||
|
||||
window.localStorage.clear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
vi.mock("@/features/editor/state/editorStore", () => {
|
||||
const useEditorStore = (selector: (state: any) => any) => selector(mockState);
|
||||
(useEditorStore as any).getState = () => mockState;
|
||||
|
||||
return {
|
||||
__esModule: true,
|
||||
useEditorStore,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("next/link", () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
default: ({ href, children }: any) => <a href={href}>{children}</a>,
|
||||
};
|
||||
});
|
||||
|
||||
describe("PreviewPage - 자동 복원", () => {
|
||||
it("pb:autosave:<slug> 스냅샷이 있으면 초기 렌더에서 replaceBlocks 와 updateProjectConfig 로 상태를 복원해야 한다", () => {
|
||||
const savedBlocks: Block[] = [
|
||||
{
|
||||
id: "saved_1",
|
||||
type: "text",
|
||||
props: {
|
||||
text: "저장된 프리뷰 블록",
|
||||
align: "center",
|
||||
size: "lg",
|
||||
},
|
||||
} as any,
|
||||
];
|
||||
|
||||
const savedConfig: ProjectConfig = {
|
||||
title: "저장된 프리뷰 프로젝트",
|
||||
slug: "preview-autosave-test",
|
||||
canvasPreset: "full",
|
||||
} as ProjectConfig;
|
||||
|
||||
const payload = {
|
||||
blocks: savedBlocks,
|
||||
projectConfig: savedConfig,
|
||||
};
|
||||
|
||||
window.localStorage.setItem("pb:autosave:preview-autosave-test", JSON.stringify(payload));
|
||||
|
||||
render(<PreviewPage />);
|
||||
|
||||
expect(mockState.replaceBlocks).toHaveBeenCalledTimes(1);
|
||||
expect(mockState.replaceBlocks).toHaveBeenCalledWith(savedBlocks as any);
|
||||
|
||||
expect(mockState.updateProjectConfig).toHaveBeenCalledTimes(1);
|
||||
expect(mockState.updateProjectConfig).toHaveBeenCalledWith(savedConfig as ProjectConfig);
|
||||
});
|
||||
});
|
||||
@@ -53,6 +53,14 @@ vi.mock("next/link", () => {
|
||||
});
|
||||
|
||||
describe("PreviewPage - ZIP Export", () => {
|
||||
it("프리뷰 헤더에 프로젝트 목록 링크가 노출되고 /projects 로 연결되어야 한다", () => {
|
||||
render(<PreviewPage />);
|
||||
|
||||
const link = screen.getByText("프로젝트 목록");
|
||||
expect(link).toBeTruthy();
|
||||
expect(link.closest("a")?.getAttribute("href")).toBe("/projects");
|
||||
});
|
||||
|
||||
it("프리뷰 헤더에 ZIP Export 버튼이 노출되어야 한다", () => {
|
||||
render(<PreviewPage />);
|
||||
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
import { describe, it, expect, afterEach, vi } from "vitest";
|
||||
import { render, screen, cleanup, waitFor, fireEvent } from "@testing-library/react";
|
||||
import ProjectsPage from "@/app/projects/page";
|
||||
|
||||
// ProjectsPage 프로젝트 목록 TDD
|
||||
// - 마운트 시 /api/projects 로 GET 요청을 보내고, 반환된 프로젝트 목록을 테이블로 렌더링해야 한다.
|
||||
// - 각 프로젝트 행에는 /editor?slug=..., /preview?slug=... 로 이동하는 "편집" / "미리보기" 링크가 있어야 한다.
|
||||
// - 각 프로젝트 행에는 프로젝트를 삭제하는 "삭제" 버튼이 있고, 클릭 시 서버에 DELETE 요청을 보내고 목록/로컬스토리지를 갱신해야 한다.
|
||||
// - 체크박스로 여러 프로젝트를 선택한 뒤 상단의 "선택 삭제" 버튼으로 일괄 삭제할 수 있어야 한다.
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe("ProjectsPage - 프로젝트 목록", () => {
|
||||
it("/api/projects 로부터 받은 프로젝트 목록을 제목/slug 기준으로 렌더링하고, 편집/미리보기 링크를 노출해야 한다", async () => {
|
||||
const projects = [
|
||||
{
|
||||
id: "1",
|
||||
title: "테스트 프로젝트 A",
|
||||
slug: "test-project-a",
|
||||
status: "DRAFT",
|
||||
createdAt: "2025-01-01T00:00:00.000Z",
|
||||
updatedAt: "2025-01-01T00:00:00.000Z",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
title: "테스트 프로젝트 B",
|
||||
slug: "test-project-b",
|
||||
status: "PUBLISHED",
|
||||
createdAt: "2025-01-02T00:00:00.000Z",
|
||||
updatedAt: "2025-01-02T00:00:00.000Z",
|
||||
},
|
||||
];
|
||||
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify(projects), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
);
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
render(<ProjectsPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
const [url, options] = fetchMock.mock.calls[0] as any;
|
||||
expect(url).toBe("/api/projects");
|
||||
expect(options).toBeUndefined();
|
||||
|
||||
expect(await screen.findByText("테스트 프로젝트 A")).toBeTruthy();
|
||||
expect(screen.getByText("test-project-a")).toBeTruthy();
|
||||
|
||||
expect(await screen.findByText("테스트 프로젝트 B")).toBeTruthy();
|
||||
expect(screen.getByText("test-project-b")).toBeTruthy();
|
||||
|
||||
const editLinks = screen.getAllByText("편집");
|
||||
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("미리보기");
|
||||
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");
|
||||
});
|
||||
|
||||
it("삭제 버튼 클릭 시 서버에 DELETE 요청을 보내고 목록과 로컬스토리지를 갱신해야 한다", async () => {
|
||||
const projects = [
|
||||
{
|
||||
id: "1",
|
||||
title: "테스트 프로젝트 A",
|
||||
slug: "test-project-a",
|
||||
status: "DRAFT",
|
||||
createdAt: "2025-01-01T00:00:00.000Z",
|
||||
updatedAt: "2025-01-01T00:00:00.000Z",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
title: "테스트 프로젝트 B",
|
||||
slug: "test-project-b",
|
||||
status: "PUBLISHED",
|
||||
createdAt: "2025-01-02T00:00:00.000Z",
|
||||
updatedAt: "2025-01-02T00:00:00.000Z",
|
||||
},
|
||||
];
|
||||
|
||||
window.localStorage.setItem("pb:project:test-project-a", "stored-project");
|
||||
window.localStorage.setItem("pb:autosave:test-project-a", "stored-autosave");
|
||||
|
||||
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/projects" && method === "GET") {
|
||||
return Promise.resolve(
|
||||
new Response(JSON.stringify(projects), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (url === "/api/projects/test-project-a" && method === "DELETE") {
|
||||
return Promise.resolve(new Response(null, { status: 200 }));
|
||||
}
|
||||
|
||||
return Promise.resolve(new Response(null, { status: 500 }));
|
||||
});
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(true as any);
|
||||
|
||||
render(<ProjectsPage />);
|
||||
|
||||
expect(await screen.findByText("테스트 프로젝트 A")).toBeTruthy();
|
||||
|
||||
const deleteButtons = screen.getAllByText("삭제");
|
||||
expect(deleteButtons.length).toBeGreaterThan(0);
|
||||
|
||||
deleteButtons[0].click();
|
||||
|
||||
await waitFor(() => {
|
||||
const deleteCall = fetchMock.mock.calls.find(([url, options]) => {
|
||||
return url === "/api/projects/test-project-a" && options?.method === "DELETE";
|
||||
});
|
||||
|
||||
expect(deleteCall).toBeDefined();
|
||||
});
|
||||
|
||||
expect(screen.queryByText("테스트 프로젝트 A")).toBeNull();
|
||||
expect(window.localStorage.getItem("pb:project:test-project-a")).toBeNull();
|
||||
expect(window.localStorage.getItem("pb:autosave:test-project-a")).toBeNull();
|
||||
|
||||
confirmSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("체크박스로 여러 프로젝트를 선택한 뒤 '선택 삭제' 버튼으로 일괄 삭제할 수 있어야 한다", async () => {
|
||||
const projects = [
|
||||
{
|
||||
id: "1",
|
||||
title: "테스트 프로젝트 A",
|
||||
slug: "test-project-a",
|
||||
status: "DRAFT",
|
||||
createdAt: "2025-01-01T00:00:00.000Z",
|
||||
updatedAt: "2025-01-01T00:00:00.000Z",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
title: "테스트 프로젝트 B",
|
||||
slug: "test-project-b",
|
||||
status: "PUBLISHED",
|
||||
createdAt: "2025-01-02T00:00:00.000Z",
|
||||
updatedAt: "2025-01-02T00:00:00.000Z",
|
||||
},
|
||||
];
|
||||
|
||||
window.localStorage.setItem("pb:project:test-project-a", "stored-project-a");
|
||||
window.localStorage.setItem("pb:autosave:test-project-a", "stored-autosave-a");
|
||||
window.localStorage.setItem("pb:project:test-project-b", "stored-project-b");
|
||||
window.localStorage.setItem("pb:autosave:test-project-b", "stored-autosave-b");
|
||||
|
||||
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/projects" && method === "GET") {
|
||||
return Promise.resolve(
|
||||
new Response(JSON.stringify(projects), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (url === "/api/projects/test-project-a" && method === "DELETE") {
|
||||
return Promise.resolve(new Response(null, { status: 200 }));
|
||||
}
|
||||
|
||||
if (url === "/api/projects/test-project-b" && method === "DELETE") {
|
||||
return Promise.resolve(new Response(null, { status: 200 }));
|
||||
}
|
||||
|
||||
return Promise.resolve(new Response(null, { status: 500 }));
|
||||
});
|
||||
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
const confirmSpy = vi.spyOn(window, "confirm").mockReturnValue(true as any);
|
||||
|
||||
render(<ProjectsPage />);
|
||||
|
||||
expect(await screen.findByText("테스트 프로젝트 A")).toBeTruthy();
|
||||
expect(await screen.findByText("테스트 프로젝트 B")).toBeTruthy();
|
||||
|
||||
const checkboxA = screen.getByLabelText("테스트 프로젝트 A 선택");
|
||||
const checkboxB = screen.getByLabelText("테스트 프로젝트 B 선택");
|
||||
|
||||
fireEvent.click(checkboxA);
|
||||
fireEvent.click(checkboxB);
|
||||
|
||||
const bulkDeleteButton = screen.getByText("선택 삭제");
|
||||
fireEvent.click(bulkDeleteButton);
|
||||
|
||||
await waitFor(() => {
|
||||
const deleteCalls = fetchMock.mock.calls.filter(([, options]) => options?.method === "DELETE");
|
||||
const hasA = deleteCalls.some(([url]) => url === "/api/projects/test-project-a");
|
||||
const hasB = deleteCalls.some(([url]) => url === "/api/projects/test-project-b");
|
||||
|
||||
expect(hasA).toBe(true);
|
||||
expect(hasB).toBe(true);
|
||||
});
|
||||
|
||||
expect(screen.queryByText("테스트 프로젝트 A")).toBeNull();
|
||||
expect(screen.queryByText("테스트 프로젝트 B")).toBeNull();
|
||||
|
||||
expect(window.localStorage.getItem("pb:project:test-project-a")).toBeNull();
|
||||
expect(window.localStorage.getItem("pb:autosave:test-project-a")).toBeNull();
|
||||
expect(window.localStorage.getItem("pb:project:test-project-b")).toBeNull();
|
||||
expect(window.localStorage.getItem("pb:autosave:test-project-b")).toBeNull();
|
||||
|
||||
confirmSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user