建立前端与 Tauri 桌面端的首个版本提交,包含核心编辑器、项目文件读写、测试与构建配置。 补充 Git 忽略规则和换行规范,排除依赖、构建产物、本地运行日志与临时验证文件,方便在其他电脑继续开发。
266 lines
9.4 KiB
TypeScript
266 lines
9.4 KiB
TypeScript
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(<App />)
|
|
|
|
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(<App />)
|
|
|
|
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(<App />)
|
|
|
|
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(<App />)
|
|
|
|
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(<App />)
|
|
|
|
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(<App />)
|
|
|
|
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(<App />)
|
|
|
|
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(<App />)
|
|
|
|
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(<App />)
|
|
|
|
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(<App />)
|
|
|
|
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(<App />)
|
|
|
|
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(<App />)
|
|
|
|
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(<App />)
|
|
|
|
expect(screen.getByTestId("preview-tab")).toHaveTextContent("user.name")
|
|
})
|
|
|
|
it("groups render errors, warnings, and logs in the debug tab", () => {
|
|
useWorkbenchStore.getState().setRightPanelTab("debug")
|
|
render(<App />)
|
|
|
|
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")
|
|
})
|
|
})
|