82 lines
3.7 KiB
TypeScript
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])
|
|
]);
|
|
});
|
|
});
|