自动封面和标题更新会推进画布版本,但页面未同步,导致后续节点保存持续冲突。元数据写入返回前一版本,客户端仅在与本地版本衔接时推进版本,并与画布保存共用队列;处理结果在合并成功后才更新本地版本。视频画布导入也按节点保存、标题更新顺序执行。 验证:17 项画布能力与同步测试通过,包含封面更新后的保存、不覆盖未同步的远端改动、结果合并冲突和切换画布;TypeScript 检查通过。
117 lines
5.8 KiB
TypeScript
117 lines
5.8 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
import { createCanvasSaveQueue } from "@/lib/client/canvas-save-queue";
|
|
import { executeCanvasToolbarCapability } from "@/lib/client/canvas-toolbar-capability";
|
|
import { createCanvasCapabilityClientCommand } from "@/lib/canvas/capabilities/client-command";
|
|
import { useCanvasStore } from "@/store/canvas";
|
|
import { canvasService } from "@/services/canvas";
|
|
|
|
const originalFetch = globalThis.fetch;
|
|
const node = (id: string) => ({ id, type: "image", position: { x: 0, y: 0 },
|
|
size: { width: 100, height: 100 }, data: { src: `https://example.com/${id}.png` } });
|
|
const command = () => createCanvasCapabilityClientCommand({
|
|
projectId: "project-a", capabilityId: "image.remove_watermark", nodeIds: ["source"], source: "toolbar",
|
|
});
|
|
test.beforeEach(() => {
|
|
useCanvasStore.getState().clearCanvas();
|
|
useCanvasStore.getState().setProject("project-a");
|
|
useCanvasStore.getState().loadCanvas({ nodes: [node("source") as any], edges: [] });
|
|
useCanvasStore.getState().setCanvasRevision("r1", "r1");
|
|
});
|
|
test.afterEach(() => { globalThis.fetch = originalFetch; });
|
|
|
|
test("waits for the selected image to be saved before submitting and keeps subsequent saves behind the result", async () => {
|
|
const queue = createCanvasSaveQueue();
|
|
const events: string[] = [];
|
|
let finishSave!: () => void;
|
|
const pendingSave = new Promise<void>(resolve => { finishSave = resolve; });
|
|
const pending = queue.enqueueOperation(async () => {
|
|
events.push("saving"); await pendingSave; events.push("saved"); return true;
|
|
}, async () => { events.push("submitted"); });
|
|
const autosave = queue.enqueue(async () => { events.push("autosaved"); });
|
|
await Promise.resolve();
|
|
expect(events).toEqual(["saving"]);
|
|
finishSave();
|
|
await Promise.all([pending, autosave]);
|
|
expect(events).toEqual(["saving", "saved", "submitted", "autosaved"]);
|
|
});
|
|
|
|
test("a failed save does not submit a paid image task or block later saves", async () => {
|
|
const queue = createCanvasSaveQueue();
|
|
let submissions = 0;
|
|
await expect(queue.enqueueOperation(async () => false, async () => { submissions++; })).rejects.toThrow("尚未保存成功");
|
|
expect(submissions).toBe(0);
|
|
expect(await queue.enqueue(async () => "saved")).toBe("saved");
|
|
});
|
|
|
|
test("merges the result with edits made while processing and advances the save revision", async () => {
|
|
globalThis.fetch = (async (url) => {
|
|
if (String(url).endsWith("/capabilities")) {
|
|
useCanvasStore.getState().moveNode("source", { x: 50, y: 80 });
|
|
return Response.json({ success: true });
|
|
}
|
|
return Response.json({ success: true, data: {
|
|
updatedAt: "r2", canvasData: { nodes: [node("source"), node("result")], edges: [] },
|
|
} });
|
|
}) as typeof fetch;
|
|
await executeCanvasToolbarCapability(command());
|
|
const state = useCanvasStore.getState();
|
|
expect(state.nodes.map(n => n.id)).toEqual(["source", "result"]);
|
|
expect(state.nodes[0].position).toEqual({ x: 50, y: 80 });
|
|
expect(state.canvasRevision).toBe("r2");
|
|
});
|
|
|
|
test("a provider failure is shown without reloading or changing the canvas", async () => {
|
|
globalThis.fetch = (async () => Response.json({ success: false, error: "去水印服务繁忙" }, { status: 503 })) as typeof fetch;
|
|
await expect(executeCanvasToolbarCapability(command())).rejects.toThrow("去水印服务繁忙");
|
|
expect(useCanvasStore.getState().nodes.map(n => n.id)).toEqual(["source"]);
|
|
expect(useCanvasStore.getState().canvasRevision).toBe("r1");
|
|
});
|
|
|
|
test("switching projects during processing does not load the old result into the new project", async () => {
|
|
globalThis.fetch = (async (url) => {
|
|
if (String(url).endsWith("/capabilities")) {
|
|
useCanvasStore.getState().setProject("project-b");
|
|
useCanvasStore.getState().loadCanvas({ nodes: [node("other") as any], edges: [] });
|
|
return Response.json({ success: true });
|
|
}
|
|
return Response.json({ success: true, data: { updatedAt: "r2", canvasData: { nodes: [node("result")], edges: [] } } });
|
|
}) as typeof fetch;
|
|
await executeCanvasToolbarCapability(command());
|
|
expect(useCanvasStore.getState().nodes.map(n => n.id)).toEqual(["other"]);
|
|
expect(useCanvasStore.getState().canvasRevision).toBe("r1");
|
|
});
|
|
|
|
test("cover updates advance the local revision before the next queued save", async () => {
|
|
const queue = createCanvasSaveQueue();
|
|
globalThis.fetch = (async () => Response.json({
|
|
success: true, previousCanvasRevision: "r1", data: { updatedAt: "r2" },
|
|
})) as typeof fetch;
|
|
const cover = queue.enqueue(() => canvasService.updateProject("project-a", { thumbnail: "https://example.com/source.png" }));
|
|
const nextSave = queue.enqueue(async () => useCanvasStore.getState().canvasRevision);
|
|
await cover;
|
|
expect(await nextSave).toBe("r2");
|
|
});
|
|
|
|
test("metadata changes never adopt an unseen remote canvas revision", async () => {
|
|
globalThis.fetch = (async () => Response.json({
|
|
success: true, previousCanvasRevision: "external-edit", data: { updatedAt: "r3" },
|
|
})) as typeof fetch;
|
|
await canvasService.updateProject("project-a", { title: "Renamed" });
|
|
expect(useCanvasStore.getState().canvasRevision).toBe("r1");
|
|
});
|
|
|
|
test("a result merge conflict retains both the local edit and its original revision", async () => {
|
|
globalThis.fetch = (async (url) => {
|
|
if (String(url).endsWith("/capabilities")) {
|
|
useCanvasStore.getState().moveNode("source", { x: 50, y: 80 });
|
|
return Response.json({ success: true });
|
|
}
|
|
return Response.json({ success: true, data: { updatedAt: "r2", canvasData: {
|
|
nodes: [{ ...node("source"), position: { x: 200, y: 80 } }, node("result")], edges: [],
|
|
} } });
|
|
}) as typeof fetch;
|
|
await expect(executeCanvasToolbarCapability(command())).rejects.toThrow("画布有冲突");
|
|
expect(useCanvasStore.getState().canvasRevision).toBe("r1");
|
|
expect(useCanvasStore.getState().nodes[0].position).toEqual({ x: 50, y: 80 });
|
|
});
|