import "@testing-library/jest-dom/vitest" import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react" import { afterEach, beforeEach, describe, expect, it } from "vitest" import { App } from "../src/app/App" import { defaultDatasetId, defaultDocumentId, defaultResourcePath, getCurrentDocument, getCurrentRenderResult, setProjectPersistenceForTests, useWorkbenchStore } from "../src/app/store/workbenchStore" describe("workbench UI shell", () => { beforeEach(() => { setProjectPersistenceForTests(null) useWorkbenchStore.getState().resetDemo() }) afterEach(() => { cleanup() }) it("starts with the demo entry template and dataset selected", () => { const state = useWorkbenchStore.getState() expect(state.selectedDocumentId).toBe(defaultDocumentId) expect(state.currentDatasetId).toBe(defaultDatasetId) expect(state.selectedResourcePath).toBe(defaultResourcePath) }) it("renders the resource tree from the project snapshot", () => { render() expect(screen.getByTestId("resource-panel")).toHaveTextContent("main.json") expect(screen.getByTestId("resource-panel")).toHaveTextContent("common-footer.json") expect(screen.getByTestId("resource-panel")).toHaveTextContent("default.json") }) it("switches inspector tabs", () => { render() fireEvent.click(screen.getByRole("button", { name: "预览" })) expect(useWorkbenchStore.getState().rightPanelTab).toBe("preview") expect(screen.getByTestId("preview-tab")).toHaveTextContent("张三") fireEvent.click(screen.getByRole("button", { name: "引用" })) expect(useWorkbenchStore.getState().rightPanelTab).toBe("references") expect(screen.getByTestId("references-tab")).toHaveTextContent("missing fragment_signature") }) it("generates preview output from the existing renderer", () => { const state = useWorkbenchStore.getState() const result = getCurrentRenderResult(state) expect(result.output).toContain("张三") expect(result.output).toContain("- 产品 A: 99") expect(result.errors).toContainEqual( expect.objectContaining({ code: "MISSING_FRAGMENT", fragmentId: "fragment_signature" }) ) }) it("shows missing reference status", () => { render() expect(screen.getByText("missing refs: 1")).toBeInTheDocument() }) it("opens a project through the persistence adapter", async () => { const diskSnapshot = { ...useWorkbenchStore.getState().snapshot, project: { ...useWorkbenchStore.getState().snapshot.project, name: "Disk Project", paths: { ...useWorkbenchStore.getState().snapshot.project.paths, root: "D:/DiskProject" } } } setProjectPersistenceForTests({ isAvailable: () => true, openProjectFromDisk: async () => diskSnapshot, saveProjectChanges: async () => diskSnapshot }) useWorkbenchStore.getState().resetDemo() render() fireEvent.click(screen.getByRole("button", { name: "Open project" })) await waitFor(() => { const state = useWorkbenchStore.getState() expect(state.projectOpenStatus).toBe("open") expect(state.snapshot.project.name).toBe("Disk Project") expect(state.dirtyDocumentIds).toEqual([]) }) expect(screen.getByText("Disk Project")).toBeInTheDocument() }) it("creates a fragment from the resource panel and selects it", () => { render() fireEvent.click(screen.getByRole("button", { name: "Create fragment" })) const state = useWorkbenchStore.getState() expect(state.snapshot.fragments.fragment_2).toMatchObject({ kind: "fragment", name: "Fragment 2" }) expect(state.selectedDocumentId).toBe("fragment_2") expect(state.selectedResourcePath).toBe("fragments/fragment-2.json") expect(state.saveStatus).toBe("dirty") expect(screen.getByTestId("resource-panel")).toHaveTextContent("fragment-2.json") }) it("saves dirty fragment changes through the persistence adapter", async () => { const savedDirtyIds: string[][] = [] setProjectPersistenceForTests({ isAvailable: () => true, openProjectFromDisk: async () => useWorkbenchStore.getState().snapshot, saveProjectChanges: async (snapshot, dirtyDocumentIds) => { savedDirtyIds.push(dirtyDocumentIds) return snapshot } }) useWorkbenchStore.getState().resetDemo() render() fireEvent.click(screen.getByRole("button", { name: "Create fragment" })) fireEvent.click(screen.getByRole("button", { name: "Save project" })) await waitFor(() => { const state = useWorkbenchStore.getState() expect(savedDirtyIds).toEqual([["fragment_2"]]) expect(state.saveStatus).toBe("saved") expect(state.dirtyDocumentIds).toEqual([]) }) }) it("shows persistence save errors", async () => { setProjectPersistenceForTests({ isAvailable: () => true, openProjectFromDisk: async () => useWorkbenchStore.getState().snapshot, saveProjectChanges: async () => { throw new Error("disk full") } }) useWorkbenchStore.getState().resetDemo() render() fireEvent.click(screen.getByRole("button", { name: "Create fragment" })) fireEvent.click(screen.getByRole("button", { name: "Save project" })) await waitFor(() => { const state = useWorkbenchStore.getState() expect(state.saveStatus).toBe("error") expect(state.lastSaveError).toBe("disk full") }) expect(screen.getByText("disk full")).toBeInTheDocument() }) it("opens a referenced fragment from the references panel", () => { useWorkbenchStore.getState().setRightPanelTab("references") render() fireEvent.click(screen.getByRole("button", { name: "Open fragment_common_footer" })) const state = useWorkbenchStore.getState() expect(state.selectedDocumentId).toBe("fragment_common_footer") expect(state.selectedResourcePath).toBe("fragments/common-footer.json") expect(getCurrentDocument(state).kind).toBe("fragment") }) it("expands the selected fragment reference into editable blocks", async () => { useWorkbenchStore.getState().setRightPanelTab("references") useWorkbenchStore.getState().setSelectedBlockId("block_footer") render() fireEvent.click(screen.getByRole("button", { name: "Expand ref" })) await waitFor(() => { const state = useWorkbenchStore.getState() const outgoing = state.snapshot.referenceIndex.outgoing.template_main?.map((item) => item.toFragmentId) ?? [] expect(outgoing).not.toContain("fragment_common_footer") expect(JSON.stringify(state.snapshot.templates.template_main?.children)).not.toContain("block_footer") expect(state.saveStatus).toBe("dirty") expect(screen.getByTestId("blockflow-editor")).not.toHaveTextContent("fragment_common_footer") }) }) it("updates the current dataset from valid JSON and refreshes preview data", async () => { useWorkbenchStore.getState().setRightPanelTab("data") render() fireEvent.change(screen.getByLabelText("Dataset JSON"), { target: { value: JSON.stringify( { user: { name: "Ada", isVip: false }, items: [] }, null, 2 ) } }) await waitFor(() => { const state = useWorkbenchStore.getState() expect(state.dataEditorStatus).toBe("valid") expect(getCurrentRenderResult(state).output).toContain("Ada") expect(state.saveStatus).toBe("dirty") }) }) it("keeps the last valid dataset when JSON editing is invalid", async () => { useWorkbenchStore.getState().setRightPanelTab("data") render() fireEvent.change(screen.getByLabelText("Dataset JSON"), { target: { value: "{" } }) await waitFor(() => { const state = useWorkbenchStore.getState() expect(state.dataEditorStatus).toBe("invalid") expect(state.snapshot.datasets[state.currentDatasetId]?.data.user).toEqual( expect.objectContaining({ name: "张三" }) ) }) expect(screen.getByTestId("data-tab")).toHaveTextContent("Expected") }) it("renders the selected block preview", () => { useWorkbenchStore.getState().setRightPanelTab("preview") useWorkbenchStore.getState().setSelectedBlockId("block_hello") render() fireEvent.click(screen.getByRole("button", { name: "当前块" })) const expected = getCurrentRenderResult(useWorkbenchStore.getState(), { targetBlockId: "block_hello" }).output expect(screen.getByTestId("preview-tab")).toHaveTextContent("block_hello") expect(screen.getByTestId("preview-output").textContent).toBe(expected) }) it("shows missing variable details in preview", () => { useWorkbenchStore.getState().setCurrentDataset("dataset_empty") useWorkbenchStore.getState().setRightPanelTab("preview") render() expect(screen.getByTestId("preview-tab")).toHaveTextContent("user.name") }) it("groups render errors, warnings, and logs in the debug tab", () => { useWorkbenchStore.getState().setRightPanelTab("debug") render() expect(screen.getByTestId("debug-tab")).toHaveTextContent("Errors") expect(screen.getByTestId("debug-tab")).toHaveTextContent("Warnings") expect(screen.getByTestId("debug-tab")).toHaveTextContent("condition user.isVip") expect(screen.getByTestId("debug-tab")).toHaveTextContent("loop items") expect(screen.getByTestId("debug-tab")).toHaveTextContent("fragment fragment_common_footer") }) })