Files
SnapOtter/tests/integration/generated/new-formats.test.ts
T
SnapOtterandGitHub f1ec3beaf7 test: settle 202 jobs instead of returning without asserting (#652)
A 202 means the sync window expired while the job was still running. Tests treated it as a terminal pass: `if (isAsyncFallback(res)) return;` checked the envelope and returned, asserting nothing about the outcome and leaving the job running into the next test, which is the leak cancelAcceptedJobAndWait exists to prevent.

Because the window only expires under load, coverage tracked runner load. On CI 44 tests took this path and verified nothing; the same tests on a dev machine asserted in full (one measured 6.4s locally against 31s on CI).

settleAsyncFallback waits for a terminal state and asserts the job finished, and that a failure carries a message rather than being a crash. A clean failure stays valid, since the exotic-format fixtures are meant to be rejected. All 82 call sites moved over.

per-fork-env no longer floors SYNC_WAIT_MS, so forcing it to 0 drives every request through its 202 path. 570 tests were validated that way and matched their normal-window results exactly.

The 29-34s band dropped from 44 tests (23.4% of test time) to 6 (3.0%). Total test time rose 5.8% and CI wall went 12.8 to 13.1 min: the forks were doing real work during that wait, so this buys determinism, not speed. Per-shard totals unchanged at 9903 tests, 9435 passed, 468 skipped.
2026-07-27 12:16:48 +08:00

279 lines
9.7 KiB
TypeScript

/**
* Integration tests for new input/output format support.
*
* - New output formats: convert PNG to jxl, bmp, ico, jp2, qoi
* - SVGZ input: verify compressed SVG decodes correctly
* - JXL quality: verify lower quality produces smaller files
*/
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { fixtureDir, fixtures, readFixture } from "../../fixtures/index.js";
import { settleAsyncFallback } from "../settle-job.js";
import {
buildTestApp,
createMultipartPayload,
loginAsAdmin,
type TestApp,
} from "../test-server.js";
describe("New format support", () => {
let testApp: TestApp;
let app: TestApp["app"];
let adminToken: string;
beforeAll(async () => {
testApp = await buildTestApp();
app = testApp.app;
adminToken = await loginAsAdmin(app);
}, 30_000);
afterAll(async () => {
await testApp?.cleanup();
}, 10_000);
// ---------------------------------------------------------------------------
// New output format conversions
// ---------------------------------------------------------------------------
const NEW_OUTPUT_FORMATS = ["jxl", "bmp", "ico", "jp2", "qoi"];
for (const format of NEW_OUTPUT_FORMATS) {
it(`converts PNG to ${format}`, async () => {
const fileBuffer = readFixture(fixtures.image.formats("png"));
const { body, contentType } = createMultipartPayload([
{
name: "file",
filename: "sample.png",
contentType: "image/png",
content: fileBuffer,
},
{
name: "settings",
content: JSON.stringify({ format, quality: 80 }),
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/image/convert",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
body,
});
// Accept 200 (success) or 422 (encoder not available in test env)
if (await settleAsyncFallback(res)) return;
expect([200, 422]).toContain(res.statusCode);
if (res.statusCode === 200) {
const json = JSON.parse(res.body);
expect(json.processedSize).toBeGreaterThan(0);
expect(json.downloadUrl).toBeTruthy();
}
});
}
// ---------------------------------------------------------------------------
// SVGZ input decoding
// ---------------------------------------------------------------------------
it("decodes SVGZ input correctly", async () => {
const fileBuffer = readFixture(fixtures.image.formats("svgz"));
const { body, contentType } = createMultipartPayload([
{
name: "file",
filename: "sample.svgz",
contentType: "image/svg+xml",
content: fileBuffer,
},
{
name: "settings",
content: JSON.stringify({ format: "png" }),
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/image/convert",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
body,
});
if (await settleAsyncFallback(res)) return;
expect([200, 422]).toContain(res.statusCode);
if (res.statusCode === 200) {
const json = JSON.parse(res.body);
expect(json.processedSize).toBeGreaterThan(0);
}
});
// ---------------------------------------------------------------------------
// JXL quality affects file size
// ---------------------------------------------------------------------------
it("JXL quality affects file size", async () => {
const fileBuffer = readFixture(fixtures.image.formats("png"));
const convert = async (quality: number) => {
const { body, contentType } = createMultipartPayload([
{
name: "file",
filename: "sample.png",
contentType: "image/png",
content: fileBuffer,
},
{
name: "settings",
content: JSON.stringify({ format: "jxl", quality }),
},
]);
return app.inject({
method: "POST",
url: "/api/v1/tools/image/convert",
headers: {
authorization: `Bearer ${adminToken}`,
"content-type": contentType,
},
body,
});
};
const lowQ = await convert(30);
const highQ = await convert(90);
// Only compare sizes if both succeeded (JXL encoder may not be available)
if (lowQ.statusCode === 200 && highQ.statusCode === 200) {
const lowJson = JSON.parse(lowQ.body);
const highJson = JSON.parse(highQ.body);
expect(lowJson.processedSize).toBeLessThan(highJson.processedSize);
}
});
describe("Extended output format matrix", () => {
const INPUTS = [
{ name: "PNG", file: "sample.png", mime: "image/png" },
{ name: "JPEG", file: "sample.jpg", mime: "image/jpeg" },
{ name: "WebP", file: "sample.webp", mime: "image/webp" },
];
const OUTPUTS = [
"jpg",
"png",
"webp",
"avif",
"tiff",
"gif",
"heic",
"heif",
"jxl",
"bmp",
"ico",
"jp2",
"qoi",
];
for (const input of INPUTS) {
for (const outFmt of OUTPUTS) {
const inLower = input.name.toLowerCase();
if (inLower === outFmt || (inLower === "jpeg" && outFmt === "jpg")) continue;
const testTimeout = outFmt === "avif" ? 120_000 : 120_000;
it(`converts ${input.name} to ${outFmt}`, { timeout: testTimeout }, async () => {
const fileBuffer = readFileSync(join(fixtureDir.formats, input.file));
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: input.file, contentType: input.mime, content: fileBuffer },
{ name: "settings", content: JSON.stringify({ format: outFmt, quality: 80 }) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/image/convert",
headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType },
body,
});
if (await settleAsyncFallback(res)) return;
expect([200, 422]).toContain(res.statusCode);
if (res.statusCode === 200) {
const json = JSON.parse(res.body);
expect(json.processedSize).toBeGreaterThan(0);
expect(json.downloadUrl).toBeTruthy();
}
});
}
}
});
describe("New input format processing via resize", () => {
const NEW_INPUT_FORMATS = [
{ name: "SVGZ", file: "sample.svgz", mime: "image/svg+xml" },
{ name: "JP2", file: "sample.jp2", mime: "image/jp2" },
{ name: "EPS", file: "sample.eps", mime: "application/postscript" },
{ name: "PPM", file: "sample.ppm", mime: "image/x-portable-pixmap" },
{ name: "PGM", file: "sample.pgm", mime: "image/x-portable-graymap" },
{ name: "PBM", file: "sample.pbm", mime: "image/x-portable-bitmap" },
{ name: "DDS", file: "sample.dds", mime: "image/vnd.ms-dds" },
{ name: "CUR", file: "sample.cur", mime: "image/x-icon" },
{ name: "DPX", file: "sample.dpx", mime: "image/x-dpx" },
{ name: "FITS", file: "sample.fits", mime: "image/fits" },
{ name: "APNG", file: "sample.apng", mime: "image/apng" },
{ name: "QOI", file: "sample.qoi", mime: "image/x-qoi" },
];
for (const fmt of NEW_INPUT_FORMATS) {
it(`resizes ${fmt.name} input to 25x25`, async () => {
const fixturePath = join(fixtureDir.formats, fmt.file);
if (!existsSync(fixturePath)) return;
const fileBuffer = readFileSync(fixturePath);
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: fmt.file, contentType: fmt.mime, content: fileBuffer },
{ name: "settings", content: JSON.stringify({ width: 25, height: 25 }) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/image/resize",
headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType },
body,
});
if (await settleAsyncFallback(res)) return;
expect([200, 400, 422]).toContain(res.statusCode);
if (res.statusCode === 200) {
const json = JSON.parse(res.body);
expect(json.downloadUrl).toBeTruthy();
expect(json.processedSize).toBeGreaterThan(0);
}
}, 60_000);
}
});
describe("Preview generation for special formats", () => {
const PREVIEW_FORMATS = [
{ name: "HEIC", file: "sample.heic", mime: "image/heic" },
{ name: "JXL", file: "sample.jxl", mime: "image/jxl" },
{ name: "ICO", file: "sample.ico", mime: "image/x-icon" },
{ name: "PSD", file: "sample.psd", mime: "image/vnd.adobe.photoshop" },
{ name: "EXR", file: "sample.exr", mime: "image/x-exr" },
];
for (const fmt of PREVIEW_FORMATS) {
it(`generates preview for ${fmt.name}`, async () => {
const fixturePath = join(fixtureDir.formats, fmt.file);
if (!existsSync(fixturePath)) return;
const fileBuffer = readFileSync(fixturePath);
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: fmt.file, contentType: fmt.mime, content: fileBuffer },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/preview",
headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType },
body,
});
if (await settleAsyncFallback(res)) return;
expect([200, 422]).toContain(res.statusCode);
if (res.statusCode === 200) {
const ct = res.headers["content-type"] as string;
expect(ct).toMatch(/image\/(webp|png)/);
}
}, 60_000);
}
});
});