239 lines
8.6 KiB
TypeScript
239 lines
8.6 KiB
TypeScript
import type { CoverageCell, Subscription, SubscriptionSlotSummary } from "@umt/contracts";
|
|
import { describe, expect, it } from "vitest";
|
|
import {
|
|
cellKind,
|
|
draftFromSubscription,
|
|
draftToParams,
|
|
emptyDraft,
|
|
entriesByDate,
|
|
foldNames,
|
|
monthGrid,
|
|
monthKey,
|
|
monthStart,
|
|
moveItem,
|
|
progressText,
|
|
seasonLabel,
|
|
shiftMonth,
|
|
slotBadge,
|
|
} 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"]);
|
|
});
|
|
});
|
|
|
|
describe("表单 ↔ params", () => {
|
|
it("普通编辑不重建补全配置;只改补全开关也保留预算精度,明确修改才更新", () => {
|
|
const params = {
|
|
autoFill: "on",
|
|
fillBudget: { unitsPerDay: 2, bytesPerDay: Math.round(1.25 * 1024 ** 3), future: true },
|
|
};
|
|
const sub = {
|
|
mediaKind: "movie",
|
|
seasonScope: "current_season",
|
|
mode: "once",
|
|
priority: "tracking",
|
|
seasonUnitId: null,
|
|
policyRevisionId: null,
|
|
params,
|
|
} as unknown as Subscription;
|
|
const d = draftFromSubscription(sub);
|
|
const ordinary = draftToParams({ ...d, searchIntervalHours: "6" }, params, {
|
|
totalChanged: false,
|
|
autoFillChanged: false,
|
|
});
|
|
expect(ordinary.params).toEqual({ ...params, searchIntervalHours: 6 });
|
|
const modeOnly = draftToParams({ ...d, autoFill: "off" }, params, { totalChanged: false, autoFillChanged: true });
|
|
expect(modeOnly.params).toEqual({ ...params, autoFill: "off" });
|
|
const changed = draftToParams({ ...d, gbPerDay: "2", unitsPerDay: "" }, params, {
|
|
totalChanged: false,
|
|
autoFillChanged: true,
|
|
});
|
|
expect(changed.params.fillBudget).toEqual({ bytesPerDay: 2 * 1024 ** 3, future: true });
|
|
const cleared = draftToParams(
|
|
{ ...d, gbPerDay: "", unitsPerDay: "" },
|
|
{ ...params, fillBudget: { unitsPerDay: 2, bytesPerDay: params.fillBudget.bytesPerDay } },
|
|
{ totalChanged: false, autoFillChanged: true },
|
|
);
|
|
expect(cleared.params.fillBudget).toBeUndefined();
|
|
});
|
|
it("保留未知键,空值不写,只在改动时带 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",
|
|
};
|
|
const { params, errors } = draftToParams(
|
|
d,
|
|
{ custom: 1, keywords: ["旧"] },
|
|
{ totalChanged: true, autoFillChanged: 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) },
|
|
});
|
|
const same = draftToParams(d, {}, { totalChanged: false, autoFillChanged: true }).params;
|
|
expect(same.best_version).toBeUndefined();
|
|
expect(same.total_episode).toBeUndefined();
|
|
});
|
|
it("校验:结束集 < 开始集", () => {
|
|
const { errors } = draftToParams(
|
|
{ ...emptyDraft("series"), episodeStart: "5", episodeEnd: "2" },
|
|
{},
|
|
{ totalChanged: false, autoFillChanged: true },
|
|
);
|
|
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);
|
|
expect(d.episodeStart).toBe("2");
|
|
expect(d.autoFill).toBe("on");
|
|
expect(d.gbPerDay).toBe("2");
|
|
expect(d.keywords).toBe("a\nb");
|
|
});
|
|
it("编辑跟踪与下载信息保留旧收录规则,不再生成统一洗版指令", () => {
|
|
const legacy = {
|
|
resolution: "4k",
|
|
source: "uhd",
|
|
effect: "atmos",
|
|
ruleGroupIds: ["quality"],
|
|
future: { keep: true },
|
|
};
|
|
const sub = {
|
|
mediaKind: "movie",
|
|
seasonScope: "current_season",
|
|
mode: "once",
|
|
priority: "tracking",
|
|
seasonUnitId: null,
|
|
policyRevisionId: null,
|
|
params: legacy,
|
|
} as unknown as Subscription;
|
|
const draft = draftFromSubscription(sub);
|
|
const { params, errors } = draftToParams({ ...draft, siteIds: ["site"], searchIntervalHours: "6" }, legacy, {
|
|
totalChanged: false,
|
|
autoFillChanged: true,
|
|
});
|
|
expect(errors).toEqual({});
|
|
expect(params).toEqual({ ...legacy, siteIds: ["site"], searchIntervalHours: 6 });
|
|
expect(params.best_version).toBeUndefined();
|
|
expect(params.best_version_full).toBeUndefined();
|
|
const cleared = draftToParams(draft, legacy, {
|
|
totalChanged: false,
|
|
autoFillChanged: false,
|
|
clearQualityPreset: true,
|
|
});
|
|
expect(cleared.params).toEqual({ ruleGroupIds: ["quality"], future: { keep: true } });
|
|
expect(draftToParams(emptyDraft("movie"), {}, { totalChanged: false, autoFillChanged: true }).params).toEqual({});
|
|
});
|
|
});
|
|
|
|
describe("日历", () => {
|
|
it("月网格从周一开始,整周铺满;跨年切月", () => {
|
|
const first = monthStart("2026-09");
|
|
const g = monthGrid(first);
|
|
expect(g[0]).toEqual({ date: "2026-08-31", inMonth: false });
|
|
expect(g.length % 7).toBe(0);
|
|
expect(g.at(-1)).toEqual({ date: "2026-10-04", inMonth: false });
|
|
expect(g.filter((d) => d.inMonth)).toHaveLength(30);
|
|
expect(monthKey(shiftMonth(monthStart("2026-12"), 1))).toBe("2027-01");
|
|
expect(monthKey(monthStart("bad", new Date(2026, 4, 9)))).toBe("2026-05");
|
|
});
|
|
it("按日期分桶", () => {
|
|
const e = (date: string) => ({ date, subscriptionId: date }) as never;
|
|
const m = entriesByDate([e("2026-09-01"), e("2026-09-01"), e("2026-09-02")]);
|
|
expect(m.get("2026-09-01")).toHaveLength(2);
|
|
expect(m.get("2026-09-03")).toBeUndefined();
|
|
});
|
|
});
|