mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-11-13 17:08:20 +00:00
test: add download mime, quality verification
This commit is contained in:
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user