Files
UHD_Media_Tool/apps/web/app/components/downloads/manual-download-dialog.test.ts
T
rootandClaude Opus 5.5 6e645bce8f feat(下载对话框): 学 MoviePilot 重做手动下载对话框
- 标题区:海报、作品名 (年份)、站点;资源行显示原始标题、促销、H&R、做种 / 下载、大小、发布时间
- 下载器与保存目录并排下拉,缺省「自动」,下方写明「将保存到」的实际路径;保留「自定义路径」
- 作品身份折叠区:类型 + TMDB 编号(可搜索作品),未识别时默认展开,指定后随下载绑定
- 已有版本先显示一行摘要,选一次性洗版才展开勾选列表
- 搜索、工作台、原站视图三处调用方补传资源字段

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
2026-10-06 12:10:03 +08:00

275 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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("");
});
});