test: add download mime, quality verification

This commit is contained in:
2026-09-26 03:33:09 +05:00
parent 746d6a6c8f
commit 712fd3cb2e
10 changed files with 181 additions and 33 deletions
+13
View File
@@ -1,3 +1,4 @@
import { readFile } from "node:fs/promises";
import {
expect,
type Download,
@@ -88,6 +89,18 @@ export async function downloadResultFile(page: Page): Promise<Download> {
return download;
}
export async function downloadResultBytes(
page: Page,
): Promise<{ name: string; bytes: Uint8Array }> {
const download = await downloadResultFile(page);
const path = await download.path();
if (!path) throw new Error("download path is unavailable");
return {
name: download.suggestedFilename(),
bytes: new Uint8Array(await readFile(path)),
};
}
export async function downloadResult(page: Page): Promise<string> {
return (await downloadResultFile(page)).suggestedFilename();
}
+52 -11
View File
@@ -1,4 +1,3 @@
import { readFile } from "node:fs/promises";
import { unzipSync } from "fflate";
import { expect, test } from "playwright/test";
import {
@@ -9,8 +8,7 @@ import {
opaquePng,
} from "./helpers/fixtures";
import {
downloadResult,
downloadResultFile,
downloadResultBytes,
errorAlert,
expectNoErrorAlert,
expectNoErrors,
@@ -58,17 +56,62 @@ test("flip-png: full flow - upload, result, download", async ({ page }) => {
await openTool(page, "flip-png");
await uploadImage(page, landscapePng);
await expect(resultImage(page)).toBeVisible();
expect(await downloadResult(page)).toBe("flip-png.png");
const { name, bytes } = await downloadResultBytes(page);
expect(name).toBe("flip-png.png");
expect(Array.from(bytes.subarray(0, 8))).toEqual([
137, 80, 78, 71, 13, 10, 26, 10,
]);
await expectNoErrorAlert(page);
expectNoErrors(sink);
});
test("convert-png-to-jpg: produces a downloadable jpg", async ({ page }) => {
test("convert-png-to-jpg: produces JPEG and quality affects size", async ({
page,
}) => {
const sink = trackErrors(page);
await openTool(page, "convert-png-to-jpg");
await uploadImage(page, largePng);
await expect(resultImage(page)).toBeVisible();
const quality = rangeInput(page, "Quality");
await quality.fill("10");
const low = await downloadResultBytes(page);
expect(low.name).toBe("convert-png-to-jpg.jpg");
expect(Array.from(low.bytes.subarray(0, 3))).toEqual([255, 216, 255]);
await quality.fill("90");
const high = await downloadResultBytes(page);
expect(high.bytes.length).toBeGreaterThan(low.bytes.length);
await expectNoErrorAlert(page);
expectNoErrors(sink);
});
test("convert-png-to-webp: produces WebP and quality affects size", async ({
page,
}) => {
const sink = trackErrors(page);
await openTool(page, "convert-png-to-webp");
await uploadImage(page, largePng);
await expect(resultImage(page)).toBeVisible();
const quality = rangeInput(page, "Quality");
await quality.fill("10");
const low = await downloadResultBytes(page);
expect(low.name).toBe("convert-png-to-webp.webp");
expect(Array.from(low.bytes.subarray(0, 4))).toEqual([82, 73, 70, 70]);
expect(Array.from(low.bytes.subarray(8, 12))).toEqual([87, 69, 66, 80]);
await quality.fill("90");
const high = await downloadResultBytes(page);
expect(high.bytes.length).toBeGreaterThan(low.bytes.length);
await expectNoErrorAlert(page);
expectNoErrors(sink);
});
test("png-to-bmp: produces a BMP download", async ({ page }) => {
const sink = trackErrors(page);
await openTool(page, "png-to-bmp");
await uploadImage(page, opaquePng);
await expect(resultImage(page)).toBeVisible();
expect(await downloadResult(page)).toBe("convert-png-to-jpg.jpg");
const { name, bytes } = await downloadResultBytes(page);
expect(name).toBe("png-to-bmp.bmp");
expect(Array.from(bytes.subarray(0, 2))).toEqual([66, 77]);
await expectNoErrorAlert(page);
expectNoErrors(sink);
});
@@ -102,11 +145,9 @@ test("split-into-parts-png downloads a zip with named PNG parts", async ({
await rangeInput(page, "Rows").fill("2");
await expect(page.getByText("part-1-3.png")).toBeVisible();
const download = await downloadResultFile(page);
expect(download.suggestedFilename()).toBe("split-into-parts-png.zip");
const path = await download.path();
if (!path) throw new Error("download path is unavailable");
const entries = unzipSync(new Uint8Array(await readFile(path)));
const { name, bytes } = await downloadResultBytes(page);
expect(name).toBe("split-into-parts-png.zip");
const entries = unzipSync(bytes);
expect(Object.keys(entries)).toEqual([
"part-1-1.png",
+35 -1
View File
@@ -1,5 +1,9 @@
import { describe, expect, it } from "vitest";
import { isSupportedImage, unsupportedImageError } from "./io";
import {
isSupportedImage,
unsupportedImageError,
validateOutputQuality,
} from "./io";
describe("isSupportedImage", () => {
it.each([
@@ -38,3 +42,33 @@ describe("unsupportedImageError", () => {
expect(err.vars?.type).toBe("unknown");
});
});
describe("validateOutputQuality", () => {
it.each(["image/jpeg", "image/webp"] as const)(
"принимает границы 0 и 1 для %s",
(mime) => {
expect(() => validateOutputQuality(mime, 0)).not.toThrow();
expect(() => validateOutputQuality(mime, 1)).not.toThrow();
},
);
it.each(["image/jpeg", "image/webp"] as const)(
"отклоняет значение вне 0..1 для %s",
(mime) => {
expect(() => validateOutputQuality(mime, -0.01)).toThrow(
"errors.qualityRange",
);
expect(() => validateOutputQuality(mime, 1.01)).toThrow(
"errors.qualityRange",
);
expect(() => validateOutputQuality(mime, Number.NaN)).toThrow(
"errors.qualityRange",
);
},
);
it("не применяет quality к PNG и BMP", () => {
expect(() => validateOutputQuality("image/png", 2)).not.toThrow();
expect(() => validateOutputQuality("image/bmp", 2)).not.toThrow();
});
});
+14 -5
View File
@@ -84,6 +84,19 @@ export async function decodeTextImage(text: string): Promise<PixelImage> {
return await decodeBytes(bytes);
}
export function validateOutputQuality(
mime: OutputMime,
quality?: number,
): void {
if (mime !== "image/jpeg" && mime !== "image/webp") return;
if (
quality !== undefined &&
(!Number.isFinite(quality) || quality < 0 || quality > 1)
) {
throw new ToolError("errors.qualityRange");
}
}
export async function encode(
img: PixelImage,
mime: OutputMime = "image/png",
@@ -92,11 +105,7 @@ export async function encode(
if (mime === "image/bmp") {
return new Blob([encodeBmpBytes(img)], { type: mime });
}
if (mime === "image/jpeg" || mime === "image/webp") {
if (quality !== undefined && (quality < 0 || quality > 1)) {
throw new ToolError("errors.qualityRange");
}
}
validateOutputQuality(mime, quality);
const canvas = document.createElement("canvas");
canvas.width = img.width;
canvas.height = img.height;
+34
View File
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import { TOOLS } from ".";
import { PREVIEW_GROUPS } from "../catalog";
import type { PixelImage } from "../core/types";
import type { OutputMime } from "../core/io";
import { defaultSchemaParams, sanitizeSchemaParams } from "../registry-schema";
import type { FileResult, ToolResult } from "./types";
@@ -78,6 +79,39 @@ describe("registry-new (переведённые инструменты)", () =>
}
});
it("output metadata согласован с MIME и quality-схемой", () => {
const extensions: Record<OutputMime, string> = {
"image/png": "png",
"image/jpeg": "jpg",
"image/webp": "webp",
"image/bmp": "bmp",
};
for (const tool of TOOLS) {
const output = tool.output;
if (!output) continue;
expect(extensions[output.mime], `${tool.id}: mime`).toBe(output.ext);
if (output.qualityParamId) {
const qualityField = tool.schema.fields[output.qualityParamId];
expect(
qualityField,
`${tool.id}.${output.qualityParamId}`,
).toBeDefined();
expect(["number", "slider"], `${tool.id}: quality kind`).toContain(
qualityField?.spec.kind,
);
} else if (output.mime === "image/jpeg" || output.mime === "image/webp") {
expect(output.qualityParamId, `${tool.id}: quality`).toBeTruthy();
}
if (output.mime === "image/bmp") {
expect(
output.qualityParamId,
`${tool.id}: bmp quality`,
).toBeUndefined();
}
}
});
it("source-aware dimension defaults применяются только к image tools", () => {
for (const tool of TOOLS) {
for (const field of Object.values(tool.schema.fields)) {