213 lines
9.1 KiB
TypeScript
213 lines
9.1 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
||
|
||
import { gatePrevisPreviewFailure, resolvePrevisActiveShot, resolvePrevisPreviewSource, type PrevisNodeContentReader } from "../src/lib/canvas/previs/previs-preview";
|
||
import { createPrevisScene } from "../src/lib/canvas/previs/previs-scene";
|
||
import type { PrevisScene, PrevisShot } from "../src/types/previs";
|
||
|
||
function scene(shots?: PrevisShot[]): PrevisScene {
|
||
const base = createPrevisScene("镜头 1");
|
||
return shots ? { ...base, shots, activeShotId: shots[0]?.id || base.activeShotId } : base;
|
||
}
|
||
|
||
/** 画布节点表;未登记的 id 视为已删除。 */
|
||
function reader(nodes: Record<string, string | undefined>): PrevisNodeContentReader {
|
||
return (nodeId) => (nodeId ? nodes[nodeId] : undefined);
|
||
}
|
||
|
||
describe("预览来源优先级", () => {
|
||
test("metadata preview 优先于 shot fallback", () => {
|
||
const current = scene();
|
||
const shot = { ...current.shots[0], previewNodeId: "from-shot" };
|
||
const source = resolvePrevisPreviewSource({
|
||
scene: current,
|
||
shot,
|
||
previewNodeId: "from-metadata",
|
||
readNodeContent: reader({ "from-metadata": "data:image/png;base64,META", "from-shot": "data:image/png;base64,SHOT" }),
|
||
});
|
||
expect(source).toEqual({ kind: "image", url: "data:image/png;base64,META" });
|
||
});
|
||
|
||
test("metadata 缺失时回落到 shot.previewNodeId", () => {
|
||
const current = scene();
|
||
const shot = { ...current.shots[0], previewNodeId: "from-shot" };
|
||
const source = resolvePrevisPreviewSource({
|
||
scene: current,
|
||
shot,
|
||
previewNodeId: undefined,
|
||
readNodeContent: reader({ "from-shot": "data:image/png;base64,SHOT" }),
|
||
});
|
||
expect(source).toEqual({ kind: "image", url: "data:image/png;base64,SHOT" });
|
||
});
|
||
|
||
test("metadata 指向的节点已删除时回落到 shot", () => {
|
||
const current = scene();
|
||
const shot = { ...current.shots[0], previewNodeId: "from-shot" };
|
||
const source = resolvePrevisPreviewSource({
|
||
scene: current,
|
||
shot,
|
||
previewNodeId: "deleted-node",
|
||
readNodeContent: reader({ "from-shot": "data:image/png;base64,SHOT" }),
|
||
});
|
||
expect(source).toEqual({ kind: "image", url: "data:image/png;base64,SHOT" });
|
||
});
|
||
|
||
test("节点已删除且无 shot 预览时为空态,绝不产出 image", () => {
|
||
const current = scene();
|
||
const source = resolvePrevisPreviewSource({
|
||
scene: current,
|
||
shot: current.shots[0],
|
||
previewNodeId: "deleted-node",
|
||
readNodeContent: reader({}),
|
||
});
|
||
expect(source).toEqual({ kind: "empty" });
|
||
});
|
||
|
||
test("空白/纯空格正文安全落空", () => {
|
||
const current = scene();
|
||
const shot = { ...current.shots[0], previewNodeId: "blank-shot" };
|
||
expect(
|
||
resolvePrevisPreviewSource({
|
||
scene: current,
|
||
shot,
|
||
previewNodeId: "blank-meta",
|
||
readNodeContent: reader({ "blank-meta": "", "blank-shot": " \n\t " }),
|
||
}),
|
||
).toEqual({ kind: "empty" });
|
||
});
|
||
|
||
test("首尾空白被剥离,URL 不会畸形地进入 <img src>", () => {
|
||
const current = scene();
|
||
expect(
|
||
resolvePrevisPreviewSource({
|
||
scene: current,
|
||
shot: current.shots[0],
|
||
previewNodeId: "padded-meta",
|
||
readNodeContent: reader({ "padded-meta": " \n data:image/png;base64,PADDED \t " }),
|
||
}),
|
||
).toEqual({ kind: "image", url: "data:image/png;base64,PADDED" });
|
||
});
|
||
|
||
test("shot 回落路径同样剥离首尾空白", () => {
|
||
const current = scene();
|
||
const shot = { ...current.shots[0], previewNodeId: "padded-shot" };
|
||
expect(
|
||
resolvePrevisPreviewSource({
|
||
scene: current,
|
||
shot,
|
||
previewNodeId: undefined,
|
||
readNodeContent: reader({ "padded-shot": "\t https://cdn.example/preview.png \n" }),
|
||
}),
|
||
).toEqual({ kind: "image", url: "https://cdn.example/preview.png" });
|
||
});
|
||
|
||
test("仅有空白的正文仍然落空,不因 trim 变成空字符串 URL", () => {
|
||
const current = scene();
|
||
const source = resolvePrevisPreviewSource({
|
||
scene: current,
|
||
shot: current.shots[0],
|
||
previewNodeId: "ws-only",
|
||
readNodeContent: reader({ "ws-only": " \n\t\r " }),
|
||
});
|
||
expect(source).toEqual({ kind: "empty" });
|
||
expect(source).not.toHaveProperty("url");
|
||
});
|
||
|
||
test("scene 为 null 时是准备态而不是空态", () => {
|
||
expect(resolvePrevisPreviewSource({ scene: null, shot: undefined, previewNodeId: undefined, readNodeContent: reader({}) })).toEqual({ kind: "loading" });
|
||
});
|
||
|
||
test("scene 为 null 但已有真实预览时仍优先显示图", () => {
|
||
expect(
|
||
resolvePrevisPreviewSource({
|
||
scene: null,
|
||
shot: undefined,
|
||
previewNodeId: "from-metadata",
|
||
readNodeContent: reader({ "from-metadata": "data:image/png;base64,META" }),
|
||
}),
|
||
).toEqual({ kind: "image", url: "data:image/png;base64,META" });
|
||
});
|
||
|
||
test("有场景但从未回写构图时是诚实空态", () => {
|
||
const current = scene();
|
||
expect(resolvePrevisPreviewSource({ scene: current, shot: current.shots[0], previewNodeId: undefined, readNodeContent: reader({}) })).toEqual({ kind: "empty" });
|
||
});
|
||
|
||
test("Apply 写入 metadata 后由空态转为 image", () => {
|
||
const current = scene();
|
||
const before = resolvePrevisPreviewSource({ scene: current, shot: current.shots[0], previewNodeId: undefined, readNodeContent: reader({}) });
|
||
expect(before).toEqual({ kind: "empty" });
|
||
const after = resolvePrevisPreviewSource({
|
||
scene: current,
|
||
shot: current.shots[0],
|
||
previewNodeId: "image-previs-1",
|
||
readNodeContent: reader({ "image-previs-1": "data:image/png;base64,APPLIED" }),
|
||
});
|
||
expect(after).toEqual({ kind: "image", url: "data:image/png;base64,APPLIED" });
|
||
});
|
||
});
|
||
|
||
describe("加载失败门控", () => {
|
||
const image = { kind: "image", url: "data:image/png;base64,A" } as const;
|
||
|
||
test("失败 URL 与当前 URL 一致时立即变空态", () => {
|
||
expect(gatePrevisPreviewFailure(image, image.url)).toEqual({ kind: "empty" });
|
||
});
|
||
|
||
test("URL 变化后自动恢复,无需清理布尔标记", () => {
|
||
expect(gatePrevisPreviewFailure({ kind: "image", url: "data:image/png;base64,B" }, image.url)).toEqual({ kind: "image", url: "data:image/png;base64,B" });
|
||
});
|
||
|
||
test("无失败记录时原样通过", () => {
|
||
expect(gatePrevisPreviewFailure(image, null)).toBe(image);
|
||
});
|
||
|
||
test("非 image 来源不受门控影响", () => {
|
||
expect(gatePrevisPreviewFailure({ kind: "loading" }, image.url)).toEqual({ kind: "loading" });
|
||
expect(gatePrevisPreviewFailure({ kind: "empty" }, image.url)).toEqual({ kind: "empty" });
|
||
});
|
||
|
||
test("失败后 Apply 出新图仍能显示(同一次会话内)", () => {
|
||
const next = resolvePrevisPreviewSource({
|
||
scene: scene(),
|
||
shot: undefined,
|
||
previewNodeId: "n1",
|
||
readNodeContent: reader({ n1: "data:image/png;base64,NEW" }),
|
||
});
|
||
expect(gatePrevisPreviewFailure(next, "data:image/png;base64,OLD")).toEqual({ kind: "image", url: "data:image/png;base64,NEW" });
|
||
});
|
||
});
|
||
|
||
describe("当前镜头解析", () => {
|
||
test("优先 metadata 指定的 shot", () => {
|
||
const first = createPrevisScene("s").shots[0];
|
||
const second: PrevisShot = { ...first, id: "shot-2", name: "镜头 2", previewNodeId: "p2" };
|
||
const current = scene([first, second]);
|
||
expect(resolvePrevisActiveShot(current, "shot-2")?.id).toBe("shot-2");
|
||
});
|
||
|
||
test("metadata 未指定或未命中时退回第一个 shot", () => {
|
||
const current = scene();
|
||
expect(resolvePrevisActiveShot(current, undefined)?.id).toBe(current.shots[0].id);
|
||
expect(resolvePrevisActiveShot(current, "missing")?.id).toBe(current.shots[0].id);
|
||
});
|
||
|
||
test("scene 为 null 时没有镜头", () => {
|
||
expect(resolvePrevisActiveShot(null, "shot-1")).toBeUndefined();
|
||
});
|
||
|
||
test("指定 shot 的 previewNodeId 参与回落,而不是第一个 shot 的", () => {
|
||
const first = { ...createPrevisScene("s").shots[0], previewNodeId: "p1" };
|
||
const second: PrevisShot = { ...first, id: "shot-2", previewNodeId: "p2" };
|
||
const current = scene([first, second]);
|
||
const shot = resolvePrevisActiveShot(current, "shot-2");
|
||
expect(
|
||
resolvePrevisPreviewSource({
|
||
scene: current,
|
||
shot,
|
||
previewNodeId: undefined,
|
||
readNodeContent: reader({ p1: "data:image/png;base64,ONE", p2: "data:image/png;base64,TWO" }),
|
||
}),
|
||
).toEqual({ kind: "image", url: "data:image/png;base64,TWO" });
|
||
});
|
||
});
|