- 标题区:海报、作品名 (年份)、站点;资源行显示原始标题、促销、H&R、做种 / 下载、大小、发布时间 - 下载器与保存目录并排下拉,缺省「自动」,下方写明「将保存到」的实际路径;保留「自定义路径」 - 作品身份折叠区:类型 + TMDB 编号(可搜索作品),未识别时默认展开,指定后随下载绑定 - 已有版本先显示一行摘要,选一次性洗版才展开勾选列表 - 搜索、工作台、原站视图三处调用方补传资源字段 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
275 lines
10 KiB
TypeScript
275 lines
10 KiB
TypeScript
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||
import type { AssetVersion, DownloadSavePaths, DownloadSaveTarget } from "@umt/contracts";
|
||
import { createElement, type ReactNode } from "react";
|
||
import { renderToStaticMarkup } from "react-dom/server";
|
||
import { describe, expect, it } from "vitest";
|
||
import { ExistingVersionsSection, IdentitySection, SaveTargetFields } from "./manual-download-dialog.tsx";
|
||
import {
|
||
buildManualOptions,
|
||
CUSTOM_DIR,
|
||
kindHintOf,
|
||
resolveSavePath,
|
||
savePathText,
|
||
targetOf,
|
||
versionSummaryText,
|
||
} from "./manual-download-model.ts";
|
||
|
||
// 手动下载对话框:保存目录解析与显示、未识别时身份区展开并随提交带上身份、已有版本摘要与洗版展开。
|
||
|
||
const A = "11111111-1111-4111-8111-111111111111";
|
||
const B = "22222222-2222-4222-8222-222222222222";
|
||
const E = "33333333-3333-4333-8333-333333333333";
|
||
|
||
const paths: DownloadSavePaths = {
|
||
autoInstanceId: B,
|
||
instances: [
|
||
{
|
||
instanceId: A,
|
||
name: "qb-a",
|
||
isDefault: false,
|
||
dirs: [{ path: "/downloads", hostPath: "/downloads", isDefault: true }],
|
||
},
|
||
{
|
||
instanceId: B,
|
||
name: "qb-main",
|
||
isDefault: true,
|
||
dirs: [
|
||
{ path: "/data/downloads", hostPath: "/mnt/dl", isDefault: true },
|
||
{ path: "/data/tv", hostPath: "/mnt/tv", isDefault: false },
|
||
],
|
||
},
|
||
],
|
||
};
|
||
|
||
const render = (node: ReactNode) =>
|
||
renderToStaticMarkup(createElement(QueryClientProvider, { client: new QueryClient() }, node));
|
||
|
||
const base = {
|
||
entityId: undefined,
|
||
currentTmdb: null,
|
||
identity: null,
|
||
instanceId: "",
|
||
target: targetOf(paths, ""),
|
||
dir: "",
|
||
custom: "",
|
||
allowUnrecognized: false,
|
||
mode: "normal" as const,
|
||
selected: [],
|
||
};
|
||
|
||
describe("保存目录", () => {
|
||
it("自动:解析出自动下载器的默认保存目录;本机路径不同时括注", () => {
|
||
const t = targetOf(paths, "");
|
||
expect(t?.instanceId).toBe(B);
|
||
const r = resolveSavePath(t, "", "");
|
||
expect(r).toEqual({ path: "/data/downloads", hostPath: "/mnt/dl" });
|
||
expect(savePathText(r!)).toBe("/data/downloads(本机 /mnt/dl)");
|
||
expect(resolveSavePath(targetOf(paths, A), "", "")).toEqual({ path: "/downloads", hostPath: "/downloads" });
|
||
expect(savePathText({ path: "/downloads", hostPath: "/downloads" })).toBe("/downloads");
|
||
});
|
||
|
||
it("选目录 / 自定义路径;自动下载器 + 指定目录时钉住解析出的下载器", () => {
|
||
const t = targetOf(paths, "");
|
||
expect(resolveSavePath(t, "/data/tv", "")).toEqual({ path: "/data/tv", hostPath: "/mnt/tv" });
|
||
expect(resolveSavePath(t, CUSTOM_DIR, " ")).toBeNull();
|
||
expect(resolveSavePath(t, CUSTOM_DIR, " /x/y ")).toEqual({ path: "/x/y", hostPath: null });
|
||
expect(buildManualOptions({ ...base, dir: "/data/tv" })).toMatchObject({
|
||
downloaderInstanceId: B,
|
||
savePath: "/data/tv",
|
||
});
|
||
const auto = buildManualOptions(base);
|
||
expect(auto).not.toHaveProperty("savePath");
|
||
expect(auto).not.toHaveProperty("downloaderInstanceId");
|
||
const custom = buildManualOptions({ ...base, dir: CUSTOM_DIR, custom: " /x/y " });
|
||
expect(custom).toMatchObject({ savePath: "/x/y" });
|
||
expect(custom).not.toHaveProperty("downloaderInstanceId");
|
||
});
|
||
|
||
it("界面:并排两个下拉,下方写明将保存到哪;自定义时展开输入框", () => {
|
||
const props = {
|
||
formId: "f",
|
||
instances: paths.instances,
|
||
autoInstanceId: B,
|
||
instanceId: "",
|
||
onInstanceChange: () => {},
|
||
target: targetOf(paths, "") as DownloadSaveTarget,
|
||
dir: "",
|
||
onDirChange: () => {},
|
||
custom: "",
|
||
onCustomChange: () => {},
|
||
};
|
||
const html = render(createElement(SaveTargetFields, props));
|
||
expect(html).toContain("下载器");
|
||
expect(html).toContain("自动:qb-main");
|
||
expect(html).toContain("将保存到:/data/downloads(本机 /mnt/dl)");
|
||
expect(html).not.toContain("自定义路径</label>");
|
||
const custom = render(createElement(SaveTargetFields, { ...props, dir: CUSTOM_DIR, custom: "/x" }));
|
||
expect(custom).toContain("将保存到:/x");
|
||
expect(custom).toContain('id="f-custom"');
|
||
const none = render(
|
||
createElement(SaveTargetFields, { ...props, instances: [], autoInstanceId: null, target: null }),
|
||
);
|
||
expect(none).toContain("没有可用的下载器");
|
||
});
|
||
});
|
||
|
||
describe("作品身份", () => {
|
||
const identityProps = {
|
||
formId: "f",
|
||
onToggle: () => {},
|
||
type: "movie" as const,
|
||
onTypeChange: () => {},
|
||
tmdbId: "",
|
||
onTmdbIdChange: () => {},
|
||
picked: null,
|
||
onPick: () => {},
|
||
onClearPick: () => {},
|
||
changed: false,
|
||
subject: "Spider-Man.2026.1080p",
|
||
};
|
||
|
||
it("未识别:默认展开,显示类型与 TMDB 编号输入和提示", () => {
|
||
const html = render(
|
||
createElement(IdentitySection, { ...identityProps, open: true, current: null, unrecognized: true }),
|
||
);
|
||
expect(html).toContain('aria-expanded="true"');
|
||
expect(html).toContain("未识别");
|
||
expect(html).toContain("TMDB 编号");
|
||
expect(html).toContain('id="f-tmdb"');
|
||
expect(html).toContain("暂未识别到作品");
|
||
});
|
||
|
||
it("已识别:折叠只显示当前作品", () => {
|
||
const html = render(
|
||
createElement(IdentitySection, {
|
||
...identityProps,
|
||
open: false,
|
||
current: { title: "蜘蛛侠:崭新之日", year: 2026, kind: "movie", tmdbId: "969681" },
|
||
unrecognized: false,
|
||
}),
|
||
);
|
||
expect(html).toContain('aria-expanded="false"');
|
||
expect(html).toContain("蜘蛛侠:崭新之日 (2026) · 电影 · TMDB 969681");
|
||
expect(html).not.toContain('id="f-tmdb"');
|
||
});
|
||
|
||
it("提交:未识别时指定 TMDB 身份就带上 identity,不再要求确认;本地作品给 mediaEntityId", () => {
|
||
const tmdb = buildManualOptions({
|
||
...base,
|
||
allowUnrecognized: true,
|
||
identity: { kind: "tmdb", type: "tv", id: "1399" },
|
||
});
|
||
expect(tmdb).toMatchObject({ identity: { external: { provider: "tmdb", entityType: "tv", externalId: "1399" } } });
|
||
expect(tmdb).not.toHaveProperty("allowUnrecognized");
|
||
expect(tmdb).not.toHaveProperty("entityId");
|
||
const local = buildManualOptions({ ...base, identity: { kind: "entity", id: E, title: "x" } });
|
||
expect(local.identity).toEqual({ mediaEntityId: E });
|
||
// 没指定或编号不完整:照旧确认后下载
|
||
expect(buildManualOptions({ ...base, allowUnrecognized: true })).toMatchObject({ allowUnrecognized: true });
|
||
expect(buildManualOptions({ ...base, identity: { kind: "tmdb", type: "movie", id: "12a" } })).not.toHaveProperty(
|
||
"identity",
|
||
);
|
||
});
|
||
|
||
it("已识别且身份没改:只带 entityId;改了身份就换成 identity 并退回正常入库", () => {
|
||
const same = buildManualOptions({
|
||
...base,
|
||
entityId: E,
|
||
currentTmdb: { type: "movie", id: "969681" },
|
||
identity: { kind: "tmdb", type: "movie", id: "969681" },
|
||
mode: "replace",
|
||
selected: [A],
|
||
});
|
||
expect(same).toMatchObject({ entityId: E, importMode: "replace", replacementAssetIds: [A] });
|
||
expect(same).not.toHaveProperty("identity");
|
||
const other = buildManualOptions({
|
||
...base,
|
||
entityId: E,
|
||
currentTmdb: { type: "movie", id: "969681" },
|
||
identity: { kind: "tmdb", type: "movie", id: "1" },
|
||
mode: "replace",
|
||
selected: [A],
|
||
});
|
||
expect(other).toMatchObject({ importMode: "normal", replacementAssetIds: [] });
|
||
expect(other).not.toHaveProperty("entityId");
|
||
});
|
||
|
||
it("类型缺省按标题解析出的季集猜", () => {
|
||
expect(kindHintOf({ season: 1 })).toBe("tv");
|
||
expect(kindHintOf({ season: null, episodes: { start: 1, end: 2 } })).toBe("tv");
|
||
expect(kindHintOf({ season: null, episodes: null })).toBe("movie");
|
||
expect(kindHintOf(undefined)).toBe("movie");
|
||
});
|
||
});
|
||
|
||
describe("已有版本", () => {
|
||
const version = (id: string, p: Partial<AssetVersion> = {}): AssetVersion =>
|
||
({
|
||
assetId: id,
|
||
status: "active",
|
||
resolution: "1080p",
|
||
source: "WEB-DL",
|
||
isEncode: true,
|
||
videoCodec: "H.264",
|
||
bitDepth: null,
|
||
hdr: [],
|
||
audio: [],
|
||
subtitleLanguages: [],
|
||
releaseGroup: null,
|
||
edition: null,
|
||
sizeBytes: 9_556_302_233,
|
||
durationSec: null,
|
||
fileName: `${id}.mkv`,
|
||
relativePath: null,
|
||
absolutePath: null,
|
||
libraryRootId: null,
|
||
locationStatus: null,
|
||
organizedAt: "2026-10-01T00:00:00.000Z",
|
||
downloadTaskId: null,
|
||
...p,
|
||
}) as AssetVersion;
|
||
|
||
it("摘要一行:规格 + 大小;多种规格只列前两种", () => {
|
||
expect(
|
||
versionSummaryText({
|
||
versionCount: 1,
|
||
sizeBytes: 9_556_302_233,
|
||
specs: [{ resolution: "1080p", source: "WEB-DL", hdr: [], count: 1 }],
|
||
}),
|
||
).toBe("媒体库已有:1080p WEB-DL · 8.9 GB");
|
||
expect(
|
||
versionSummaryText({
|
||
versionCount: 3,
|
||
sizeBytes: 0,
|
||
specs: [
|
||
{ resolution: "2160p", source: "WEB-DL", hdr: ["DV"], count: 1 },
|
||
{ resolution: "1080p", source: "BluRay", hdr: [], count: 1 },
|
||
{ resolution: "720p", source: "HDTV", hdr: [], count: 1 },
|
||
],
|
||
}),
|
||
).toBe("媒体库已有:2160p WEB-DL DV、1080p BluRay 等 3 种 · 3 个版本");
|
||
expect(versionSummaryText({ versionCount: 0, sizeBytes: 0, specs: [] })).toBeNull();
|
||
});
|
||
|
||
it("正常入库只显示摘要;选一次性洗版才展开勾选列表", () => {
|
||
const props = {
|
||
formId: "f",
|
||
loading: false,
|
||
error: false,
|
||
summary: "媒体库已有:1080p WEB-DL · 8.9 GB",
|
||
versions: [version(A), version(B)],
|
||
onModeChange: () => {},
|
||
selected: [A],
|
||
onSelectedChange: () => {},
|
||
};
|
||
const normal = render(createElement(ExistingVersionsSection, { ...props, mode: "normal" }));
|
||
expect(normal).toContain("媒体库已有:1080p WEB-DL · 8.9 GB");
|
||
expect(normal).not.toContain('type="checkbox"');
|
||
const replace = render(createElement(ExistingVersionsSection, { ...props, mode: "replace" }));
|
||
expect(replace).toContain("选择要替换的已有文件");
|
||
expect(replace.match(/type="checkbox"/g)).toHaveLength(2);
|
||
expect(replace).toContain(`${A}.mkv`);
|
||
expect(render(createElement(ExistingVersionsSection, { ...props, versions: [], mode: "normal" }))).toBe("");
|
||
});
|
||
});
|