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]) ]); }); });