mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-11-13 17:08:20 +00:00
test: add download zip flow
This commit is contained in:
@@ -62,7 +62,7 @@
|
||||
? `${result.width} × ${result.height} px`
|
||||
: "—"
|
||||
: resultKind === "files"
|
||||
? fileResult
|
||||
? fileResult && fileResult.files.length > 0
|
||||
? `${fileResult.files.length} ${t("resultCard.parts")}`
|
||||
: "—"
|
||||
: textResult
|
||||
@@ -74,7 +74,7 @@
|
||||
resultKind === "image"
|
||||
? Boolean(result)
|
||||
: resultKind === "files"
|
||||
? Boolean(fileResult)
|
||||
? Boolean(fileResult && fileResult.files.length > 0)
|
||||
: Boolean(textResult),
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import SchemaPreview from "$lib/components/SchemaPreview.svelte";
|
||||
import { debounce } from "$lib/core/debounce";
|
||||
import { ToolError, type ErrorVars } from "$lib/core/errors";
|
||||
import { decodeFile, encode } from "$lib/core/io";
|
||||
import { decodeFile, downloadBlob, encode } from "$lib/core/io";
|
||||
import type { PixelImage } from "$lib/core/types";
|
||||
import { execute } from "$lib/executor";
|
||||
import {
|
||||
@@ -136,23 +136,26 @@
|
||||
|
||||
async function download() {
|
||||
if (!schema) return;
|
||||
if (resultKind === "files") {
|
||||
if (!fileResult) return;
|
||||
await downloadZip(fileResult.files, `${tool.id}.zip`);
|
||||
return;
|
||||
running = true;
|
||||
displayError = null;
|
||||
try {
|
||||
if (resultKind === "files") {
|
||||
if (!fileResult) return;
|
||||
await downloadZip(fileResult.files, `${tool.id}.zip`);
|
||||
return;
|
||||
}
|
||||
if (!result) return;
|
||||
const out = tool.output;
|
||||
const quality = out?.qualityParamId
|
||||
? Number(values[out.qualityParamId]) / 100
|
||||
: undefined;
|
||||
const blob = await encode(result, out?.mime ?? "image/png", quality);
|
||||
downloadBlob(blob, `${tool.id}.${out?.ext ?? "png"}`);
|
||||
} catch (e) {
|
||||
displayError = toDisplayError(e);
|
||||
} finally {
|
||||
running = false;
|
||||
}
|
||||
if (!result) return;
|
||||
const out = tool.output;
|
||||
const quality = out?.qualityParamId
|
||||
? Number(values[out.qualityParamId]) / 100
|
||||
: undefined;
|
||||
const blob = await encode(result, out?.mime ?? "image/png", quality);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `${tool.id}.${out?.ext ?? "png"}`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
async function copyText() {
|
||||
@@ -170,13 +173,12 @@
|
||||
|
||||
async function downloadText() {
|
||||
if (!textResult) return;
|
||||
const blob = new Blob([textResult], { type: "text/plain" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `${tool.id}.txt`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
try {
|
||||
const blob = new Blob([textResult], { type: "text/plain" });
|
||||
downloadBlob(blob, `${tool.id}.txt`);
|
||||
} catch (e) {
|
||||
displayError = toDisplayError(e);
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { ToolError } from "../core/errors";
|
||||
import type { PixelImage } from "../core/types";
|
||||
import {
|
||||
createExecutor,
|
||||
type ExecuteContext,
|
||||
@@ -8,6 +9,7 @@ import {
|
||||
} from "./executor";
|
||||
import {
|
||||
decodeWorkerResponse,
|
||||
encodeWorkerResponse,
|
||||
type WorkerRequest,
|
||||
type WorkerResponse,
|
||||
} from "./protocol";
|
||||
@@ -244,6 +246,38 @@ describe("createExecutor", () => {
|
||||
files: [{ name: "part.png", image: { width: 1, height: 1 } }],
|
||||
});
|
||||
});
|
||||
|
||||
it("round-trips FileResult through the worker protocol", () => {
|
||||
const fileImage: PixelImage = {
|
||||
width: 1,
|
||||
height: 1,
|
||||
data: new Uint8ClampedArray([1, 2, 3, 4]),
|
||||
};
|
||||
const fileResult = {
|
||||
files: [{ name: "part-1-1.png", image: fileImage }],
|
||||
};
|
||||
const encoded = encodeWorkerResponse(4, fileResult);
|
||||
|
||||
expect(encoded.transfer).toHaveLength(1);
|
||||
expect(decodeWorkerResponse(encoded.response)).toEqual(fileResult);
|
||||
});
|
||||
|
||||
it("falls back when a files payload is malformed", async () => {
|
||||
const { factory, workers } = createFactory();
|
||||
const execute = createExecutor({ workerFactory: factory });
|
||||
const tool = makeTool(() => "direct");
|
||||
const promise = execute(tool, { params: {} });
|
||||
const worker = workers[0];
|
||||
const request = worker.requests[0];
|
||||
worker.respond({
|
||||
id: request.id,
|
||||
ok: true,
|
||||
files: [{ name: "part.png", width: 1, height: 1 }],
|
||||
} as unknown as WorkerResponse);
|
||||
|
||||
await expect(promise).resolves.toBe("direct");
|
||||
expect(worker.terminated).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleWorkerRequest", () => {
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { unzipSync, zipSync } from "fflate";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { PixelImage } from "./core/types";
|
||||
import { buildZipEntries, type ZipImageEncoder } from "./zip";
|
||||
import type { ToolImageFile } from "./registry";
|
||||
|
||||
function image(value: number): PixelImage {
|
||||
return {
|
||||
width: 1,
|
||||
height: 1,
|
||||
data: new Uint8ClampedArray([value, value + 1, value + 2, 255]),
|
||||
};
|
||||
}
|
||||
|
||||
describe("buildZipEntries", () => {
|
||||
it("preserves file order and encoded bytes", async () => {
|
||||
const files: ToolImageFile[] = [
|
||||
{ name: "part-1-1.png", image: image(10) },
|
||||
{ name: "part-1-2.png", image: image(20) },
|
||||
];
|
||||
const encodeImage = vi.fn(
|
||||
async (source: PixelImage) =>
|
||||
new Uint8Array([source.data[0], source.data[3]]),
|
||||
);
|
||||
|
||||
const entries = await buildZipEntries(files, encodeImage);
|
||||
const unzipped = unzipSync(zipSync(entries));
|
||||
|
||||
expect(Object.keys(entries)).toEqual(["part-1-1.png", "part-1-2.png"]);
|
||||
expect(Object.keys(unzipped)).toEqual(["part-1-1.png", "part-1-2.png"]);
|
||||
expect(unzipped["part-1-1.png"]).toEqual(new Uint8Array([10, 255]));
|
||||
expect(unzipped["part-1-2.png"]).toEqual(new Uint8Array([20, 255]));
|
||||
expect(encodeImage).toHaveBeenCalledTimes(2);
|
||||
expect(encodeImage.mock.calls[0]?.[0]).toBe(files[0]?.image);
|
||||
expect(encodeImage.mock.calls[1]?.[0]).toBe(files[1]?.image);
|
||||
});
|
||||
|
||||
it("returns an empty entry set without calling the encoder", async () => {
|
||||
const encodeImage = vi.fn<ZipImageEncoder>();
|
||||
|
||||
await expect(buildZipEntries([], encodeImage)).resolves.toEqual({});
|
||||
expect(encodeImage).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
+18
-5
@@ -1,7 +1,21 @@
|
||||
import { zipSync } from "fflate";
|
||||
import { downloadBlob, encode } from "./core/io";
|
||||
import type { PixelImage } from "./core/types";
|
||||
import type { ToolImageFile } from "./registry";
|
||||
|
||||
export type ZipImageEncoder = (image: PixelImage) => Promise<Uint8Array>;
|
||||
|
||||
export async function buildZipEntries(
|
||||
files: ToolImageFile[],
|
||||
encodeImage: ZipImageEncoder,
|
||||
): Promise<Record<string, Uint8Array>> {
|
||||
const entries: Record<string, Uint8Array> = {};
|
||||
for (const file of files) {
|
||||
entries[file.name] = await encodeImage(file.image);
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Собирает набор картинок (1 → many) в zip-архив и скачивает его.
|
||||
* Картинки кодируются как PNG; кодирование требует DOM — вызов из UI-слоя.
|
||||
@@ -11,11 +25,10 @@ export async function downloadZip(
|
||||
zipName: string,
|
||||
): Promise<void> {
|
||||
if (files.length === 0) return;
|
||||
const entries: Record<string, Uint8Array> = {};
|
||||
for (const file of files) {
|
||||
const blob = await encode(file.image, "image/png");
|
||||
entries[file.name] = new Uint8Array(await blob.arrayBuffer());
|
||||
}
|
||||
const entries = await buildZipEntries(files, async (image) => {
|
||||
const blob = await encode(image, "image/png");
|
||||
return new Uint8Array(await blob.arrayBuffer());
|
||||
});
|
||||
const zipped = zipSync(entries);
|
||||
downloadBlob(new Blob([zipped], { type: "application/zip" }), zipName);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user