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("自定义路径"); 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 => ({ 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(""); }); });