import { afterEach, expect, test, vi } from "vitest"; import { parseResourceFolderFiles, resourcePlanToPayload, translateResourceCodes } from "./batchUpload.js"; afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); }); test("parses folder resource filenames into resource plans", () => { const files = [ file("头像框_星光_99_7_cover.png"), file("头像框_星光_99_7_animation.svga"), file("勋章_守护_长_cover.png"), file("勋章_守护_长_animation.pag"), file("背景卡_夜色_cover.png"), file("背景卡_夜色_animation.svga"), file("mic声波_律动_cover.png"), file("mic声波_律动_animation.svga"), file("礼物_玫瑰_520_cover.png"), file("礼物_玫瑰_520_animation.svga"), ]; const plan = parseResourceFolderFiles(files, 1770000000000); expect(plan.errors).toEqual([]); expect(plan.resources.map((item) => [item.name, item.resourceType, item.price, item.badgeForm])).toEqual([ ["星光", "avatar_frame", 99, "tile"], ["守护", "badge", 0, "strip"], ["夜色", "profile_card", 0, "tile"], ["律动", "mic_seat_animation", 0, "tile"], ["玫瑰", "gift", 520, "tile"], ]); expect(plan.resources.map((item) => item.resourceCode)).toEqual(["星光", "守护", "夜色", "律动", "玫瑰"]); expect(resourcePlanToPayload({ ...plan.resources[0], coverUrl: "cover", animationUrl: "animation" })).toMatchObject( { animationUrl: "animation", coinPrice: 99, previewUrl: "cover", priceType: "coin", resourceType: "avatar_frame", }, ); expect(resourcePlanToPayload({ ...plan.resources[4], coverUrl: "cover", animationUrl: "animation" })).toMatchObject( { coinPrice: 520, priceType: "coin", resourceType: "gift", }, ); expect(resourcePlanToPayload({ ...plan.resources[1], coverUrl: "cover", animationUrl: "animation" })).toMatchObject( { badgeForm: "strip", badgeKind: "normal", resourceType: "badge", }, ); expect( resourcePlanToPayload({ ...plan.resources[2], animationUrl: "animation", coverUrl: "cover", metadataJson: '{"profile_card_layout":{"source_width":1136,"source_height":1680,"color_content_width":750,"content_top":117,"content_bottom":1666,"content_height":1550,"content_top_ratio":0.069643,"content_height_ratio":0.922619,"detect_version":1}}', }), ).toMatchObject({ metadataJson: expect.stringContaining("profile_card_layout"), resourceType: "profile_card", }); }); test("silently ignores invalid and unpaired material", () => { const plan = parseResourceFolderFiles( [ file("frame/vip_TOPO2.png"), file("气泡_清新_cover.png"), file("头像框_星光_99_7_cover.png"), file("头像框_星光_99_7_animation.svga"), ], 1770000000000, ); expect(plan.errors).toEqual([]); expect(plan.resources).toHaveLength(1); expect(plan.resources[0]).toMatchObject({ name: "星光", resourceCode: "星光", resourceType: "avatar_frame" }); }); test("keeps mp4 alpha layout metadata for gift and vehicle batch payloads", () => { const giftPayload = resourcePlanToPayload({ animationUrl: "https://media.haiyihy.com/resource/gift.mp4", coverUrl: "cover", metadataJson: JSON.stringify({ mp4_alpha_layout: leftAlphaRightRgbLayout() }), name: "Love", price: 30000, resourceCode: "love", resourceType: "gift", }); const vehiclePayload = resourcePlanToPayload({ animationUrl: "https://media.haiyihy.com/resource/vehicle.mp4", coverUrl: "cover", metadataJson: JSON.stringify({ mp4_alpha_layout: leftAlphaRightRgbLayout() }), name: "Knight", price: 49000, resourceCode: "knight", resourceType: "vehicle", }); expect(JSON.parse(giftPayload.metadataJson)).toMatchObject({ animation_format: "mp4", mp4_alpha_layout: { alpha_frame: [0, 0, 750, 1334], alpha_layout: "alpha_left_rgb_right", rgb_frame: [750, 0, 750, 1334], }, }); expect(JSON.parse(vehiclePayload.metadataJson).mp4_alpha_layout.alpha_layout).toBe("alpha_left_rgb_right"); }); test("keeps profile card layout while adding mp4 alpha layout", () => { const payload = resourcePlanToPayload({ animationUrl: "https://media.haiyihy.com/resource/profile-card.mp4", coverUrl: "cover", metadataJson: JSON.stringify({ mp4_alpha_layout: leftAlphaRightRgbLayout(), profile_card_layout: { color_content_width: 750, content_bottom: 1666, content_height: 1550, content_height_ratio: 0.922619, content_top: 117, content_top_ratio: 0.069643, detect_version: 1, source_height: 1680, source_width: 1136, }, }), name: "Night", price: 0, resourceCode: "night", resourceType: "profile_card", }); const metadata = JSON.parse(payload.metadataJson); expect(metadata.profile_card_layout.content_top).toBe(117); expect(metadata.mp4_alpha_layout.rgb_frame).toEqual([750, 0, 750, 1334]); }); test("translates chinese names to english resource codes with API fallback", async () => { vi.spyOn(Math, "random") .mockReturnValueOnce(0) .mockReturnValueOnce(51.5 / 52) .mockReturnValueOnce(26 / 52) .mockReturnValueOnce(27 / 52); vi.stubGlobal( "fetch", vi.fn(async (url) => { if (String(url).includes("mymemory")) { return new Response(JSON.stringify({ responseData: { translatedText: "Starlight" } })); } return new Response("{}", { status: 500 }); }), ); const translated = await translateResourceCodes([ { name: "星光", resourceCode: "星光", resourceType: "avatar_frame", }, { name: "c11", resourceCode: "c11", resourceType: "avatar_frame", }, ]); expect(translated.map((item) => item.resourceCode)).toEqual(["starlight_aZ", "c11_AB"]); }); test("uses local dictionary when public translation APIs fail", async () => { vi.spyOn(Math, "random").mockReturnValue(0); vi.stubGlobal( "fetch", vi.fn(async () => new Response("{}", { status: 500 })), ); const translated = await translateResourceCodes([ { name: "守护", resourceCode: "守护", resourceType: "badge", }, ]); expect(translated[0].resourceCode).toBe("guardian_aa"); }); test("reports translate progress after suffix is added", async () => { vi.spyOn(Math, "random").mockReturnValue(0); vi.stubGlobal( "fetch", vi.fn(async () => new Response("{}", { status: 500 })), ); const onProgress = vi.fn(); await translateResourceCodes( [ { name: "守护", resourceCode: "守护", resourceType: "badge", }, ], { onProgress }, ); expect(onProgress).toHaveBeenCalledWith( expect.objectContaining({ completed: 1, index: 0, resource: expect.objectContaining({ resourceCode: "guardian_aa" }), total: 1, }), ); }); function file(name) { return new File(["payload"], name, { type: "image/png" }); } function leftAlphaRightRgbLayout() { return { alpha_frame: [0, 0, 750, 1334], alpha_layout: "alpha_left_rgb_right", confirmed: true, detect_version: 1, rgb_frame: [750, 0, 750, 1334], video_h: 1334, video_w: 1500, }; }