chore: 初始化 BlockFlow Workbench 仓库
建立前端与 Tauri 桌面端的首个版本提交,包含核心编辑器、项目文件读写、测试与构建配置。 补充 Git 忽略规则和换行规范,排除依赖、构建产物、本地运行日志与临时验证文件,方便在其他电脑继续开发。
This commit is contained in:
256
tests/project.test.ts
Normal file
256
tests/project.test.ts
Normal file
@@ -0,0 +1,256 @@
|
||||
import fs from "node:fs/promises"
|
||||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
import { afterEach, describe, expect, it } from "vitest"
|
||||
import type { BlockNode, DataSetDocument, FragmentDocument, InlineNode, TemplateDocument } from "../src/core/types"
|
||||
import { nodeProjectFileSystem } from "../src/project/nodeFileSystem"
|
||||
import { getProjectPaths, joinPath, normalizePath, toRelativePath } from "../src/project/paths"
|
||||
import {
|
||||
createProject,
|
||||
openProject,
|
||||
readDocument,
|
||||
scanProject,
|
||||
writeDocument
|
||||
} from "../src/project"
|
||||
|
||||
const tempRoots: string[] = []
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(tempRoots.map((root) => fs.rm(root, { recursive: true, force: true })))
|
||||
tempRoots.length = 0
|
||||
})
|
||||
|
||||
describe("project folder system", () => {
|
||||
it("normalizes project paths without Node path helpers", () => {
|
||||
expect(normalizePath("D:\\Projects\\BlockFlow\\templates\\..\\fragments")).toBe("D:/Projects/BlockFlow/fragments")
|
||||
expect(joinPath("D:\\Projects\\BlockFlow", "templates", "main.json")).toBe("D:/Projects/BlockFlow/templates/main.json")
|
||||
expect(toRelativePath("D:\\Projects\\BlockFlow", "D:\\Projects\\BlockFlow\\templates\\main.json")).toBe("templates/main.json")
|
||||
expect(getProjectPaths("D:\\Projects\\BlockFlow").fragmentsDir).toBe("D:/Projects/BlockFlow/fragments")
|
||||
})
|
||||
|
||||
it("creates a minimal project folder", async () => {
|
||||
const root = await makeTempRoot()
|
||||
|
||||
const result = await createProject(root, {
|
||||
id: "project_test",
|
||||
name: "Test Project",
|
||||
entryTemplateId: "template_main",
|
||||
includeExportsDir: true,
|
||||
fs: nodeProjectFileSystem,
|
||||
now: () => new Date("2026-05-24T00:00:00.000Z")
|
||||
})
|
||||
|
||||
await expectPathExists(path.join(root, "project.json"))
|
||||
await expectPathExists(path.join(root, "templates"))
|
||||
await expectPathExists(path.join(root, "fragments"))
|
||||
await expectPathExists(path.join(root, "datasets"))
|
||||
await expectPathExists(path.join(root, "exports"))
|
||||
expect(result.project.id).toBe("project_test")
|
||||
|
||||
const projectJson = JSON.parse(await fs.readFile(path.join(root, "project.json"), "utf8")) as {
|
||||
paths?: unknown
|
||||
entryTemplateId?: string
|
||||
}
|
||||
expect(projectJson.paths).toBeUndefined()
|
||||
expect(projectJson.entryTemplateId).toBe("template_main")
|
||||
})
|
||||
|
||||
it("opens templates, fragments, datasets, and resource tree entries", async () => {
|
||||
const root = await makeProjectWithResources()
|
||||
|
||||
const snapshot = await openProject(root, { fs: nodeProjectFileSystem })
|
||||
|
||||
expect(snapshot.project.entryTemplateId).toBe("template_main")
|
||||
expect(Object.keys(snapshot.templates)).toEqual(["template_main"])
|
||||
expect(Object.keys(snapshot.fragments)).toEqual(["fragment_footer"])
|
||||
expect(Object.keys(snapshot.datasets)).toEqual(["dataset_default"])
|
||||
expect(snapshot.templates.template_main?.filePath).toContain("templates/main.json")
|
||||
expect(snapshot.datasets.dataset_default?.data).toEqual({ user: { name: "张三" } })
|
||||
|
||||
const templateFile = findResource(snapshot.resourceTree.children, "templates/main.json")
|
||||
expect(templateFile).toMatchObject({
|
||||
type: "file",
|
||||
resourceKind: "template",
|
||||
documentId: "template_main"
|
||||
})
|
||||
})
|
||||
|
||||
it("detects missing fragment references", async () => {
|
||||
const root = await makeProjectWithResources({
|
||||
templateChildren: [{ id: "missing_ref", type: "fragmentRef", fragmentId: "missing" }]
|
||||
})
|
||||
|
||||
const snapshot = await openProject(root, { fs: nodeProjectFileSystem })
|
||||
|
||||
expect(snapshot.referenceIndex.missingFragments).toContainEqual(
|
||||
expect.objectContaining({ fromId: "template_main", fragmentId: "missing" })
|
||||
)
|
||||
expect(snapshot.diagnostics).toContainEqual(
|
||||
expect.objectContaining({ code: "MISSING_FRAGMENT", fragmentId: "missing" })
|
||||
)
|
||||
})
|
||||
|
||||
it("skips invalid resource JSON and returns diagnostics", async () => {
|
||||
const root = await makeProjectWithResources()
|
||||
await fs.writeFile(path.join(root, "templates", "broken.json"), "{ nope", "utf8")
|
||||
|
||||
const snapshot = await openProject(root, { fs: nodeProjectFileSystem })
|
||||
|
||||
expect(snapshot.templates.template_main).toBeDefined()
|
||||
expect(snapshot.templates.broken).toBeUndefined()
|
||||
expect(snapshot.diagnostics).toContainEqual(
|
||||
expect.objectContaining({ code: "INVALID_JSON" })
|
||||
)
|
||||
})
|
||||
|
||||
it("detects duplicate document ids across scanned files", async () => {
|
||||
const root = await makeProjectWithResources()
|
||||
await writeJson(path.join(root, "fragments", "duplicate.json"), {
|
||||
kind: "fragment",
|
||||
id: "template_main",
|
||||
name: "Duplicate",
|
||||
children: []
|
||||
})
|
||||
|
||||
const snapshot = await openProject(root, { fs: nodeProjectFileSystem })
|
||||
|
||||
expect(snapshot.diagnostics).toContainEqual(
|
||||
expect.objectContaining({
|
||||
code: "DUPLICATE_DOCUMENT_ID",
|
||||
documentId: "template_main"
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("writes documents without persisting runtime filePath", async () => {
|
||||
const root = await makeProjectWithResources()
|
||||
const filePath = path.join(root, "templates", "main.json")
|
||||
const document = await readDocument(filePath, { fs: nodeProjectFileSystem }) as TemplateDocument
|
||||
|
||||
document.children = [paragraph([{ type: "text", content: "更新" }])]
|
||||
const writtenPath = await writeDocument(root, document, { fs: nodeProjectFileSystem })
|
||||
|
||||
expect(writtenPath).toBe(filePath)
|
||||
const saved = JSON.parse(await fs.readFile(filePath, "utf8")) as { filePath?: unknown }
|
||||
expect(saved.filePath).toBeUndefined()
|
||||
|
||||
const reopened = await openProject(root, { fs: nodeProjectFileSystem })
|
||||
expect(reopened.templates.template_main?.children).toEqual(document.children)
|
||||
})
|
||||
|
||||
it("writes new fragments to the fragments directory", async () => {
|
||||
const root = await makeProjectWithResources()
|
||||
const fragment: FragmentDocument = {
|
||||
kind: "fragment",
|
||||
id: "fragment_new",
|
||||
name: "New Fragment",
|
||||
children: [paragraph([{ type: "text", content: "New body" }])]
|
||||
}
|
||||
|
||||
const writtenPath = await writeDocument(root, fragment, { fs: nodeProjectFileSystem })
|
||||
|
||||
expect(writtenPath).toContain("fragments/fragment_new.json")
|
||||
const saved = JSON.parse(await fs.readFile(writtenPath, "utf8")) as { filePath?: unknown }
|
||||
expect(saved.filePath).toBeUndefined()
|
||||
|
||||
const reopened = await openProject(root, { fs: nodeProjectFileSystem })
|
||||
expect(reopened.fragments.fragment_new?.children).toEqual(fragment.children)
|
||||
})
|
||||
|
||||
it("scans nested json resource files", async () => {
|
||||
const root = await makeProjectWithResources()
|
||||
await fs.mkdir(path.join(root, "templates", "nested"), { recursive: true })
|
||||
await writeJson(path.join(root, "templates", "nested", "extra.json"), {
|
||||
kind: "template",
|
||||
id: "template_extra",
|
||||
name: "Extra",
|
||||
children: []
|
||||
})
|
||||
|
||||
const scanned = await scanProject(root, { fs: nodeProjectFileSystem })
|
||||
|
||||
expect(scanned.resourceFiles).toContainEqual(
|
||||
expect.objectContaining({
|
||||
relativePath: "templates/nested/extra.json",
|
||||
resourceKind: "template"
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
async function makeProjectWithResources(options: { templateChildren?: BlockNode[] } = {}): Promise<string> {
|
||||
const root = await makeTempRoot()
|
||||
await createProject(root, {
|
||||
entryTemplateId: "template_main",
|
||||
fs: nodeProjectFileSystem,
|
||||
now: () => new Date("2026-05-24T00:00:00.000Z")
|
||||
})
|
||||
|
||||
await writeJson(path.join(root, "templates", "main.json"), {
|
||||
kind: "template",
|
||||
id: "template_main",
|
||||
name: "Main",
|
||||
children: options.templateChildren ?? [
|
||||
paragraph([{ type: "text", content: "正文" }]),
|
||||
{ id: "ref_footer", type: "fragmentRef", fragmentId: "fragment_footer" }
|
||||
]
|
||||
})
|
||||
await writeJson(path.join(root, "fragments", "footer.json"), {
|
||||
kind: "fragment",
|
||||
id: "fragment_footer",
|
||||
name: "Footer",
|
||||
children: [paragraph([{ type: "text", content: "以上。" }])]
|
||||
})
|
||||
await writeJson(path.join(root, "datasets", "default.json"), {
|
||||
kind: "dataset",
|
||||
id: "dataset_default",
|
||||
name: "Default",
|
||||
data: { user: { name: "张三" } }
|
||||
} satisfies DataSetDocument)
|
||||
|
||||
return root
|
||||
}
|
||||
|
||||
function paragraph(inlines: InlineNode[]): BlockNode {
|
||||
return {
|
||||
id: `p_${Math.random().toString(36).slice(2)}`,
|
||||
type: "paragraph",
|
||||
inlines
|
||||
}
|
||||
}
|
||||
|
||||
async function makeTempRoot(): Promise<string> {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "blockflow-project-"))
|
||||
tempRoots.push(root)
|
||||
return root
|
||||
}
|
||||
|
||||
async function writeJson(filePath: string, value: unknown): Promise<void> {
|
||||
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
||||
await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8")
|
||||
}
|
||||
|
||||
async function expectPathExists(filePath: string): Promise<void> {
|
||||
await expect(fs.stat(filePath)).resolves.toBeDefined()
|
||||
}
|
||||
|
||||
function findResource(
|
||||
nodes: Array<{ type: string; relativePath: string; children?: unknown[] }>,
|
||||
relativePath: string
|
||||
): unknown {
|
||||
for (const node of nodes) {
|
||||
if (node.relativePath === relativePath) {
|
||||
return node
|
||||
}
|
||||
if (Array.isArray(node.children)) {
|
||||
const found = findResource(
|
||||
node.children as Array<{ type: string; relativePath: string; children?: unknown[] }>,
|
||||
relativePath
|
||||
)
|
||||
if (found !== undefined) {
|
||||
return found
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
Reference in New Issue
Block a user