mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
Fix 2 failing unit tests (landing hero text mismatch) and broken coverage tooling (brace-expansion v5 override breaking minimatch). Add ~1,097 new test cases via 14-agent parallel expansion: - Unit: +290 tests (AI bridge, image-engine, stores, API helpers) - Integration: +504 tests (all tools, cross-format matrix, adversarial) - E2E: +363 tests (navigation, tool UI, batch/pipeline, settings, visual regression, accessibility, performance, cross-browser) Total: 4,223 unit + 6,057 integration + 1,563 E2E = 11,843 tests
202 lines
7.5 KiB
TypeScript
202 lines
7.5 KiB
TypeScript
/**
|
|
* Expanded engine tests covering:
|
|
* - FORMAT_MAP coverage for jxl output
|
|
* - All OPERATION_MAP entries not exercised through processImage in engine.test.ts
|
|
* - Empty operations list with format conversion
|
|
* - Multiple operations with output format
|
|
* - Queued operation ordering
|
|
* - processImage with ico/jp2/qoi/bmp/heic in OutputFormat (unsupported check)
|
|
*/
|
|
import { readFileSync } from "node:fs";
|
|
import { createRequire } from "node:module";
|
|
import path from "node:path";
|
|
import { beforeAll, describe, expect, it } from "vitest";
|
|
|
|
const require = createRequire(
|
|
path.resolve(__dirname, "../../../packages/image-engine/src/index.ts"),
|
|
);
|
|
const _sharp = require("sharp") as typeof import("sharp").default;
|
|
|
|
import { processImage } from "@snapotter/image-engine";
|
|
|
|
const FIXTURES_DIR = path.resolve(__dirname, "../../fixtures");
|
|
|
|
let png200x150: Buffer;
|
|
let jpg100x100: Buffer;
|
|
|
|
beforeAll(() => {
|
|
png200x150 = readFileSync(path.join(FIXTURES_DIR, "test-200x150.png"));
|
|
jpg100x100 = readFileSync(path.join(FIXTURES_DIR, "test-100x100.jpg"));
|
|
});
|
|
|
|
describe("processImage FORMAT_MAP coverage", () => {
|
|
it("jxl is a valid FORMAT_MAP entry (may fail if libvips lacks JXL support)", async () => {
|
|
// JXL support depends on the local libvips build. On systems without libjxl,
|
|
// Sharp throws "class jxlsave_buffer not found". We verify the FORMAT_MAP
|
|
// entry exists by checking the error message is format-specific, not "unknown".
|
|
try {
|
|
const result = await processImage(png200x150, [], "jxl");
|
|
expect(result.buffer.length).toBeGreaterThan(0);
|
|
} catch (err: unknown) {
|
|
// If JXL is not supported, Sharp throws a vips-specific error, not our
|
|
// "Unsupported output format" error -- meaning the FORMAT_MAP entry works.
|
|
const msg = err instanceof Error ? err.message : "";
|
|
expect(msg).not.toContain("Unsupported output format");
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("processImage OPERATION_MAP full coverage", () => {
|
|
it("compress operation through processImage", async () => {
|
|
const result = await processImage(png200x150, [
|
|
{ type: "compress", options: { quality: 60, format: "jpg" } },
|
|
]);
|
|
expect(result.buffer.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("convert operation through processImage", async () => {
|
|
const result = await processImage(png200x150, [
|
|
{ type: "convert", options: { format: "webp" } },
|
|
]);
|
|
expect(result.info.format).toBe("webp");
|
|
});
|
|
|
|
it("strip-metadata operation through processImage", async () => {
|
|
const result = await processImage(jpg100x100, [{ type: "strip-metadata", options: {} }]);
|
|
expect(result.buffer.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("crop operation through processImage", async () => {
|
|
const result = await processImage(png200x150, [
|
|
{ type: "crop", options: { left: 10, top: 10, width: 50, height: 40 } },
|
|
]);
|
|
expect(result.info.width).toBe(50);
|
|
expect(result.info.height).toBe(40);
|
|
});
|
|
|
|
it("flip operation through processImage", async () => {
|
|
const result = await processImage(png200x150, [
|
|
{ type: "flip", options: { horizontal: true, vertical: true } },
|
|
]);
|
|
expect(result.info.width).toBe(200);
|
|
expect(result.info.height).toBe(150);
|
|
});
|
|
|
|
it("resize operation through processImage", async () => {
|
|
const result = await processImage(png200x150, [
|
|
{ type: "resize", options: { percentage: 50 } },
|
|
]);
|
|
expect(result.info.width).toBe(100);
|
|
expect(result.info.height).toBe(75);
|
|
});
|
|
|
|
it("rotate operation through processImage", async () => {
|
|
const result = await processImage(png200x150, [{ type: "rotate", options: { angle: 90 } }]);
|
|
expect(result.info.width).toBe(150);
|
|
expect(result.info.height).toBe(200);
|
|
});
|
|
|
|
it("brightness operation through processImage", async () => {
|
|
const result = await processImage(png200x150, [{ type: "brightness", options: { value: 20 } }]);
|
|
expect(result.buffer.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("contrast operation through processImage", async () => {
|
|
const result = await processImage(png200x150, [{ type: "contrast", options: { value: 15 } }]);
|
|
expect(result.buffer.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("saturation operation through processImage", async () => {
|
|
const result = await processImage(png200x150, [
|
|
{ type: "saturation", options: { value: -30 } },
|
|
]);
|
|
expect(result.buffer.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("color-channels operation through processImage", async () => {
|
|
const result = await processImage(png200x150, [
|
|
{ type: "color-channels", options: { red: 120, green: 80, blue: 100 } },
|
|
]);
|
|
expect(result.buffer.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("grayscale operation through processImage", async () => {
|
|
const result = await processImage(png200x150, [{ type: "grayscale", options: {} }]);
|
|
expect(result.buffer.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("sepia operation through processImage", async () => {
|
|
const result = await processImage(png200x150, [{ type: "sepia", options: {} }]);
|
|
expect(result.buffer.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("invert operation through processImage", async () => {
|
|
const result = await processImage(png200x150, [{ type: "invert", options: {} }]);
|
|
expect(result.buffer.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe("processImage unsupported output formats", () => {
|
|
it("throws for ico output format", async () => {
|
|
await expect(processImage(png200x150, [], "ico" as any)).rejects.toThrow(
|
|
"Unsupported output format: ico",
|
|
);
|
|
});
|
|
|
|
it("throws for jp2 output format", async () => {
|
|
await expect(processImage(png200x150, [], "jp2" as any)).rejects.toThrow(
|
|
"Unsupported output format: jp2",
|
|
);
|
|
});
|
|
|
|
it("throws for qoi output format", async () => {
|
|
await expect(processImage(png200x150, [], "qoi" as any)).rejects.toThrow(
|
|
"Unsupported output format: qoi",
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("processImage combined scenarios", () => {
|
|
it("empty operations with format conversion only", async () => {
|
|
const result = await processImage(png200x150, [], "webp");
|
|
expect(result.info.format).toBe("webp");
|
|
expect(result.info.width).toBe(200);
|
|
expect(result.info.height).toBe(150);
|
|
});
|
|
|
|
it("multiple chained operations preserve correct order", async () => {
|
|
// resize to 100x100, then crop 50x50 from top-left
|
|
const result = await processImage(png200x150, [
|
|
{ type: "resize", options: { width: 100, height: 100 } },
|
|
{ type: "crop", options: { left: 0, top: 0, width: 50, height: 50 } },
|
|
]);
|
|
expect(result.info.width).toBe(50);
|
|
expect(result.info.height).toBe(50);
|
|
});
|
|
|
|
it("all OPERATION_MAP entries in one pipeline", async () => {
|
|
// Chain as many operations as possible without conflicting
|
|
const result = await processImage(png200x150, [
|
|
{ type: "resize", options: { width: 80, height: 60 } },
|
|
{ type: "rotate", options: { angle: 0 } },
|
|
{ type: "flip", options: { horizontal: true } },
|
|
{ type: "brightness", options: { value: 5 } },
|
|
{ type: "contrast", options: { value: 5 } },
|
|
{ type: "saturation", options: { value: 5 } },
|
|
{ type: "sharpen", options: { value: 10 } },
|
|
{ type: "grayscale", options: {} },
|
|
{ type: "invert", options: {} },
|
|
]);
|
|
expect(result.buffer.length).toBeGreaterThan(0);
|
|
expect(result.info.width).toBe(80);
|
|
expect(result.info.height).toBe(60);
|
|
});
|
|
|
|
it("result info.size equals buffer.length", async () => {
|
|
const result = await processImage(png200x150, [
|
|
{ type: "resize", options: { width: 40, height: 30 } },
|
|
]);
|
|
expect(result.info.size).toBe(result.buffer.length);
|
|
});
|
|
});
|