Files
UHD_Media_Tool/apps/web/app/components/library/episode-structure.test.ts
T

178 lines
6.5 KiB
TypeScript

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import type { EpisodeGroupList, EpisodeStructureSeasons } from "@umt/contracts";
import { createElement, type ReactElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vitest";
import { episodeMappingKeys } from "~/api/episode-mapping.ts";
import {
EpisodeStructureSwitcher,
SeasonCardGrid,
StructureTrack,
SubscriptionSeasonPicker,
} from "./episode-structure.tsx";
import { hasStructureChoice, inLibraryText, originText, seasonLine, structureOptions } from "./episode-structure-model.ts";
const ENTITY = "6b1f2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d";
const groups = (current: string | null): EpisodeGroupList => ({
applicable: true,
current: current ? { groupId: current, name: "制作季", setAt: null, setBy: null } : null,
groups: [
{
id: "grp-production",
name: "制作季",
type: 6,
typeLabel: "制作季",
description: null,
groupCount: 2,
episodeCount: 25,
network: null,
},
{
id: "grp-aired",
name: "播出顺序",
type: 1,
typeLabel: "原始播出",
description: null,
groupCount: 1,
episodeCount: 25,
network: null,
},
],
error: null,
});
const structure: EpisodeStructureSeasons = {
groupId: "grp-production",
name: "制作季",
current: true,
seasons: [
{
seasonNumber: 1,
name: "第一部",
episodeCount: 12,
airDate: "2024-01-07",
posterPath: null,
inLibrary: 0,
seasonUnitId: "1b1f2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
episodes: [],
},
{
seasonNumber: 2,
name: "第二部",
episodeCount: 13,
airDate: "2024-07-07",
posterPath: null,
inLibrary: 1,
seasonUnitId: "2b1f2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
episodes: [
{
episodeNumber: 2,
name: "下篇第2话",
airDate: "2024-07-14",
tmdbSeason: 1,
tmdbEpisode: 14,
inLibrary: true,
},
],
},
],
};
function withCache(el: ReactElement, list: EpisodeGroupList | null, seasons?: EpisodeStructureSeasons) {
const qc = new QueryClient({ defaultOptions: { queries: { retry: false, staleTime: Number.POSITIVE_INFINITY } } });
if (list) qc.setQueryData(episodeMappingKeys.groups(ENTITY), list);
if (seasons) qc.setQueryData(episodeMappingKeys.structure(ENTITY, seasons.groupId), seasons);
return renderToStaticMarkup(createElement(QueryClientProvider, { client: qc }, el));
}
describe("季集结构:纯函数", () => {
it("轨道选项:TMDB 默认季在前,剧集组带类型 · 季数 · 集数,当前选择打标", () => {
const opts = structureOptions(groups("grp-production"));
expect(opts.map((o) => [o.groupId, o.label, o.meta, o.current])).toEqual([
[null, "TMDB 默认季", null, false],
["grp-production", "制作季", "制作季 · 2 季 · 25 集", true],
["grp-aired", "播出顺序", "原始播出 · 1 季 · 25 集", false],
]);
// 当前剧集组已从 TMDB 删除:仍列出
const gone = structureOptions({ ...groups("old"), groups: [] });
expect(gone.at(-1)).toMatchObject({ groupId: "old", current: true, meta: "TMDB 上已不存在" });
expect(hasStructureChoice({ ...groups(null), groups: [] })).toBe(false);
expect(hasStructureChoice(groups(null))).toBe(true);
});
it("季卡片文案与原季集对照", () => {
expect(seasonLine({ airDate: "2024-07-07", episodeCount: 13 })).toBe("2024 · 13 集");
expect(inLibraryText({ inLibrary: 0, episodeCount: 13 })).toBeNull();
expect(inLibraryText({ inLibrary: 3, episodeCount: 13 })).toBe("已入库 3/13");
expect(originText(2, { episodeNumber: 2, tmdbSeason: 1, tmdbEpisode: 14 })).toBe("原 S01E14");
expect(originText(1, { episodeNumber: 2, tmdbSeason: 1, tmdbEpisode: 2 })).toBeNull();
});
});
describe("季集结构:组件", () => {
it("剧集组轨道:每项一张小卡,选中的 aria-pressed", () => {
const html = renderToStaticMarkup(
createElement(StructureTrack, {
options: structureOptions(groups(null)),
value: "grp-aired",
onChange: () => {},
}),
);
expect(html).toContain("TMDB 默认季");
expect(html).toContain("制作季 · 2 季 · 25 集");
expect(html).toMatch(/aria-pressed="true"[^>]*>.*播出顺序/);
expect(html).toContain('aria-label="当前"');
});
it("季卡片:季名、首播年 · 集数、已入库 x/y,选中的高亮", () => {
const html = renderToStaticMarkup(
createElement(SeasonCardGrid, { seasons: structure.seasons, selected: 2, onPick: () => {} }),
);
expect(html).toContain("第一部");
expect(html).toContain("2024 · 12 集");
expect(html).toContain("已入库 1/13");
expect(html).not.toContain("已入库 0/12");
expect(html).toMatch(/aria-pressed="true"[^>]*class="[^"]*pill-selected/);
});
it("订阅选季:有剧集组时显示轨道 + 全部季 + 季卡片,当前结构下按季单元回显选中", () => {
const html = withCache(
createElement(SubscriptionSeasonPicker, {
entityId: ENTITY,
value: "2b1f2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
onChange: () => {},
}),
groups("grp-production"),
structure,
);
expect(html).toContain("季集结构");
expect(html).toContain("全部季");
expect(html).toContain("第二部");
expect(html).toMatch(/aria-pressed="true"[^>]*class="[^"]*pill-selected[^"]*"[^>]*>.*?第二部/);
expect(html).not.toContain("不是作品当前的季集结构");
});
it("没有剧集组时:订阅选季不出现(表单用下拉),作品页只显示原来的季与集", () => {
const empty = { ...groups(null), groups: [] };
expect(
withCache(createElement(SubscriptionSeasonPicker, { entityId: ENTITY, value: "", onChange: () => {} }), empty),
).toBe("");
const html = withCache(
createElement(EpisodeStructureSwitcher, { entityId: ENTITY, children: createElement("p", null, "原季列表") }),
empty,
);
expect(html).toBe("<p>原季列表</p>");
});
it("作品页:有剧集组时轨道在季列表上方,当前结构显示原来的季与集", () => {
const html = withCache(
createElement(EpisodeStructureSwitcher, { entityId: ENTITY, children: createElement("p", null, "原季列表") }),
groups("grp-production"),
);
expect(html).toContain("季集结构");
expect(html).toContain("原季列表");
expect(html).not.toContain("改用这套结构");
});
});