chore: 初始化 BlockFlow Workbench 仓库
建立前端与 Tauri 桌面端的首个版本提交,包含核心编辑器、项目文件读写、测试与构建配置。 补充 Git 忽略规则和换行规范,排除依赖、构建产物、本地运行日志与临时验证文件,方便在其他电脑继续开发。
This commit is contained in:
265
tests/app.test.tsx
Normal file
265
tests/app.test.tsx
Normal file
@@ -0,0 +1,265 @@
|
||||
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")
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user