Files
NianAIGC/tests/zip-download.test.ts
2026-09-23 13:33:47 +08:00

82 lines
3.7 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from "vitest";
import { downloadFilesAsZip } from "@/lib/client/zip-download";
const png = Uint8Array.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 1, 2]);
const jpeg = Uint8Array.from([0xff, 0xd8, 0xff, 0xe0, 3, 4]);
const webp = Uint8Array.from([0x52, 0x49, 0x46, 0x46, 4, 0, 0, 0, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50, 0x38, 0x20, 5]);
const gif = Uint8Array.from([0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 6]);
const bmp = Uint8Array.from([0x42, 0x4d, 7]);
const ico = Uint8Array.from([0, 0, 1, 0, 8]);
function zipEntries(data: Uint8Array) {
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
const entries: { name: string; bytes: Uint8Array }[] = [];
let offset = 0;
while (view.getUint32(offset, true) === 0x04034b50) {
const size = view.getUint32(offset + 18, true);
const nameSize = view.getUint16(offset + 26, true);
const extraSize = view.getUint16(offset + 28, true);
const nameStart = offset + 30;
const bytesStart = nameStart + nameSize + extraSize;
entries.push({
name: new TextDecoder().decode(data.subarray(nameStart, nameStart + nameSize)),
bytes: data.slice(bytesStart, bytesStart + size)
});
offset = bytesStart + size;
}
return entries;
}
describe("ZIP layer downloads", () => {
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
it("uses image signatures for entry extensions and keeps the original bytes", async () => {
const files = [
{ name: "portrait-face.image", url: "/png" },
{ name: "portrait-face.image", url: "/png-copy" },
{ name: "sketch.image", url: "/jpeg" },
{ name: "original.JPEG", url: "/jpeg-original" },
{ name: "another.JPE", url: "/jpeg-jpe" },
{ name: "mask.image", url: "/webp" },
{ name: "animated.image", url: "/gif" },
{ name: "bitmap.image", url: "/bmp" },
{ name: "icon.image", url: "/ico" },
{ name: "invalid-webp.image", url: "/invalid-webp" },
{ name: "unrecognized.image", url: "/unknown" }
];
const bytesByUrl = new Map([
["/png", png], ["/png-copy", png], ["/jpeg", jpeg],
["/jpeg-original", jpeg], ["/jpeg-jpe", jpeg], ["/webp", webp],
["/gif", gif], ["/bmp", bmp], ["/ico", ico],
["/invalid-webp", Uint8Array.from([0x52, 0x49, 0x46, 0x46, 4, 0, 0, 0, 0x57, 0x45, 0x42, 0x50, 5])],
["/unknown", Uint8Array.from([1, 2, 3])]
]);
let archive: Blob | undefined;
const anchor = { href: "", download: "", click: vi.fn(), remove: vi.fn() };
vi.stubGlobal("fetch", vi.fn(async (url: string) => new Response(bytesByUrl.get(url), { status: 200 })));
vi.stubGlobal("document", { createElement: () => anchor, body: { appendChild: vi.fn() } });
vi.stubGlobal("window", { setTimeout: vi.fn() });
vi.stubGlobal("URL", { createObjectURL: (blob: Blob) => { archive = blob; return "blob:layers"; }, revokeObjectURL: vi.fn() });
await downloadFilesAsZip(files, "layers.zip");
expect(anchor.download).toBe("layers.zip");
expect(anchor.click).toHaveBeenCalledOnce();
expect(archive).toBeDefined();
const entries = zipEntries(new Uint8Array(await archive!.arrayBuffer()));
expect(entries.map((entry) => entry.name)).toEqual([
"portrait-face.png", "portrait-face-2.png", "sketch.jpg", "original.JPEG", "another.JPE",
"mask.webp", "animated.gif", "bitmap.bmp", "icon.ico", "invalid-webp.image", "unrecognized.image"
]);
expect(entries.map((entry) => entry.bytes)).toEqual([
png, png, jpeg, jpeg, jpeg, webp, gif, bmp, ico,
Uint8Array.from([0x52, 0x49, 0x46, 0x46, 4, 0, 0, 0, 0x57, 0x45, 0x42, 0x50, 5]),
Uint8Array.from([1, 2, 3])
]);
});
});