test: fill image-engine unit test branch coverage gaps

This commit is contained in:
SnapOtter
2026-05-01 02:19:56 +08:00
parent 710b580cee
commit 0a0512a153
4 changed files with 191 additions and 0 deletions
+11
View File
@@ -198,6 +198,17 @@ describe("detectFormat", () => {
expect(format).not.toBe("avif");
});
it("rejects ftyp box when buffer too short to read brand (< 12 bytes)", async () => {
// Buffer has ftyp at offset 4 but is only 8 bytes long -- too short for brand
const buf = Buffer.alloc(8);
buf[4] = 0x66;
buf[5] = 0x74;
buf[6] = 0x79;
buf[7] = 0x70; // "ftyp"
const format = await detectFormat(buf);
expect(format).not.toBe("avif");
});
it("detects JXL ISOBMFF container magic bytes", async () => {
const buf = Buffer.from([0x00, 0x00, 0x00, 0x0c, 0x4a, 0x58, 0x4c, 0x20, 0, 0, 0, 0]);
const format = await detectFormat(buf);
@@ -153,6 +153,18 @@ describe("editMetadata", () => {
expect(buf.length).toBeGreaterThan(0);
});
it("removes a Photo (IFD2) field via fieldsToRemove", async () => {
const img = sharp(jpgWithExif);
// DateTimeOriginal is in the Photo (IFD2) section
const result = await editMetadata(img, {
fieldsToRemove: ["DateTimeOriginal"],
});
const exif = await getExif(result);
expect(exif?.Photo?.DateTimeOriginal).toBeUndefined();
// Other IFD0 fields should be preserved
expect(exif?.Image?.Artist).toBe("Test Artist");
});
// -- Combined edit + remove ------------------------------------------------
it("handles both edits and removals together", async () => {
@@ -200,4 +212,37 @@ describe("editMetadata", () => {
const buf = await result.toBuffer();
expect(buf.length).toBeGreaterThan(0);
});
it("survives corrupt EXIF during removal (catch branch)", async () => {
// Corrupt the byte order marker in the EXIF so exif-reader throws,
// but sharp still sees the EXIF segment as present.
const rawBuf = Buffer.from(jpgWithExif);
// Find the 'Exif' header in the raw JPEG
let exifStart = -1;
for (let i = 0; i < rawBuf.length - 4; i++) {
if (
rawBuf[i] === 0x45 &&
rawBuf[i + 1] === 0x78 &&
rawBuf[i + 2] === 0x69 &&
rawBuf[i + 3] === 0x66
) {
exifStart = i;
break;
}
}
expect(exifStart).toBeGreaterThanOrEqual(0);
// Corrupt the byte order marker (offset +6 after 'Exif\0\0')
const corruptBuf = Buffer.from(rawBuf);
corruptBuf[exifStart + 6] = 0xde;
corruptBuf[exifStart + 7] = 0xad;
const img = sharp(corruptBuf);
// fieldsToRemove triggers the removal path which tries to parse EXIF
const result = await editMetadata(img, {
fieldsToRemove: ["Software"],
artist: "Survivor",
});
const buf = await result.toBuffer();
expect(buf.length).toBeGreaterThan(0);
});
});
+20
View File
@@ -219,6 +219,26 @@ describe("parseExif", () => {
expect(result.image).toEqual({});
expect(result.photo).toEqual({});
});
it("parses GPSInfo section when GPS data is present", async () => {
// Create a JPEG with GPS EXIF data via withExif IFD3
const buf = await sharp({
create: { width: 10, height: 10, channels: 3, background: "#808080" },
})
.withExif({
IFD0: { Artist: "GPS Test" },
IFD3: { GPSLatitudeRef: "N" },
})
.jpeg()
.toBuffer();
const metadata = await sharp(buf).metadata();
expect(metadata.exif).toBeTruthy();
const result = parseExif(metadata.exif!);
// The GPSInfo section should be populated with sanitized values
expect(typeof result.gps).toBe("object");
expect(Object.keys(result.gps).length).toBeGreaterThan(0);
expect(result.gps.GPSLatitudeRef).toBe("N");
});
});
// ---------------------------------------------------------------------------
+115
View File
@@ -198,6 +198,28 @@ describe("resize", () => {
);
});
it("withoutEnlargement clamps height only (no width given)", async () => {
const img = sharp(png1x1);
const result = await resize(img, {
height: 500,
withoutEnlargement: true,
});
const meta = await getMeta(result);
expect(meta.height).toBeLessThanOrEqual(1);
});
it("withoutEnlargement does not clamp when target is smaller", async () => {
const img = sharp(png200x150);
const result = await resize(img, {
width: 50,
height: 50,
withoutEnlargement: true,
});
const meta = await getMeta(result);
expect(meta.width).toBe(50);
expect(meta.height).toBe(50);
});
it("percentage on a 1x1 image rounds to at least 1px (percentage >= 50)", async () => {
// 1 * 50 / 100 = 0.5 -> rounds to 1 (Math.round), so width=1
const img = sharp(png1x1);
@@ -253,6 +275,35 @@ describe("crop", () => {
expect(meta.height).toBe(1);
});
it("crops using percent unit", async () => {
const img = sharp(png200x150);
// 50% of 200 = 100, 50% of 150 = 75, starting at 10% left, 10% top
const result = await crop(img, {
left: 10,
top: 10,
width: 50,
height: 50,
unit: "percent",
});
const meta = await getMeta(result);
expect(meta.width).toBe(100); // 50% of 200
expect(meta.height).toBe(75); // 50% of 150 (Math.round)
});
it("crops 100% of image using percent unit", async () => {
const img = sharp(png200x150);
const result = await crop(img, {
left: 0,
top: 0,
width: 100,
height: 100,
unit: "percent",
});
const meta = await getMeta(result);
expect(meta.width).toBe(200);
expect(meta.height).toBe(150);
});
// -- Error cases --
it("throws on width = 0", async () => {
@@ -688,6 +739,49 @@ describe("compress", () => {
"Target size must be greater than 0",
);
});
it("falls back to PNG for SVG input (NO_ENCODER format)", async () => {
const svgBuf = readFileSync(path.join(FIXTURES_DIR, "formats/sample.svg"));
const img = sharp(svgBuf);
const result = await compress(img, { quality: 80 });
const meta = await getMeta(result);
expect(meta.format).toBe("png");
});
it("target size early break when within tolerance", async () => {
// Use a large solid-color image so binary search finishes quickly
const bigBuf = await sharp({
create: { width: 800, height: 600, channels: 3, background: "#cc6633" },
})
.jpeg({ quality: 95 })
.toBuffer();
// Set target to the actual size so it matches within tolerance immediately
const targetBytes = bigBuf.length;
const result = await compress(sharp(bigBuf), {
targetSizeBytes: targetBytes,
format: "jpg",
});
const buf = await result.toBuffer();
// Should be within 5% tolerance of target
expect(Math.abs(buf.length - targetBytes) / targetBytes).toBeLessThan(0.5);
});
it("target size falls back to bestQuality=1 when bestBuffer stays null", async () => {
// Use a very tiny target that forces all iterations to overshoot,
// so bestBuffer never gets assigned and the fallback path runs
const bigBuf = await sharp({
create: { width: 200, height: 200, channels: 3, background: "#ff8800" },
})
.jpeg({ quality: 100 })
.toBuffer();
// Target of 1 byte -- impossible to hit, so every iteration overshoots
const result = await compress(sharp(bigBuf), {
targetSizeBytes: 1,
format: "jpg",
});
const buf = await result.toBuffer();
expect(buf.length).toBeGreaterThan(0);
});
});
// ---------------------------------------------------------------------------
@@ -729,6 +823,27 @@ describe("stripMetadata", () => {
expect(meta.width).toBe(200);
expect(meta.height).toBe(150);
});
it("stripIcc=true enters selective stripping path", async () => {
const img = sharp(jpg100x100);
const result = await stripMetadata(img, { stripIcc: true });
const buf = await result.toBuffer();
expect(buf.length).toBeGreaterThan(0);
});
it("stripGps=true enters selective stripping path", async () => {
const img = sharp(jpg100x100);
const result = await stripMetadata(img, { stripGps: true });
const buf = await result.toBuffer();
expect(buf.length).toBeGreaterThan(0);
});
it("stripXmp=true enters selective stripping path", async () => {
const img = sharp(jpg100x100);
const result = await stripMetadata(img, { stripXmp: true });
const buf = await result.toBuffer();
expect(buf.length).toBeGreaterThan(0);
});
});
// ---------------------------------------------------------------------------