merge: resolve conflict with main branch in tool-registry.tsx

This commit is contained in:
SnapOtter
2026-05-08 18:55:28 +08:00
196 changed files with 36147 additions and 130 deletions
+1 -1
View File
@@ -1000,7 +1000,7 @@ describe("Tool processing", () => {
it("rejects unsupported format", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "file", filename: "convert.png", contentType: "image/png", content: PNG_1x1 },
{ name: "settings", content: JSON.stringify({ format: "bmp" }) },
{ name: "settings", content: JSON.stringify({ format: "xyz" }) },
]);
const res = await app.inject({
+492
View File
@@ -0,0 +1,492 @@
/**
* Integration tests for the beautify tool (/api/v1/tools/beautify).
*
* Beautify adds polished backgrounds, shadows, device frames, watermarks,
* and social media sizing to screenshots. Tests exercise all background types,
* frame variants, shadow presets, social presets, watermarks, padding/radius
* extremes, format forcing, and error handling through the HTTP layer.
*/
import { readFileSync } from "node:fs";
import { join } from "node:path";
import sharp from "sharp";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { buildTestApp, createMultipartPayload, loginAsAdmin, type TestApp } from "./test-server.js";
const FIXTURES = join(__dirname, "..", "fixtures");
const PNG = readFileSync(join(FIXTURES, "test-200x150.png"));
const JPG = readFileSync(join(FIXTURES, "test-100x100.jpg"));
let testApp: TestApp;
let app: TestApp["app"];
let adminToken: string;
beforeAll(async () => {
testApp = await buildTestApp();
app = testApp.app;
adminToken = await loginAsAdmin(app);
}, 30_000);
afterAll(async () => {
await testApp.cleanup();
}, 10_000);
function post(url: string, payload: { body: Buffer; contentType: string }) {
return app.inject({
method: "POST",
url,
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": payload.contentType,
},
body: payload.body,
});
}
describe("Beautify", () => {
// ── Background types ────────────────────────────────────────────────
it("default settings produce valid PNG", async () => {
const payload = createMultipartPayload([
{ name: "file", filename: "test.png", contentType: "image/png", content: PNG },
{ name: "settings", content: JSON.stringify({}) },
]);
const res = await post("/api/v1/tools/beautify", payload);
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
expect(result.downloadUrl).toBeDefined();
expect(result.jobId).toBeDefined();
expect(result.processedSize).toBeGreaterThan(0);
});
it("solid background", async () => {
const payload = createMultipartPayload([
{ name: "file", filename: "test.png", contentType: "image/png", content: PNG },
{
name: "settings",
content: JSON.stringify({
backgroundType: "solid",
backgroundColor: "#ff0000",
}),
},
]);
const res = await post("/api/v1/tools/beautify", payload);
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
expect(result.downloadUrl).toBeDefined();
});
it("linear gradient background", async () => {
const payload = createMultipartPayload([
{ name: "file", filename: "test.png", contentType: "image/png", content: PNG },
{
name: "settings",
content: JSON.stringify({
backgroundType: "linear-gradient",
gradientStops: [
{ color: "#ff0000", position: 0 },
{ color: "#0000ff", position: 100 },
],
gradientAngle: 45,
}),
},
]);
const res = await post("/api/v1/tools/beautify", payload);
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
expect(result.downloadUrl).toBeDefined();
});
it("radial gradient background", async () => {
const payload = createMultipartPayload([
{ name: "file", filename: "test.png", contentType: "image/png", content: PNG },
{
name: "settings",
content: JSON.stringify({
backgroundType: "radial-gradient",
gradientStops: [
{ color: "#ffffff", position: 0 },
{ color: "#000000", position: 100 },
],
}),
},
]);
const res = await post("/api/v1/tools/beautify", payload);
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
expect(result.downloadUrl).toBeDefined();
});
it("transparent background", async () => {
const payload = createMultipartPayload([
{ name: "file", filename: "test.png", contentType: "image/png", content: PNG },
{
name: "settings",
content: JSON.stringify({ backgroundType: "transparent" }),
},
]);
const res = await post("/api/v1/tools/beautify", payload);
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
expect(result.downloadUrl).toBeDefined();
});
// ── Image background ────────────────────────────────────────────────
it("image background with second file", async () => {
const bgImage = await sharp({
create: {
width: 400,
height: 300,
channels: 4,
background: { r: 0, g: 0, b: 255, alpha: 1 },
},
})
.png()
.toBuffer();
const payload = createMultipartPayload([
{ name: "file", filename: "test.png", contentType: "image/png", content: PNG },
{ name: "backgroundImage", filename: "bg.png", contentType: "image/png", content: bgImage },
{
name: "settings",
content: JSON.stringify({ backgroundType: "image" }),
},
]);
const res = await post("/api/v1/tools/beautify", payload);
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
expect(result.downloadUrl).toBeDefined();
expect(result.processedSize).toBeGreaterThan(0);
});
// ── Multi-stop gradient ─────────────────────────────────────────────
it("three-stop gradient", async () => {
const payload = createMultipartPayload([
{ name: "file", filename: "test.png", contentType: "image/png", content: PNG },
{
name: "settings",
content: JSON.stringify({
backgroundType: "linear-gradient",
gradientStops: [
{ color: "#ff0000", position: 0 },
{ color: "#00ff00", position: 50 },
{ color: "#0000ff", position: 100 },
],
gradientAngle: 90,
}),
},
]);
const res = await post("/api/v1/tools/beautify", payload);
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
expect(result.downloadUrl).toBeDefined();
});
// ── Frames ──────────────────────────────────────────────────────────
const FRAME_TYPES = [
"macos-light",
"macos-dark",
"windows-light",
"windows-dark",
"browser-light",
"browser-dark",
"iphone",
"macbook",
"ipad",
] as const;
for (const frame of FRAME_TYPES) {
it(`frame: ${frame}`, async () => {
const payload = createMultipartPayload([
{ name: "file", filename: "test.png", contentType: "image/png", content: PNG },
{
name: "settings",
content: JSON.stringify({ frame, shadowPreset: "none" }),
},
]);
const res = await post("/api/v1/tools/beautify", payload);
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
expect(result.downloadUrl).toBeDefined();
const dlRes = await app.inject({
method: "GET",
url: result.downloadUrl,
headers: { authorization: `Bearer ${adminToken}` },
});
expect(dlRes.statusCode).toBe(200);
const meta = await sharp(dlRes.rawPayload).metadata();
expect(meta.width).toBeGreaterThan(0);
expect(meta.height).toBeGreaterThan(0);
});
}
// ── Shadows ─────────────────────────────────────────────────────────
const SHADOW_PRESETS = ["none", "subtle", "medium", "dramatic"] as const;
for (const shadowPreset of SHADOW_PRESETS) {
it(`shadow preset: ${shadowPreset}`, async () => {
const payload = createMultipartPayload([
{ name: "file", filename: "test.png", contentType: "image/png", content: PNG },
{
name: "settings",
content: JSON.stringify({ shadowPreset }),
},
]);
const res = await post("/api/v1/tools/beautify", payload);
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
expect(result.downloadUrl).toBeDefined();
});
}
it("custom shadow", async () => {
const payload = createMultipartPayload([
{ name: "file", filename: "test.png", contentType: "image/png", content: PNG },
{
name: "settings",
content: JSON.stringify({
shadowPreset: "custom",
shadowBlur: 50,
shadowOffsetX: 10,
shadowOffsetY: 15,
shadowColor: "#ff0000",
shadowOpacity: 60,
}),
},
]);
const res = await post("/api/v1/tools/beautify", payload);
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
expect(result.downloadUrl).toBeDefined();
});
// ── Social presets with dimension verification ──────────────────────
const SOCIAL_PRESETS: Record<string, { w: number; h: number }> = {
twitter: { w: 1600, h: 900 },
linkedin: { w: 1200, h: 627 },
"instagram-square": { w: 1080, h: 1080 },
"instagram-story": { w: 1080, h: 1920 },
facebook: { w: 1200, h: 630 },
producthunt: { w: 1270, h: 760 },
};
for (const [preset, dims] of Object.entries(SOCIAL_PRESETS)) {
it(`social preset: ${preset} (${dims.w}x${dims.h})`, async () => {
const payload = createMultipartPayload([
{ name: "file", filename: "test.png", contentType: "image/png", content: PNG },
{
name: "settings",
content: JSON.stringify({ socialPreset: preset, shadowPreset: "none" }),
},
]);
const res = await post("/api/v1/tools/beautify", payload);
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
expect(result.downloadUrl).toBeDefined();
const dlRes = await app.inject({
method: "GET",
url: result.downloadUrl,
headers: { authorization: `Bearer ${adminToken}` },
});
expect(dlRes.statusCode).toBe(200);
const meta = await sharp(dlRes.rawPayload).metadata();
expect(meta.width).toBe(dims.w);
expect(meta.height).toBe(dims.h);
});
}
// ── Watermark ───────────────────────────────────────────────────────
it("watermark text bottom-right", async () => {
const payload = createMultipartPayload([
{ name: "file", filename: "test.png", contentType: "image/png", content: PNG },
{
name: "settings",
content: JSON.stringify({
watermarkText: "SnapOtter",
watermarkPosition: "bottom-right",
watermarkOpacity: 80,
}),
},
]);
const res = await post("/api/v1/tools/beautify", payload);
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
expect(result.downloadUrl).toBeDefined();
});
// ── Padding extremes ────────────────────────────────────────────────
it("padding 0 with no shadow", async () => {
const payload = createMultipartPayload([
{ name: "file", filename: "test.png", contentType: "image/png", content: PNG },
{
name: "settings",
content: JSON.stringify({ padding: 0, shadowPreset: "none" }),
},
]);
const res = await post("/api/v1/tools/beautify", payload);
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
expect(result.downloadUrl).toBeDefined();
});
it("padding 256", async () => {
const payload = createMultipartPayload([
{ name: "file", filename: "test.png", contentType: "image/png", content: PNG },
{
name: "settings",
content: JSON.stringify({ padding: 256 }),
},
]);
const res = await post("/api/v1/tools/beautify", payload);
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
expect(result.downloadUrl).toBeDefined();
});
// ── Border radius ───────────────────────────────────────────────────
it("border radius 64", async () => {
const payload = createMultipartPayload([
{ name: "file", filename: "test.png", contentType: "image/png", content: PNG },
{
name: "settings",
content: JSON.stringify({ borderRadius: 64 }),
},
]);
const res = await post("/api/v1/tools/beautify", payload);
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
expect(result.downloadUrl).toBeDefined();
});
// ── Error cases ─────────────────────────────────────────────────────
it("missing file returns 400", async () => {
const payload = createMultipartPayload([{ name: "settings", content: JSON.stringify({}) }]);
const res = await post("/api/v1/tools/beautify", payload);
expect(res.statusCode).toBe(400);
const result = JSON.parse(res.body);
expect(result.error).toMatch(/no image/i);
});
it("invalid parameters return 400", async () => {
const payload = createMultipartPayload([
{ name: "file", filename: "test.png", contentType: "image/png", content: PNG },
{
name: "settings",
content: JSON.stringify({ padding: -1 }),
},
]);
const res = await post("/api/v1/tools/beautify", payload);
expect(res.statusCode).toBe(400);
const result = JSON.parse(res.body);
expect(result.error).toMatch(/invalid settings/i);
});
// ── Format forcing ──────────────────────────────────────────────────
it("JPEG input + shadow produces PNG", async () => {
const payload = createMultipartPayload([
{ name: "file", filename: "photo.jpg", contentType: "image/jpeg", content: JPG },
{
name: "settings",
content: JSON.stringify({ shadowPreset: "medium" }),
},
]);
const res = await post("/api/v1/tools/beautify", payload);
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
expect(result.downloadUrl).toContain(".png");
});
it("JPEG input + opaque settings honors JPEG output", async () => {
const payload = createMultipartPayload([
{ name: "file", filename: "photo.jpg", contentType: "image/jpeg", content: JPG },
{
name: "settings",
content: JSON.stringify({
backgroundType: "solid",
shadowPreset: "none",
borderRadius: 0,
frame: "none",
outputFormat: "jpeg",
}),
},
]);
const res = await post("/api/v1/tools/beautify", payload);
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
expect(result.downloadUrl).toContain(".jpeg");
});
// ── Device frame + radius ───────────────────────────────────────────
it("iPhone frame with borderRadius > 0 (radius silently ignored)", async () => {
const payload = createMultipartPayload([
{ name: "file", filename: "test.png", contentType: "image/png", content: PNG },
{
name: "settings",
content: JSON.stringify({
frame: "iphone",
borderRadius: 32,
shadowPreset: "none",
}),
},
]);
const res = await post("/api/v1/tools/beautify", payload);
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
expect(result.downloadUrl).toBeDefined();
});
});
+216
View File
@@ -0,0 +1,216 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import sharp from "sharp";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { buildTestApp, createMultipartPayload, loginAsAdmin, type TestApp } from "./test-server.js";
const FIXTURES = join(__dirname, "..", "fixtures");
const PNG = readFileSync(join(FIXTURES, "test-200x150.png"));
const JPG = readFileSync(join(FIXTURES, "test-100x100.jpg"));
const ALL_TYPES = [
"protanopia",
"deuteranopia",
"tritanopia",
"protanomaly",
"deuteranomaly",
"tritanomaly",
"achromatopsia",
"blueConeMonochromacy",
] as const;
let testApp: TestApp;
let app: TestApp["app"];
let adminToken: string;
beforeAll(async () => {
testApp = await buildTestApp();
app = testApp.app;
adminToken = await loginAsAdmin(app);
}, 30_000);
afterAll(async () => {
await testApp.cleanup();
}, 10_000);
function makePayload(
settings: Record<string, unknown>,
buffer: Buffer = PNG,
filename = "test.png",
contentType = "image/png",
) {
return createMultipartPayload([
{ name: "file", filename, contentType, content: buffer },
{ name: "settings", content: JSON.stringify(settings) },
]);
}
async function postTool(
settings: Record<string, unknown>,
buffer?: Buffer,
filename?: string,
ct?: string,
) {
const { body: payload, contentType } = makePayload(settings, buffer, filename, ct);
return app.inject({
method: "POST",
url: "/api/v1/tools/color-blindness",
payload,
headers: {
"content-type": contentType,
authorization: `Bearer ${adminToken}`,
},
});
}
describe("Default settings", () => {
it("processes with default settings (deuteranomaly)", async () => {
const res = await postTool({});
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
expect(result.downloadUrl).toBeDefined();
expect(result.processedSize).toBeGreaterThan(0);
});
});
describe("All 8 simulation types", () => {
for (const type of ALL_TYPES) {
it(`processes with simulationType=${type}`, async () => {
const res = await postTool({ simulationType: type });
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
expect(result.downloadUrl).toBeDefined();
});
}
it("different types produce different outputs", async () => {
const buffers: Buffer[] = [];
for (const type of ["protanopia", "tritanopia", "achromatopsia"] as const) {
const res = await postTool({ simulationType: type });
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
const dlRes = await app.inject({
method: "GET",
url: result.downloadUrl,
headers: { authorization: `Bearer ${adminToken}` },
});
buffers.push(dlRes.rawPayload);
}
const pixelSets = await Promise.all(
buffers.map(async (buf) => {
const { data } = await sharp(buf).removeAlpha().raw().toBuffer({ resolveWithObject: true });
return `${data[0]},${data[1]},${data[2]}`;
}),
);
const unique = new Set(pixelSets);
expect(unique.size).toBeGreaterThan(1);
});
});
describe("Dimension preservation", () => {
it("output has same dimensions as input", async () => {
const res = await postTool({ simulationType: "protanopia" });
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
const dlRes = await app.inject({
method: "GET",
url: result.downloadUrl,
headers: { authorization: `Bearer ${adminToken}` },
});
const meta = await sharp(dlRes.rawPayload).metadata();
expect(meta.width).toBe(200);
expect(meta.height).toBe(150);
});
});
describe("Multiple input formats", () => {
it("processes JPEG input", async () => {
const res = await postTool({ simulationType: "deuteranopia" }, JPG, "test.jpg", "image/jpeg");
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
expect(result.downloadUrl).toBeDefined();
});
it("processes WebP input", async () => {
const WEBP = readFileSync(join(FIXTURES, "test-50x50.webp"));
const res = await postTool({ simulationType: "tritanopia" }, WEBP, "test.webp", "image/webp");
expect(res.statusCode).toBe(200);
});
it("processes HEIC input", { timeout: 120_000 }, async () => {
const HEIC = readFileSync(join(FIXTURES, "test-200x150.heic"));
const res = await postTool({ simulationType: "protanomaly" }, HEIC, "photo.heic", "image/heic");
expect(res.statusCode).toBe(200);
});
it("processes SVG input", async () => {
const SVG = readFileSync(join(FIXTURES, "test-100x100.svg"));
const res = await postTool(
{ simulationType: "achromatopsia" },
SVG,
"icon.svg",
"image/svg+xml",
);
expect(res.statusCode).toBe(200);
});
it("processes animated GIF input", async () => {
const GIF = readFileSync(join(FIXTURES, "animated.gif"));
const res = await postTool({ simulationType: "deuteranomaly" }, GIF, "anim.gif", "image/gif");
expect(res.statusCode).toBe(200);
});
});
describe("Error handling", () => {
it("returns 400 when no file is provided", async () => {
const { body: payload, contentType } = createMultipartPayload([
{ name: "settings", content: JSON.stringify({ simulationType: "protanopia" }) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/color-blindness",
payload,
headers: {
"content-type": contentType,
authorization: `Bearer ${adminToken}`,
},
});
expect(res.statusCode).toBe(400);
});
it("returns 400 for invalid simulationType value", async () => {
const res = await postTool({ simulationType: "invalid-type" });
expect(res.statusCode).toBe(400);
});
});
describe("Edge cases", () => {
it("processes 1x1 pixel image", async () => {
const TINY = readFileSync(join(FIXTURES, "test-1x1.png"));
const res = await postTool({ simulationType: "deuteranomaly" }, TINY, "tiny.png", "image/png");
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
expect(result.processedSize).toBeGreaterThan(0);
});
it("processes stress-large.jpg", async () => {
const LARGE = readFileSync(join(FIXTURES, "content", "stress-large.jpg"));
const res = await postTool({ simulationType: "protanopia" }, LARGE, "large.jpg", "image/jpeg");
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
expect(result.processedSize).toBeGreaterThan(0);
});
});
describe("Authentication", () => {
it("rejects unauthenticated request", async () => {
const { body: payload, contentType } = makePayload({ simulationType: "protanopia" });
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/color-blindness",
payload,
headers: { "content-type": contentType },
});
expect(res.statusCode).toBe(401);
});
});
+96
View File
@@ -179,6 +179,102 @@ const FORMAT_SAMPLES: FormatSample[] = [
needsHeifDecoder: false,
mayFailValidation: true,
},
{
name: "SVGZ",
file: "sample.svgz",
mime: "image/svg+xml",
needsCliDecoder: false,
needsHeifDecoder: false,
mayFailValidation: false,
},
{
name: "JP2",
file: "sample.jp2",
mime: "image/jp2",
needsCliDecoder: true,
needsHeifDecoder: false,
mayFailValidation: false,
},
{
name: "EPS",
file: "sample.eps",
mime: "application/postscript",
needsCliDecoder: true,
needsHeifDecoder: false,
mayFailValidation: false,
},
{
name: "PPM",
file: "sample.ppm",
mime: "image/x-portable-pixmap",
needsCliDecoder: true,
needsHeifDecoder: false,
mayFailValidation: false,
},
{
name: "PGM",
file: "sample.pgm",
mime: "image/x-portable-graymap",
needsCliDecoder: true,
needsHeifDecoder: false,
mayFailValidation: false,
},
{
name: "PBM",
file: "sample.pbm",
mime: "image/x-portable-bitmap",
needsCliDecoder: true,
needsHeifDecoder: false,
mayFailValidation: false,
},
{
name: "DDS",
file: "sample.dds",
mime: "image/vnd.ms-dds",
needsCliDecoder: true,
needsHeifDecoder: false,
mayFailValidation: false,
},
{
name: "CUR",
file: "sample.cur",
mime: "image/x-icon",
needsCliDecoder: true,
needsHeifDecoder: false,
mayFailValidation: false,
},
{
name: "DPX",
file: "sample.dpx",
mime: "image/x-dpx",
needsCliDecoder: true,
needsHeifDecoder: false,
mayFailValidation: false,
},
{
name: "FITS",
file: "sample.fits",
mime: "image/fits",
needsCliDecoder: true,
needsHeifDecoder: false,
mayFailValidation: false,
},
{
name: "APNG",
file: "sample.apng",
mime: "image/apng",
needsCliDecoder: false,
needsHeifDecoder: false,
mayFailValidation: false,
},
{
name: "QOI",
file: "sample.qoi",
mime: "image/x-qoi",
needsCliDecoder: true,
needsHeifDecoder: false,
mayFailValidation: false,
},
];
// ---------------------------------------------------------------------------
+267
View File
@@ -0,0 +1,267 @@
/**
* Integration tests for new input/output format support.
*
* - New output formats: convert PNG to jxl, bmp, ico, jp2, qoi
* - SVGZ input: verify compressed SVG decodes correctly
* - JXL quality: verify lower quality produces smaller files
*/
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { buildTestApp, createMultipartPayload, loginAsAdmin, type TestApp } from "./test-server.js";
const FORMATS_DIR = join(__dirname, "..", "fixtures", "formats");
describe("New format support", () => {
let testApp: TestApp;
let app: TestApp["app"];
let adminToken: string;
beforeAll(async () => {
testApp = await buildTestApp();
app = testApp.app;
adminToken = await loginAsAdmin(app);
}, 30_000);
afterAll(async () => {
await testApp?.cleanup();
}, 10_000);
// ---------------------------------------------------------------------------
// New output format conversions
// ---------------------------------------------------------------------------
const NEW_OUTPUT_FORMATS = ["jxl", "bmp", "ico", "jp2", "qoi"];
for (const format of NEW_OUTPUT_FORMATS) {
it(`converts PNG to ${format}`, async () => {
const fileBuffer = readFileSync(join(FORMATS_DIR, "sample.png"));
const { body, contentType } = createMultipartPayload([
{
name: "file",
filename: "sample.png",
contentType: "image/png",
content: fileBuffer,
},
{
name: "settings",
content: JSON.stringify({ format, quality: 80 }),
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/convert",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
body,
});
// Accept 200 (success) or 422 (encoder not available in test env)
expect([200, 422]).toContain(res.statusCode);
if (res.statusCode === 200) {
const json = JSON.parse(res.body);
expect(json.processedSize).toBeGreaterThan(0);
expect(json.downloadUrl).toBeTruthy();
}
});
}
// ---------------------------------------------------------------------------
// SVGZ input decoding
// ---------------------------------------------------------------------------
it("decodes SVGZ input correctly", async () => {
const fileBuffer = readFileSync(join(FORMATS_DIR, "sample.svgz"));
const { body, contentType } = createMultipartPayload([
{
name: "file",
filename: "sample.svgz",
contentType: "image/svg+xml",
content: fileBuffer,
},
{
name: "settings",
content: JSON.stringify({ format: "png" }),
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/convert",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
body,
});
expect([200, 422]).toContain(res.statusCode);
if (res.statusCode === 200) {
const json = JSON.parse(res.body);
expect(json.processedSize).toBeGreaterThan(0);
}
});
// ---------------------------------------------------------------------------
// JXL quality affects file size
// ---------------------------------------------------------------------------
it("JXL quality affects file size", async () => {
const fileBuffer = readFileSync(join(FORMATS_DIR, "sample.png"));
const convert = async (quality: number) => {
const { body, contentType } = createMultipartPayload([
{
name: "file",
filename: "sample.png",
contentType: "image/png",
content: fileBuffer,
},
{
name: "settings",
content: JSON.stringify({ format: "jxl", quality }),
},
]);
return app.inject({
method: "POST",
url: "/api/v1/tools/convert",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
body,
});
};
const lowQ = await convert(30);
const highQ = await convert(90);
// Only compare sizes if both succeeded (JXL encoder may not be available)
if (lowQ.statusCode === 200 && highQ.statusCode === 200) {
const lowJson = JSON.parse(lowQ.body);
const highJson = JSON.parse(highQ.body);
expect(lowJson.processedSize).toBeLessThan(highJson.processedSize);
}
});
describe("Extended output format matrix", () => {
const INPUTS = [
{ name: "PNG", file: "sample.png", mime: "image/png" },
{ name: "JPEG", file: "sample.jpg", mime: "image/jpeg" },
{ name: "WebP", file: "sample.webp", mime: "image/webp" },
];
const OUTPUTS = [
"jpg",
"png",
"webp",
"avif",
"tiff",
"gif",
"heic",
"heif",
"jxl",
"bmp",
"ico",
"jp2",
"qoi",
];
for (const input of INPUTS) {
for (const outFmt of OUTPUTS) {
const inLower = input.name.toLowerCase();
if (inLower === outFmt || (inLower === "jpeg" && outFmt === "jpg")) continue;
it(`converts ${input.name} to ${outFmt}`, async () => {
const fileBuffer = readFileSync(join(FORMATS_DIR, input.file));
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: input.file, contentType: input.mime, content: fileBuffer },
{ name: "settings", content: JSON.stringify({ format: outFmt, quality: 80 }) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/convert",
headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType },
body,
});
expect([200, 422]).toContain(res.statusCode);
if (res.statusCode === 200) {
const json = JSON.parse(res.body);
expect(json.processedSize).toBeGreaterThan(0);
expect(json.downloadUrl).toBeTruthy();
}
});
}
}
});
describe("New input format processing via resize", () => {
const NEW_INPUT_FORMATS = [
{ name: "SVGZ", file: "sample.svgz", mime: "image/svg+xml" },
{ name: "JP2", file: "sample.jp2", mime: "image/jp2" },
{ name: "EPS", file: "sample.eps", mime: "application/postscript" },
{ name: "PPM", file: "sample.ppm", mime: "image/x-portable-pixmap" },
{ name: "PGM", file: "sample.pgm", mime: "image/x-portable-graymap" },
{ name: "PBM", file: "sample.pbm", mime: "image/x-portable-bitmap" },
{ name: "DDS", file: "sample.dds", mime: "image/vnd.ms-dds" },
{ name: "CUR", file: "sample.cur", mime: "image/x-icon" },
{ name: "DPX", file: "sample.dpx", mime: "image/x-dpx" },
{ name: "FITS", file: "sample.fits", mime: "image/fits" },
{ name: "APNG", file: "sample.apng", mime: "image/apng" },
{ name: "QOI", file: "sample.qoi", mime: "image/x-qoi" },
];
for (const fmt of NEW_INPUT_FORMATS) {
it(`resizes ${fmt.name} input to 25x25`, async () => {
const fixturePath = join(FORMATS_DIR, fmt.file);
if (!existsSync(fixturePath)) return;
const fileBuffer = readFileSync(fixturePath);
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: fmt.file, contentType: fmt.mime, content: fileBuffer },
{ name: "settings", content: JSON.stringify({ width: 25, height: 25 }) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/resize",
headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType },
body,
});
expect([200, 400, 422]).toContain(res.statusCode);
if (res.statusCode === 200) {
const json = JSON.parse(res.body);
expect(json.downloadUrl).toBeTruthy();
expect(json.processedSize).toBeGreaterThan(0);
}
}, 60_000);
}
});
describe("Preview generation for special formats", () => {
const PREVIEW_FORMATS = [
{ name: "HEIC", file: "sample.heic", mime: "image/heic" },
{ name: "JXL", file: "sample.jxl", mime: "image/jxl" },
{ name: "ICO", file: "sample.ico", mime: "image/x-icon" },
{ name: "PSD", file: "sample.psd", mime: "image/vnd.adobe.photoshop" },
{ name: "EXR", file: "sample.exr", mime: "image/x-exr" },
];
for (const fmt of PREVIEW_FORMATS) {
it(`generates preview for ${fmt.name}`, async () => {
const fixturePath = join(FORMATS_DIR, fmt.file);
if (!existsSync(fixturePath)) return;
const fileBuffer = readFileSync(fixturePath);
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: fmt.file, contentType: fmt.mime, content: fileBuffer },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/preview",
headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType },
body,
});
expect([200, 422]).toContain(res.statusCode);
if (res.statusCode === 200) {
const ct = res.headers["content-type"] as string;
expect(ct).toMatch(/image\/(webp|png)/);
}
}, 60_000);
}
});
});