mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
A release-readiness QA pass over the whole product. The commits split into defects a user would hit and gates that were reporting green while measuring nothing. ## Fixes that change behaviour Rate limiting was bypassable on every install: TRUST_PROXY defaulted to true, so request.ip came from a client-set header and a forged X-Forwarded-For got past the login limiter. The default is now a private-network trust list. A transient Postgres outage stranded in-flight jobs, leaving finished output on disk with no row pointing at it. A reconciler now resolves those rows and adopts the bytes rather than dropping the work. A Redis connection that moved to a new address wedged every read-blocked consumer, so completions stopped signalling while health still answered 200. Socket timeouts plus subscriber pings recover it. Installing more than one AI bundle left the shared venv multi-versioned and silently broke three tools. The installer now reconciles distributions to one version each. Converting an image to JXL at quality 1 through 4 returned a 500, because libjxl 0.7 rejects the distance those values compute. The quality is floored at what the encoder honours. A missing ffmpeg was also reported to the user as a corrupt upload; it now says the engine is unavailable. RAW uploads reached an unpatched LibRaw on arm64, so it is built from source at 0.22.2, and the release scan was split so it can fail on an unfixed critical instead of hiding it behind ignore-unfixed. ## Gates that could not fail Two mutation lanes ran zero mutants because Stryker crawled the gitignored docs build; coverage discarded its whole report on any failing test; the lint gate skipped root tests, scripts, and two workspaces; and several generated matrices counted a host missing ffmpeg as a passing tool. Each now measures what it claims. Full evidence and the outstanding release items are tracked locally and are not part of this branch.
1226 lines
49 KiB
TypeScript
1226 lines
49 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { expect, test } from "@playwright/test";
|
|
|
|
// ─── Batch Processing ──────────────────────────────────────────────
|
|
// Tests for multi-file batch uploads across every tool category.
|
|
// Verifies ZIP output, batch limits, and correct processing of all files.
|
|
|
|
const FIXTURES = join(process.cwd(), "tests", "fixtures", "image", "valid");
|
|
const FORMATS = join(process.cwd(), "tests", "fixtures", "image", "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));
|
|
}
|
|
|
|
/**
|
|
* 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}`,
|
|
};
|
|
}
|
|
|
|
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");
|
|
|
|
// ─── Batch Resize ──────────────────────────────────────────────────
|
|
|
|
test.describe("Batch Resize", () => {
|
|
test("batch resize 3 images returns ZIP", 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({ width: 80, fit: "contain" }) }],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/resize/batch", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: reqBody,
|
|
});
|
|
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 {
|
|
// Binary ZIP response
|
|
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);
|
|
}
|
|
});
|
|
|
|
test("batch resize 5 mixed-format 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: "file",
|
|
filename: "e.jpg",
|
|
contentType: "image/jpeg",
|
|
buffer: formatFixture("sample.jpg"),
|
|
},
|
|
],
|
|
[{ name: "settings", value: JSON.stringify({ width: 64, fit: "cover" }) }],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/resize/batch", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: reqBody,
|
|
});
|
|
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);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── Batch Crop ────────────────────────────────────────────────────
|
|
|
|
test.describe("Batch Crop", () => {
|
|
test("batch crop 3 images to same 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: "settings",
|
|
value: JSON.stringify({ left: 5, top: 5, width: 40, height: 40 }),
|
|
},
|
|
],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/crop/batch", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: reqBody,
|
|
});
|
|
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);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── Batch Rotate ──────────────────────────────────────────────────
|
|
|
|
test.describe("Batch Rotate", () => {
|
|
test("batch rotate 3 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: "settings", value: JSON.stringify({ angle: 90 }) }],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/rotate/batch", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: reqBody,
|
|
});
|
|
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);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── Batch Convert ─────────────────────────────────────────────────
|
|
|
|
test.describe("Batch Convert", () => {
|
|
test("batch convert 4 images to WebP", 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.heic", contentType: "image/heic", buffer: HEIC_200x150 },
|
|
{
|
|
name: "file",
|
|
filename: "d.jpg",
|
|
contentType: "image/jpeg",
|
|
buffer: formatFixture("sample.jpg"),
|
|
},
|
|
],
|
|
[{ name: "settings", value: JSON.stringify({ format: "webp" }) }],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/convert/batch", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: reqBody,
|
|
});
|
|
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);
|
|
}
|
|
});
|
|
|
|
test("batch convert to AVIF format", 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: "settings", value: JSON.stringify({ format: "avif" }) }],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/convert/batch", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: reqBody,
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ─── Batch Compress ────────────────────────────────────────────────
|
|
|
|
test.describe("Batch Compress", () => {
|
|
test("batch compress 4 images with low 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_100x100 },
|
|
{ name: "file", filename: "c.webp", contentType: "image/webp", buffer: WEBP_50x50 },
|
|
{
|
|
name: "file",
|
|
filename: "d.jpg",
|
|
contentType: "image/jpeg",
|
|
buffer: formatFixture("sample.jpg"),
|
|
},
|
|
],
|
|
[{ name: "settings", value: JSON.stringify({ quality: 30 }) }],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/compress/batch", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: reqBody,
|
|
});
|
|
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);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── Batch Color Adjustments ───────────────────────────────────────
|
|
|
|
test.describe("Batch Color Adjustments", () => {
|
|
test("batch adjust colors 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.webp", contentType: "image/webp", buffer: WEBP_50x50 },
|
|
],
|
|
[
|
|
{
|
|
name: "settings",
|
|
value: JSON.stringify({ brightness: 15, contrast: 10, saturation: -5 }),
|
|
},
|
|
],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/adjust-colors/batch", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: reqBody,
|
|
});
|
|
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);
|
|
}
|
|
});
|
|
|
|
test("batch grayscale conversion", 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: "settings", value: JSON.stringify({ grayscale: true }) }],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/adjust-colors/batch", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: reqBody,
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ─── Batch Sharpening ──────────────────────────────────────────────
|
|
|
|
test.describe("Batch Sharpening", () => {
|
|
test("batch sharpen 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({ sigma: 1.5 }) }],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/sharpening/batch", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: reqBody,
|
|
});
|
|
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);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── Batch Watermark Text ──────────────────────────────────────────
|
|
|
|
test.describe("Batch Watermark Text", () => {
|
|
test("batch watermark 3 images with center text", 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({
|
|
text: "BATCH WM",
|
|
fontSize: 24,
|
|
color: "#ff0000",
|
|
opacity: 40,
|
|
position: "center",
|
|
}),
|
|
},
|
|
],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/watermark-text/batch", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: reqBody,
|
|
});
|
|
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);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── Batch Strip Metadata ──────────────────────────────────────────
|
|
|
|
test.describe("Batch Strip Metadata", () => {
|
|
test("batch strip metadata from 3 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: "settings", value: JSON.stringify({}) }],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/strip-metadata/batch", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: reqBody,
|
|
});
|
|
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);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── Batch Optimize for Web ────────────────────────────────────────
|
|
|
|
test.describe("Batch Optimize for Web", () => {
|
|
test("batch optimize 4 images for web", 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.heic", contentType: "image/heic", buffer: HEIC_200x150 },
|
|
{
|
|
name: "file",
|
|
filename: "d.jpg",
|
|
contentType: "image/jpeg",
|
|
buffer: formatFixture("sample.jpg"),
|
|
},
|
|
],
|
|
[{ name: "settings", value: JSON.stringify({ maxWidth: 800, quality: 70 }) }],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/optimize-for-web/batch", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: reqBody,
|
|
});
|
|
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);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── Batch Border ──────────────────────────────────────────────────
|
|
|
|
test.describe("Batch Border", () => {
|
|
test("batch add border to 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({ size: 10, color: "#0000ff" }) }],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/border/batch", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: reqBody,
|
|
});
|
|
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);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── Batch Replace Color ───────────────────────────────────────────
|
|
|
|
test.describe("Batch Replace Color", () => {
|
|
test("batch replace color 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.webp", contentType: "image/webp", buffer: WEBP_50x50 },
|
|
],
|
|
[
|
|
{
|
|
name: "settings",
|
|
value: JSON.stringify({
|
|
targetColor: "#ffffff",
|
|
replacementColor: "#ff00ff",
|
|
tolerance: 25,
|
|
}),
|
|
},
|
|
],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/replace-color/batch", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: reqBody,
|
|
});
|
|
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);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── Batch Enhancement ─────────────────────────────────────────────
|
|
|
|
test.describe("Batch Enhancement", () => {
|
|
test("batch enhance 3 images with auto 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: "settings", value: JSON.stringify({ preset: "auto" }) }],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/image-enhancement/batch", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: reqBody,
|
|
});
|
|
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);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── Batch Vectorize ───────────────────────────────────────────────
|
|
|
|
test.describe("Batch Vectorize", () => {
|
|
test("batch vectorize 3 images to SVG", 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({}) }],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/vectorize/batch", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: reqBody,
|
|
});
|
|
// Vectorize does not register in the batch tool registry, so batch
|
|
// endpoint may return 404. Accept either success or 404.
|
|
if (res.status() === 404) {
|
|
const json = await res.json();
|
|
expect(json.error).toBeDefined();
|
|
return;
|
|
}
|
|
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);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── Batch Text Overlay ────────────────────────────────────────────
|
|
|
|
test.describe("Batch Text Overlay", () => {
|
|
test("batch text overlay 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.webp", contentType: "image/webp", buffer: WEBP_50x50 },
|
|
],
|
|
[
|
|
{
|
|
name: "settings",
|
|
value: JSON.stringify({
|
|
text: "BATCH TEXT",
|
|
fontSize: 20,
|
|
color: "#ffffff",
|
|
position: "bottom",
|
|
}),
|
|
},
|
|
],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/text-overlay/batch", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: reqBody,
|
|
});
|
|
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);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── Batch Favicon ────────────────────────────────────────────────
|
|
|
|
test.describe("Batch Favicon", () => {
|
|
test("batch favicon from 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({}) }],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/favicon/batch", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: reqBody,
|
|
});
|
|
// Favicon batch may not be registered — accept 200 or 404
|
|
if (res.status() === 404) {
|
|
const json = await res.json();
|
|
expect(json.error).toBeDefined();
|
|
return;
|
|
}
|
|
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);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── Batch Edit Metadata ──────────────────────────────────────────
|
|
|
|
test.describe("Batch Edit Metadata", () => {
|
|
test("batch edit metadata on 3 images", async ({ request }) => {
|
|
const { body: reqBody, contentType } = buildMultipart(
|
|
[
|
|
{ name: "file", filename: "a.jpg", contentType: "image/jpeg", buffer: JPG_100x100 },
|
|
{ name: "file", filename: "b.png", contentType: "image/png", buffer: PNG_200x150 },
|
|
{ name: "file", filename: "c.webp", contentType: "image/webp", buffer: WEBP_50x50 },
|
|
],
|
|
[
|
|
{
|
|
name: "settings",
|
|
value: JSON.stringify({ artist: "Batch Test", copyright: "CC0" }),
|
|
},
|
|
],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/edit-metadata/batch", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: reqBody,
|
|
});
|
|
// edit-metadata batch may not be registered — accept 200 or 404
|
|
if (res.status() === 404) {
|
|
const json = await res.json();
|
|
expect(json.error).toBeDefined();
|
|
return;
|
|
}
|
|
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);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── Batch Convert to Multiple Formats ────────────────────────────
|
|
|
|
test.describe("Batch Convert — format variety", () => {
|
|
test("batch convert to PNG", async ({ request }) => {
|
|
const { body: reqBody, contentType } = buildMultipart(
|
|
[
|
|
{ name: "file", filename: "a.jpg", contentType: "image/jpeg", buffer: JPG_100x100 },
|
|
{ name: "file", filename: "b.webp", contentType: "image/webp", buffer: WEBP_50x50 },
|
|
{ name: "file", filename: "c.heic", contentType: "image/heic", buffer: HEIC_200x150 },
|
|
],
|
|
[{ name: "settings", value: JSON.stringify({ format: "png" }) }],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/convert/batch", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: reqBody,
|
|
});
|
|
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);
|
|
}
|
|
});
|
|
|
|
test("batch convert 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/image/convert/batch", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: reqBody,
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ─── Batch Rotate with Various Angles ─────────────────────────────
|
|
|
|
test.describe("Batch Rotate — angles", () => {
|
|
test("batch rotate 180 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: "settings", value: JSON.stringify({ angle: 180 }) }],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/rotate/batch", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: reqBody,
|
|
});
|
|
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);
|
|
}
|
|
});
|
|
|
|
test("batch rotate 270 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: "settings", value: JSON.stringify({ angle: 270 }) }],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/rotate/batch", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: reqBody,
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ─── Batch with HEIC Input ────────────────────────────────────────
|
|
|
|
test.describe("Batch with HEIC input", () => {
|
|
test("batch resize HEIC and other formats", 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_100x100 },
|
|
],
|
|
[{ name: "settings", value: JSON.stringify({ width: 64, fit: "contain" }) }],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/resize/batch", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: reqBody,
|
|
});
|
|
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);
|
|
}
|
|
});
|
|
|
|
test("batch compress HEIC with other formats", async ({ request }) => {
|
|
const { body: reqBody, contentType } = buildMultipart(
|
|
[
|
|
{ name: "file", filename: "a.heic", contentType: "image/heic", buffer: HEIC_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({ quality: 50 }) }],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/compress/batch", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: reqBody,
|
|
});
|
|
expect(res.ok()).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ─── Batch Color Blindness ────────────────────────────────────────
|
|
|
|
test.describe("Batch Color Blindness", () => {
|
|
test("batch color-blindness simulation 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.webp", contentType: "image/webp", buffer: WEBP_50x50 },
|
|
],
|
|
[{ name: "settings", value: JSON.stringify({ simulationType: "deuteranopia" }) }],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/color-blindness/batch", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: reqBody,
|
|
});
|
|
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);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── Batch Meme Generator ─────────────────────────────────────────
|
|
|
|
test.describe("Batch Meme Generator", () => {
|
|
test("batch meme-generator 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.webp", contentType: "image/webp", buffer: WEBP_50x50 },
|
|
],
|
|
[
|
|
{
|
|
name: "settings",
|
|
value: JSON.stringify({
|
|
textLayout: "top-bottom",
|
|
textBoxes: [
|
|
{ id: "top", text: "BATCH" },
|
|
{ id: "bottom", text: "MEME" },
|
|
],
|
|
}),
|
|
},
|
|
],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/meme-generator/batch", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: reqBody,
|
|
});
|
|
// meme-generator batch may not be registered
|
|
if (res.status() === 404) {
|
|
const json = await res.json();
|
|
expect(json.error).toBeDefined();
|
|
return;
|
|
}
|
|
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);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── Batch Beautify ───────────────────────────────────────────────
|
|
|
|
test.describe("Batch Beautify", () => {
|
|
test("batch beautify 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({
|
|
backgroundType: "solid",
|
|
backgroundColor: "#1a1a2e",
|
|
padding: 20,
|
|
borderRadius: 8,
|
|
shadowPreset: "none",
|
|
frame: "none",
|
|
}),
|
|
},
|
|
],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/beautify/batch", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: reqBody,
|
|
});
|
|
// beautify batch may not be registered
|
|
if (res.status() === 404) {
|
|
const json = await res.json();
|
|
expect(json.error).toBeDefined();
|
|
return;
|
|
}
|
|
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);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── Batch Resize -- 5+ Images with Download Verification ────────
|
|
|
|
test.describe("Batch Resize -- 5+ images verified", () => {
|
|
test("batch resize 6 mixed-format images and verify ZIP", 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: "file",
|
|
filename: "e.jpg",
|
|
contentType: "image/jpeg",
|
|
buffer: formatFixture("sample.jpg"),
|
|
},
|
|
{ name: "file", filename: "f.png", contentType: "image/png", buffer: PNG_200x150 },
|
|
],
|
|
[{ name: "settings", value: JSON.stringify({ width: 50, fit: "contain" }) }],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/resize/batch", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: reqBody,
|
|
});
|
|
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();
|
|
|
|
// Download the ZIP and verify
|
|
const dlRes = await request.get(json.downloadUrl, {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
expect(dlRes.ok()).toBe(true);
|
|
const buffer = Buffer.from(await dlRes.body());
|
|
expect(buffer.length).toBeGreaterThan(0);
|
|
// ZIP magic bytes
|
|
expect(buffer[0]).toBe(0x50);
|
|
expect(buffer[1]).toBe(0x4b);
|
|
} else {
|
|
const buffer = Buffer.from(await res.body());
|
|
expect(buffer.length).toBeGreaterThan(0);
|
|
expect(buffer[0]).toBe(0x50);
|
|
expect(buffer[1]).toBe(0x4b);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── Batch Convert -- 5+ Images ──────────────────────────────────
|
|
|
|
test.describe("Batch Convert -- 5+ images", () => {
|
|
test("batch convert 5 images to JPEG", async ({ request }) => {
|
|
const { body: reqBody, contentType } = buildMultipart(
|
|
[
|
|
{ name: "file", filename: "a.png", contentType: "image/png", buffer: PNG_200x150 },
|
|
{ name: "file", filename: "b.webp", contentType: "image/webp", buffer: WEBP_50x50 },
|
|
{ name: "file", filename: "c.heic", contentType: "image/heic", buffer: HEIC_200x150 },
|
|
{
|
|
name: "file",
|
|
filename: "d.jpg",
|
|
contentType: "image/jpeg",
|
|
buffer: formatFixture("sample.jpg"),
|
|
},
|
|
{ name: "file", filename: "e.png", contentType: "image/png", buffer: PNG_200x150 },
|
|
],
|
|
[{ name: "settings", value: JSON.stringify({ format: "jpg", quality: 80 }) }],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/convert/batch", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: reqBody,
|
|
});
|
|
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);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── Batch Compress -- 5+ Images ─────────────────────────────────
|
|
|
|
test.describe("Batch Compress -- 5+ images", () => {
|
|
test("batch compress 5 images with varying formats", 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: "file",
|
|
filename: "e.jpg",
|
|
contentType: "image/jpeg",
|
|
buffer: formatFixture("sample.jpg"),
|
|
},
|
|
],
|
|
[{ name: "settings", value: JSON.stringify({ quality: 40 }) }],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/compress/batch", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: reqBody,
|
|
});
|
|
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);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── Batch Enhancement -- 5+ Images ──────────────────────────────
|
|
|
|
test.describe("Batch Enhancement -- 5+ images", () => {
|
|
test("batch enhance 5 images with auto 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.heic", contentType: "image/heic", buffer: HEIC_200x150 },
|
|
{
|
|
name: "file",
|
|
filename: "e.jpg",
|
|
contentType: "image/jpeg",
|
|
buffer: formatFixture("sample.jpg"),
|
|
},
|
|
],
|
|
[{ name: "settings", value: JSON.stringify({ preset: "auto" }) }],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/image-enhancement/batch", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: reqBody,
|
|
});
|
|
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);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── Batch Watermark Text -- 5+ Images ───────────────────────────
|
|
|
|
test.describe("Batch Watermark Text -- 5+ images", () => {
|
|
test("batch watermark 5 images with tiled text", 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: "file",
|
|
filename: "e.jpg",
|
|
contentType: "image/jpeg",
|
|
buffer: formatFixture("sample.jpg"),
|
|
},
|
|
],
|
|
[
|
|
{
|
|
name: "settings",
|
|
value: JSON.stringify({
|
|
text: "DO NOT COPY",
|
|
fontSize: 16,
|
|
color: "#999999",
|
|
opacity: 20,
|
|
position: "tiled",
|
|
rotation: -30,
|
|
}),
|
|
},
|
|
],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/watermark-text/batch", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: reqBody,
|
|
});
|
|
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);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── Batch Optimize -- Download Verification ─────────────────────
|
|
|
|
test.describe("Batch Optimize -- download verification", () => {
|
|
test("batch optimize and verify ZIP download", 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({ maxWidth: 400, quality: 60 }) }],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/optimize-for-web/batch", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: reqBody,
|
|
});
|
|
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();
|
|
|
|
const dlRes = await request.get(json.downloadUrl, {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
expect(dlRes.ok()).toBe(true);
|
|
const buffer = Buffer.from(await dlRes.body());
|
|
expect(buffer.length).toBeGreaterThan(0);
|
|
expect(buffer[0]).toBe(0x50);
|
|
expect(buffer[1]).toBe(0x4b);
|
|
} else {
|
|
const buffer = Buffer.from(await res.body());
|
|
expect(buffer.length).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ─── Batch with Empty File List ────────────────────────────────────
|
|
|
|
test.describe("Batch validation", () => {
|
|
test("batch resize rejects empty file list", async ({ request }) => {
|
|
const { body: reqBody, contentType } = buildMultipart(
|
|
[],
|
|
[{ name: "settings", value: JSON.stringify({ width: 80 }) }],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/resize/batch", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: reqBody,
|
|
});
|
|
expect(res.ok()).toBe(false);
|
|
const json = await res.json();
|
|
expect(json.error).toBeDefined();
|
|
});
|
|
|
|
test("batch compress rejects single file (should use non-batch endpoint)", async ({
|
|
request,
|
|
}) => {
|
|
// Single file to batch endpoint may be accepted or redirected;
|
|
// verify the response is coherent in either case
|
|
const { body: reqBody, contentType } = buildMultipart(
|
|
[{ name: "file", filename: "a.png", contentType: "image/png", buffer: PNG_200x150 }],
|
|
[{ name: "settings", value: JSON.stringify({ quality: 50 }) }],
|
|
);
|
|
const res = await request.post("/api/v1/tools/image/compress/batch", {
|
|
headers: { Authorization: `Bearer ${token}`, "Content-Type": contentType },
|
|
data: reqBody,
|
|
});
|
|
// Either accepts the single file or returns an error — both valid
|
|
const resContentType = res.headers()["content-type"] ?? "";
|
|
if (res.ok()) {
|
|
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);
|
|
}
|
|
} else {
|
|
const json = await res.json();
|
|
expect(json.error).toBeDefined();
|
|
}
|
|
});
|
|
});
|