Files
UHD_Media_Tool/apps/web/app/components/downloads/manual-download-dialog.test.ts
T

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("");
});
});