mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
test: expand API and GUI test coverage across all tools
Add ~500 new E2E tests and ~300 new integration tests covering: - 24 new GUI E2E specs: navigation, responsive layout, keyboard shortcuts, tool UI for all 35 non-AI tools, batch/pipeline workflows, settings/RBAC, visual regression, accessibility, and performance budgets - 3 new E2E-Docker specs: batch workflows, advanced pipelines, cross-format - 1 new adversarial integration test: memory pressure, corrupted files, unicode filenames, extreme dimensions, pipeline/batch edge cases - 29 expanded integration test files: HEIC/HEIF input, large files, parameter boundaries, batch processing, format edge cases across all tools - Cross-format matrix expanded: 641 tests covering every tool x 18 formats - AI bridge unit tests expanded: lifecycle, tool modules, error propagation - Unit test gaps filled: analytics, tool-registry, web stores Also fixes: - vitest.config.ts: exclude e2e-docs and e2e-landing from Vitest runner - AI E2E specs: add sidecar health check to skip gracefully when Python AI backend is not running instead of timing out
This commit is contained in:
@@ -0,0 +1,741 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
// ─── Batch Workflows ──────────────────────────────────────────────
|
||||
// Comprehensive batch processing tests for every tool category.
|
||||
// Each batch uploads 3-5 files, processes them, and verifies all
|
||||
// outputs are valid (ZIP download or JSON with downloadUrl).
|
||||
|
||||
const FIXTURES = join(process.cwd(), "tests", "fixtures");
|
||||
const FORMATS = join(FIXTURES, "formats");
|
||||
const CONTENT = join(FIXTURES, "content");
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a raw multipart/form-data body. Playwright's `multipart` option
|
||||
* does not support arrays for the same field name, so multi-file uploads
|
||||
* must be assembled manually.
|
||||
*/
|
||||
function buildMultipart(
|
||||
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 file of files) {
|
||||
parts.push(
|
||||
Buffer.from(
|
||||
`--${boundary}\r\nContent-Disposition: form-data; name="${file.name}"; filename="${file.filename}"\r\nContent-Type: ${file.contentType}\r\n\r\n`,
|
||||
),
|
||||
);
|
||||
parts.push(file.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}`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert a batch response is valid: either a JSON with downloadUrl
|
||||
* or a raw ZIP binary (PK magic bytes).
|
||||
*/
|
||||
async function expectBatchSuccess(res: import("@playwright/test").APIResponse): Promise<void> {
|
||||
expect(res.ok()).toBe(true);
|
||||
const resContentType = res.headers()["content-type"] ?? "";
|
||||
if (resContentType.includes("application/json")) {
|
||||
const json = await res.json();
|
||||
expect(json.downloadUrl).toBeTruthy();
|
||||
} else {
|
||||
const buffer = Buffer.from(await res.body());
|
||||
expect(buffer.length).toBeGreaterThan(0);
|
||||
// ZIP magic bytes: PK\x03\x04
|
||||
expect(buffer[0]).toBe(0x50);
|
||||
expect(buffer[1]).toBe(0x4b);
|
||||
}
|
||||
}
|
||||
|
||||
const PNG_200x150 = fixture("test-200x150.png");
|
||||
const JPG_100x100 = fixture("test-100x100.jpg");
|
||||
const WEBP_50x50 = fixture("test-50x50.webp");
|
||||
const HEIC_200x150 = fixture("test-200x150.heic");
|
||||
const JPG_SAMPLE = formatFixture("sample.jpg");
|
||||
|
||||
// ─── Essential: Batch Resize 5 Images ─────────────────────────────
|
||||
|
||||
test.describe("Essential: batch resize 5 images", () => {
|
||||
test("resize 5 mixed-format images to 120px wide", async ({ request }) => {
|
||||
const avifSample = formatFixture("sample.avif");
|
||||
const { body: reqBody, contentType } = buildMultipart(
|
||||
[
|
||||
{ name: "file", filename: "a.png", contentType: "image/png", buffer: PNG_200x150 },
|
||||
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", buffer: JPG_100x100 },
|
||||
{ name: "file", filename: "c.webp", contentType: "image/webp", buffer: WEBP_50x50 },
|
||||
{ name: "file", filename: "d.heic", contentType: "image/heic", buffer: HEIC_200x150 },
|
||||
{ name: "file", filename: "e.avif", contentType: "image/avif", buffer: avifSample },
|
||||
],
|
||||
[{ name: "settings", value: JSON.stringify({ width: 120, fit: "contain" }) }],
|
||||
);
|
||||
const res = await request.post("/api/v1/tools/resize/batch", {
|
||||
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
||||
data: reqBody,
|
||||
});
|
||||
await expectBatchSuccess(res);
|
||||
});
|
||||
|
||||
test("resize 5 images with explicit width and height", async ({ request }) => {
|
||||
const tiffSample = formatFixture("sample.tiff");
|
||||
const { body: reqBody, contentType } = buildMultipart(
|
||||
[
|
||||
{ name: "file", filename: "a.png", contentType: "image/png", buffer: PNG_200x150 },
|
||||
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", buffer: JPG_100x100 },
|
||||
{ name: "file", filename: "c.webp", contentType: "image/webp", buffer: WEBP_50x50 },
|
||||
{ name: "file", filename: "d.jpg", contentType: "image/jpeg", buffer: JPG_SAMPLE },
|
||||
{ name: "file", filename: "e.tiff", contentType: "image/tiff", buffer: tiffSample },
|
||||
],
|
||||
[{ name: "settings", value: JSON.stringify({ width: 64, height: 64, fit: "cover" }) }],
|
||||
);
|
||||
const res = await request.post("/api/v1/tools/resize/batch", {
|
||||
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
||||
data: reqBody,
|
||||
});
|
||||
await expectBatchSuccess(res);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Essential: Batch Compress ────────────────────────────────────
|
||||
|
||||
test.describe("Essential: batch compress", () => {
|
||||
test("compress 5 images with aggressive quality", async ({ request }) => {
|
||||
const pngSample = formatFixture("sample.png");
|
||||
const { body: reqBody, contentType } = buildMultipart(
|
||||
[
|
||||
{ name: "file", filename: "a.png", contentType: "image/png", buffer: PNG_200x150 },
|
||||
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", buffer: JPG_100x100 },
|
||||
{ name: "file", filename: "c.webp", contentType: "image/webp", buffer: WEBP_50x50 },
|
||||
{ name: "file", filename: "d.jpg", contentType: "image/jpeg", buffer: JPG_SAMPLE },
|
||||
{ name: "file", filename: "e.png", contentType: "image/png", buffer: pngSample },
|
||||
],
|
||||
[{ name: "settings", value: JSON.stringify({ quality: 20 }) }],
|
||||
);
|
||||
const res = await request.post("/api/v1/tools/compress/batch", {
|
||||
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
||||
data: reqBody,
|
||||
});
|
||||
await expectBatchSuccess(res);
|
||||
});
|
||||
|
||||
test("compress 3 images with moderate quality", async ({ request }) => {
|
||||
const { body: reqBody, contentType } = buildMultipart(
|
||||
[
|
||||
{ name: "file", filename: "a.png", contentType: "image/png", buffer: PNG_200x150 },
|
||||
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", buffer: JPG_SAMPLE },
|
||||
{ name: "file", filename: "c.heic", contentType: "image/heic", buffer: HEIC_200x150 },
|
||||
],
|
||||
[{ name: "settings", value: JSON.stringify({ quality: 60 }) }],
|
||||
);
|
||||
const res = await request.post("/api/v1/tools/compress/batch", {
|
||||
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
||||
data: reqBody,
|
||||
});
|
||||
await expectBatchSuccess(res);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Essential: Batch Rotate ──────────────────────────────────────
|
||||
|
||||
test.describe("Essential: batch rotate", () => {
|
||||
test("rotate 4 images by 90 degrees", async ({ request }) => {
|
||||
const { body: reqBody, contentType } = buildMultipart(
|
||||
[
|
||||
{ name: "file", filename: "a.png", contentType: "image/png", buffer: PNG_200x150 },
|
||||
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", buffer: JPG_100x100 },
|
||||
{ name: "file", filename: "c.webp", contentType: "image/webp", buffer: WEBP_50x50 },
|
||||
{ name: "file", filename: "d.heic", contentType: "image/heic", buffer: HEIC_200x150 },
|
||||
],
|
||||
[{ name: "settings", value: JSON.stringify({ angle: 90 }) }],
|
||||
);
|
||||
const res = await request.post("/api/v1/tools/rotate/batch", {
|
||||
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
||||
data: reqBody,
|
||||
});
|
||||
await expectBatchSuccess(res);
|
||||
});
|
||||
|
||||
test("rotate 3 images by 45 degrees with white background", async ({ request }) => {
|
||||
const { body: reqBody, contentType } = buildMultipart(
|
||||
[
|
||||
{ name: "file", filename: "a.png", contentType: "image/png", buffer: PNG_200x150 },
|
||||
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", buffer: JPG_100x100 },
|
||||
{ name: "file", filename: "c.jpg", contentType: "image/jpeg", buffer: JPG_SAMPLE },
|
||||
],
|
||||
[{ name: "settings", value: JSON.stringify({ angle: 45, background: "#ffffff" }) }],
|
||||
);
|
||||
const res = await request.post("/api/v1/tools/rotate/batch", {
|
||||
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
||||
data: reqBody,
|
||||
});
|
||||
await expectBatchSuccess(res);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Adjustment: Batch Color Adjustments ──────────────────────────
|
||||
|
||||
test.describe("Adjustment: batch color adjustments", () => {
|
||||
test("adjust brightness and contrast on 4 images", async ({ request }) => {
|
||||
const { body: reqBody, contentType } = buildMultipart(
|
||||
[
|
||||
{ name: "file", filename: "a.png", contentType: "image/png", buffer: PNG_200x150 },
|
||||
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", buffer: JPG_100x100 },
|
||||
{ name: "file", filename: "c.webp", contentType: "image/webp", buffer: WEBP_50x50 },
|
||||
{ name: "file", filename: "d.jpg", contentType: "image/jpeg", buffer: JPG_SAMPLE },
|
||||
],
|
||||
[
|
||||
{
|
||||
name: "settings",
|
||||
value: JSON.stringify({ brightness: 25, contrast: 15, saturation: -10 }),
|
||||
},
|
||||
],
|
||||
);
|
||||
const res = await request.post("/api/v1/tools/adjust-colors/batch", {
|
||||
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
||||
data: reqBody,
|
||||
});
|
||||
await expectBatchSuccess(res);
|
||||
});
|
||||
|
||||
test("batch grayscale conversion on 5 images", async ({ request }) => {
|
||||
const avifSample = formatFixture("sample.avif");
|
||||
const { body: reqBody, contentType } = buildMultipart(
|
||||
[
|
||||
{ name: "file", filename: "a.png", contentType: "image/png", buffer: PNG_200x150 },
|
||||
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", buffer: JPG_100x100 },
|
||||
{ name: "file", filename: "c.webp", contentType: "image/webp", buffer: WEBP_50x50 },
|
||||
{ name: "file", filename: "d.heic", contentType: "image/heic", buffer: HEIC_200x150 },
|
||||
{ name: "file", filename: "e.avif", contentType: "image/avif", buffer: avifSample },
|
||||
],
|
||||
[{ name: "settings", value: JSON.stringify({ grayscale: true }) }],
|
||||
);
|
||||
const res = await request.post("/api/v1/tools/adjust-colors/batch", {
|
||||
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
||||
data: reqBody,
|
||||
});
|
||||
await expectBatchSuccess(res);
|
||||
});
|
||||
|
||||
test("batch negative brightness on 3 images", async ({ request }) => {
|
||||
const { body: reqBody, contentType } = buildMultipart(
|
||||
[
|
||||
{ name: "file", filename: "a.png", contentType: "image/png", buffer: PNG_200x150 },
|
||||
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", buffer: JPG_100x100 },
|
||||
{ name: "file", filename: "c.jpg", contentType: "image/jpeg", buffer: JPG_SAMPLE },
|
||||
],
|
||||
[{ name: "settings", value: JSON.stringify({ brightness: -30, contrast: 20 }) }],
|
||||
);
|
||||
const res = await request.post("/api/v1/tools/adjust-colors/batch", {
|
||||
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
||||
data: reqBody,
|
||||
});
|
||||
await expectBatchSuccess(res);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Adjustment: Batch Sharpening ─────────────────────────────────
|
||||
|
||||
test.describe("Adjustment: batch sharpening", () => {
|
||||
test("sharpen 4 images with sigma 2.0", async ({ request }) => {
|
||||
const { body: reqBody, contentType } = buildMultipart(
|
||||
[
|
||||
{ name: "file", filename: "a.png", contentType: "image/png", buffer: PNG_200x150 },
|
||||
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", buffer: JPG_100x100 },
|
||||
{ name: "file", filename: "c.webp", contentType: "image/webp", buffer: WEBP_50x50 },
|
||||
{ name: "file", filename: "d.jpg", contentType: "image/jpeg", buffer: JPG_SAMPLE },
|
||||
],
|
||||
[{ name: "settings", value: JSON.stringify({ sigma: 2.0 }) }],
|
||||
);
|
||||
const res = await request.post("/api/v1/tools/sharpening/batch", {
|
||||
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
||||
data: reqBody,
|
||||
});
|
||||
await expectBatchSuccess(res);
|
||||
});
|
||||
|
||||
test("sharpen 3 images with low sigma for subtle effect", async ({ request }) => {
|
||||
const { body: reqBody, contentType } = buildMultipart(
|
||||
[
|
||||
{ name: "file", filename: "a.heic", contentType: "image/heic", buffer: HEIC_200x150 },
|
||||
{ name: "file", filename: "b.png", contentType: "image/png", buffer: PNG_200x150 },
|
||||
{ name: "file", filename: "c.jpg", contentType: "image/jpeg", buffer: JPG_SAMPLE },
|
||||
],
|
||||
[{ name: "settings", value: JSON.stringify({ sigma: 0.5 }) }],
|
||||
);
|
||||
const res = await request.post("/api/v1/tools/sharpening/batch", {
|
||||
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
||||
data: reqBody,
|
||||
});
|
||||
await expectBatchSuccess(res);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Overlay: Batch Watermark Text ────────────────────────────────
|
||||
|
||||
test.describe("Overlay: batch watermark text", () => {
|
||||
test("watermark 5 images with tiled text", async ({ request }) => {
|
||||
const webpSample = formatFixture("sample.webp");
|
||||
const { body: reqBody, contentType } = buildMultipart(
|
||||
[
|
||||
{ name: "file", filename: "a.png", contentType: "image/png", buffer: PNG_200x150 },
|
||||
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", buffer: JPG_100x100 },
|
||||
{ name: "file", filename: "c.webp", contentType: "image/webp", buffer: WEBP_50x50 },
|
||||
{ name: "file", filename: "d.jpg", contentType: "image/jpeg", buffer: JPG_SAMPLE },
|
||||
{ name: "file", filename: "e.webp", contentType: "image/webp", buffer: webpSample },
|
||||
],
|
||||
[
|
||||
{
|
||||
name: "settings",
|
||||
value: JSON.stringify({
|
||||
text: "CONFIDENTIAL",
|
||||
fontSize: 18,
|
||||
color: "#ff0000",
|
||||
opacity: 30,
|
||||
position: "tiled",
|
||||
rotation: -45,
|
||||
}),
|
||||
},
|
||||
],
|
||||
);
|
||||
const res = await request.post("/api/v1/tools/watermark-text/batch", {
|
||||
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
||||
data: reqBody,
|
||||
});
|
||||
await expectBatchSuccess(res);
|
||||
});
|
||||
|
||||
test("watermark 3 images at bottom-right", async ({ request }) => {
|
||||
const { body: reqBody, contentType } = buildMultipart(
|
||||
[
|
||||
{ name: "file", filename: "a.png", contentType: "image/png", buffer: PNG_200x150 },
|
||||
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", buffer: JPG_SAMPLE },
|
||||
{ name: "file", filename: "c.heic", contentType: "image/heic", buffer: HEIC_200x150 },
|
||||
],
|
||||
[
|
||||
{
|
||||
name: "settings",
|
||||
value: JSON.stringify({
|
||||
text: "@snapotter",
|
||||
fontSize: 14,
|
||||
color: "#ffffff",
|
||||
opacity: 50,
|
||||
position: "bottom-right",
|
||||
}),
|
||||
},
|
||||
],
|
||||
);
|
||||
const res = await request.post("/api/v1/tools/watermark-text/batch", {
|
||||
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
||||
data: reqBody,
|
||||
});
|
||||
await expectBatchSuccess(res);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Overlay: Batch Watermark Image ───────────────────────────────
|
||||
|
||||
test.describe("Overlay: batch watermark image", () => {
|
||||
test("watermark 3 images with a logo overlay", async ({ request }) => {
|
||||
const watermarkImg = contentFixture("watermark.jpg");
|
||||
const { body: reqBody, contentType } = buildMultipart(
|
||||
[
|
||||
{ name: "file", filename: "a.png", contentType: "image/png", buffer: PNG_200x150 },
|
||||
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", buffer: JPG_SAMPLE },
|
||||
{ name: "file", filename: "c.webp", contentType: "image/webp", buffer: WEBP_50x50 },
|
||||
{
|
||||
name: "watermark",
|
||||
filename: "logo.jpg",
|
||||
contentType: "image/jpeg",
|
||||
buffer: watermarkImg,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
name: "settings",
|
||||
value: JSON.stringify({ position: "bottom-right", opacity: 50, scale: 20 }),
|
||||
},
|
||||
],
|
||||
);
|
||||
const res = await request.post("/api/v1/tools/watermark-image/batch", {
|
||||
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
||||
data: reqBody,
|
||||
});
|
||||
// watermark-image batch may not be registered
|
||||
if (res.status() === 404) {
|
||||
const json = await res.json();
|
||||
expect(json.error).toBeDefined();
|
||||
return;
|
||||
}
|
||||
await expectBatchSuccess(res);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Format: Batch Convert JPEG to PNG ────────────────────────────
|
||||
|
||||
test.describe("Format: batch convert JPEG to PNG", () => {
|
||||
test("convert 5 JPEG files to PNG", async ({ request }) => {
|
||||
const portrait = contentFixture("portrait-color.jpg");
|
||||
const portraitBw = contentFixture("portrait-bw.jpeg");
|
||||
const { body: reqBody, contentType } = buildMultipart(
|
||||
[
|
||||
{ name: "file", filename: "a.jpg", contentType: "image/jpeg", buffer: JPG_100x100 },
|
||||
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", buffer: JPG_SAMPLE },
|
||||
{
|
||||
name: "file",
|
||||
filename: "c.jpg",
|
||||
contentType: "image/jpeg",
|
||||
buffer: fixture("test-with-exif.jpg"),
|
||||
},
|
||||
{ name: "file", filename: "d.jpg", contentType: "image/jpeg", buffer: portrait },
|
||||
{ name: "file", filename: "e.jpeg", contentType: "image/jpeg", buffer: portraitBw },
|
||||
],
|
||||
[{ name: "settings", value: JSON.stringify({ format: "png" }) }],
|
||||
);
|
||||
const res = await request.post("/api/v1/tools/convert/batch", {
|
||||
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
||||
data: reqBody,
|
||||
});
|
||||
await expectBatchSuccess(res);
|
||||
});
|
||||
|
||||
test("convert 4 mixed formats to WebP", async ({ request }) => {
|
||||
const tiffSample = formatFixture("sample.tiff");
|
||||
const { body: reqBody, contentType } = buildMultipart(
|
||||
[
|
||||
{ name: "file", filename: "a.png", contentType: "image/png", buffer: PNG_200x150 },
|
||||
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", buffer: JPG_SAMPLE },
|
||||
{ name: "file", filename: "c.heic", contentType: "image/heic", buffer: HEIC_200x150 },
|
||||
{ name: "file", filename: "d.tiff", contentType: "image/tiff", buffer: tiffSample },
|
||||
],
|
||||
[{ name: "settings", value: JSON.stringify({ format: "webp", quality: 80 }) }],
|
||||
);
|
||||
const res = await request.post("/api/v1/tools/convert/batch", {
|
||||
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
||||
data: reqBody,
|
||||
});
|
||||
await expectBatchSuccess(res);
|
||||
});
|
||||
|
||||
test("convert 3 formats to AVIF", async ({ request }) => {
|
||||
const { body: reqBody, contentType } = buildMultipart(
|
||||
[
|
||||
{ name: "file", filename: "a.png", contentType: "image/png", buffer: PNG_200x150 },
|
||||
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", buffer: JPG_100x100 },
|
||||
{ name: "file", filename: "c.webp", contentType: "image/webp", buffer: WEBP_50x50 },
|
||||
],
|
||||
[{ name: "settings", value: JSON.stringify({ format: "avif" }) }],
|
||||
);
|
||||
const res = await request.post("/api/v1/tools/convert/batch", {
|
||||
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
||||
data: reqBody,
|
||||
});
|
||||
await expectBatchSuccess(res);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Utility: Batch Info ──────────────────────────────────────────
|
||||
|
||||
test.describe("Utility: batch info", () => {
|
||||
test("get info for 5 images of different formats", async ({ request }) => {
|
||||
const tiffSample = formatFixture("sample.tiff");
|
||||
const { body: reqBody, contentType } = buildMultipart(
|
||||
[
|
||||
{ name: "file", filename: "a.png", contentType: "image/png", buffer: PNG_200x150 },
|
||||
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", buffer: JPG_100x100 },
|
||||
{ name: "file", filename: "c.webp", contentType: "image/webp", buffer: WEBP_50x50 },
|
||||
{ name: "file", filename: "d.heic", contentType: "image/heic", buffer: HEIC_200x150 },
|
||||
{ name: "file", filename: "e.tiff", contentType: "image/tiff", buffer: tiffSample },
|
||||
],
|
||||
[],
|
||||
);
|
||||
const res = await request.post("/api/v1/tools/info/batch", {
|
||||
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
||||
data: reqBody,
|
||||
});
|
||||
// info/batch may not exist as a batch endpoint
|
||||
if (res.status() === 404) {
|
||||
const json = await res.json();
|
||||
expect(json.error).toBeDefined();
|
||||
return;
|
||||
}
|
||||
expect(res.ok()).toBe(true);
|
||||
const json = await res.json();
|
||||
// Batch info should return metadata for each file
|
||||
if (json.results) {
|
||||
expect(json.results).toBeInstanceOf(Array);
|
||||
expect(json.results.length).toBe(5);
|
||||
} else if (json.downloadUrl) {
|
||||
expect(json.downloadUrl).toBeTruthy();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Utility: Batch Strip Metadata ────────────────────────────────
|
||||
|
||||
test.describe("Utility: batch strip metadata", () => {
|
||||
test("strip metadata from 4 images", async ({ request }) => {
|
||||
const jpgExif = fixture("test-with-exif.jpg");
|
||||
const { body: reqBody, contentType } = buildMultipart(
|
||||
[
|
||||
{ name: "file", filename: "a.jpg", contentType: "image/jpeg", buffer: jpgExif },
|
||||
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", buffer: JPG_100x100 },
|
||||
{ name: "file", filename: "c.png", contentType: "image/png", buffer: PNG_200x150 },
|
||||
{ name: "file", filename: "d.jpg", contentType: "image/jpeg", buffer: JPG_SAMPLE },
|
||||
],
|
||||
[{ name: "settings", value: JSON.stringify({}) }],
|
||||
);
|
||||
const res = await request.post("/api/v1/tools/strip-metadata/batch", {
|
||||
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
||||
data: reqBody,
|
||||
});
|
||||
await expectBatchSuccess(res);
|
||||
});
|
||||
|
||||
test("strip metadata from 3 images including HEIC", async ({ request }) => {
|
||||
const jpgExif = fixture("test-with-exif.jpg");
|
||||
const { body: reqBody, contentType } = buildMultipart(
|
||||
[
|
||||
{ name: "file", filename: "a.jpg", contentType: "image/jpeg", buffer: jpgExif },
|
||||
{ name: "file", filename: "b.heic", contentType: "image/heic", buffer: HEIC_200x150 },
|
||||
{ name: "file", filename: "c.webp", contentType: "image/webp", buffer: WEBP_50x50 },
|
||||
],
|
||||
[{ name: "settings", value: JSON.stringify({}) }],
|
||||
);
|
||||
const res = await request.post("/api/v1/tools/strip-metadata/batch", {
|
||||
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
||||
data: reqBody,
|
||||
});
|
||||
await expectBatchSuccess(res);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Essential: Batch Crop ────────────────────────────────────────
|
||||
|
||||
test.describe("Essential: batch crop", () => {
|
||||
test("crop 4 images to a small center region", async ({ request }) => {
|
||||
const { body: reqBody, contentType } = buildMultipart(
|
||||
[
|
||||
{ name: "file", filename: "a.png", contentType: "image/png", buffer: PNG_200x150 },
|
||||
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", buffer: JPG_100x100 },
|
||||
{ name: "file", filename: "c.webp", contentType: "image/webp", buffer: WEBP_50x50 },
|
||||
{ name: "file", filename: "d.jpg", contentType: "image/jpeg", buffer: JPG_SAMPLE },
|
||||
],
|
||||
[
|
||||
{
|
||||
name: "settings",
|
||||
value: JSON.stringify({ left: 5, top: 5, width: 40, height: 40 }),
|
||||
},
|
||||
],
|
||||
);
|
||||
const res = await request.post("/api/v1/tools/crop/batch", {
|
||||
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
||||
data: reqBody,
|
||||
});
|
||||
await expectBatchSuccess(res);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Overlay: Batch Text Overlay ──────────────────────────────────
|
||||
|
||||
test.describe("Overlay: batch text overlay", () => {
|
||||
test("text overlay on 4 images with background box", async ({ request }) => {
|
||||
const { body: reqBody, contentType } = buildMultipart(
|
||||
[
|
||||
{ name: "file", filename: "a.png", contentType: "image/png", buffer: PNG_200x150 },
|
||||
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", buffer: JPG_100x100 },
|
||||
{ name: "file", filename: "c.webp", contentType: "image/webp", buffer: WEBP_50x50 },
|
||||
{ name: "file", filename: "d.jpg", contentType: "image/jpeg", buffer: JPG_SAMPLE },
|
||||
],
|
||||
[
|
||||
{
|
||||
name: "settings",
|
||||
value: JSON.stringify({
|
||||
text: "BATCH CAPTION",
|
||||
fontSize: 16,
|
||||
color: "#ffffff",
|
||||
position: "bottom",
|
||||
backgroundBox: true,
|
||||
backgroundColor: "#000000",
|
||||
}),
|
||||
},
|
||||
],
|
||||
);
|
||||
const res = await request.post("/api/v1/tools/text-overlay/batch", {
|
||||
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
||||
data: reqBody,
|
||||
});
|
||||
await expectBatchSuccess(res);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Format: Batch Convert to TIFF ────────────────────────────────
|
||||
|
||||
test.describe("Format: batch convert to TIFF", () => {
|
||||
test("convert 3 images to TIFF", async ({ request }) => {
|
||||
const { body: reqBody, contentType } = buildMultipart(
|
||||
[
|
||||
{ name: "file", filename: "a.png", contentType: "image/png", buffer: PNG_200x150 },
|
||||
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", buffer: JPG_100x100 },
|
||||
{ name: "file", filename: "c.webp", contentType: "image/webp", buffer: WEBP_50x50 },
|
||||
],
|
||||
[{ name: "settings", value: JSON.stringify({ format: "tiff" }) }],
|
||||
);
|
||||
const res = await request.post("/api/v1/tools/convert/batch", {
|
||||
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
||||
data: reqBody,
|
||||
});
|
||||
await expectBatchSuccess(res);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Adjustment: Batch Enhance ────────────────────────────────────
|
||||
|
||||
test.describe("Adjustment: batch enhance", () => {
|
||||
test("enhance 4 images with vivid preset", async ({ request }) => {
|
||||
const { body: reqBody, contentType } = buildMultipart(
|
||||
[
|
||||
{ name: "file", filename: "a.png", contentType: "image/png", buffer: PNG_200x150 },
|
||||
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", buffer: JPG_100x100 },
|
||||
{ name: "file", filename: "c.webp", contentType: "image/webp", buffer: WEBP_50x50 },
|
||||
{ name: "file", filename: "d.jpg", contentType: "image/jpeg", buffer: JPG_SAMPLE },
|
||||
],
|
||||
[{ name: "settings", value: JSON.stringify({ preset: "vivid" }) }],
|
||||
);
|
||||
const res = await request.post("/api/v1/tools/image-enhancement/batch", {
|
||||
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
||||
data: reqBody,
|
||||
});
|
||||
await expectBatchSuccess(res);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Utility: Batch Optimize for Web ──────────────────────────────
|
||||
|
||||
test.describe("Utility: batch optimize for web", () => {
|
||||
test("optimize 5 images for web delivery", async ({ request }) => {
|
||||
const webpSample = formatFixture("sample.webp");
|
||||
const { body: reqBody, contentType } = buildMultipart(
|
||||
[
|
||||
{ name: "file", filename: "a.png", contentType: "image/png", buffer: PNG_200x150 },
|
||||
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", buffer: JPG_100x100 },
|
||||
{ name: "file", filename: "c.webp", contentType: "image/webp", buffer: WEBP_50x50 },
|
||||
{ name: "file", filename: "d.jpg", contentType: "image/jpeg", buffer: JPG_SAMPLE },
|
||||
{ name: "file", filename: "e.webp", contentType: "image/webp", buffer: webpSample },
|
||||
],
|
||||
[{ name: "settings", value: JSON.stringify({ maxWidth: 600, quality: 65 }) }],
|
||||
);
|
||||
const res = await request.post("/api/v1/tools/optimize-for-web/batch", {
|
||||
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
||||
data: reqBody,
|
||||
});
|
||||
await expectBatchSuccess(res);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Overlay: Batch Border ────────────────────────────────────────
|
||||
|
||||
test.describe("Overlay: batch border", () => {
|
||||
test("add 20px red border to 4 images", async ({ request }) => {
|
||||
const { body: reqBody, contentType } = buildMultipart(
|
||||
[
|
||||
{ name: "file", filename: "a.png", contentType: "image/png", buffer: PNG_200x150 },
|
||||
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", buffer: JPG_100x100 },
|
||||
{ name: "file", filename: "c.webp", contentType: "image/webp", buffer: WEBP_50x50 },
|
||||
{ name: "file", filename: "d.heic", contentType: "image/heic", buffer: HEIC_200x150 },
|
||||
],
|
||||
[{ name: "settings", value: JSON.stringify({ size: 20, color: "#ff0000" }) }],
|
||||
);
|
||||
const res = await request.post("/api/v1/tools/border/batch", {
|
||||
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
||||
data: reqBody,
|
||||
});
|
||||
await expectBatchSuccess(res);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Format: Batch Convert to GIF ─────────────────────────────────
|
||||
|
||||
test.describe("Format: batch convert to GIF", () => {
|
||||
test("convert 3 images to GIF", async ({ request }) => {
|
||||
const { body: reqBody, contentType } = buildMultipart(
|
||||
[
|
||||
{ name: "file", filename: "a.png", contentType: "image/png", buffer: PNG_200x150 },
|
||||
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", buffer: JPG_100x100 },
|
||||
{ name: "file", filename: "c.webp", contentType: "image/webp", buffer: WEBP_50x50 },
|
||||
],
|
||||
[{ name: "settings", value: JSON.stringify({ format: "gif" }) }],
|
||||
);
|
||||
const res = await request.post("/api/v1/tools/convert/batch", {
|
||||
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
||||
data: reqBody,
|
||||
});
|
||||
await expectBatchSuccess(res);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Adjustment: Batch Replace Color ──────────────────────────────
|
||||
|
||||
test.describe("Adjustment: batch replace color", () => {
|
||||
test("replace white with light gray in 3 images", async ({ request }) => {
|
||||
const { body: reqBody, contentType } = buildMultipart(
|
||||
[
|
||||
{ name: "file", filename: "a.png", contentType: "image/png", buffer: PNG_200x150 },
|
||||
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", buffer: JPG_100x100 },
|
||||
{ name: "file", filename: "c.webp", contentType: "image/webp", buffer: WEBP_50x50 },
|
||||
],
|
||||
[
|
||||
{
|
||||
name: "settings",
|
||||
value: JSON.stringify({
|
||||
targetColor: "#ffffff",
|
||||
replacementColor: "#e0e0e0",
|
||||
tolerance: 30,
|
||||
}),
|
||||
},
|
||||
],
|
||||
);
|
||||
const res = await request.post("/api/v1/tools/replace-color/batch", {
|
||||
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
||||
data: reqBody,
|
||||
});
|
||||
await expectBatchSuccess(res);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,915 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
// ─── Cross-Format Docker Tests ────────────────────────────────────
|
||||
// Tests key tools against multiple input formats through the real
|
||||
// Docker container with real Sharp processing. Verifies actual
|
||||
// output dimensions, format, and file size.
|
||||
|
||||
const FIXTURES = join(process.cwd(), "tests", "fixtures");
|
||||
const FORMATS = join(FIXTURES, "formats");
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
const PNG_200x150 = fixture("test-200x150.png");
|
||||
const JPG_100x100 = fixture("test-100x100.jpg");
|
||||
const WEBP_50x50 = fixture("test-50x50.webp");
|
||||
const HEIC_200x150 = fixture("test-200x150.heic");
|
||||
|
||||
// ─── Resize: Format Matrix ───────────────────────────────────────
|
||||
|
||||
test.describe("Resize: JPEG", () => {
|
||||
test("resize JPEG to 50px wide", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/tools/resize", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "test.jpg", mimeType: "image/jpeg", buffer: JPG_100x100 },
|
||||
settings: JSON.stringify({ width: 50, fit: "contain" }),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toBeTruthy();
|
||||
expect(body.processedSize).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Resize: PNG", () => {
|
||||
test("resize PNG to 80px wide", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/tools/resize", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "test.png", mimeType: "image/png", buffer: PNG_200x150 },
|
||||
settings: JSON.stringify({ width: 80, fit: "contain" }),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toBeTruthy();
|
||||
expect(body.processedSize).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Resize: WebP", () => {
|
||||
test("resize WebP to 30px wide", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/tools/resize", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "test.webp", mimeType: "image/webp", buffer: WEBP_50x50 },
|
||||
settings: JSON.stringify({ width: 30, fit: "contain" }),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toBeTruthy();
|
||||
expect(body.processedSize).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Resize: AVIF", () => {
|
||||
test("resize AVIF to 60px wide", async ({ request }) => {
|
||||
const avif = formatFixture("sample.avif");
|
||||
const res = await request.post("/api/v1/tools/resize", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "sample.avif", mimeType: "image/avif", buffer: avif },
|
||||
settings: JSON.stringify({ width: 60, fit: "contain" }),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toBeTruthy();
|
||||
expect(body.processedSize).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Resize: HEIC", () => {
|
||||
test("resize HEIC to 80px wide", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/tools/resize", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "test.heic", mimeType: "image/heic", buffer: HEIC_200x150 },
|
||||
settings: JSON.stringify({ width: 80, fit: "contain" }),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toBeTruthy();
|
||||
expect(body.processedSize).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Resize: TIFF", () => {
|
||||
test("resize TIFF to 60px wide", async ({ request }) => {
|
||||
const tiff = formatFixture("sample.tiff");
|
||||
const res = await request.post("/api/v1/tools/resize", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "sample.tiff", mimeType: "image/tiff", buffer: tiff },
|
||||
settings: JSON.stringify({ width: 60, fit: "contain" }),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toBeTruthy();
|
||||
expect(body.processedSize).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Resize: BMP", () => {
|
||||
test("resize BMP to 40px wide", async ({ request }) => {
|
||||
const bmp = formatFixture("sample.bmp");
|
||||
const res = await request.post("/api/v1/tools/resize", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "sample.bmp", mimeType: "image/bmp", buffer: bmp },
|
||||
settings: JSON.stringify({ width: 40, fit: "contain" }),
|
||||
},
|
||||
});
|
||||
// BMP may not be supported by Sharp natively
|
||||
if (res.ok()) {
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toBeTruthy();
|
||||
expect(body.processedSize).toBeGreaterThan(0);
|
||||
} else {
|
||||
expect([400, 422]).toContain(res.status());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Resize: GIF", () => {
|
||||
test("resize GIF to 40px wide", async ({ request }) => {
|
||||
const gif = formatFixture("sample.gif");
|
||||
const res = await request.post("/api/v1/tools/resize", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "sample.gif", mimeType: "image/gif", buffer: gif },
|
||||
settings: JSON.stringify({ width: 40, fit: "contain" }),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toBeTruthy();
|
||||
expect(body.processedSize).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Convert: From Each Format to PNG ─────────────────────────────
|
||||
|
||||
test.describe("Convert to PNG: from JPEG", () => {
|
||||
test("JPEG to PNG", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/tools/convert", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "test.jpg", mimeType: "image/jpeg", buffer: JPG_100x100 },
|
||||
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.describe("Convert to PNG: from WebP", () => {
|
||||
test("WebP to PNG", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/tools/convert", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "test.webp", mimeType: "image/webp", buffer: WEBP_50x50 },
|
||||
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.describe("Convert to PNG: from AVIF", () => {
|
||||
test("AVIF to PNG", async ({ request }) => {
|
||||
const avif = formatFixture("sample.avif");
|
||||
const res = await request.post("/api/v1/tools/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");
|
||||
expect(body.processedSize).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Convert to PNG: from HEIC", () => {
|
||||
test("HEIC to PNG", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/tools/convert", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "test.heic", mimeType: "image/heic", buffer: HEIC_200x150 },
|
||||
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.describe("Convert to PNG: from TIFF", () => {
|
||||
test("TIFF to PNG", async ({ request }) => {
|
||||
const tiff = formatFixture("sample.tiff");
|
||||
const res = await request.post("/api/v1/tools/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.describe("Convert to PNG: from GIF", () => {
|
||||
test("GIF to PNG", async ({ request }) => {
|
||||
const gif = formatFixture("sample.gif");
|
||||
const res = await request.post("/api/v1/tools/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");
|
||||
expect(body.processedSize).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Convert to PNG: from HEIF", () => {
|
||||
test("HEIF to PNG", async ({ request }) => {
|
||||
const heif = formatFixture("sample.heif");
|
||||
const res = await request.post("/api/v1/tools/convert", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "sample.heif", mimeType: "image/heif", buffer: heif },
|
||||
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.describe("Convert to PNG: from BMP", () => {
|
||||
test("BMP to PNG", async ({ request }) => {
|
||||
const bmp = formatFixture("sample.bmp");
|
||||
const res = await request.post("/api/v1/tools/convert", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "sample.bmp", mimeType: "image/bmp", buffer: bmp },
|
||||
settings: JSON.stringify({ format: "png" }),
|
||||
},
|
||||
});
|
||||
// BMP decode may not be supported
|
||||
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());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Compress: Format Matrix ──────────────────────────────────────
|
||||
|
||||
test.describe("Compress: JPEG", () => {
|
||||
test("compress JPEG with quality 30", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/tools/compress", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "sample.jpg", mimeType: "image/jpeg", buffer: formatFixture("sample.jpg") },
|
||||
settings: JSON.stringify({ quality: 30 }),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toBeTruthy();
|
||||
expect(body.processedSize).toBeGreaterThan(0);
|
||||
expect(body.processedSize).toBeLessThan(body.originalSize);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Compress: PNG", () => {
|
||||
test("compress PNG with quality 40", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/tools/compress", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "test.png", mimeType: "image/png", buffer: PNG_200x150 },
|
||||
settings: JSON.stringify({ quality: 40 }),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toBeTruthy();
|
||||
expect(body.processedSize).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Compress: WebP", () => {
|
||||
test("compress WebP with quality 25", async ({ request }) => {
|
||||
const webp = formatFixture("sample.webp");
|
||||
const res = await request.post("/api/v1/tools/compress", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "sample.webp", mimeType: "image/webp", buffer: webp },
|
||||
settings: JSON.stringify({ quality: 25 }),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toBeTruthy();
|
||||
expect(body.processedSize).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Compress: AVIF", () => {
|
||||
test("compress AVIF with quality 40", async ({ request }) => {
|
||||
const avif = formatFixture("sample.avif");
|
||||
const res = await request.post("/api/v1/tools/compress", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "sample.avif", mimeType: "image/avif", buffer: avif },
|
||||
settings: JSON.stringify({ quality: 40 }),
|
||||
},
|
||||
});
|
||||
// AVIF compression may not be supported
|
||||
if (res.ok()) {
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toBeTruthy();
|
||||
expect(body.processedSize).toBeGreaterThan(0);
|
||||
} else {
|
||||
expect([400, 422]).toContain(res.status());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Compress: HEIC", () => {
|
||||
test("compress HEIC with quality 50", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/tools/compress", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "test.heic", mimeType: "image/heic", buffer: HEIC_200x150 },
|
||||
settings: JSON.stringify({ quality: 50 }),
|
||||
},
|
||||
});
|
||||
// HEIC compression may not be directly supported
|
||||
if (res.ok()) {
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toBeTruthy();
|
||||
expect(body.processedSize).toBeGreaterThan(0);
|
||||
} else {
|
||||
expect([400, 422]).toContain(res.status());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Info: Every Format ───────────────────────────────────────────
|
||||
|
||||
test.describe("Info: JPEG", () => {
|
||||
test("get info for JPEG returns correct dimensions", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/tools/info", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "test.jpg", mimeType: "image/jpeg", buffer: JPG_100x100 },
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.width).toBe(100);
|
||||
expect(body.height).toBe(100);
|
||||
expect(body.format).toBe("jpeg");
|
||||
expect(body.fileSize).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Info: PNG", () => {
|
||||
test("get info for PNG returns correct dimensions", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/tools/info", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "test.png", mimeType: "image/png", buffer: PNG_200x150 },
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.width).toBe(200);
|
||||
expect(body.height).toBe(150);
|
||||
expect(body.format).toBe("png");
|
||||
expect(body.fileSize).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Info: WebP", () => {
|
||||
test("get info for WebP returns correct dimensions", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/tools/info", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "test.webp", mimeType: "image/webp", buffer: WEBP_50x50 },
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.width).toBe(50);
|
||||
expect(body.height).toBe(50);
|
||||
expect(body.format).toBe("webp");
|
||||
expect(body.fileSize).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Info: AVIF", () => {
|
||||
test("get info for AVIF returns dimensions and format", async ({ request }) => {
|
||||
const avif = formatFixture("sample.avif");
|
||||
const res = await request.post("/api/v1/tools/info", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "sample.avif", mimeType: "image/avif", buffer: avif },
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.width).toBeGreaterThan(0);
|
||||
expect(body.height).toBeGreaterThan(0);
|
||||
expect(body.fileSize).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Info: HEIC", () => {
|
||||
test("get info for HEIC returns dimensions", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/tools/info", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "test.heic", mimeType: "image/heic", buffer: HEIC_200x150 },
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.width).toBeGreaterThan(0);
|
||||
expect(body.height).toBeGreaterThan(0);
|
||||
expect(body.fileSize).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Info: TIFF", () => {
|
||||
test("get info for TIFF returns dimensions and format", async ({ request }) => {
|
||||
const tiff = formatFixture("sample.tiff");
|
||||
const res = await request.post("/api/v1/tools/info", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "sample.tiff", mimeType: "image/tiff", buffer: tiff },
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.width).toBeGreaterThan(0);
|
||||
expect(body.height).toBeGreaterThan(0);
|
||||
expect(body.fileSize).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Info: GIF", () => {
|
||||
test("get info for GIF returns dimensions and format", async ({ request }) => {
|
||||
const gif = formatFixture("sample.gif");
|
||||
const res = await request.post("/api/v1/tools/info", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "sample.gif", mimeType: "image/gif", buffer: gif },
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.width).toBeGreaterThan(0);
|
||||
expect(body.height).toBeGreaterThan(0);
|
||||
expect(body.format).toBe("gif");
|
||||
expect(body.fileSize).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Info: HEIF", () => {
|
||||
test("get info for HEIF returns dimensions", async ({ request }) => {
|
||||
const heif = formatFixture("sample.heif");
|
||||
const res = await request.post("/api/v1/tools/info", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "sample.heif", mimeType: "image/heif", buffer: heif },
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.width).toBeGreaterThan(0);
|
||||
expect(body.height).toBeGreaterThan(0);
|
||||
expect(body.fileSize).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Info: BMP", () => {
|
||||
test("get info for BMP returns dimensions if supported", async ({ request }) => {
|
||||
const bmp = formatFixture("sample.bmp");
|
||||
const res = await request.post("/api/v1/tools/info", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "sample.bmp", mimeType: "image/bmp", buffer: bmp },
|
||||
},
|
||||
});
|
||||
if (res.ok()) {
|
||||
const body = await res.json();
|
||||
expect(body.width).toBeGreaterThan(0);
|
||||
expect(body.height).toBeGreaterThan(0);
|
||||
expect(body.fileSize).toBeGreaterThan(0);
|
||||
} else {
|
||||
expect([400, 422]).toContain(res.status());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Cross-Format Conversions: Full Matrix ────────────────────────
|
||||
|
||||
test.describe("Cross-format: JPEG to all targets", () => {
|
||||
test("JPEG to WebP", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/tools/convert", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "test.jpg", mimeType: "image/jpeg", buffer: JPG_100x100 },
|
||||
settings: JSON.stringify({ format: "webp" }),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toContain(".webp");
|
||||
expect(body.processedSize).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("JPEG to AVIF", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/tools/convert", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "test.jpg", mimeType: "image/jpeg", buffer: JPG_100x100 },
|
||||
settings: JSON.stringify({ format: "avif" }),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toContain(".avif");
|
||||
expect(body.processedSize).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("JPEG to TIFF", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/tools/convert", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "test.jpg", mimeType: "image/jpeg", buffer: JPG_100x100 },
|
||||
settings: JSON.stringify({ format: "tiff" }),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toContain(".tiff");
|
||||
expect(body.processedSize).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("JPEG to GIF", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/tools/convert", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "test.jpg", mimeType: "image/jpeg", buffer: JPG_100x100 },
|
||||
settings: JSON.stringify({ format: "gif" }),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toContain(".gif");
|
||||
expect(body.processedSize).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("JPEG to HEIC", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/tools/convert", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "test.jpg", mimeType: "image/jpeg", buffer: JPG_100x100 },
|
||||
settings: JSON.stringify({ format: "heic" }),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toContain(".heic");
|
||||
expect(body.processedSize).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Cross-format: PNG to all targets", () => {
|
||||
test("PNG to JPEG", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/tools/convert", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "test.png", mimeType: "image/png", buffer: PNG_200x150 },
|
||||
settings: JSON.stringify({ format: "jpg", quality: 85 }),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toContain(".jpg");
|
||||
expect(body.processedSize).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("PNG to WebP", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/tools/convert", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "test.png", mimeType: "image/png", buffer: PNG_200x150 },
|
||||
settings: JSON.stringify({ format: "webp" }),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toContain(".webp");
|
||||
expect(body.processedSize).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("PNG to AVIF", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/tools/convert", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "test.png", mimeType: "image/png", buffer: PNG_200x150 },
|
||||
settings: JSON.stringify({ format: "avif" }),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toContain(".avif");
|
||||
expect(body.processedSize).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Cross-format: WebP to all targets", () => {
|
||||
test("WebP to JPEG", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/tools/convert", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "test.webp", mimeType: "image/webp", buffer: WEBP_50x50 },
|
||||
settings: JSON.stringify({ format: "jpg" }),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toContain(".jpg");
|
||||
expect(body.processedSize).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("WebP to AVIF", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/tools/convert", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "test.webp", mimeType: "image/webp", buffer: WEBP_50x50 },
|
||||
settings: JSON.stringify({ format: "avif" }),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toContain(".avif");
|
||||
expect(body.processedSize).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("WebP to TIFF", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/tools/convert", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "test.webp", mimeType: "image/webp", buffer: WEBP_50x50 },
|
||||
settings: JSON.stringify({ format: "tiff" }),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toContain(".tiff");
|
||||
expect(body.processedSize).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Cross-format: HEIC to all targets", () => {
|
||||
test("HEIC to JPEG", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/tools/convert", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "test.heic", mimeType: "image/heic", buffer: HEIC_200x150 },
|
||||
settings: JSON.stringify({ format: "jpg" }),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toContain(".jpg");
|
||||
expect(body.processedSize).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("HEIC to WebP", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/tools/convert", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "test.heic", mimeType: "image/heic", buffer: HEIC_200x150 },
|
||||
settings: JSON.stringify({ format: "webp" }),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toContain(".webp");
|
||||
expect(body.processedSize).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("HEIC to AVIF", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/tools/convert", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "test.heic", mimeType: "image/heic", buffer: HEIC_200x150 },
|
||||
settings: JSON.stringify({ format: "avif" }),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toContain(".avif");
|
||||
expect(body.processedSize).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("HEIC to TIFF", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/tools/convert", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "test.heic", mimeType: "image/heic", buffer: HEIC_200x150 },
|
||||
settings: JSON.stringify({ format: "tiff" }),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toContain(".tiff");
|
||||
expect(body.processedSize).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Resize + Info Verification ───────────────────────────────────
|
||||
|
||||
test.describe("Resize + info verification: JPEG", () => {
|
||||
test("resize JPEG then verify dimensions via info", async ({ request }) => {
|
||||
// Step 1: Resize
|
||||
const resizeRes = await request.post("/api/v1/tools/resize", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "test.jpg", mimeType: "image/jpeg", buffer: JPG_100x100 },
|
||||
settings: JSON.stringify({ width: 50, height: 50, fit: "fill" }),
|
||||
},
|
||||
});
|
||||
expect(resizeRes.ok()).toBe(true);
|
||||
const resizeBody = await resizeRes.json();
|
||||
expect(resizeBody.downloadUrl).toBeTruthy();
|
||||
|
||||
// Step 2: Download resized image
|
||||
const dlRes = await request.get(resizeBody.downloadUrl, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
expect(dlRes.ok()).toBe(true);
|
||||
const resizedBuffer = Buffer.from(await dlRes.body());
|
||||
|
||||
// Step 3: Verify dimensions via info
|
||||
const infoRes = await request.post("/api/v1/tools/info", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "resized.jpg", mimeType: "image/jpeg", buffer: resizedBuffer },
|
||||
},
|
||||
});
|
||||
expect(infoRes.ok()).toBe(true);
|
||||
const infoBody = await infoRes.json();
|
||||
expect(infoBody.width).toBe(50);
|
||||
expect(infoBody.height).toBe(50);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Resize + info verification: PNG", () => {
|
||||
test("resize PNG then verify dimensions via info", async ({ request }) => {
|
||||
const resizeRes = await request.post("/api/v1/tools/resize", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "test.png", mimeType: "image/png", buffer: PNG_200x150 },
|
||||
settings: JSON.stringify({ width: 100, fit: "contain" }),
|
||||
},
|
||||
});
|
||||
expect(resizeRes.ok()).toBe(true);
|
||||
const resizeBody = await resizeRes.json();
|
||||
|
||||
const dlRes = await request.get(resizeBody.downloadUrl, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
expect(dlRes.ok()).toBe(true);
|
||||
const resizedBuffer = Buffer.from(await dlRes.body());
|
||||
|
||||
const infoRes = await request.post("/api/v1/tools/info", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "resized.png", mimeType: "image/png", buffer: resizedBuffer },
|
||||
},
|
||||
});
|
||||
expect(infoRes.ok()).toBe(true);
|
||||
const infoBody = await infoRes.json();
|
||||
expect(infoBody.width).toBe(100);
|
||||
// Aspect ratio preserved: 200x150 -> 100x75
|
||||
expect(infoBody.height).toBe(75);
|
||||
expect(infoBody.format).toBe("png");
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Convert + Info Verification ──────────────────────────────────
|
||||
|
||||
test.describe("Convert + info verification", () => {
|
||||
test("convert JPEG to WebP then verify format via info", async ({ request }) => {
|
||||
const convertRes = await request.post("/api/v1/tools/convert", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "test.jpg", mimeType: "image/jpeg", buffer: JPG_100x100 },
|
||||
settings: JSON.stringify({ format: "webp" }),
|
||||
},
|
||||
});
|
||||
expect(convertRes.ok()).toBe(true);
|
||||
const convertBody = await convertRes.json();
|
||||
|
||||
const dlRes = await request.get(convertBody.downloadUrl, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
expect(dlRes.ok()).toBe(true);
|
||||
const convertedBuffer = Buffer.from(await dlRes.body());
|
||||
|
||||
const infoRes = await request.post("/api/v1/tools/info", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "converted.webp", mimeType: "image/webp", buffer: convertedBuffer },
|
||||
},
|
||||
});
|
||||
expect(infoRes.ok()).toBe(true);
|
||||
const infoBody = await infoRes.json();
|
||||
expect(infoBody.format).toBe("webp");
|
||||
expect(infoBody.width).toBe(100);
|
||||
expect(infoBody.height).toBe(100);
|
||||
});
|
||||
|
||||
test("convert PNG to AVIF then verify format via info", async ({ request }) => {
|
||||
const convertRes = await request.post("/api/v1/tools/convert", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "test.png", mimeType: "image/png", buffer: PNG_200x150 },
|
||||
settings: JSON.stringify({ format: "avif" }),
|
||||
},
|
||||
});
|
||||
expect(convertRes.ok()).toBe(true);
|
||||
const convertBody = await convertRes.json();
|
||||
|
||||
const dlRes = await request.get(convertBody.downloadUrl, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
expect(dlRes.ok()).toBe(true);
|
||||
const convertedBuffer = Buffer.from(await dlRes.body());
|
||||
|
||||
const infoRes = await request.post("/api/v1/tools/info", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "converted.avif", mimeType: "image/avif", buffer: convertedBuffer },
|
||||
},
|
||||
});
|
||||
expect(infoRes.ok()).toBe(true);
|
||||
const infoBody = await infoRes.json();
|
||||
expect(infoBody.width).toBe(200);
|
||||
expect(infoBody.height).toBe(150);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,677 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
// ─── Pipeline Advanced ────────────────────────────────────────────
|
||||
// Multi-step pipeline chain tests with 3+ steps. Covers complex
|
||||
// real-world workflows, duplicate steps, format changes mid-chain,
|
||||
// and deep pipelines (5+ steps).
|
||||
|
||||
const FIXTURES = join(process.cwd(), "tests", "fixtures");
|
||||
const FORMATS = join(FIXTURES, "formats");
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
const PNG_200x150 = fixture("test-200x150.png");
|
||||
const JPG_100x100 = fixture("test-100x100.jpg");
|
||||
const HEIC_200x150 = fixture("test-200x150.heic");
|
||||
const JPG_SAMPLE = formatFixture("sample.jpg");
|
||||
const JPG_WITH_EXIF = fixture("test-with-exif.jpg");
|
||||
const WEBP_50x50 = fixture("test-50x50.webp");
|
||||
|
||||
// ─── 3-Step: Resize -> Compress -> Convert (JPEG to WebP) ────────
|
||||
|
||||
test.describe("3-step: resize -> compress -> convert", () => {
|
||||
test("resize, compress, then convert JPEG to WebP", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/pipeline/execute", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "sample.jpg", mimeType: "image/jpeg", buffer: JPG_SAMPLE },
|
||||
pipeline: JSON.stringify({
|
||||
steps: [
|
||||
{ toolId: "resize", settings: { width: 640, fit: "contain" } },
|
||||
{ toolId: "compress", settings: { quality: 60 } },
|
||||
{ toolId: "convert", settings: { format: "webp" } },
|
||||
],
|
||||
}),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toBeTruthy();
|
||||
expect(body.downloadUrl).toContain(".webp");
|
||||
expect(body.processedSize).toBeGreaterThan(0);
|
||||
expect(body.processedSize).toBeLessThan(body.originalSize);
|
||||
});
|
||||
|
||||
test("resize, compress, then convert PNG to AVIF", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/pipeline/execute", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "test.png", mimeType: "image/png", buffer: PNG_200x150 },
|
||||
pipeline: JSON.stringify({
|
||||
steps: [
|
||||
{ toolId: "resize", settings: { width: 100, fit: "contain" } },
|
||||
{ toolId: "compress", settings: { quality: 50 } },
|
||||
{ toolId: "convert", settings: { format: "avif" } },
|
||||
],
|
||||
}),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toBeTruthy();
|
||||
expect(body.downloadUrl).toContain(".avif");
|
||||
});
|
||||
});
|
||||
|
||||
// ─── 4-Step: Rotate -> Resize -> Sharpening -> Compress ──────────
|
||||
|
||||
test.describe("4-step: rotate -> resize -> sharpening -> compress", () => {
|
||||
test("full 4-step image preparation pipeline", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/pipeline/execute", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "sample.jpg", mimeType: "image/jpeg", buffer: JPG_SAMPLE },
|
||||
pipeline: JSON.stringify({
|
||||
steps: [
|
||||
{ toolId: "rotate", settings: { angle: 90 } },
|
||||
{ toolId: "resize", settings: { width: 500, fit: "contain" } },
|
||||
{ toolId: "sharpening", settings: { sigma: 1.5 } },
|
||||
{ toolId: "compress", settings: { quality: 70 } },
|
||||
],
|
||||
}),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toBeTruthy();
|
||||
expect(body.processedSize).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("rotate 180 -> resize -> sharpen -> compress on PNG", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/pipeline/execute", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "test.png", mimeType: "image/png", buffer: PNG_200x150 },
|
||||
pipeline: JSON.stringify({
|
||||
steps: [
|
||||
{ toolId: "rotate", settings: { angle: 180 } },
|
||||
{ toolId: "resize", settings: { width: 150, height: 100, fit: "fill" } },
|
||||
{ toolId: "sharpening", settings: { sigma: 2.0 } },
|
||||
{ toolId: "compress", settings: { quality: 80 } },
|
||||
],
|
||||
}),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── 5-Step: Strip Metadata -> Resize -> Adjust Colors -> Compress -> Convert ─
|
||||
|
||||
test.describe("5-step: strip-metadata -> resize -> adjust-colors -> compress -> convert", () => {
|
||||
test("full 5-step processing pipeline on JPEG with EXIF", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/pipeline/execute", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "photo.jpg", mimeType: "image/jpeg", buffer: JPG_WITH_EXIF },
|
||||
pipeline: JSON.stringify({
|
||||
steps: [
|
||||
{ toolId: "strip-metadata", settings: {} },
|
||||
{ toolId: "resize", settings: { width: 800, fit: "contain" } },
|
||||
{ toolId: "adjust-colors", settings: { brightness: 10, contrast: 15, saturation: 5 } },
|
||||
{ toolId: "compress", settings: { quality: 75 } },
|
||||
{ toolId: "convert", settings: { format: "webp" } },
|
||||
],
|
||||
}),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toBeTruthy();
|
||||
expect(body.downloadUrl).toContain(".webp");
|
||||
expect(body.processedSize).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("full 5-step pipeline on high-res sample image", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/pipeline/execute", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "sample.jpg", mimeType: "image/jpeg", buffer: JPG_SAMPLE },
|
||||
pipeline: JSON.stringify({
|
||||
steps: [
|
||||
{ toolId: "strip-metadata", settings: {} },
|
||||
{ toolId: "resize", settings: { width: 1200, fit: "contain" } },
|
||||
{ toolId: "adjust-colors", settings: { brightness: -5, contrast: 10 } },
|
||||
{ toolId: "compress", settings: { quality: 65 } },
|
||||
{ toolId: "convert", settings: { format: "avif" } },
|
||||
],
|
||||
}),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toBeTruthy();
|
||||
expect(body.downloadUrl).toContain(".avif");
|
||||
expect(body.processedSize).toBeLessThan(body.originalSize);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Pipeline with Same Step Twice: Resize -> Resize ─────────────
|
||||
|
||||
test.describe("Pipeline with same step twice", () => {
|
||||
test("resize 200->100, then resize 100->50 (two sequential resizes)", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/pipeline/execute", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "test.png", mimeType: "image/png", buffer: PNG_200x150 },
|
||||
pipeline: JSON.stringify({
|
||||
steps: [
|
||||
{ toolId: "resize", settings: { width: 100, fit: "contain" } },
|
||||
{ toolId: "resize", settings: { width: 50, fit: "contain" } },
|
||||
],
|
||||
}),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toBeTruthy();
|
||||
expect(body.processedSize).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("resize with different fits: cover then fill", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/pipeline/execute", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "sample.jpg", mimeType: "image/jpeg", buffer: JPG_SAMPLE },
|
||||
pipeline: JSON.stringify({
|
||||
steps: [
|
||||
{ toolId: "resize", settings: { width: 300, height: 300, fit: "cover" } },
|
||||
{ toolId: "resize", settings: { width: 200, height: 150, fit: "fill" } },
|
||||
],
|
||||
}),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toBeTruthy();
|
||||
});
|
||||
|
||||
test("double compress with decreasing quality", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/pipeline/execute", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "sample.jpg", mimeType: "image/jpeg", buffer: JPG_SAMPLE },
|
||||
pipeline: JSON.stringify({
|
||||
steps: [
|
||||
{ toolId: "compress", settings: { quality: 80 } },
|
||||
{ toolId: "compress", settings: { quality: 30 } },
|
||||
],
|
||||
}),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toBeTruthy();
|
||||
expect(body.processedSize).toBeLessThan(body.originalSize);
|
||||
});
|
||||
|
||||
test("double sharpen with different sigma values", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/pipeline/execute", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "test.jpg", mimeType: "image/jpeg", buffer: JPG_100x100 },
|
||||
pipeline: JSON.stringify({
|
||||
steps: [
|
||||
{ toolId: "sharpening", settings: { sigma: 0.5 } },
|
||||
{ toolId: "sharpening", settings: { sigma: 2.0 } },
|
||||
],
|
||||
}),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Pipeline with Format Change Mid-Chain ────────────────────────
|
||||
|
||||
test.describe("Pipeline with format change mid-chain", () => {
|
||||
test("convert JPEG to PNG, resize, then convert to WebP", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/pipeline/execute", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "sample.jpg", mimeType: "image/jpeg", buffer: JPG_SAMPLE },
|
||||
pipeline: JSON.stringify({
|
||||
steps: [
|
||||
{ toolId: "convert", settings: { format: "png" } },
|
||||
{ toolId: "resize", settings: { width: 400, fit: "contain" } },
|
||||
{ toolId: "convert", settings: { format: "webp" } },
|
||||
],
|
||||
}),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toBeTruthy();
|
||||
expect(body.downloadUrl).toContain(".webp");
|
||||
});
|
||||
|
||||
test("convert PNG to JPEG, enhance, then convert to AVIF", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/pipeline/execute", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "test.png", mimeType: "image/png", buffer: PNG_200x150 },
|
||||
pipeline: JSON.stringify({
|
||||
steps: [
|
||||
{ toolId: "convert", settings: { format: "jpg", quality: 90 } },
|
||||
{ toolId: "image-enhancement", settings: { preset: "auto" } },
|
||||
{ toolId: "convert", settings: { format: "avif" } },
|
||||
],
|
||||
}),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toBeTruthy();
|
||||
expect(body.downloadUrl).toContain(".avif");
|
||||
});
|
||||
|
||||
test("convert to TIFF mid-chain then back to WebP", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/pipeline/execute", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "test.jpg", mimeType: "image/jpeg", buffer: JPG_100x100 },
|
||||
pipeline: JSON.stringify({
|
||||
steps: [
|
||||
{ toolId: "convert", settings: { format: "tiff" } },
|
||||
{ toolId: "resize", settings: { width: 80, fit: "contain" } },
|
||||
{ toolId: "convert", settings: { format: "webp" } },
|
||||
],
|
||||
}),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toBeTruthy();
|
||||
expect(body.downloadUrl).toContain(".webp");
|
||||
});
|
||||
});
|
||||
|
||||
// ─── HEIC Input Through Multi-Step Pipelines ──────────────────────
|
||||
|
||||
test.describe("HEIC input through multi-step pipelines", () => {
|
||||
test("HEIC: 3-step resize -> sharpen -> convert to PNG", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/pipeline/execute", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "test.heic", mimeType: "image/heic", buffer: HEIC_200x150 },
|
||||
pipeline: JSON.stringify({
|
||||
steps: [
|
||||
{ toolId: "resize", settings: { width: 100, fit: "contain" } },
|
||||
{ toolId: "sharpening", settings: { sigma: 1.0 } },
|
||||
{ toolId: "convert", settings: { format: "png" } },
|
||||
],
|
||||
}),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toBeTruthy();
|
||||
expect(body.downloadUrl).toContain(".png");
|
||||
});
|
||||
|
||||
test("HEIC: 4-step adjust-colors -> resize -> border -> compress", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/pipeline/execute", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "test.heic", mimeType: "image/heic", buffer: HEIC_200x150 },
|
||||
pipeline: JSON.stringify({
|
||||
steps: [
|
||||
{ toolId: "adjust-colors", settings: { brightness: 15, contrast: 10 } },
|
||||
{ toolId: "resize", settings: { width: 150, fit: "contain" } },
|
||||
{ toolId: "border", settings: { size: 5, color: "#000000" } },
|
||||
{ toolId: "compress", settings: { quality: 70 } },
|
||||
],
|
||||
}),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toBeTruthy();
|
||||
expect(body.processedSize).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Deep Pipeline: 6+ Steps ──────────────────────────────────────
|
||||
|
||||
test.describe("Deep pipelines (6+ steps)", () => {
|
||||
test("7-step full processing pipeline", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/pipeline/execute", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "sample.jpg", mimeType: "image/jpeg", buffer: JPG_SAMPLE },
|
||||
pipeline: JSON.stringify({
|
||||
steps: [
|
||||
{ toolId: "strip-metadata", settings: {} },
|
||||
{ toolId: "rotate", settings: { angle: 90 } },
|
||||
{ toolId: "resize", settings: { width: 800, fit: "contain" } },
|
||||
{ toolId: "adjust-colors", settings: { brightness: 5, contrast: 10, saturation: -5 } },
|
||||
{ toolId: "sharpening", settings: { sigma: 1.0 } },
|
||||
{
|
||||
toolId: "watermark-text",
|
||||
settings: {
|
||||
text: "DEEP PIPELINE",
|
||||
fontSize: 14,
|
||||
color: "#808080",
|
||||
opacity: 20,
|
||||
position: "bottom-right",
|
||||
},
|
||||
},
|
||||
{ toolId: "compress", settings: { quality: 70 } },
|
||||
],
|
||||
}),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toBeTruthy();
|
||||
expect(body.processedSize).toBeGreaterThan(0);
|
||||
expect(body.processedSize).toBeLessThan(body.originalSize);
|
||||
});
|
||||
|
||||
test("6-step pipeline with format change at the end", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/pipeline/execute", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "photo.jpg", mimeType: "image/jpeg", buffer: JPG_WITH_EXIF },
|
||||
pipeline: JSON.stringify({
|
||||
steps: [
|
||||
{ toolId: "strip-metadata", settings: {} },
|
||||
{ toolId: "resize", settings: { width: 600, fit: "contain" } },
|
||||
{ toolId: "adjust-colors", settings: { grayscale: true } },
|
||||
{ toolId: "sharpening", settings: { sigma: 1.5 } },
|
||||
{ toolId: "border", settings: { size: 8, color: "#ffffff" } },
|
||||
{ toolId: "convert", settings: { format: "webp" } },
|
||||
],
|
||||
}),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toBeTruthy();
|
||||
expect(body.downloadUrl).toContain(".webp");
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Workflow: E-commerce Product Pipeline ────────────────────────
|
||||
|
||||
test.describe("Workflow: e-commerce product pipeline", () => {
|
||||
test("crop -> resize -> enhance -> watermark -> compress -> convert", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/pipeline/execute", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "sample.jpg", mimeType: "image/jpeg", buffer: JPG_SAMPLE },
|
||||
pipeline: JSON.stringify({
|
||||
steps: [
|
||||
{ toolId: "crop", settings: { left: 50, top: 50, width: 400, height: 400 } },
|
||||
{ toolId: "resize", settings: { width: 800, height: 800, fit: "contain" } },
|
||||
{ toolId: "image-enhancement", settings: { preset: "vivid" } },
|
||||
{
|
||||
toolId: "watermark-text",
|
||||
settings: {
|
||||
text: "SAMPLE",
|
||||
fontSize: 20,
|
||||
color: "#cccccc",
|
||||
opacity: 25,
|
||||
position: "center",
|
||||
},
|
||||
},
|
||||
{ toolId: "compress", settings: { quality: 85 } },
|
||||
{ toolId: "convert", settings: { format: "webp" } },
|
||||
],
|
||||
}),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toBeTruthy();
|
||||
expect(body.downloadUrl).toContain(".webp");
|
||||
expect(body.processedSize).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Workflow: Blog Post Image Pipeline ───────────────────────────
|
||||
|
||||
test.describe("Workflow: blog post image pipeline", () => {
|
||||
test("strip-metadata -> resize -> text-overlay -> optimize-for-web", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/pipeline/execute", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "photo.jpg", mimeType: "image/jpeg", buffer: JPG_WITH_EXIF },
|
||||
pipeline: JSON.stringify({
|
||||
steps: [
|
||||
{ toolId: "strip-metadata", settings: {} },
|
||||
{ toolId: "resize", settings: { width: 1200, fit: "contain" } },
|
||||
{
|
||||
toolId: "text-overlay",
|
||||
settings: {
|
||||
text: "Blog Header Image",
|
||||
fontSize: 36,
|
||||
color: "#ffffff",
|
||||
position: "bottom",
|
||||
backgroundBox: true,
|
||||
backgroundColor: "#333333",
|
||||
},
|
||||
},
|
||||
{ toolId: "optimize-for-web", settings: { maxWidth: 1200, quality: 80 } },
|
||||
],
|
||||
}),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toBeTruthy();
|
||||
expect(body.processedSize).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Workflow: Archive Preparation Pipeline ───────────────────────
|
||||
|
||||
test.describe("Workflow: archive preparation pipeline", () => {
|
||||
test("strip-metadata -> adjust-colors -> resize -> convert to TIFF", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/pipeline/execute", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "sample.jpg", mimeType: "image/jpeg", buffer: JPG_SAMPLE },
|
||||
pipeline: JSON.stringify({
|
||||
steps: [
|
||||
{ toolId: "strip-metadata", settings: {} },
|
||||
{ toolId: "adjust-colors", settings: { brightness: 0, contrast: 5 } },
|
||||
{ toolId: "resize", settings: { width: 2000, fit: "contain" } },
|
||||
{ toolId: "convert", settings: { format: "tiff" } },
|
||||
],
|
||||
}),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toBeTruthy();
|
||||
expect(body.downloadUrl).toContain(".tiff");
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Pipeline Step Metadata Validation ────────────────────────────
|
||||
|
||||
test.describe("Pipeline step metadata validation", () => {
|
||||
test("3-step pipeline returns step metadata if available", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/pipeline/execute", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "test.png", mimeType: "image/png", buffer: PNG_200x150 },
|
||||
pipeline: JSON.stringify({
|
||||
steps: [
|
||||
{ toolId: "resize", settings: { width: 100, fit: "contain" } },
|
||||
{ toolId: "sharpening", settings: { sigma: 1.0 } },
|
||||
{ toolId: "compress", settings: { quality: 60 } },
|
||||
],
|
||||
}),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toBeTruthy();
|
||||
if (body.steps) {
|
||||
expect(body.steps).toBeInstanceOf(Array);
|
||||
expect(body.steps.length).toBe(3);
|
||||
}
|
||||
});
|
||||
|
||||
test("5-step pipeline returns step metadata if available", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/pipeline/execute", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "sample.jpg", mimeType: "image/jpeg", buffer: JPG_SAMPLE },
|
||||
pipeline: JSON.stringify({
|
||||
steps: [
|
||||
{ toolId: "strip-metadata", settings: {} },
|
||||
{ toolId: "resize", settings: { width: 500, fit: "contain" } },
|
||||
{ toolId: "adjust-colors", settings: { brightness: 10 } },
|
||||
{ toolId: "sharpening", settings: { sigma: 0.8 } },
|
||||
{ toolId: "compress", settings: { quality: 70 } },
|
||||
],
|
||||
}),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toBeTruthy();
|
||||
if (body.steps) {
|
||||
expect(body.steps).toBeInstanceOf(Array);
|
||||
expect(body.steps.length).toBe(5);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── WebP Input Through Multi-Step Pipelines ──────────────────────
|
||||
|
||||
test.describe("WebP input through multi-step pipelines", () => {
|
||||
test("WebP: 3-step resize -> adjust-colors -> convert to PNG", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/pipeline/execute", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "test.webp", mimeType: "image/webp", buffer: WEBP_50x50 },
|
||||
pipeline: JSON.stringify({
|
||||
steps: [
|
||||
{ toolId: "resize", settings: { width: 100, height: 100, fit: "fill" } },
|
||||
{ toolId: "adjust-colors", settings: { brightness: 20, saturation: 15 } },
|
||||
{ toolId: "convert", settings: { format: "png" } },
|
||||
],
|
||||
}),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toBeTruthy();
|
||||
expect(body.downloadUrl).toContain(".png");
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Pipeline with Enhancement + Border + Convert ─────────────────
|
||||
|
||||
test.describe("Enhancement + Border + Convert pipeline", () => {
|
||||
test("3-step enhance -> border -> convert to WebP", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/pipeline/execute", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "test.jpg", mimeType: "image/jpeg", buffer: JPG_100x100 },
|
||||
pipeline: JSON.stringify({
|
||||
steps: [
|
||||
{ toolId: "image-enhancement", settings: { preset: "auto" } },
|
||||
{ toolId: "border", settings: { size: 10, color: "#333333" } },
|
||||
{ toolId: "convert", settings: { format: "webp" } },
|
||||
],
|
||||
}),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toBeTruthy();
|
||||
expect(body.downloadUrl).toContain(".webp");
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Pipeline with Multiple Color Operations ──────────────────────
|
||||
|
||||
test.describe("Pipeline with multiple color operations", () => {
|
||||
test("adjust-colors -> replace-color -> adjust-colors (grayscale)", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/pipeline/execute", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "test.png", mimeType: "image/png", buffer: PNG_200x150 },
|
||||
pipeline: JSON.stringify({
|
||||
steps: [
|
||||
{ toolId: "adjust-colors", settings: { brightness: 20, contrast: 10 } },
|
||||
{
|
||||
toolId: "replace-color",
|
||||
settings: {
|
||||
targetColor: "#ffffff",
|
||||
replacementColor: "#f0f0e0",
|
||||
tolerance: 25,
|
||||
},
|
||||
},
|
||||
{ toolId: "adjust-colors", settings: { grayscale: true } },
|
||||
],
|
||||
}),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Pipeline with Crop After Rotate ──────────────────────────────
|
||||
|
||||
test.describe("Pipeline with crop after rotate", () => {
|
||||
test("rotate 90 -> crop center -> resize -> compress", async ({ request }) => {
|
||||
const res = await request.post("/api/v1/pipeline/execute", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: {
|
||||
file: { name: "sample.jpg", mimeType: "image/jpeg", buffer: JPG_SAMPLE },
|
||||
pipeline: JSON.stringify({
|
||||
steps: [
|
||||
{ toolId: "rotate", settings: { angle: 90 } },
|
||||
{ toolId: "crop", settings: { left: 20, top: 20, width: 200, height: 200 } },
|
||||
{ toolId: "resize", settings: { width: 100, fit: "contain" } },
|
||||
{ toolId: "compress", settings: { quality: 60 } },
|
||||
],
|
||||
}),
|
||||
},
|
||||
});
|
||||
expect(res.ok()).toBe(true);
|
||||
const body = await res.json();
|
||||
expect(body.downloadUrl).toBeTruthy();
|
||||
expect(body.processedSize).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user