- lib/slots.ts 按位置注册(侧栏入口、作品页操作区 / 面板区、整理记录行操作、设置分区),默认直接显示; components/slots 挂载点:面板类直接渲染,动作类渲染成按钮 / 菜单项、对话框在菜单外渲染 - 作品页操作区与面板区、整理记录行菜单接入插槽;补字幕改由 features/subtitles.tsx 注册, 作品页按钮行为不变,整理记录行菜单新增「补字幕」(成功 / 部分成功的入库类整理) Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
96 lines
4.3 KiB
TypeScript
96 lines
4.3 KiB
TypeScript
import type { PublicOrganizationRecord } from "@umt/contracts";
|
|
import { createElement } from "react";
|
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
import { describe, expect, it } from "vitest";
|
|
import { createSlotRegistry, type SlotEntity, slots, visibleActions } from "~/lib/slots.ts";
|
|
import { SlotPanels, useSlotActions } from "./slot-mounts.tsx";
|
|
|
|
// 前端插槽(ADR-006):按位置注册、按 order 与注册顺序排列、默认直接显示(没有开关)、挂载点渲染。
|
|
|
|
const entity: SlotEntity = { id: "e1", title: "示例剧", kind: "series", status: "active" };
|
|
const record = (over: Partial<PublicOrganizationRecord> = {}) =>
|
|
({
|
|
id: "r1",
|
|
status: "succeeded",
|
|
action: "copy",
|
|
media: { entityId: "e1", title: "示例剧" },
|
|
...over,
|
|
}) as unknown as PublicOrganizationRecord;
|
|
|
|
describe("插槽注册表", () => {
|
|
it("按 order 排序,同序保持注册顺序;同一位置 id 重复报错", () => {
|
|
const reg = createSlotRegistry();
|
|
const Panel = (name: string) => () => createElement("p", null, name);
|
|
reg.register("entity.panels", { id: "b", feature: "x", Component: Panel("B") });
|
|
reg.register("entity.panels", { id: "a", feature: "x", order: 10, Component: Panel("A") });
|
|
reg.register("entity.panels", { id: "c", feature: "x", Component: Panel("C") });
|
|
expect(reg.entries("entity.panels").map((e) => e.id)).toEqual(["a", "b", "c"]);
|
|
expect(reg.entries("settings.sections")).toEqual([]);
|
|
expect(() => reg.register("entity.panels", { id: "a", feature: "x", Component: Panel("A2") })).toThrow(/已有 a/);
|
|
});
|
|
|
|
it("面板类挂载点按顺序渲染全部组件,拿到页面给的 props;没有注册时什么都不渲染", () => {
|
|
const reg = createSlotRegistry();
|
|
reg.register("entity.panels", {
|
|
id: "p",
|
|
feature: "x",
|
|
Component: ({ entity: e }) => createElement("section", null, `面板:${e.title}`),
|
|
});
|
|
const html = renderToStaticMarkup(
|
|
createElement(SlotPanels, { position: "entity.panels", props: { entity }, registry: reg }),
|
|
);
|
|
expect(html).toBe("<section>面板:示例剧</section>");
|
|
expect(
|
|
renderToStaticMarkup(
|
|
createElement(SlotPanels, { position: "entity.panels", props: { entity }, registry: createSlotRegistry() }),
|
|
),
|
|
).toBe("");
|
|
});
|
|
|
|
it("动作类:visible 过滤,缺省显示;挂载点给出可见项", () => {
|
|
const reg = createSlotRegistry();
|
|
const Dialog = () => null;
|
|
reg.register("entity.actions", { id: "always", feature: "x", label: "总在", Dialog });
|
|
reg.register("entity.actions", {
|
|
id: "active-only",
|
|
feature: "x",
|
|
label: "未合并才有",
|
|
visible: ({ entity: e }) => e.status !== "merged",
|
|
Dialog,
|
|
});
|
|
expect(visibleActions(reg, "entity.actions", { entity }).map((a) => a.id)).toEqual(["always", "active-only"]);
|
|
expect(visibleActions(reg, "entity.actions", { entity: { ...entity, status: "merged" } }).map((a) => a.id)).toEqual(
|
|
["always"],
|
|
);
|
|
const Probe = () => {
|
|
const s = useSlotActions("entity.actions", reg);
|
|
return createElement(
|
|
"ul",
|
|
null,
|
|
s.items({ entity }).map((a) => createElement("li", { key: a.id }, a.label)),
|
|
s.dialog,
|
|
);
|
|
};
|
|
expect(renderToStaticMarkup(createElement(Probe))).toBe("<ul><li>总在</li><li>未合并才有</li></ul>");
|
|
});
|
|
});
|
|
|
|
describe("补字幕样板挂在两个真实位置", () => {
|
|
it("作品页操作区「补字幕」:合并后的作品不显示", () => {
|
|
expect(visibleActions(slots, "entity.actions", { entity }).map((a) => [a.feature, a.label])).toEqual([
|
|
["subtitles", "补字幕"],
|
|
]);
|
|
expect(visibleActions(slots, "entity.actions", { entity: { ...entity, status: "merged" } })).toEqual([]);
|
|
});
|
|
|
|
it("整理记录行菜单「补字幕」:只给成功 / 部分成功、认到作品的入库类整理", () => {
|
|
const labels = (r: PublicOrganizationRecord) =>
|
|
visibleActions(slots, "organize.record.actions", { record: r }).map((a) => a.label);
|
|
expect(labels(record())).toEqual(["补字幕"]);
|
|
expect(labels(record({ status: "partial", action: "move" }))).toEqual(["补字幕"]);
|
|
expect(labels(record({ status: "failed" }))).toEqual([]);
|
|
expect(labels(record({ action: "trash" }))).toEqual([]);
|
|
expect(labels(record({ media: null }))).toEqual([]);
|
|
});
|
|
});
|