2026-07-27 00:36:51 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* Cross-format matrix, part 2 of 2 (see format-matrix.shared.ts).
|
|
|
|
|
|
*
|
|
|
|
|
|
* Vitest shards by file and runs a file's tests serially in one fork, so the
|
|
|
|
|
|
* cross-format matrix is striped across two part files that together cover
|
|
|
|
|
|
* every entry of FORMAT_SAMPLES exactly once.
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import { existsSync, readFileSync } from "node:fs";
|
|
|
|
|
|
import { join } from "node:path";
|
|
|
|
|
|
import { apiToolPath } from "@snapotter/shared";
|
|
|
|
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
|
|
import { fixtureDir } from "../../fixtures/index.js";
|
2026-07-27 12:16:48 +08:00
|
|
|
|
import { settleAsyncFallback } from "../settle-job.js";
|
2026-07-27 00:36:51 +08:00
|
|
|
|
import {
|
|
|
|
|
|
ACCEPTABLE_FALLBACK_CODES,
|
|
|
|
|
|
adminToken,
|
|
|
|
|
|
app,
|
|
|
|
|
|
buildPayload,
|
|
|
|
|
|
formatSamplesForPart,
|
|
|
|
|
|
needsFallback,
|
|
|
|
|
|
setupMatrixApp,
|
|
|
|
|
|
TOOLS,
|
|
|
|
|
|
} from "./format-matrix.shared.js";
|
|
|
|
|
|
|
|
|
|
|
|
setupMatrixApp();
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Cross-format matrix: every tool x every format
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
describe("Cross-format matrix", () => {
|
|
|
|
|
|
for (const fmt of formatSamplesForPart(2)) {
|
|
|
|
|
|
describe(`${fmt.name} input (${fmt.file})`, () => {
|
|
|
|
|
|
const fixturePath = join(fixtureDir.formats, fmt.file);
|
|
|
|
|
|
|
|
|
|
|
|
for (const tool of TOOLS) {
|
|
|
|
|
|
// Skip "Convert to PNG" when input is already PNG (no-op conversion)
|
|
|
|
|
|
if (tool.id === "convert" && fmt.name === "PNG") continue;
|
|
|
|
|
|
|
|
|
|
|
|
const perTestTimeout =
|
|
|
|
|
|
(fmt.needsHeifDecoder || fmt.needsCliDecoder) && tool.id === "image-enhancement"
|
|
|
|
|
|
? 300_000
|
|
|
|
|
|
: fmt.needsHeifDecoder || fmt.needsCliDecoder
|
|
|
|
|
|
? 180_000
|
|
|
|
|
|
: tool.id === "image-enhancement"
|
|
|
|
|
|
? 120_000
|
|
|
|
|
|
: 60_000; // 2× SYNC_WAIT_MS so a 202 fallback never races the Vitest timeout
|
|
|
|
|
|
|
|
|
|
|
|
it(
|
|
|
|
|
|
`${tool.label}`,
|
|
|
|
|
|
async () => {
|
|
|
|
|
|
if (!existsSync(fixturePath)) return;
|
|
|
|
|
|
|
|
|
|
|
|
const buffer = readFileSync(fixturePath);
|
|
|
|
|
|
const { body: payload, contentType } = buildPayload(fmt, tool, buffer);
|
|
|
|
|
|
|
|
|
|
|
|
const res = await app.inject({
|
|
|
|
|
|
method: "POST",
|
|
|
|
|
|
url: apiToolPath(tool.id),
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
authorization: `Bearer ${adminToken}`,
|
|
|
|
|
|
"content-type": contentType,
|
|
|
|
|
|
},
|
|
|
|
|
|
body: payload,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ------------------------------------------------------------------
|
|
|
|
|
|
// Assert status code
|
|
|
|
|
|
// ------------------------------------------------------------------
|
|
|
|
|
|
// A heavy encode may fall back to async (202) under CI load -- accept it.
|
2026-07-27 12:16:48 +08:00
|
|
|
|
if (await settleAsyncFallback(res)) return;
|
2026-07-27 00:36:51 +08:00
|
|
|
|
|
|
|
|
|
|
if (needsFallback(fmt)) {
|
|
|
|
|
|
// Formats with optional decoders: accept success or graceful error
|
|
|
|
|
|
expect(ACCEPTABLE_FALLBACK_CODES).toContain(res.statusCode);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Core formats must always succeed
|
|
|
|
|
|
expect(res.statusCode).toBe(200);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ------------------------------------------------------------------
|
|
|
|
|
|
// If successful, validate the response shape
|
|
|
|
|
|
// ------------------------------------------------------------------
|
|
|
|
|
|
if (res.statusCode === 200) {
|
|
|
|
|
|
const body = JSON.parse(res.body);
|
|
|
|
|
|
|
|
|
|
|
|
switch (tool.responseType) {
|
|
|
|
|
|
case "download":
|
|
|
|
|
|
expect(body.downloadUrl).toBeDefined();
|
|
|
|
|
|
expect(typeof body.downloadUrl).toBe("string");
|
|
|
|
|
|
expect(body.processedSize).toBeGreaterThan(0);
|
|
|
|
|
|
expect(body.originalSize).toBeGreaterThan(0);
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case "info":
|
|
|
|
|
|
expect(body.width).toBeGreaterThan(0);
|
|
|
|
|
|
expect(body.height).toBeGreaterThan(0);
|
|
|
|
|
|
expect(body.fileSize).toBeGreaterThan(0);
|
|
|
|
|
|
expect(body.format).toBeDefined();
|
|
|
|
|
|
expect(body.channels).toBeGreaterThan(0);
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case "base64":
|
|
|
|
|
|
// image-to-base64 returns { results: [...], errors: [...] }
|
|
|
|
|
|
expect(Array.isArray(body.results)).toBe(true);
|
|
|
|
|
|
expect(body.results.length + body.errors.length).toBeGreaterThan(0);
|
|
|
|
|
|
if (body.results.length > 0) {
|
|
|
|
|
|
const r = body.results[0];
|
|
|
|
|
|
expect(r.base64).toBeDefined();
|
|
|
|
|
|
expect(typeof r.base64).toBe("string");
|
|
|
|
|
|
expect(r.base64.length).toBeGreaterThan(0);
|
|
|
|
|
|
expect(r.dataUri).toMatch(/^data:/);
|
|
|
|
|
|
expect(r.width).toBeGreaterThan(0);
|
|
|
|
|
|
expect(r.height).toBeGreaterThan(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case "palette":
|
|
|
|
|
|
// color-palette returns { colors: string[], count: number }
|
|
|
|
|
|
expect(Array.isArray(body.colors)).toBe(true);
|
|
|
|
|
|
expect(body.colors.length).toBeGreaterThan(0);
|
|
|
|
|
|
expect(body.count).toBeGreaterThan(0);
|
|
|
|
|
|
// Each color should be a hex string
|
|
|
|
|
|
for (const color of body.colors) {
|
|
|
|
|
|
expect(color).toMatch(/^#[0-9a-f]{6}$/);
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case "pdf":
|
|
|
|
|
|
// image-to-pdf returns { downloadUrl, processedSize, pages }
|
|
|
|
|
|
expect(body.downloadUrl).toBeDefined();
|
|
|
|
|
|
expect(typeof body.downloadUrl).toBe("string");
|
|
|
|
|
|
expect(body.processedSize).toBeGreaterThan(0);
|
|
|
|
|
|
expect(body.pages).toBeGreaterThanOrEqual(1);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ------------------------------------------------------------------
|
|
|
|
|
|
// If the API returned an error, verify it is a clean JSON error
|
|
|
|
|
|
// (not a raw crash / stack trace / HTML error page)
|
|
|
|
|
|
// ------------------------------------------------------------------
|
|
|
|
|
|
if (res.statusCode >= 400) {
|
|
|
|
|
|
const body = JSON.parse(res.body);
|
|
|
|
|
|
expect(body.error).toBeDefined();
|
|
|
|
|
|
expect(typeof body.error).toBe("string");
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
perTestTimeout,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|