Files
SnapOtter/tests/e2e-docker/format-conversion-tools.spec.ts
T
SnapOtterandGitHub d10d0f544f fix: release QA hardening across processing, media, security, and CI gates (#649)
A release-readiness QA pass over the whole product. The commits split into
defects a user would hit and gates that were reporting green while measuring
nothing.

## Fixes that change behaviour

Rate limiting was bypassable on every install: TRUST_PROXY defaulted to true, so
request.ip came from a client-set header and a forged X-Forwarded-For got past
the login limiter. The default is now a private-network trust list.

A transient Postgres outage stranded in-flight jobs, leaving finished output on
disk with no row pointing at it. A reconciler now resolves those rows and adopts
the bytes rather than dropping the work.

A Redis connection that moved to a new address wedged every read-blocked
consumer, so completions stopped signalling while health still answered 200.
Socket timeouts plus subscriber pings recover it.

Installing more than one AI bundle left the shared venv multi-versioned and
silently broke three tools. The installer now reconciles distributions to one
version each.

Converting an image to JXL at quality 1 through 4 returned a 500, because
libjxl 0.7 rejects the distance those values compute. The quality is floored at
what the encoder honours. A missing ffmpeg was also reported to the user as a
corrupt upload; it now says the engine is unavailable.

RAW uploads reached an unpatched LibRaw on arm64, so it is built from source at
0.22.2, and the release scan was split so it can fail on an unfixed critical
instead of hiding it behind ignore-unfixed.

## Gates that could not fail

Two mutation lanes ran zero mutants because Stryker crawled the gitignored docs
build; coverage discarded its whole report on any failing test; the lint gate
skipped root tests, scripts, and two workspaces; and several generated matrices
counted a host missing ffmpeg as a passing tool. Each now measures what it
claims.

Full evidence and the outstanding release items are tracked locally and are not
part of this branch.
2026-07-27 15:37:30 +08:00

1287 lines
48 KiB
TypeScript

import { readFileSync } from "node:fs";
import { join } from "node:path";
import { expect, test } from "@playwright/test";
// ─── Format Conversion Tools ───────────────────────────────────────
// Comprehensive format conversion coverage: SVG-to-raster with all
// output formats, vectorize from all raster formats, GIF tools with
// various operations, PDF-to-image with all DPI and format options.
// Complements conversion-tools.spec.ts with deeper fixture coverage.
const FIXTURES = join(process.cwd(), "tests", "fixtures", "image", "valid");
const FORMATS = join(process.cwd(), "tests", "fixtures", "image", "formats");
const CONTENT = FIXTURES;
const DOCUMENTS = join(process.cwd(), "tests", "fixtures", "document", "valid");
let token: string;
test.beforeAll(async ({ request }) => {
const res = await request.post("/api/auth/login", {
data: { username: "admin", password: "admin" },
});
const body = await res.json();
token = body.token;
});
function fixture(name: string): Buffer {
return readFileSync(join(FIXTURES, name));
}
function formatFixture(name: string): Buffer {
return readFileSync(join(FORMATS, name));
}
function contentFixture(name: string): Buffer {
return readFileSync(join(CONTENT, name));
}
function documentFixture(name: string): Buffer {
return readFileSync(join(DOCUMENTS, name));
}
const PNG_200x150 = fixture("test-200x150.png");
const SVG_100x100 = fixture("test-100x100.svg");
const HEIC_200x150 = fixture("test-200x150.heic");
const WEBP_50x50 = fixture("test-50x50.webp");
const ANIMATED_GIF = fixture("animated.gif");
const PDF_3PAGE = documentFixture("test-3page.pdf");
// ─── SVG to Raster — Extended Formats ──────────────────────────────
test.describe("SVG to Raster — extended", () => {
test("convert SVG to AVIF", async ({ request }) => {
const res = await request.post("/api/v1/tools/image/svg-to-raster", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "test.svg", mimeType: "image/svg+xml", buffer: SVG_100x100 },
settings: JSON.stringify({ format: "avif", width: 256 }),
},
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.downloadUrl).toBeTruthy();
expect(body.processedSize).toBeGreaterThan(0);
});
test("convert SVG to TIFF", async ({ request }) => {
const res = await request.post("/api/v1/tools/image/svg-to-raster", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "test.svg", mimeType: "image/svg+xml", buffer: SVG_100x100 },
settings: JSON.stringify({ format: "tiff", width: 400 }),
},
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.downloadUrl).toBeTruthy();
});
test("convert SVG with large render width (1024px)", async ({ request }) => {
const res = await request.post("/api/v1/tools/image/svg-to-raster", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "test.svg", mimeType: "image/svg+xml", buffer: SVG_100x100 },
settings: JSON.stringify({ format: "png", width: 1024 }),
},
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.downloadUrl).toBeTruthy();
expect(body.processedSize).toBeGreaterThan(0);
});
test("convert QR code SVG to raster", async ({ request }) => {
const qrSvg = contentFixture("qr-code.svg");
const res = await request.post("/api/v1/tools/image/svg-to-raster", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "qr.svg", mimeType: "image/svg+xml", buffer: qrSvg },
settings: JSON.stringify({ format: "png", width: 400 }),
},
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.downloadUrl).toBeTruthy();
});
});
// ─── Vectorize — Extended Formats ──────────────────────────────────
test.describe("Vectorize — extended", () => {
test("vectorize WebP to SVG", async ({ request }) => {
const res = await request.post("/api/v1/tools/image/vectorize", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "test.webp", mimeType: "image/webp", buffer: WEBP_50x50 },
settings: JSON.stringify({}),
},
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.downloadUrl).toBeTruthy();
expect(body.downloadUrl).toContain(".svg");
});
test("vectorize HEIC to SVG", async ({ request }) => {
const res = await request.post("/api/v1/tools/image/vectorize", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "test.heic", mimeType: "image/heic", buffer: HEIC_200x150 },
settings: JSON.stringify({}),
},
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.downloadUrl).toBeTruthy();
});
test("vectorize AVIF sample to SVG", async ({ request }) => {
const avif = formatFixture("sample.avif");
const res = await request.post("/api/v1/tools/image/vectorize", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "sample.avif", mimeType: "image/avif", buffer: avif },
settings: JSON.stringify({}),
},
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.downloadUrl).toBeTruthy();
});
test("vectorize TIFF sample to SVG", async ({ request }) => {
const tiff = formatFixture("sample.tiff");
const res = await request.post("/api/v1/tools/image/vectorize", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "sample.tiff", mimeType: "image/tiff", buffer: tiff },
settings: JSON.stringify({}),
},
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.downloadUrl).toBeTruthy();
});
});
// ─── GIF Tools — Extended ──────────────────────────────────────────
test.describe("GIF Tools — extended", () => {
test("GIF tool with resize settings", async ({ request }) => {
const res = await request.post("/api/v1/tools/image/gif-tools", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "animated.gif", mimeType: "image/gif", buffer: ANIMATED_GIF },
settings: JSON.stringify({ action: "resize", width: 100 }),
},
});
if (res.ok()) {
const body = await res.json();
expect(body.downloadUrl || body.frames).toBeTruthy();
} else {
const body = await res.json();
expect(body.error).toBeDefined();
}
});
test("GIF tool with extract-frames action", async ({ request }) => {
const res = await request.post("/api/v1/tools/image/gif-tools", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "animated.gif", mimeType: "image/gif", buffer: ANIMATED_GIF },
settings: JSON.stringify({ action: "extract-frames" }),
},
});
if (res.ok()) {
const body = await res.json();
expect(body.downloadUrl || body.frames).toBeTruthy();
} else {
const body = await res.json();
expect(body.error).toBeDefined();
}
});
test("GIF tool with speed adjustment", async ({ request }) => {
const simpsons = contentFixture("animated-simpsons.gif");
const res = await request.post("/api/v1/tools/image/gif-tools", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "simpsons.gif", mimeType: "image/gif", buffer: simpsons },
settings: JSON.stringify({ action: "speed", speedFactor: 2 }),
},
});
if (res.ok()) {
const body = await res.json();
expect(body.downloadUrl || body.frames).toBeTruthy();
} else {
const body = await res.json();
expect(body.error).toBeDefined();
}
});
test("GIF tool with reverse action", async ({ request }) => {
const res = await request.post("/api/v1/tools/image/gif-tools", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "animated.gif", mimeType: "image/gif", buffer: ANIMATED_GIF },
settings: JSON.stringify({ action: "reverse" }),
},
});
if (res.ok()) {
const body = await res.json();
expect(body.downloadUrl || body.frames).toBeTruthy();
} else {
const body = await res.json();
expect(body.error).toBeDefined();
}
});
test("reject non-GIF file", async ({ request }) => {
const res = await request.post("/api/v1/tools/image/gif-tools", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "test.png", mimeType: "image/png", buffer: PNG_200x150 },
settings: JSON.stringify({}),
},
});
// May accept and convert, or reject with error
if (!res.ok()) {
const body = await res.json();
expect(body.error).toBeDefined();
}
});
});
// ─── PDF to Image — Extended ───────────────────────────────────────
test.describe("PDF to Image — extended", () => {
test("convert PDF to AVIF format", async ({ request }) => {
const res = await request.post("/api/v1/tools/pdf/pdf-to-image", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "test.pdf", mimeType: "application/pdf", buffer: PDF_3PAGE },
settings: JSON.stringify({ format: "avif", dpi: 150, pages: "1" }),
},
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.downloadUrl || body.pages || body.jobId).toBeTruthy();
});
test("convert PDF at high DPI (300)", async ({ request }) => {
const res = await request.post("/api/v1/tools/pdf/pdf-to-image", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "test.pdf", mimeType: "application/pdf", buffer: PDF_3PAGE },
settings: JSON.stringify({ format: "png", dpi: 300, pages: "1" }),
},
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.downloadUrl || body.pages || body.jobId).toBeTruthy();
});
test("convert PDF at low DPI (72)", async ({ request }) => {
const res = await request.post("/api/v1/tools/pdf/pdf-to-image", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "test.pdf", mimeType: "application/pdf", buffer: PDF_3PAGE },
settings: JSON.stringify({ format: "jpg", dpi: 72, pages: "all" }),
},
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.downloadUrl || body.pages || body.jobId).toBeTruthy();
});
test("convert specific middle page", async ({ request }) => {
const res = await request.post("/api/v1/tools/pdf/pdf-to-image", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "test.pdf", mimeType: "application/pdf", buffer: PDF_3PAGE },
settings: JSON.stringify({ format: "png", dpi: 150, pages: "2" }),
},
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.downloadUrl || body.pages || body.jobId).toBeTruthy();
});
test("convert last page only", async ({ request }) => {
const res = await request.post("/api/v1/tools/pdf/pdf-to-image", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "test.pdf", mimeType: "application/pdf", buffer: PDF_3PAGE },
settings: JSON.stringify({ format: "png", dpi: 150, pages: "3" }),
},
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.downloadUrl || body.pages || body.jobId).toBeTruthy();
});
});
// ─── Convert from Exotic Formats ───────────────────────────────────
test.describe("Convert from exotic formats", () => {
test("TIFF to PNG", async ({ request }) => {
const tiff = formatFixture("sample.tiff");
const res = await request.post("/api/v1/tools/image/convert", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "sample.tiff", mimeType: "image/tiff", buffer: tiff },
settings: JSON.stringify({ format: "png" }),
},
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.downloadUrl).toContain(".png");
expect(body.processedSize).toBeGreaterThan(0);
});
test("TIFF to JPEG", async ({ request }) => {
const tiff = formatFixture("sample.tiff");
const res = await request.post("/api/v1/tools/image/convert", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "sample.tiff", mimeType: "image/tiff", buffer: tiff },
settings: JSON.stringify({ format: "jpg", quality: 85 }),
},
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.downloadUrl).toContain(".jpg");
});
test("AVIF to PNG", async ({ request }) => {
const avif = formatFixture("sample.avif");
const res = await request.post("/api/v1/tools/image/convert", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "sample.avif", mimeType: "image/avif", buffer: avif },
settings: JSON.stringify({ format: "png" }),
},
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.downloadUrl).toContain(".png");
});
test("AVIF to WebP", async ({ request }) => {
const avif = formatFixture("sample.avif");
const res = await request.post("/api/v1/tools/image/convert", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "sample.avif", mimeType: "image/avif", buffer: avif },
settings: JSON.stringify({ format: "webp" }),
},
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.downloadUrl).toContain(".webp");
});
test("GIF to PNG (single frame)", async ({ request }) => {
const gif = formatFixture("sample.gif");
const res = await request.post("/api/v1/tools/image/convert", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "sample.gif", mimeType: "image/gif", buffer: gif },
settings: JSON.stringify({ format: "png" }),
},
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.downloadUrl).toContain(".png");
});
test("HEIF to WebP", async ({ request }) => {
const heif = formatFixture("sample.heif");
const res = await request.post("/api/v1/tools/image/convert", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "sample.heif", mimeType: "image/heif", buffer: heif },
settings: JSON.stringify({ format: "webp" }),
},
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.downloadUrl).toContain(".webp");
});
test("HEIF to JPEG", async ({ request }) => {
const heif = formatFixture("sample.heif");
const res = await request.post("/api/v1/tools/image/convert", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "sample.heif", mimeType: "image/heif", buffer: heif },
settings: JSON.stringify({ format: "jpg" }),
},
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.downloadUrl).toContain(".jpg");
});
});
// ─── Image to Base64 — Extended Formats ────────────────────────────
test.describe("Image to Base64 — extended", () => {
test("encode WebP to base64", async ({ request }) => {
const res = await request.post("/api/v1/tools/image/image-to-base64", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "test.webp", mimeType: "image/webp", buffer: WEBP_50x50 },
settings: JSON.stringify({}),
},
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.results).toBeInstanceOf(Array);
expect(body.results[0].base64).toBeTruthy();
expect(body.results[0].width).toBe(50);
expect(body.results[0].height).toBe(50);
});
test("encode HEIC to base64 with format conversion", async ({ request }) => {
const res = await request.post("/api/v1/tools/image/image-to-base64", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "test.heic", mimeType: "image/heic", buffer: HEIC_200x150 },
settings: JSON.stringify({ outputFormat: "png" }),
},
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.results[0].base64).toBeTruthy();
expect(body.results[0].mimeType).toBe("image/png");
});
test("encode with both maxWidth and format conversion", async ({ request }) => {
const sample = formatFixture("sample.jpg");
const res = await request.post("/api/v1/tools/image/image-to-base64", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "sample.jpg", mimeType: "image/jpeg", buffer: sample },
settings: JSON.stringify({ outputFormat: "webp", maxWidth: 100, quality: 60 }),
},
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.results[0].mimeType).toBe("image/webp");
expect(body.results[0].width).toBeLessThanOrEqual(100);
});
});
// ─── Multipage TIFF ────────────────────────────────────────────────
test.describe("Multipage TIFF handling", () => {
test("convert multipage TIFF to PNG", async ({ request }) => {
const multiTiff = formatFixture("multipage.tiff");
const res = await request.post("/api/v1/tools/image/convert", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "multipage.tiff", mimeType: "image/tiff", buffer: multiTiff },
settings: JSON.stringify({ format: "png" }),
},
});
// May return first page or all pages — verify no crash
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.downloadUrl || body.pages).toBeTruthy();
});
test("get info from multipage TIFF", async ({ request }) => {
const multiTiff = formatFixture("multipage.tiff");
const res = await request.post("/api/v1/tools/image/info", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "multipage.tiff", mimeType: "image/tiff", buffer: multiTiff },
},
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.width).toBeGreaterThan(0);
expect(body.height).toBeGreaterThan(0);
expect(body.format).toBeTruthy();
});
});
// ─── JXL Format Handling ──────────────────────────────────────────
test.describe("JXL format handling", () => {
test("convert JXL to PNG", async ({ request }) => {
const jxl = formatFixture("sample.jxl");
const res = await request.post("/api/v1/tools/image/convert", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "sample.jxl", mimeType: "image/jxl", buffer: jxl },
settings: JSON.stringify({ format: "png" }),
},
});
// JXL decode may require fallback — accept success or unsupported format
if (res.ok()) {
const body = await res.json();
expect(body.downloadUrl).toContain(".png");
expect(body.processedSize).toBeGreaterThan(0);
} else {
expect([400, 422]).toContain(res.status());
}
});
test("convert JXL to JPEG", async ({ request }) => {
const jxl = formatFixture("sample.jxl");
const res = await request.post("/api/v1/tools/image/convert", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "sample.jxl", mimeType: "image/jxl", buffer: jxl },
settings: JSON.stringify({ format: "jpg" }),
},
});
if (res.ok()) {
const body = await res.json();
expect(body.downloadUrl).toContain(".jpg");
} else {
expect([400, 422]).toContain(res.status());
}
});
test("get info from JXL file", async ({ request }) => {
const jxl = formatFixture("sample.jxl");
const res = await request.post("/api/v1/tools/image/info", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "sample.jxl", mimeType: "image/jxl", buffer: jxl },
},
});
if (res.ok()) {
const body = await res.json();
expect(body.width).toBeGreaterThan(0);
expect(body.height).toBeGreaterThan(0);
} else {
// JXL may not be fully supported
expect([400, 422]).toContain(res.status());
}
});
});
// ─── ICO Format Handling ──────────────────────────────────────────
test.describe("ICO format handling", () => {
test("convert ICO to PNG", async ({ request }) => {
const ico = formatFixture("sample.ico");
const res = await request.post("/api/v1/tools/image/convert", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "sample.ico", mimeType: "image/x-icon", buffer: ico },
settings: JSON.stringify({ format: "png" }),
},
});
if (res.ok()) {
const body = await res.json();
expect(body.downloadUrl).toContain(".png");
} else {
// ICO decode may not be supported
expect([400, 422]).toContain(res.status());
}
});
});
// ─── SVG to Raster — All Output Formats ──────────────────────────
test.describe("SVG to Raster — all output formats", () => {
test("convert SVG to GIF", async ({ request }) => {
const res = await request.post("/api/v1/tools/image/svg-to-raster", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "test.svg", mimeType: "image/svg+xml", buffer: SVG_100x100 },
settings: JSON.stringify({ format: "gif", width: 200 }),
},
});
// GIF output from SVG may not be supported
if (res.ok()) {
const body = await res.json();
expect(body.downloadUrl).toBeTruthy();
} else {
const body = await res.json();
expect(body.error).toBeDefined();
}
});
test("convert SVG with small render width (32px)", async ({ request }) => {
const res = await request.post("/api/v1/tools/image/svg-to-raster", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "test.svg", mimeType: "image/svg+xml", buffer: SVG_100x100 },
settings: JSON.stringify({ format: "png", width: 32 }),
},
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.downloadUrl).toBeTruthy();
expect(body.processedSize).toBeGreaterThan(0);
});
test("convert SVG logo to WebP", async ({ request }) => {
const svgLogo = contentFixture("svg-logo.svg");
const res = await request.post("/api/v1/tools/image/svg-to-raster", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "logo.svg", mimeType: "image/svg+xml", buffer: svgLogo },
settings: JSON.stringify({ format: "webp", width: 300 }),
},
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.downloadUrl).toBeTruthy();
});
});
// ─── Vectorize — Round-trip Verification ──────────────────────────
test.describe("Vectorize — round-trip", () => {
test("vectorize PNG then convert SVG back to raster", async ({ request }) => {
// Step 1: Vectorize PNG to SVG
const vecRes = await request.post("/api/v1/tools/image/vectorize", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "test.png", mimeType: "image/png", buffer: PNG_200x150 },
settings: JSON.stringify({}),
},
});
expect(vecRes.ok()).toBe(true);
const vecBody = await vecRes.json();
expect(vecBody.downloadUrl).toContain(".svg");
// Step 2: Download the SVG
const dlRes = await request.get(vecBody.downloadUrl, {
headers: { Authorization: `Bearer ${token}` },
});
expect(dlRes.ok()).toBe(true);
const svgBuffer = Buffer.from(await dlRes.body());
// Step 3: Convert SVG back to PNG
const rasterRes = await request.post("/api/v1/tools/image/svg-to-raster", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "vectorized.svg", mimeType: "image/svg+xml", buffer: svgBuffer },
settings: JSON.stringify({ format: "png", width: 200 }),
},
});
expect(rasterRes.ok()).toBe(true);
const rasterBody = await rasterRes.json();
expect(rasterBody.downloadUrl).toBeTruthy();
expect(rasterBody.processedSize).toBeGreaterThan(0);
});
});
// ─── PDF to Image — WebP and TIFF Output ──────────────────────────
test.describe("PDF to Image — additional formats", () => {
test("convert PDF to TIFF format", async ({ request }) => {
const res = await request.post("/api/v1/tools/pdf/pdf-to-image", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "test.pdf", mimeType: "application/pdf", buffer: PDF_3PAGE },
settings: JSON.stringify({ format: "tiff", dpi: 150, pages: "1" }),
},
});
// TIFF output from PDF may not be supported
if (res.ok()) {
const body = await res.json();
expect(body.downloadUrl || body.pages || body.jobId).toBeTruthy();
} else {
const body = await res.json();
expect(body.error).toBeDefined();
}
});
test("convert all 3 pages to WebP", async ({ request }) => {
const res = await request.post("/api/v1/tools/pdf/pdf-to-image", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "test.pdf", mimeType: "application/pdf", buffer: PDF_3PAGE },
settings: JSON.stringify({ format: "webp", dpi: 150, pages: "all" }),
},
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.downloadUrl || body.pages || body.jobId).toBeTruthy();
});
test("convert page range 1-3 to PNG", async ({ request }) => {
const res = await request.post("/api/v1/tools/pdf/pdf-to-image", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "test.pdf", mimeType: "application/pdf", buffer: PDF_3PAGE },
settings: JSON.stringify({ format: "png", dpi: 200, pages: "1-3" }),
},
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.downloadUrl || body.pages || body.jobId).toBeTruthy();
});
});
// ─── SVG to Raster — Output Verification ────────────────────────
test.describe("SVG to Raster — output verification", () => {
test("SVG-to-raster output can be downloaded", async ({ request }) => {
const res = await request.post("/api/v1/tools/image/svg-to-raster", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "test.svg", mimeType: "image/svg+xml", buffer: SVG_100x100 },
settings: JSON.stringify({ format: "png", width: 256 }),
},
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.downloadUrl).toBeTruthy();
// Download and verify the rasterized image
const dlRes = await request.get(body.downloadUrl, {
headers: { Authorization: `Bearer ${token}` },
});
expect(dlRes.ok()).toBe(true);
const buffer = Buffer.from(await dlRes.body());
expect(buffer.length).toBeGreaterThan(0);
});
});
// ─── GIF Tools — Output Verification ────────────────────────────
test.describe("GIF Tools — output verification", () => {
test("GIF tool with optimize action", async ({ request }) => {
const res = await request.post("/api/v1/tools/image/gif-tools", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "animated.gif", mimeType: "image/gif", buffer: ANIMATED_GIF },
settings: JSON.stringify({ action: "optimize" }),
},
});
if (res.ok()) {
const body = await res.json();
expect(body.downloadUrl || body.frames).toBeTruthy();
} else {
const body = await res.json();
expect(body.error).toBeDefined();
}
});
test("GIF tool with crop action", async ({ request }) => {
const res = await request.post("/api/v1/tools/image/gif-tools", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "animated.gif", mimeType: "image/gif", buffer: ANIMATED_GIF },
settings: JSON.stringify({
action: "crop",
left: 0,
top: 0,
width: 50,
height: 50,
}),
},
});
if (res.ok()) {
const body = await res.json();
expect(body.downloadUrl || body.frames).toBeTruthy();
} else {
const body = await res.json();
expect(body.error).toBeDefined();
}
});
});
// ─── Vectorize — Output Verification ────────────────────────────
test.describe("Vectorize — output verification", () => {
test("vectorized SVG output can be downloaded", async ({ request }) => {
const res = await request.post("/api/v1/tools/image/vectorize", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "test.png", mimeType: "image/png", buffer: PNG_200x150 },
settings: JSON.stringify({}),
},
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.downloadUrl).toContain(".svg");
// Download the SVG
const dlRes = await request.get(body.downloadUrl, {
headers: { Authorization: `Bearer ${token}` },
});
expect(dlRes.ok()).toBe(true);
const svgContent = await dlRes.text();
// Verify it's actually SVG content
expect(svgContent).toContain("<svg");
});
});
// ─── PDF to Image — Output Verification ─────────────────────────
test.describe("PDF to Image — output verification", () => {
test("PDF conversion output can be downloaded", async ({ request }) => {
const res = await request.post("/api/v1/tools/pdf/pdf-to-image", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "test.pdf", mimeType: "application/pdf", buffer: PDF_3PAGE },
settings: JSON.stringify({ format: "png", dpi: 150, pages: "1" }),
},
});
expect(res.ok()).toBe(true);
const body = await res.json();
const downloadUrl = body.downloadUrl || body.pages?.[0]?.downloadUrl;
if (downloadUrl) {
const dlRes = await request.get(downloadUrl, {
headers: { Authorization: `Bearer ${token}` },
});
expect(dlRes.ok()).toBe(true);
const buffer = Buffer.from(await dlRes.body());
expect(buffer.length).toBeGreaterThan(0);
}
});
});
// ─── SVG to Raster -- Background Color ──────────────────────────
test.describe("SVG to Raster -- background color", () => {
test("SVG to PNG with white background", async ({ request }) => {
const res = await request.post("/api/v1/tools/image/svg-to-raster", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "test.svg", mimeType: "image/svg+xml", buffer: SVG_100x100 },
settings: JSON.stringify({ format: "png", width: 256, background: "#FFFFFF" }),
},
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.downloadUrl).toBeTruthy();
expect(body.processedSize).toBeGreaterThan(0);
});
test("SVG to JPEG with red background", async ({ request }) => {
const res = await request.post("/api/v1/tools/image/svg-to-raster", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "test.svg", mimeType: "image/svg+xml", buffer: SVG_100x100 },
settings: JSON.stringify({ format: "jpg", width: 200, background: "#FF0000" }),
},
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.downloadUrl).toBeTruthy();
});
test("SVG to WebP with transparent background", async ({ request }) => {
const res = await request.post("/api/v1/tools/image/svg-to-raster", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "test.svg", mimeType: "image/svg+xml", buffer: SVG_100x100 },
settings: JSON.stringify({ format: "webp", width: 300, background: "#00000000" }),
},
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.downloadUrl).toBeTruthy();
});
});
// ─── SVG to Raster -- Width/Height Combos ───────────────────────
test.describe("SVG to Raster -- dimensions", () => {
test("SVG to PNG with width only", async ({ request }) => {
const res = await request.post("/api/v1/tools/image/svg-to-raster", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "test.svg", mimeType: "image/svg+xml", buffer: SVG_100x100 },
settings: JSON.stringify({ format: "png", width: 512 }),
},
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.downloadUrl).toBeTruthy();
expect(body.processedSize).toBeGreaterThan(0);
});
test("SVG to PNG with height only", async ({ request }) => {
const res = await request.post("/api/v1/tools/image/svg-to-raster", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "test.svg", mimeType: "image/svg+xml", buffer: SVG_100x100 },
settings: JSON.stringify({ format: "png", height: 512 }),
},
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.downloadUrl).toBeTruthy();
});
test("SVG to PNG with both width and height", async ({ request }) => {
const res = await request.post("/api/v1/tools/image/svg-to-raster", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "test.svg", mimeType: "image/svg+xml", buffer: SVG_100x100 },
settings: JSON.stringify({ format: "png", width: 400, height: 300 }),
},
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.downloadUrl).toBeTruthy();
});
});
// ─── Vectorize -- Detail and Color Options ──────────────────────
test.describe("Vectorize -- options", () => {
test("vectorize with default options", async ({ request }) => {
const res = await request.post("/api/v1/tools/image/vectorize", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "test.jpg", mimeType: "image/jpeg", buffer: fixture("test-100x100.jpg") },
settings: JSON.stringify({}),
},
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.downloadUrl).toContain(".svg");
expect(body.processedSize).toBeGreaterThan(0);
});
test("vectorize with high detail (if supported)", async ({ request }) => {
const res = await request.post("/api/v1/tools/image/vectorize", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "test.png", mimeType: "image/png", buffer: PNG_200x150 },
settings: JSON.stringify({ detail: "high" }),
},
});
// detail param may not exist; either succeed or ignore it
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.downloadUrl).toContain(".svg");
});
test("vectorize with color count (if supported)", async ({ request }) => {
const res = await request.post("/api/v1/tools/image/vectorize", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "test.png", mimeType: "image/png", buffer: PNG_200x150 },
settings: JSON.stringify({ colors: 4 }),
},
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.downloadUrl).toContain(".svg");
});
test("vectorized SVG content is valid XML", async ({ request }) => {
const res = await request.post("/api/v1/tools/image/vectorize", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "test.png", mimeType: "image/png", buffer: PNG_200x150 },
settings: JSON.stringify({}),
},
});
expect(res.ok()).toBe(true);
const body = await res.json();
const dlRes = await request.get(body.downloadUrl, {
headers: { Authorization: `Bearer ${token}` },
});
expect(dlRes.ok()).toBe(true);
const svgContent = await dlRes.text();
expect(svgContent).toContain("<svg");
expect(svgContent).toContain("</svg>");
});
});
// ─── GIF Tools -- All Modes ─────────────────────────────────────
test.describe("GIF Tools -- all modes", () => {
test("GIF tool with slow speed (0.5x)", async ({ request }) => {
const simpsons = contentFixture("animated-simpsons.gif");
const res = await request.post("/api/v1/tools/image/gif-tools", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "simpsons.gif", mimeType: "image/gif", buffer: simpsons },
settings: JSON.stringify({ action: "speed", speedFactor: 0.5 }),
},
});
if (res.ok()) {
const body = await res.json();
expect(body.downloadUrl || body.frames).toBeTruthy();
} else {
const body = await res.json();
expect(body.error).toBeDefined();
}
});
test("GIF tool with fast speed (4x)", async ({ request }) => {
const simpsons = contentFixture("animated-simpsons.gif");
const res = await request.post("/api/v1/tools/image/gif-tools", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "simpsons.gif", mimeType: "image/gif", buffer: simpsons },
settings: JSON.stringify({ action: "speed", speedFactor: 4 }),
},
});
if (res.ok()) {
const body = await res.json();
expect(body.downloadUrl || body.frames).toBeTruthy();
} else {
const body = await res.json();
expect(body.error).toBeDefined();
}
});
test("GIF tool with bounce (forward+reverse)", async ({ request }) => {
const res = await request.post("/api/v1/tools/image/gif-tools", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "animated.gif", mimeType: "image/gif", buffer: ANIMATED_GIF },
settings: JSON.stringify({ action: "bounce" }),
},
});
if (res.ok()) {
const body = await res.json();
expect(body.downloadUrl || body.frames).toBeTruthy();
} else {
const body = await res.json();
expect(body.error).toBeDefined();
}
});
});
// ─── PDF to Image -- Page Selection ─────────────────────────────
test.describe("PDF to Image -- page selection", () => {
test("convert PDF page range 1-2", async ({ request }) => {
const res = await request.post("/api/v1/tools/pdf/pdf-to-image", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "test.pdf", mimeType: "application/pdf", buffer: PDF_3PAGE },
settings: JSON.stringify({ format: "png", dpi: 150, pages: "1-2" }),
},
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.downloadUrl || body.pages || body.jobId).toBeTruthy();
});
test("convert PDF page range 2-3", async ({ request }) => {
const res = await request.post("/api/v1/tools/pdf/pdf-to-image", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "test.pdf", mimeType: "application/pdf", buffer: PDF_3PAGE },
settings: JSON.stringify({ format: "jpg", dpi: 200, pages: "2-3" }),
},
});
expect(res.ok()).toBe(true);
const body = await res.json();
expect(body.downloadUrl || body.pages || body.jobId).toBeTruthy();
});
});
// ─── PDF to Image -- DPI Variations ──────────────────────────────
test.describe("PDF to Image -- DPI variations", () => {
const dpis = [72, 100, 150, 200, 300] as const;
for (const dpi of dpis) {
test(`convert PDF at DPI=${dpi}`, async ({ request }) => {
const res = await request.post("/api/v1/tools/pdf/pdf-to-image", {
headers: { Authorization: `Bearer ${token}` },
multipart: {
file: { name: "test.pdf", mimeType: "application/pdf", buffer: PDF_3PAGE },
settings: JSON.stringify({ format: "png", dpi, pages: "1" }),
},
});
expect(res.ok(), `PDF at DPI=${dpi} should succeed`).toBe(true);
const body = await res.json();
expect(body.downloadUrl || body.pages || body.jobId).toBeTruthy();
});
}
});
// ─── Image to PDF -- Orientation ─────────────────────────────────
test.describe("Image to PDF -- orientation", () => {
/**
* Build a raw multipart/form-data body for multi-file uploads.
*/
function buildMultipartLocal(
files: Array<{ name: string; filename: string; contentType: string; buffer: Buffer }>,
fields: Array<{ name: string; value: string }>,
): { body: Buffer; contentType: string } {
const boundary = `----PlaywrightBoundary${Date.now()}`;
const parts: Buffer[] = [];
for (const f of files) {
parts.push(
Buffer.from(
`--${boundary}\r\nContent-Disposition: form-data; name="${f.name}"; filename="${f.filename}"\r\nContent-Type: ${f.contentType}\r\n\r\n`,
),
);
parts.push(f.buffer);
parts.push(Buffer.from("\r\n"));
}
for (const field of fields) {
parts.push(
Buffer.from(
`--${boundary}\r\nContent-Disposition: form-data; name="${field.name}"\r\n\r\n${field.value}\r\n`,
),
);
}
parts.push(Buffer.from(`--${boundary}--\r\n`));
return {
body: Buffer.concat(parts),
contentType: `multipart/form-data; boundary=${boundary}`,
};
}
test("convert image to PDF with landscape orientation", async ({ request }) => {
const { body, contentType } = buildMultipartLocal(
[{ name: "file", filename: "test.png", contentType: "image/png", buffer: PNG_200x150 }],
[{ name: "settings", value: JSON.stringify({ pageSize: "A4", orientation: "landscape" }) }],
);
const res = await request.post("/api/v1/tools/image/image-to-pdf", {
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
data: body,
});
expect(res.ok()).toBe(true);
const json = await res.json();
expect(json.downloadUrl).toBeTruthy();
expect(json.processedSize).toBeGreaterThan(0);
});
test("convert image to PDF with portrait orientation", async ({ request }) => {
const { body, contentType } = buildMultipartLocal(
[{ name: "file", filename: "test.png", contentType: "image/png", buffer: PNG_200x150 }],
[{ name: "settings", value: JSON.stringify({ pageSize: "A4", orientation: "portrait" }) }],
);
const res = await request.post("/api/v1/tools/image/image-to-pdf", {
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
data: body,
});
expect(res.ok()).toBe(true);
const json = await res.json();
expect(json.downloadUrl).toBeTruthy();
});
test("convert multiple images to multi-page PDF", async ({ request }) => {
const { body, contentType } = buildMultipartLocal(
[
{ name: "file", filename: "a.png", contentType: "image/png", buffer: PNG_200x150 },
{
name: "file",
filename: "b.jpg",
contentType: "image/jpeg",
buffer: fixture("test-100x100.jpg"),
},
{ name: "file", filename: "c.webp", contentType: "image/webp", buffer: WEBP_50x50 },
{
name: "file",
filename: "d.heic",
contentType: "image/heic",
buffer: fixture("test-200x150.heic"),
},
],
[{ name: "settings", value: JSON.stringify({ pageSize: "Letter" }) }],
);
const res = await request.post("/api/v1/tools/image/image-to-pdf", {
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
data: body,
});
expect(res.ok()).toBe(true);
const json = await res.json();
expect(json.downloadUrl).toBeTruthy();
expect(json.processedSize).toBeGreaterThan(0);
});
test("image-to-pdf output is downloadable as valid PDF", async ({ request }) => {
const { body, contentType } = buildMultipartLocal(
[
{
name: "file",
filename: "test.jpg",
contentType: "image/jpeg",
buffer: fixture("test-100x100.jpg"),
},
],
[{ name: "settings", value: JSON.stringify({ pageSize: "A4" }) }],
);
const res = await request.post("/api/v1/tools/image/image-to-pdf", {
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
data: body,
});
expect(res.ok()).toBe(true);
const json = await res.json();
const dlRes = await request.get(json.downloadUrl, {
headers: { Authorization: `Bearer ${token}` },
});
expect(dlRes.ok()).toBe(true);
const buffer = Buffer.from(await dlRes.body());
expect(buffer.length).toBeGreaterThan(0);
// PDF magic bytes: %PDF
expect(buffer[0]).toBe(0x25); // %
expect(buffer[1]).toBe(0x50); // P
expect(buffer[2]).toBe(0x44); // D
expect(buffer[3]).toBe(0x46); // F
});
});
// ─── Auth Failure ──────────────────────────────────────────────────
test.describe("Auth failure", () => {
test("svg-to-raster without token returns 401", async ({ request }) => {
const res = await request.post("/api/v1/tools/image/svg-to-raster", {
multipart: {
file: { name: "test.svg", mimeType: "image/svg+xml", buffer: SVG_100x100 },
settings: JSON.stringify({ format: "png", width: 200 }),
},
});
expect(res.status()).toBe(401);
});
test("vectorize without token returns 401", async ({ request }) => {
const res = await request.post("/api/v1/tools/image/vectorize", {
multipart: {
file: { name: "test.png", mimeType: "image/png", buffer: PNG_200x150 },
settings: JSON.stringify({}),
},
});
expect(res.status()).toBe(401);
});
test("pdf-to-image without token returns 401", async ({ request }) => {
const res = await request.post("/api/v1/tools/pdf/pdf-to-image", {
multipart: {
file: { name: "test.pdf", mimeType: "application/pdf", buffer: PDF_3PAGE },
settings: JSON.stringify({ format: "png", dpi: 150, pages: "1" }),
},
});
expect(res.status()).toBe(401);
});
test("gif-tools without token returns 401", async ({ request }) => {
const res = await request.post("/api/v1/tools/image/gif-tools", {
multipart: {
file: { name: "animated.gif", mimeType: "image/gif", buffer: ANIMATED_GIF },
settings: JSON.stringify({}),
},
});
expect(res.status()).toBe(401);
});
test("convert without token returns 401", async ({ request }) => {
const res = await request.post("/api/v1/tools/image/convert", {
multipart: {
file: { name: "test.png", mimeType: "image/png", buffer: PNG_200x150 },
settings: JSON.stringify({ format: "jpg" }),
},
});
expect(res.status()).toBe(401);
});
});