Files
UHD_Media_Tool/apps/web/app/components/subscriptions/model.test.ts
T
rootandClaude Opus 5.5 b7a7fd3958 feat(web): subscriptions overview groups, coverage matrix view and v2 form (FE-5)
- Overview: kind tabs, five groups with server counts, card/list views,
  slot badges, progress (unknown total = "?"), filters in URL, batch actions
- Detail /console/subscriptions/🆔 season tabs, episode x version-target
  matrix with cell sheet (manual fill, plan link), drag slot order,
  auto-fill switch and fill budget, refresh coverage
- Create/edit dialog (basic/advanced): work picker via global search,
  season/range/total, sites, rule groups, include/exclude, templates,
  upgrade strategy, downloader/save path; 409 links to existing subscription

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 16:27:29 +08:00

157 lines
5.4 KiB
TypeScript

import type { CoverageCell, Subscription, SubscriptionSlotSummary } from "@umt/contracts";
import { describe, expect, it } from "vitest";
import {
cellKind,
draftFromSubscription,
draftToParams,
emptyDraft,
foldNames,
moveItem,
progressText,
seasonLabel,
slotBadge,
upgradeOf,
} from "./model.ts";
const slot = (x: Partial<SubscriptionSlotSummary>): SubscriptionSlotSummary => ({
slotId: "s",
name: "4K",
primary: true,
status: "empty",
have: 0,
inflight: 0,
missing: 0,
total: null,
upgrading: false,
minPri: null,
targetPri: null,
autoFill: true,
ruleGroupIds: [],
...x,
});
const cell = (x: Partial<CoverageCell>): CoverageCell => ({
unitKey: "s1e1",
state: "missing",
assetId: null,
quality: null,
pri: null,
planId: null,
planStage: null,
reason: null,
upgradable: false,
deferred: false,
...x,
});
describe("订阅总览文案", () => {
it("进度总未知显示 ?", () => {
expect(progressText({ have: 3, total: null, inflight: 0 })).toBe("3/?");
expect(progressText({ have: 3, total: 10, inflight: 1 })).toBe("3/10");
});
it("季标签:特别篇 / 接续 / 电影", () => {
expect(seasonLabel({ mediaKind: "series", seasonNumber: 0, seasonScope: "current_season" })).toBe("特别篇");
expect(seasonLabel({ mediaKind: "series", seasonNumber: 2, seasonScope: "following_seasons" })).toBe("S2 起");
expect(seasonLabel({ mediaKind: "series", seasonNumber: 1, seasonScope: "current_season" })).toBe("S1");
expect(seasonLabel({ mediaKind: "movie", seasonNumber: null, seasonScope: "current_season" })).toBeNull();
});
it("版本徽标按 洗版 → 在途 → 缺 → 已齐 判定", () => {
expect(slotBadge(slot({ upgrading: true, minPri: 97, targetPri: 100, missing: 2 })).label).toBe("洗版中 97→100");
expect(slotBadge(slot({ inflight: 2, missing: 3 }))).toEqual({ label: "在途 2", tone: "neutral" });
expect(slotBadge(slot({ missing: 3 })).tone).toBe("destructive");
expect(slotBadge(slot({ have: 10, total: 10 }))).toEqual({ label: "已齐", tone: "success" });
expect(slotBadge(slot({})).label).toBe("未开始");
});
it("名称折叠", () => {
expect(foldNames([])).toBe("全部站点");
expect(foldNames(["a", "b", "c", "d"])).toBe("a、b +2");
});
});
describe("覆盖矩阵格", () => {
it("state + 附带字段 → 显示类别", () => {
expect(cellKind(cell({ state: "have" }))).toBe("have");
expect(cellKind(cell({ state: "have", upgradable: true }))).toBe("upgradable");
expect(cellKind(cell({ state: "missing", reason: "suspected" }))).toBe("suspected");
expect(cellKind(cell({ state: "missing", deferred: true }))).toBe("deferred");
expect(cellKind(cell({ state: "skipped", reason: "not_aired" }))).toBe("not_aired");
expect(cellKind(cell({ state: "skipped", reason: "out_of_range" }))).toBe("skipped");
expect(cellKind(cell({ state: "inflight" }))).toBe("inflight");
});
it("拖动排序", () => {
expect(moveItem(["a", "b", "c"], 2, 0)).toEqual(["c", "a", "b"]);
expect(moveItem(["a", "b"], 0, 5)).toEqual(["a", "b"]);
});
it("升级策略取自版本目标", () => {
expect(upgradeOf([{ upgradeThreshold: {} }], "series")).toBe("off");
expect(upgradeOf([{ upgradeThreshold: { enabled: true, mode: "full_pack" } }], "series")).toBe("full_pack");
expect(upgradeOf([{ upgradeThreshold: { enabled: true } }], "movie")).toBe("movie");
});
});
describe("表单 ↔ params", () => {
it("保留未知键,空值不写,只在改动时带 best_version / total_episode", () => {
const d = {
...emptyDraft("series"),
siteIds: ["x"],
episodeStart: "3",
include: "中字\n\n 4K ",
autoFill: "off" as const,
unitsPerDay: "2",
gbPerDay: "1.5",
total: "12",
upgrade: "full_pack" as const,
};
const { params, errors } = draftToParams(
d,
{ custom: 1, keywords: ["旧"] },
{ upgradeChanged: true, totalChanged: true },
);
expect(errors).toEqual({});
expect(params).toEqual({
custom: 1,
siteIds: ["x"],
include: ["中字", "4K"],
episodeRange: { start: 3 },
total_episode: 12,
manual_total_episode: false,
autoFill: "off",
fillBudget: { unitsPerDay: 2, bytesPerDay: Math.round(1.5 * 1024 ** 3) },
best_version: true,
best_version_full: true,
});
const same = draftToParams(d, {}, { upgradeChanged: false, totalChanged: false }).params;
expect(same.best_version).toBeUndefined();
expect(same.total_episode).toBeUndefined();
});
it("校验:结束集 < 开始集", () => {
const { errors } = draftToParams(
{ ...emptyDraft("series"), episodeStart: "5", episodeEnd: "2" },
{},
{ upgradeChanged: false, totalChanged: false },
);
expect(errors.episodeEnd).toBeTruthy();
});
it("从订阅回填", () => {
const sub = {
mediaKind: "series",
seasonUnitId: null,
seasonScope: "current_season",
mode: "continuous",
priority: "tracking",
policyRevisionId: null,
params: {
episodeRange: { start: 2 },
autoFill: "on",
fillBudget: { bytesPerDay: 2 * 1024 ** 3 },
keywords: ["a", "b"],
},
} as unknown as Subscription;
const d = draftFromSubscription(sub, "per_unit");
expect(d.episodeStart).toBe("2");
expect(d.autoFill).toBe("on");
expect(d.gbPerDay).toBe("2");
expect(d.keywords).toBe("a\nb");
expect(d.upgrade).toBe("per_unit");
});
});