Merge branch 'worktree-agent-ac2de4e3'

This commit is contained in:
SnapOtter
2026-05-01 03:05:31 +08:00
11 changed files with 549 additions and 0 deletions
+66
View File
@@ -573,6 +573,72 @@ describe("Border", () => {
expect(result.processedSize).toBeGreaterThan(0);
});
it("handles HEIF input (motorcycle.heif)", async () => {
const HEIF = readFileSync(join(FIXTURES, "content", "motorcycle.heif"));
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "photo.heif", contentType: "image/heif", content: HEIF },
{
name: "settings",
content: JSON.stringify({ borderWidth: 10, borderColor: "#00FF00" }),
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/border",
headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType },
body,
});
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
expect(result.processedSize).toBeGreaterThan(0);
});
it("handles SVG input", async () => {
const SVG = readFileSync(join(FIXTURES, "test-100x100.svg"));
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "icon.svg", contentType: "image/svg+xml", content: SVG },
{
name: "settings",
content: JSON.stringify({ borderWidth: 10, borderColor: "#FF0000" }),
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/border",
headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType },
body,
});
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
expect(result.processedSize).toBeGreaterThan(0);
});
it("handles animated GIF input", async () => {
const GIF = readFileSync(join(FIXTURES, "animated.gif"));
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "anim.gif", contentType: "image/gif", content: GIF },
{
name: "settings",
content: JSON.stringify({ borderWidth: 5, borderColor: "#000000" }),
},
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/border",
headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType },
body,
});
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
expect(result.processedSize).toBeGreaterThan(0);
});
it("rejects negative border width", async () => {
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "test.png", contentType: "image/png", content: PNG },
@@ -537,3 +537,42 @@ describe("No-op effect", () => {
expect(result.downloadUrl).toBeDefined();
});
});
// ── HEIF input ──────────────────────────────────────────────────
describe("HEIF input", () => {
it("processes HEIF input (motorcycle.heif)", async () => {
const HEIF = readFileSync(join(FIXTURES, "content", "motorcycle.heif"));
const res = await postTool(
"adjust-colors",
{ brightness: 20 },
HEIF,
"photo.heif",
"image/heif",
);
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
expect(result.downloadUrl).toBeDefined();
}, 60_000);
});
// ── Animated GIF input ──────────────────────────────────────────
describe("Animated GIF input", () => {
it("processes animated GIF", async () => {
const GIF = readFileSync(join(FIXTURES, "animated.gif"));
const res = await postTool("adjust-colors", { saturation: 30 }, GIF, "anim.gif", "image/gif");
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
expect(result.downloadUrl).toBeDefined();
});
});
// ── SVG input ───────────────────────────────────────────────────
describe("SVG input", () => {
it("processes SVG input", async () => {
const SVG = readFileSync(join(FIXTURES, "test-100x100.svg"));
const res = await postTool("adjust-colors", { contrast: 20 }, SVG, "icon.svg", "image/svg+xml");
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
expect(result.downloadUrl).toBeDefined();
});
});
+40
View File
@@ -400,6 +400,46 @@ describe("HEIF input", () => {
});
});
// ── Animated GIF input ──────────────────────────────────────────
describe("Animated GIF input", () => {
it("extracts palette from animated GIF", async () => {
const GIF = readFileSync(join(FIXTURES, "animated.gif"));
const { body: payload, contentType } = makeFilePayload(GIF, "anim.gif", "image/gif");
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/color-palette",
payload,
headers: {
"content-type": contentType,
authorization: `Bearer ${adminToken}`,
},
});
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
expect(result.colors.length).toBeGreaterThan(0);
});
});
// ── SVG input ───────────────────────────────────────────────────
describe("SVG input", () => {
it("extracts palette from SVG image", async () => {
const SVG = readFileSync(join(FIXTURES, "test-100x100.svg"));
const { body: payload, contentType } = makeFilePayload(SVG, "icon.svg", "image/svg+xml");
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/color-palette",
payload,
headers: {
"content-type": contentType,
authorization: `Bearer ${adminToken}`,
},
});
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
expect(result.colors.length).toBeGreaterThan(0);
});
});
// ── Filename preserved in response ──────────────────────────────
describe("Filename tracking", () => {
it("returns the original filename in the response", async () => {
+53
View File
@@ -627,6 +627,59 @@ describe("favicon", () => {
expect(manifest.name).toBe("my.app.logo");
});
// ── Animated GIF input ────────────────────────────────────────────
it("generates favicons from animated GIF input", async () => {
const GIF = readFileSync(join(FIXTURES, "animated.gif"));
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "anim.gif", contentType: "image/gif", content: GIF },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/favicon",
headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType },
body,
});
expect(res.statusCode).toBe(200);
const zip = new AdmZip(Buffer.from(res.rawPayload));
const entries = zip.getEntries().map((e) => e.entryName);
expect(entries).toContain("favicon-16x16.png");
expect(entries).toContain("favicon.ico");
});
// ── Batch: 5+ images ────────────────────────────────────────────
it("generates favicons for 5 images in subfolders", async () => {
const JPG = readFileSync(join(FIXTURES, "test-100x100.jpg"));
const WEBP = readFileSync(join(FIXTURES, "test-50x50.webp"));
const TINY = readFileSync(join(FIXTURES, "test-1x1.png"));
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "a.png", contentType: "image/png", content: PNG },
{ name: "file", filename: "b.jpg", contentType: "image/jpeg", content: JPG },
{ name: "file", filename: "c.webp", contentType: "image/webp", content: WEBP },
{ name: "file", filename: "d.png", contentType: "image/png", content: TINY },
{ name: "file", filename: "e.png", contentType: "image/png", content: PNG },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/favicon",
headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType },
body,
});
expect(res.statusCode).toBe(200);
const zip = new AdmZip(Buffer.from(res.rawPayload));
const entries = zip.getEntries().map((e) => e.entryName);
expect(entries.some((e) => e.startsWith("a/"))).toBe(true);
expect(entries.some((e) => e.startsWith("b/"))).toBe(true);
expect(entries.some((e) => e.startsWith("c/"))).toBe(true);
expect(entries.some((e) => e.startsWith("d/"))).toBe(true);
expect(entries.some((e) => e.startsWith("e/"))).toBe(true);
});
// ── HEIF format input ────────────────────────────────────────────
it("generates favicons from HEIF input", async () => {
+23
View File
@@ -788,6 +788,29 @@ describe("image-to-base64", () => {
expect(res.statusCode).toBe(400);
});
// ── Animated GIF input ──────────────────────────────────────────
it("converts animated GIF to base64", async () => {
const GIF = readFileSync(join(FIXTURES, "animated.gif"));
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "anim.gif", contentType: "image/gif", content: GIF },
{ name: "settings", content: JSON.stringify({}) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/image-to-base64",
headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType },
body,
});
expect(res.statusCode).toBe(200);
const json = JSON.parse(res.body);
expect(json.results).toHaveLength(1);
expect(json.results[0].base64.length).toBeGreaterThan(0);
expect(json.results[0].dataUri).toMatch(/^data:image\/.+;base64,/);
});
// ── maxHeight=0 means no resize ──────────────────────────────────
it("maxHeight=0 means no height resize (pass-through)", async () => {
+66
View File
@@ -739,6 +739,72 @@ describe("image-to-pdf", () => {
expect(json.pages).toBe(1);
});
// ── HEIF input ────────────────────────────────────────────────────
it("converts HEIF image to PDF", async () => {
const HEIF = readFileSync(join(FIXTURES, "content", "motorcycle.heif"));
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "photo.heif", contentType: "image/heif", content: HEIF },
{ name: "settings", content: JSON.stringify({}) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/image-to-pdf",
headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType },
body,
});
expect(res.statusCode).toBe(200);
const json = JSON.parse(res.body);
expect(json.pages).toBe(1);
expect(json.processedSize).toBeGreaterThan(0);
}, 60_000);
// ── Animated GIF input ──────────────────────────────────────────
it("converts animated GIF to PDF", async () => {
const GIF = readFileSync(join(FIXTURES, "animated.gif"));
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "anim.gif", contentType: "image/gif", content: GIF },
{ name: "settings", content: JSON.stringify({}) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/image-to-pdf",
headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType },
body,
});
expect(res.statusCode).toBe(200);
const json = JSON.parse(res.body);
expect(json.pages).toBe(1);
expect(json.processedSize).toBeGreaterThan(0);
});
// ── SVG input ───────────────────────────────────────────────────
it("converts SVG to PDF", async () => {
const SVG = readFileSync(join(FIXTURES, "test-100x100.svg"));
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "icon.svg", contentType: "image/svg+xml", content: SVG },
{ name: "settings", content: JSON.stringify({}) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/image-to-pdf",
headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType },
body,
});
expect(res.statusCode).toBe(200);
const json = JSON.parse(res.body);
expect(json.pages).toBe(1);
expect(json.processedSize).toBeGreaterThan(0);
});
// ── Rejects invalid orientation ──────────────────────────────────
it("rejects invalid orientation value", async () => {
+48
View File
@@ -420,3 +420,51 @@ describe("Target color validation", () => {
expect(res.statusCode).toBe(400);
});
});
// ── HEIF input ─────────────────────────────────────────────────
describe("HEIF input", () => {
it("processes HEIF image (motorcycle.heif)", async () => {
const HEIF = readFileSync(join(FIXTURES, "content", "motorcycle.heif"));
const res = await postTool(
{ sourceColor: "#808080", targetColor: "#FF0000", tolerance: 50 },
HEIF,
"photo.heif",
"image/heif",
);
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
expect(result.downloadUrl).toBeDefined();
}, 60_000);
});
// ── Animated GIF input ─────────────────────────────────────────
describe("Animated GIF input", () => {
it("processes animated GIF", async () => {
const GIF = readFileSync(join(FIXTURES, "animated.gif"));
const res = await postTool(
{ sourceColor: "#808080", targetColor: "#00FF00", tolerance: 60 },
GIF,
"anim.gif",
"image/gif",
);
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
expect(result.downloadUrl).toBeDefined();
});
});
// ── SVG input ──────────────────────────────────────────────────
describe("SVG input", () => {
it("processes SVG image", async () => {
const SVG = readFileSync(join(FIXTURES, "test-100x100.svg"));
const res = await postTool(
{ sourceColor: "#000000", targetColor: "#FF0000", tolerance: 30 },
SVG,
"icon.svg",
"image/svg+xml",
);
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
expect(result.downloadUrl).toBeDefined();
});
});
+43
View File
@@ -452,3 +452,46 @@ describe("HEIC with different methods", () => {
expect(res.statusCode).toBe(200);
});
});
// ── HEIF input ──────────────────────────────────────────────────
describe("HEIF input", () => {
it("processes HEIF image (motorcycle.heif)", async () => {
const HEIF = readFileSync(join(FIXTURES, "content", "motorcycle.heif"));
const res = await postTool(
{ method: "adaptive", sigma: 2.0 },
HEIF,
"photo.heif",
"image/heif",
);
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
expect(result.downloadUrl).toBeDefined();
}, 60_000);
});
// ── Animated GIF input ──────────────────────────────────────────
describe("Animated GIF input", () => {
it("processes animated GIF", async () => {
const GIF = readFileSync(join(FIXTURES, "animated.gif"));
const res = await postTool(
{ method: "unsharp-mask", amount: 150 },
GIF,
"anim.gif",
"image/gif",
);
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
expect(result.downloadUrl).toBeDefined();
});
});
// ── SVG input ───────────────────────────────────────────────────
describe("SVG input", () => {
it("processes SVG image", async () => {
const SVG = readFileSync(join(FIXTURES, "test-100x100.svg"));
const res = await postTool({ method: "adaptive" }, SVG, "icon.svg", "image/svg+xml");
expect(res.statusCode).toBe(200);
const result = JSON.parse(res.body);
expect(result.downloadUrl).toBeDefined();
});
});
+63
View File
@@ -530,6 +530,69 @@ describe("text-overlay", () => {
expect(res.statusCode).toBe(400);
});
// ── HEIF input ───────────────────────────────────────────────────
it("handles HEIF input (motorcycle.heif)", async () => {
const HEIF = readFileSync(join(FIXTURES, "content", "motorcycle.heif"));
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "photo.heif", contentType: "image/heif", content: HEIF },
{ name: "settings", content: JSON.stringify({ text: "HEIF overlay" }) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/text-overlay",
headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType },
body,
});
expect(res.statusCode).toBe(200);
const json = JSON.parse(res.body);
expect(json.downloadUrl).toBeDefined();
}, 60_000);
// ── Animated GIF input ──────────────────────────────────────────
it("handles animated GIF input", async () => {
const GIF = readFileSync(join(FIXTURES, "animated.gif"));
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "anim.gif", contentType: "image/gif", content: GIF },
{ name: "settings", content: JSON.stringify({ text: "GIF overlay" }) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/text-overlay",
headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType },
body,
});
expect(res.statusCode).toBe(200);
const json = JSON.parse(res.body);
expect(json.downloadUrl).toBeDefined();
});
// ── SVG input ───────────────────────────────────────────────────
it("handles SVG input", async () => {
const SVG = readFileSync(join(FIXTURES, "test-100x100.svg"));
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "icon.svg", contentType: "image/svg+xml", content: SVG },
{ name: "settings", content: JSON.stringify({ text: "SVG overlay" }) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/text-overlay",
headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType },
body,
});
expect(res.statusCode).toBe(200);
const json = JSON.parse(res.body);
expect(json.downloadUrl).toBeDefined();
});
// ── Preserves image dimensions ───────────────────────────────────
it("preserves image dimensions after text overlay", async () => {
+45
View File
@@ -483,6 +483,51 @@ describe("watermark-image", () => {
expect(res.statusCode).toBe(401);
});
// ── HEIF input ────────────────────────────────────────────────────
it("processes HEIF main image (motorcycle.heif)", async () => {
const HEIF = readFileSync(join(FIXTURES, "content", "motorcycle.heif"));
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "photo.heif", contentType: "image/heif", content: HEIF },
{ name: "watermark", filename: "wm.png", contentType: "image/png", content: SMALL_PNG },
{ name: "settings", content: JSON.stringify({ scale: 10 }) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/watermark-image",
headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType },
body,
});
expect(res.statusCode).toBe(200);
const json = JSON.parse(res.body);
expect(json.downloadUrl).toBeDefined();
expect(json.processedSize).toBeGreaterThan(0);
}, 60_000);
// ── Animated GIF input ──────────────────────────────────────────
it("processes animated GIF main image", async () => {
const GIF = readFileSync(join(FIXTURES, "animated.gif"));
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "anim.gif", contentType: "image/gif", content: GIF },
{ name: "watermark", filename: "wm.png", contentType: "image/png", content: SMALL_PNG },
{ name: "settings", content: JSON.stringify({ scale: 20 }) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/watermark-image",
headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType },
body,
});
expect(res.statusCode).toBe(200);
const json = JSON.parse(res.body);
expect(json.processedSize).toBeGreaterThan(0);
});
// ── WebP watermark image ─────────────────────────────────────────
it("processes WebP watermark image", async () => {
+63
View File
@@ -518,6 +518,69 @@ describe("watermark-text", () => {
expect(res.statusCode).toBe(400);
});
// ── HEIF input ───────────────────────────────────────────────────
it("handles HEIF input (motorcycle.heif)", async () => {
const HEIF = readFileSync(join(FIXTURES, "content", "motorcycle.heif"));
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "photo.heif", contentType: "image/heif", content: HEIF },
{ name: "settings", content: JSON.stringify({ text: "HEIF Test" }) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/watermark-text",
headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType },
body,
});
expect(res.statusCode).toBe(200);
const json = JSON.parse(res.body);
expect(json.downloadUrl).toBeDefined();
}, 60_000);
// ── Animated GIF input ──────────────────────────────────────────
it("handles animated GIF input", async () => {
const GIF = readFileSync(join(FIXTURES, "animated.gif"));
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "anim.gif", contentType: "image/gif", content: GIF },
{ name: "settings", content: JSON.stringify({ text: "GIF Test" }) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/watermark-text",
headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType },
body,
});
expect(res.statusCode).toBe(200);
const json = JSON.parse(res.body);
expect(json.downloadUrl).toBeDefined();
});
// ── SVG input ───────────────────────────────────────────────────
it("handles SVG input", async () => {
const SVG = readFileSync(join(FIXTURES, "test-100x100.svg"));
const { body, contentType } = createMultipartPayload([
{ name: "file", filename: "icon.svg", contentType: "image/svg+xml", content: SVG },
{ name: "settings", content: JSON.stringify({ text: "SVG Test" }) },
]);
const res = await app.inject({
method: "POST",
url: "/api/v1/tools/watermark-text",
headers: { authorization: `Bearer ${adminToken}`, "content-type": contentType },
body,
});
expect(res.statusCode).toBe(200);
const json = JSON.parse(res.body);
expect(json.downloadUrl).toBeDefined();
});
// ── Preserves image dimensions ───────────────────────────────────
it("preserves image dimensions after watermark", async () => {