Merge branch 'worktree-test+suite-overhaul-and-real-fixtures' into chore/consolidate-v2.0.0

# Conflicts:
#	tests/integration/generated/settings-matrix.test.ts
#	tests/integration/platform/api.test.ts
#	tests/integration/platform/concurrent.test.ts
#	tests/integration/platform/factory-multi-input.test.ts
#	tests/integration/security/adversarial-comprehensive.test.ts
#	tests/integration/security/adversarial-coverage-gaps.test.ts
#	tests/integration/security/adversarial-extended.test.ts
#	tests/integration/security/adversarial-final-gaps.test.ts
#	tests/integration/security/adversarial-matrix.test.ts
#	tests/integration/security/adversarial-security.test.ts
#	tests/integration/security/adversarial.test.ts
#	tests/integration/tools/image/color-adjustments.test.ts
This commit is contained in:
SnapOtter
2026-06-21 02:18:53 +08:00
547 changed files with 37191 additions and 4059 deletions
@@ -3,7 +3,7 @@ import { describe, expect, it } from "vitest";
import {
generateBackground,
getDominantBackground,
} from "../../apps/api/src/lib/beautify/backgrounds.js";
} from "../../../apps/api/src/lib/beautify/backgrounds.js";
describe("generateBackground", () => {
it("generates a solid color background", async () => {
@@ -1,6 +1,6 @@
import sharp from "sharp";
import { describe, expect, it } from "vitest";
import { renderFrame } from "../../apps/api/src/lib/beautify/frames.js";
import { renderFrame } from "../../../apps/api/src/lib/beautify/frames.js";
describe("renderFrame", () => {
const makeImage = (w: number, h: number) =>
@@ -1,6 +1,6 @@
import sharp from "sharp";
import { describe, expect, it } from "vitest";
import { applyShadow } from "../../apps/api/src/lib/beautify/shadow.js";
import { applyShadow } from "../../../apps/api/src/lib/beautify/shadow.js";
describe("applyShadow", () => {
const makeImage = (w: number, h: number) =>
-3
View File
@@ -1,4 +1,3 @@
import { join } from "node:path";
import sharp from "sharp";
import { describe, expect, it } from "vitest";
import {
@@ -10,8 +9,6 @@ import {
createGradientBackground,
} from "../../../apps/api/src/lib/bg-effects.js";
const _FIXTURES = join(__dirname, "../../fixtures");
async function createTestImage(
width: number,
height: number,
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { friendlyError } from "../../apps/api/src/lib/errors.js";
import { friendlyError } from "../../../apps/api/src/lib/errors.js";
const GENERIC = "Processing failed. The file may be in an unsupported or corrupted format.";
+10 -13
View File
@@ -1,5 +1,3 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import sharp from "sharp";
import { describe, expect, it } from "vitest";
import type { EditMetadataSettings } from "../../../apps/api/src/lib/exiftool.js";
@@ -8,8 +6,7 @@ import {
inspectMetadata,
writeMetadata,
} from "../../../apps/api/src/lib/exiftool.js";
const FIXTURES = join(import.meta.dirname, "../../fixtures");
import { fixtures, readFixture } from "../../fixtures/index.js";
describe("buildTagArgs", () => {
it("returns empty array for empty settings", () => {
@@ -289,33 +286,33 @@ describe("buildTagArgs", () => {
describe("inspectMetadata", () => {
it("returns correct filename and fileSize for JPEG with EXIF", async () => {
const buf = readFileSync(join(FIXTURES, "test-with-exif.jpg"));
const buf = readFixture(fixtures.image.exifGps);
const result = await inspectMetadata(buf, "test-with-exif.jpg");
expect(result.filename).toBe("test-with-exif.jpg");
expect(result.fileSize).toBe(buf.length);
});
it("returns non-null exif object for JPEG with EXIF data", async () => {
const buf = readFileSync(join(FIXTURES, "test-with-exif.jpg"));
const buf = readFixture(fixtures.image.exifGps);
const result = await inspectMetadata(buf, "test-with-exif.jpg");
expect(result.exif).not.toBeNull();
expect(typeof result.exif).toBe("object");
});
it("returns keywords array (may be empty)", async () => {
const buf = readFileSync(join(FIXTURES, "test-with-exif.jpg"));
const buf = readFixture(fixtures.image.exifGps);
const result = await inspectMetadata(buf, "test-with-exif.jpg");
expect(Array.isArray(result.keywords)).toBe(true);
});
it("returns null for GPS when no GPS data present", async () => {
const buf = readFileSync(join(FIXTURES, "test-with-exif.jpg"));
const buf = readFixture(fixtures.image.exifGps);
const result = await inspectMetadata(buf, "test-with-exif.jpg");
expect(result.gps).toBeNull();
});
it("works with PNG which has no EXIF -- returns null for exif, iptc, xmp, gps", async () => {
const buf = readFileSync(join(FIXTURES, "test-1x1.png"));
const buf = readFixture(fixtures.image.edge.px1);
const result = await inspectMetadata(buf, "test-1x1.png");
expect(result.filename).toBe("test-1x1.png");
expect(result.fileSize).toBe(buf.length);
@@ -328,13 +325,13 @@ describe("inspectMetadata", () => {
describe("writeMetadata", () => {
it("empty tags array returns buffer unchanged", async () => {
const buf = readFileSync(join(FIXTURES, "test-with-exif.jpg"));
const buf = readFixture(fixtures.image.exifGps);
const result = await writeMetadata(buf, "test-with-exif.jpg", []);
expect(Buffer.compare(result, buf)).toBe(0);
});
it("writing -Artist=TestArtist then inspecting shows the artist", async () => {
const buf = readFileSync(join(FIXTURES, "test-with-exif.jpg"));
const buf = readFixture(fixtures.image.exifGps);
const written = await writeMetadata(buf, "test-with-exif.jpg", ["-Artist=TestArtist"]);
const result = await inspectMetadata(written, "test-with-exif.jpg");
expect(result.exif).not.toBeNull();
@@ -342,7 +339,7 @@ describe("writeMetadata", () => {
});
it("writing multiple tags works", async () => {
const buf = readFileSync(join(FIXTURES, "test-with-exif.jpg"));
const buf = readFixture(fixtures.image.exifGps);
const written = await writeMetadata(buf, "test-with-exif.jpg", [
"-Artist=MultiTest",
"-Copyright=2024 Test Corp",
@@ -353,7 +350,7 @@ describe("writeMetadata", () => {
});
it("returns a valid image buffer that Sharp can read", async () => {
const buf = readFileSync(join(FIXTURES, "test-with-exif.jpg"));
const buf = readFixture(fixtures.image.exifGps);
const written = await writeMetadata(buf, "test-with-exif.jpg", ["-Software=SnapOtter"]);
const meta = await sharp(written).metadata();
expect(meta.format).toBe("jpeg");
+21 -24
View File
@@ -1,11 +1,8 @@
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import sharp from "sharp";
import { describe, expect, it } from "vitest";
import { decodeToSharpCompat, needsCliDecode } from "../../../apps/api/src/lib/format-decoders.js";
import { encodeQoi } from "../../../apps/api/src/lib/format-encoders.js";
const FIXTURES = join(__dirname, "../../fixtures");
import { fixtures, readFixture } from "../../fixtures/index.js";
function isImageMagickError(err: unknown): boolean {
if (!(err instanceof Error)) return false;
@@ -176,7 +173,7 @@ describe("decodeToSharpCompat", () => {
it("decodes BMP to valid PNG", async () => {
try {
const input = await readFile(join(FIXTURES, "formats/sample.bmp"));
const input = readFixture(fixtures.image.formats("bmp"));
const result = await decodeToSharpCompat(input, "bmp");
expect(isPng(result)).toBe(true);
await assertValidImage(result);
@@ -188,7 +185,7 @@ describe("decodeToSharpCompat", () => {
it("decodes ICO to valid PNG", async () => {
try {
const input = await readFile(join(FIXTURES, "formats/sample.ico"));
const input = readFixture(fixtures.image.formats("ico"));
const result = await decodeToSharpCompat(input, "ico");
expect(isPng(result)).toBe(true);
await assertValidImage(result);
@@ -200,7 +197,7 @@ describe("decodeToSharpCompat", () => {
it("decodes TGA to valid PNG", async () => {
try {
const input = await readFile(join(FIXTURES, "formats/sample.tga"));
const input = readFixture(fixtures.image.formats("tga"));
const result = await decodeToSharpCompat(input, "tga");
expect(isPng(result)).toBe(true);
await assertValidImage(result);
@@ -212,7 +209,7 @@ describe("decodeToSharpCompat", () => {
it("decodes PSD to valid PNG", async () => {
try {
const input = await readFile(join(FIXTURES, "formats/sample.psd"));
const input = readFixture(fixtures.image.formats("psd"));
const result = await decodeToSharpCompat(input, "psd");
expect(isPng(result)).toBe(true);
await assertValidImage(result);
@@ -224,7 +221,7 @@ describe("decodeToSharpCompat", () => {
it("decodes EXR to valid PNG (requires EXR delegate)", async () => {
try {
const input = await readFile(join(FIXTURES, "formats/sample.exr"));
const input = readFixture(fixtures.image.formats("exr"));
const result = await decodeToSharpCompat(input, "exr");
expect(isPng(result)).toBe(true);
await assertValidImage(result);
@@ -238,7 +235,7 @@ describe("decodeToSharpCompat", () => {
it("decodes HDR to valid PNG", async () => {
try {
const input = await readFile(join(FIXTURES, "formats/sample.hdr"));
const input = readFixture(fixtures.image.formats("hdr"));
const result = await decodeToSharpCompat(input, "hdr");
expect(isPng(result)).toBe(true);
await assertValidImage(result);
@@ -250,7 +247,7 @@ describe("decodeToSharpCompat", () => {
it("decodes JXL to valid PNG", async () => {
try {
const input = await readFile(join(FIXTURES, "formats/sample.jxl"));
const input = readFixture(fixtures.image.formats("jxl"));
const result = await decodeToSharpCompat(input, "jxl");
expect(isPng(result)).toBe(true);
await assertValidImage(result);
@@ -262,7 +259,7 @@ describe("decodeToSharpCompat", () => {
it("decodes JP2 to valid PNG", async () => {
try {
const input = await readFile(join(FIXTURES, "formats/sample.jp2"));
const input = readFixture(fixtures.image.formats("jp2"));
const result = await decodeToSharpCompat(input, "jp2");
expect(isPng(result)).toBe(true);
await assertValidImage(result);
@@ -274,7 +271,7 @@ describe("decodeToSharpCompat", () => {
it("decodes DDS to valid PNG", async () => {
try {
const input = await readFile(join(FIXTURES, "formats/sample.dds"));
const input = readFixture(fixtures.image.formats("dds"));
const result = await decodeToSharpCompat(input, "dds");
expect(isPng(result)).toBe(true);
await assertValidImage(result);
@@ -286,7 +283,7 @@ describe("decodeToSharpCompat", () => {
it("decodes CUR using ICO decoder to valid PNG", async () => {
try {
const input = await readFile(join(FIXTURES, "formats/sample.cur"));
const input = readFixture(fixtures.image.formats("cur"));
const result = await decodeToSharpCompat(input, "cur");
expect(isPng(result)).toBe(true);
await assertValidImage(result);
@@ -298,7 +295,7 @@ describe("decodeToSharpCompat", () => {
it("decodes DPX to valid PNG", async () => {
try {
const input = await readFile(join(FIXTURES, "formats/sample.dpx"));
const input = readFixture(fixtures.image.formats("dpx"));
const result = await decodeToSharpCompat(input, "dpx");
expect(isPng(result)).toBe(true);
await assertValidImage(result);
@@ -310,7 +307,7 @@ describe("decodeToSharpCompat", () => {
it("decodes FITS to valid PNG (requires FITS delegate)", async () => {
try {
const input = await readFile(join(FIXTURES, "formats/sample.fits"));
const input = readFixture(fixtures.image.formats("fits"));
const result = await decodeToSharpCompat(input, "fits");
expect(isPng(result)).toBe(true);
await assertValidImage(result);
@@ -324,7 +321,7 @@ describe("decodeToSharpCompat", () => {
it("decodes PPM to valid PNG", async () => {
try {
const input = await readFile(join(FIXTURES, "formats/sample.ppm"));
const input = readFixture(fixtures.image.formats("ppm"));
const result = await decodeToSharpCompat(input, "ppm");
expect(isPng(result)).toBe(true);
await assertValidImage(result);
@@ -336,7 +333,7 @@ describe("decodeToSharpCompat", () => {
it("decodes PGM to valid PNG", async () => {
try {
const input = await readFile(join(FIXTURES, "formats/sample.pgm"));
const input = readFixture(fixtures.image.formats("pgm"));
const result = await decodeToSharpCompat(input, "pgm");
expect(isPng(result)).toBe(true);
await assertValidImage(result);
@@ -348,7 +345,7 @@ describe("decodeToSharpCompat", () => {
it("decodes PBM to valid PNG", async () => {
try {
const input = await readFile(join(FIXTURES, "formats/sample.pbm"));
const input = readFixture(fixtures.image.formats("pbm"));
const result = await decodeToSharpCompat(input, "pbm");
expect(isPng(result)).toBe(true);
await assertValidImage(result);
@@ -365,7 +362,7 @@ describe("decodeToSharpCompat - individual decoder verification", () => {
for (const fmt of formats) {
it(`${fmt}: output has non-zero dimensions`, async () => {
try {
const input = await readFile(join(FIXTURES, `formats/sample.${fmt}`));
const input = readFixture(fixtures.image.formats(fmt));
const result = await decodeToSharpCompat(input, fmt);
const { width, height } = await assertValidImage(result);
expect(width).toBeGreaterThan(0);
@@ -382,7 +379,7 @@ describe("decodeToSharpCompat - individual decoder verification", () => {
for (const fmt of delegateFormats) {
it(`${fmt}: output has non-zero dimensions (requires delegate)`, async () => {
try {
const input = await readFile(join(FIXTURES, `formats/sample.${fmt}`));
const input = readFixture(fixtures.image.formats(fmt));
const result = await decodeToSharpCompat(input, fmt);
const { width, height } = await assertValidImage(result);
expect(width).toBeGreaterThan(0);
@@ -399,14 +396,14 @@ describe("decodeToSharpCompat - individual decoder verification", () => {
describe("decodeToSharpCompat - QOI decoder", () => {
it("decodes QOI fixture to valid PNG", async () => {
const input = await readFile(join(FIXTURES, "formats/sample.qoi"));
const input = readFixture(fixtures.image.formats("qoi"));
const result = await decodeToSharpCompat(input, "qoi");
expect(isPng(result)).toBe(true);
await assertValidImage(result);
});
it("round-trips PNG -> QOI -> PNG", async () => {
const png = await readFile(join(FIXTURES, "formats/sample.png"));
const png = readFixture(fixtures.image.formats("png"));
const qoi = await encodeQoi(png);
const decoded = await decodeToSharpCompat(Buffer.from(qoi), "qoi");
expect(isPng(decoded)).toBe(true);
@@ -417,7 +414,7 @@ describe("decodeToSharpCompat - QOI decoder", () => {
});
it("decoded QOI produces a buffer sharp can process further", async () => {
const input = await readFile(join(FIXTURES, "formats/sample.qoi"));
const input = readFixture(fixtures.image.formats("qoi"));
const decoded = await decodeToSharpCompat(input, "qoi");
const resized = await sharp(decoded).resize(10, 10).png().toBuffer();
expect(resized.length).toBeGreaterThan(0);
+3 -5
View File
@@ -1,5 +1,4 @@
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import sharp from "sharp";
import { describe, expect, it } from "vitest";
import {
@@ -8,8 +7,7 @@ import {
encodeJp2,
encodeQoi,
} from "../../../apps/api/src/lib/format-encoders.js";
const FIXTURES = join(__dirname, "../../fixtures");
import { fixtures, readFixture } from "../../fixtures/index.js";
async function createTestPng(width = 50, height = 50): Promise<Buffer> {
return sharp({
@@ -52,7 +50,7 @@ describe("encodeQoi", () => {
});
it("encodes fixture image to QOI", async () => {
const input = await readFile(join(FIXTURES, "test-200x150.png"));
const input = readFixture(fixtures.image.base.png200);
const result = await encodeQoi(input);
expect(result.length).toBeGreaterThan(100);
});
@@ -170,7 +168,7 @@ describe("encodeJp2", () => {
});
it("produces valid output at different quality levels", async () => {
const input = await readFile(join(FIXTURES, "test-200x150.png"));
const input = readFixture(fixtures.image.base.png200);
try {
const low = await encodeJp2(input, 10);
const high = await encodeJp2(input, 90);
+9 -11
View File
@@ -1,10 +1,8 @@
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import sharp from "sharp";
import { describe, expect, it } from "vitest";
import { decodeHeic, ensureSharpCompat } from "../../../apps/api/src/lib/heic-converter.js";
const FIXTURES = join(__dirname, "../../fixtures");
import { fixtures, readFixture } from "../../fixtures/index.js";
function isPng(buf: Buffer): boolean {
return buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47;
@@ -20,7 +18,7 @@ function makeHeicHeader(brand: string): Buffer {
describe("decodeHeic", () => {
it("decodes sample.heic to a valid PNG buffer", async () => {
const heicBuf = await readFile(join(FIXTURES, "formats/sample.heic"));
const heicBuf = readFixture(fixtures.image.formats("heic"));
const result = await decodeHeic(heicBuf);
expect(isPng(result)).toBe(true);
const meta = await sharp(result).metadata();
@@ -29,7 +27,7 @@ describe("decodeHeic", () => {
});
it("decodes sample.heif to a valid PNG buffer", { timeout: 60_000 }, async () => {
const heifBuf = await readFile(join(FIXTURES, "formats/sample.heif"));
const heifBuf = readFixture(fixtures.image.formats("heif"));
const result = await decodeHeic(heifBuf);
expect(isPng(result)).toBe(true);
const meta = await sharp(result).metadata();
@@ -38,7 +36,7 @@ describe("decodeHeic", () => {
});
it("preserves image dimensions after decoding", async () => {
const heicBuf = await readFile(join(FIXTURES, "formats/sample.heic"));
const heicBuf = readFixture(fixtures.image.formats("heic"));
const result = await decodeHeic(heicBuf);
const meta = await sharp(result).metadata();
expect(meta.width).toBeGreaterThan(0);
@@ -56,7 +54,7 @@ describe("decodeHeic", () => {
});
it("cleans up temp files after success", { timeout: 60_000 }, async () => {
const heicBuf = await readFile(join(FIXTURES, "formats/sample.heic"));
const heicBuf = readFixture(fixtures.image.formats("heic"));
const { tmpdir } = await import("node:os");
const { readdirSync } = await import("node:fs");
const beforeSet = new Set(
@@ -75,25 +73,25 @@ describe("decodeHeic", () => {
describe("ensureSharpCompat", () => {
it("decodes a HEIC buffer to PNG", async () => {
const heicBuf = await readFile(join(FIXTURES, "formats/sample.heic"));
const heicBuf = readFixture(fixtures.image.formats("heic"));
const result = await ensureSharpCompat(heicBuf);
expect(isPng(result)).toBe(true);
});
it("passes through a PNG buffer unchanged", async () => {
const pngBuf = await readFile(join(FIXTURES, "formats/sample.png"));
const pngBuf = readFixture(fixtures.image.formats("png"));
const result = await ensureSharpCompat(pngBuf);
expect(result).toBe(pngBuf);
});
it("passes through a JPEG buffer unchanged", async () => {
const jpgBuf = await readFile(join(FIXTURES, "formats/sample.jpg"));
const jpgBuf = readFixture(fixtures.image.formats("jpg"));
const result = await ensureSharpCompat(jpgBuf);
expect(result).toBe(jpgBuf);
});
it("passes through a WebP buffer unchanged", async () => {
const webpBuf = await readFile(join(FIXTURES, "formats/sample.webp"));
const webpBuf = readFixture(fixtures.image.formats("webp"));
const result = await ensureSharpCompat(webpBuf);
expect(result).toBe(webpBuf);
});
@@ -2,7 +2,7 @@ import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
const TEMPLATES_DIR = join(__dirname, "../../apps/api/static/meme-templates");
const TEMPLATES_DIR = join(__dirname, "../../../apps/api/static/meme-templates");
const MANIFEST_PATH = join(TEMPLATES_DIR, "meme-templates.json");
const VALID_CATEGORIES = ["reaction", "comparison", "opinion", "animals", "classic"];
+4 -6
View File
@@ -1,12 +1,10 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { resolveOutputFormat } from "../../../apps/api/src/lib/output-format.js";
import { fixtures, readFixture } from "../../fixtures/index.js";
const FIXTURES = join(__dirname, "..", "..", "fixtures");
const JPG = readFileSync(join(FIXTURES, "test-100x100.jpg"));
const PNG = readFileSync(join(FIXTURES, "test-200x150.png"));
const WEBP = readFileSync(join(FIXTURES, "test-50x50.webp"));
const JPG = readFixture(fixtures.image.base.jpg100);
const PNG = readFixture(fixtures.image.base.png200);
const WEBP = readFixture(fixtures.image.base.webp50);
describe("resolveOutputFormat", () => {
it("detects JPEG input and returns jpeg config", async () => {
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { parsePageRange } from "../../apps/api/src/routes/tools/pdf-to-image.js";
import { parsePageRange } from "../../../apps/api/src/routes/tools/pdf-to-image.js";
describe("parsePageRange", () => {
it("returns all pages for 'all'", () => {
+20 -22
View File
@@ -4,8 +4,7 @@ import { mkdir, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { basename, join } from "node:path";
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const FIXTURES = join(__dirname, "../../fixtures");
import { fixtures, readFixture } from "../../fixtures/index.js";
// ---------------------------------------------------------------------------
// 1. File validation
@@ -32,7 +31,7 @@ describe("validateImageBuffer", () => {
// -- Valid formats --------------------------------------------------------
it("accepts a valid PNG file", async () => {
const buf = await readFile(join(FIXTURES, "test-200x150.png"));
const buf = readFixture(fixtures.image.base.png200);
const result = await validateImageBuffer(buf);
expect(result.valid).toBe(true);
if (result.valid) {
@@ -43,7 +42,7 @@ describe("validateImageBuffer", () => {
});
it("accepts a valid JPEG file", async () => {
const buf = await readFile(join(FIXTURES, "test-100x100.jpg"));
const buf = readFixture(fixtures.image.base.jpg100);
const result = await validateImageBuffer(buf);
expect(result.valid).toBe(true);
if (result.valid) {
@@ -54,7 +53,7 @@ describe("validateImageBuffer", () => {
});
it("accepts a valid WebP file", async () => {
const buf = await readFile(join(FIXTURES, "test-50x50.webp"));
const buf = readFixture(fixtures.image.base.webp50);
const result = await validateImageBuffer(buf);
expect(result.valid).toBe(true);
if (result.valid) {
@@ -65,7 +64,7 @@ describe("validateImageBuffer", () => {
});
it("accepts a tiny 1x1 PNG file", async () => {
const buf = await readFile(join(FIXTURES, "test-1x1.png"));
const buf = readFixture(fixtures.image.edge.px1);
const result = await validateImageBuffer(buf);
expect(result.valid).toBe(true);
if (result.valid) {
@@ -130,7 +129,7 @@ describe("validateImageBuffer", () => {
});
it("accepts a HEIC file with correct magic bytes", async () => {
const heicBuf = await readFile(join(FIXTURES, "test-200x150.heic"));
const heicBuf = readFixture(fixtures.image.base.heic200);
const result = await validateImageBuffer(heicBuf);
expect(result.valid).toBe(true);
if (result.valid) {
@@ -323,7 +322,7 @@ describe("validateImageBuffer", () => {
origMod.env.MAX_MEGAPIXELS = 0.0001; // 100 pixels -- 200x150 = 30000px >> 100
try {
const buf = await readFile(join(FIXTURES, "test-200x150.png"));
const buf = readFixture(fixtures.image.base.png200);
const result = await validateImageBuffer(buf);
expect(result.valid).toBe(false);
if (!result.valid) {
@@ -572,73 +571,72 @@ describe("validateImageBuffer", () => {
describe("validates exotic formats", () => {
const { readFileSync } = require("node:fs");
const FORMATS_DIR = join(FIXTURES, "formats");
it("accepts PBM file", async () => {
const buf = readFileSync(join(FORMATS_DIR, "sample.pbm"));
const buf = readFixture(fixtures.image.formats("pbm"));
const result = await validateImageBuffer(buf, "sample.pbm");
expect(result.valid).toBe(true);
if (result.valid) expect(result.format).toBe("pbm");
});
it("accepts PGM file", async () => {
const buf = readFileSync(join(FORMATS_DIR, "sample.pgm"));
const buf = readFixture(fixtures.image.formats("pgm"));
const result = await validateImageBuffer(buf, "sample.pgm");
expect(result.valid).toBe(true);
if (result.valid) expect(result.format).toBe("pgm");
});
it("accepts PPM file", async () => {
const buf = readFileSync(join(FORMATS_DIR, "sample.ppm"));
const buf = readFixture(fixtures.image.formats("ppm"));
const result = await validateImageBuffer(buf, "sample.ppm");
expect(result.valid).toBe(true);
if (result.valid) expect(result.format).toBe("ppm");
});
it("accepts DDS file", async () => {
const buf = readFileSync(join(FORMATS_DIR, "sample.dds"));
const buf = readFixture(fixtures.image.formats("dds"));
const result = await validateImageBuffer(buf, "sample.dds");
expect(result.valid).toBe(true);
if (result.valid) expect(result.format).toBe("dds");
});
it("accepts DPX file", async () => {
const buf = readFileSync(join(FORMATS_DIR, "sample.dpx"));
const buf = readFixture(fixtures.image.formats("dpx"));
const result = await validateImageBuffer(buf, "sample.dpx");
expect(result.valid).toBe(true);
if (result.valid) expect(result.format).toBe("dpx");
});
it("accepts FITS file", async () => {
const buf = readFileSync(join(FORMATS_DIR, "sample.fits"));
const buf = readFixture(fixtures.image.formats("fits"));
const result = await validateImageBuffer(buf, "sample.fits");
expect(result.valid).toBe(true);
if (result.valid) expect(result.format).toBe("fits");
});
it("accepts JP2 file", async () => {
const buf = readFileSync(join(FORMATS_DIR, "sample.jp2"));
const buf = readFixture(fixtures.image.formats("jp2"));
const result = await validateImageBuffer(buf, "sample.jp2");
expect(result.valid).toBe(true);
if (result.valid) expect(result.format).toBe("jp2");
});
it("accepts QOI file", async () => {
const buf = readFileSync(join(FORMATS_DIR, "sample.qoi"));
const buf = readFixture(fixtures.image.formats("qoi"));
const result = await validateImageBuffer(buf, "sample.qoi");
expect(result.valid).toBe(true);
if (result.valid) expect(result.format).toBe("qoi");
});
it("accepts SVGZ file (detected as svg)", async () => {
const buf = readFileSync(join(FORMATS_DIR, "sample.svgz"));
const buf = readFixture(fixtures.image.formats("svgz"));
const result = await validateImageBuffer(buf, "sample.svgz");
expect(result.valid).toBe(true);
if (result.valid) expect(result.format).toBe("svg");
});
it("accepts EPS file", async () => {
const buf = readFileSync(join(FORMATS_DIR, "sample.eps"));
const buf = readFixture(fixtures.image.formats("eps"));
const result = await validateImageBuffer(buf, "sample.eps");
expect(result.valid).toBe(true);
if (result.valid) expect(result.format).toBe("eps");
@@ -659,21 +657,21 @@ describe("validateImageBuffer", () => {
});
it("accepts HEIF file", async () => {
const buf = readFileSync(join(FORMATS_DIR, "sample.heif"));
const buf = readFixture(fixtures.image.formats("heif"));
const result = await validateImageBuffer(buf, "sample.heif");
expect(result.valid).toBe(true);
if (result.valid) expect(result.format).toBe("heif");
});
it("accepts APNG file (detected as png)", async () => {
const buf = readFileSync(join(FORMATS_DIR, "sample.apng"));
const buf = readFixture(fixtures.image.formats("apng"));
const result = await validateImageBuffer(buf, "sample.apng");
expect(result.valid).toBe(true);
if (result.valid) expect(result.format).toBe("png");
});
it("accepts DNG file (detected as raw)", async () => {
const buf = readFileSync(join(FORMATS_DIR, "sample.dng"));
const buf = readFixture(fixtures.image.formats("dng"));
const result = await validateImageBuffer(buf, "sample.dng");
expect(result.valid).toBe(true);
if (result.valid) {
@@ -0,0 +1,43 @@
import { readdirSync, statSync } from "node:fs";
import { dirname, extname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "../../fixtures");
// Per-extension MB ceilings. RAW/codec containers get headroom; everything else is tight.
const RAW = new Set([".arw", ".nef", ".rw2", ".orf", ".cr2", ".dng", ".exr", ".psd"]);
function capMb(file: string): number {
const ext = extname(file).toLowerCase();
if (RAW.has(ext)) return 24;
if ([".mp4", ".mov", ".webm", ".mkv", ".avi"].includes(ext)) return 8;
if ([".jpg", ".jpeg", ".png", ".webp", ".heic", ".heif", ".avif", ".gif"].includes(ext)) return 8;
if ([".wav", ".mp3", ".flac", ".ogg", ".m4a", ".aac", ".opus"].includes(ext)) return 4;
if ([".pdf", ".docx", ".xlsx", ".pptx", ".epub", ".odt"].includes(ext)) return 4;
return 1;
}
function walk(dir: string): string[] {
return readdirSync(dir, { withFileTypes: true }).flatMap((e) => {
const full = join(dir, e.name);
if (e.isDirectory()) return walk(full);
if (
e.name === "index.ts" ||
e.name === ".gitkeep" ||
e.name.endsWith(".md") ||
e.name.endsWith(".json") ||
e.name.endsWith(".mjs")
)
return [];
return [full];
});
}
describe("fixture size budget", () => {
const files = walk(ROOT);
it("finds fixtures", () => expect(files.length).toBeGreaterThan(100));
it.each(files)("within budget: %s", (f) => {
const mb = statSync(f).size / (1024 * 1024);
expect(mb, `${f} is ${mb.toFixed(1)}MB > ${capMb(f)}MB cap`).toBeLessThanOrEqual(capMb(f));
});
});
@@ -0,0 +1,40 @@
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "../../fixtures");
const manifest = JSON.parse(readFileSync(join(ROOT, "manifest.json"), "utf8"));
describe("fixture manifest is consistent with disk", () => {
it("has entries", () => expect(manifest.assets.length).toBeGreaterThan(20));
// Phase 1 hard-checks bytes + sha256; license may be "UNVERIFIED" (tracked in
// LICENSES.md). Phase 2 does the real provenance audit and removes that tolerance.
const ALLOWED = ["CC0", "CC-BY", "CC-BY-SA", "public-domain", "UNVERIFIED", "UNVERIFIED-REVIEW"];
it.each(manifest.assets)("$path matches sha256 + bytes and declares a license", (asset: {
path: string;
bytes: number;
sha256: string;
license: string;
}) => {
const buf = readFileSync(join(ROOT, asset.path));
expect(buf.length, `${asset.path} byte mismatch`).toBe(asset.bytes);
expect(createHash("sha256").update(buf).digest("hex"), `${asset.path} sha256 mismatch`).toBe(
asset.sha256,
);
expect(ALLOWED, `${asset.path} license '${asset.license}' not allowed`).toContain(
asset.license,
);
});
it("reports how many assets still need provenance (Phase 2 drives this to 0)", () => {
const unverified = manifest.assets
.filter((a: { license: string; path: string }) => a.license === "UNVERIFIED")
.map((a: { path: string }) => a.path);
if (unverified.length)
console.warn(`UNVERIFIED provenance (verify/replace in Phase 2): ${unverified.join(", ")}`);
expect(unverified.length).toBeLessThanOrEqual(manifest.assets.length); // informational; never fails Phase 1
});
});
@@ -0,0 +1,20 @@
import { existsSync, statSync } from "node:fs";
import { describe, expect, it } from "vitest";
import { fixtures, flattenFixturePaths, readFixture } from "../../fixtures/index.js";
describe("fixture registry resolves", () => {
const paths = flattenFixturePaths(fixtures);
it("registers at least the known core fixtures", () => {
expect(paths.length).toBeGreaterThan(50); // ~71 expected; floor is a sanity check
});
it.each(paths)("exists and is a non-empty file: %s", (p) => {
expect(existsSync(p), `missing fixture: ${p}`).toBe(true);
expect(statSync(p).size, `zero-byte fixture: ${p}`).toBeGreaterThan(0);
});
it("readFixture returns bytes for the workhorse PNG", () => {
expect(readFixture(fixtures.image.base.png200).byteLength).toBeGreaterThan(0);
});
});
@@ -1,11 +1,9 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { analyzeImage, applyCorrections, scaleCorrections } from "@snapotter/image-engine";
import sharp from "sharp";
import { describe, expect, it } from "vitest";
import { fixtures, readFixture } from "../../fixtures/index.js";
const FIXTURES = join(__dirname, "..", "fixtures");
const PNG_200x150 = readFileSync(join(FIXTURES, "test-200x150.png"));
const PNG_200x150 = readFixture(fixtures.image.base.png200);
describe("analyzeImage", () => {
it("returns scores, corrections, issues, and suggestedMode", async () => {
+15 -16
View File
@@ -2,8 +2,7 @@ import { readFileSync } from "node:fs";
import path from "node:path";
import { detectFormat } from "@snapotter/image-engine";
import { describe, expect, it } from "vitest";
const FORMATS_DIR = path.resolve(__dirname, "../../fixtures/formats");
import { fixtureDir, fixtures, readFixture } from "../../fixtures/index.js";
// ---------------------------------------------------------------------------
// Format detection via Sharp metadata + magic byte fallback
@@ -22,7 +21,7 @@ describe("detectFormat", () => {
for (const { file, expected } of sharpNativeFormats) {
it(`detects ${file} as "${expected}" via Sharp metadata`, async () => {
const buffer = readFileSync(path.join(FORMATS_DIR, file));
const buffer = readFileSync(path.join(fixtureDir.formats, file));
const format = await detectFormat(buffer);
expect(format).toBe(expected);
});
@@ -38,7 +37,7 @@ describe("detectFormat", () => {
for (const { file, expected } of magicByteFormats) {
it(`detects ${file} as "${expected}" via magic bytes`, async () => {
const buffer = readFileSync(path.join(FORMATS_DIR, file));
const buffer = readFileSync(path.join(fixtureDir.formats, file));
const format = await detectFormat(buffer);
expect(format).toBe(expected);
});
@@ -46,21 +45,21 @@ describe("detectFormat", () => {
// HEIC/HEIF - Sharp may or may not handle these depending on libheif
it("detects sample.heic via Sharp or magic bytes", async () => {
const buffer = readFileSync(path.join(FORMATS_DIR, "sample.heic"));
const buffer = readFixture(fixtures.image.formats("heic"));
const format = await detectFormat(buffer);
// Sharp may report "heif" or magic bytes may detect "avif" (ftyp box)
expect(["heif", "avif"]).toContain(format);
});
it("detects sample.heif via Sharp or magic bytes", async () => {
const buffer = readFileSync(path.join(FORMATS_DIR, "sample.heif"));
const buffer = readFixture(fixtures.image.formats("heif"));
const format = await detectFormat(buffer);
expect(["heif", "avif"]).toContain(format);
});
// JXL detection
it("detects sample.jxl", async () => {
const buffer = readFileSync(path.join(FORMATS_DIR, "sample.jxl"));
const buffer = readFixture(fixtures.image.formats("jxl"));
const format = await detectFormat(buffer);
// Sharp may detect "jxl" natively or magic bytes catch it
expect(["jxl", "unknown"]).toContain(format);
@@ -301,45 +300,45 @@ describe("detectFormat", () => {
describe("exotic format detection from fixtures", () => {
it("detects sample.ppm as ppm (P6 magic bytes)", async () => {
const buffer = readFileSync(path.join(FORMATS_DIR, "sample.ppm"));
const buffer = readFixture(fixtures.image.formats("ppm"));
expect(await detectFormat(buffer)).toBe("ppm");
});
it("detects sample.dng as tiff (DNG shares TIFF magic bytes)", async () => {
const buffer = readFileSync(path.join(FORMATS_DIR, "sample.dng"));
const buffer = readFixture(fixtures.image.formats("dng"));
expect(await detectFormat(buffer)).toBe("tiff");
});
it("detects sample.jp2 as jp2 (JPEG 2000 box signature)", async () => {
const buffer = readFileSync(path.join(FORMATS_DIR, "sample.jp2"));
const buffer = readFixture(fixtures.image.formats("jp2"));
expect(await detectFormat(buffer)).toBe("jp2");
});
it("detects sample.svgz as svg (Sharp reads gzip-compressed SVG)", async () => {
const buffer = readFileSync(path.join(FORMATS_DIR, "sample.svgz"));
const buffer = readFixture(fixtures.image.formats("svgz"));
expect(await detectFormat(buffer)).toBe("svg");
});
// PBM (P4) and PGM (P5) have no magic byte entries and Sharp cannot parse them
it("cannot detect sample.pbm (no P4 magic bytes registered)", async () => {
const buffer = readFileSync(path.join(FORMATS_DIR, "sample.pbm"));
const buffer = readFixture(fixtures.image.formats("pbm"));
expect(await detectFormat(buffer)).toBe("unknown");
});
it("cannot detect sample.pgm (no P5 magic bytes registered)", async () => {
const buffer = readFileSync(path.join(FORMATS_DIR, "sample.pgm"));
const buffer = readFixture(fixtures.image.formats("pgm"));
expect(await detectFormat(buffer)).toBe("unknown");
});
// HDR has a text header (#?RGBE) not matched by magic byte table
it("cannot detect sample.hdr (Radiance text header not in magic bytes)", async () => {
const buffer = readFileSync(path.join(FORMATS_DIR, "sample.hdr"));
const buffer = readFixture(fixtures.image.formats("hdr"));
expect(await detectFormat(buffer)).toBe("unknown");
});
// TGA has no reliable magic bytes; its header can collide with other formats
it("does not reliably detect sample.tga (no TGA-specific magic bytes)", async () => {
const buffer = readFileSync(path.join(FORMATS_DIR, "sample.tga"));
const buffer = readFixture(fixtures.image.formats("tga"));
// TGA detection is extension-based in validateImageBuffer, not in detectFormat
expect(await detectFormat(buffer)).not.toBe("tga");
});
@@ -358,7 +357,7 @@ describe("detectFormat", () => {
for (const { file, expected } of fixtures) {
it(`detects ${file}`, async () => {
const buffer = readFileSync(path.join(FORMATS_DIR, file));
const buffer = readFileSync(path.join(fixtureDir.formats, file));
const format = await detectFormat(buffer);
expect(expected).toContain(format);
});
@@ -1,4 +1,3 @@
import { readFileSync } from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";
import { beforeAll, describe, expect, it } from "vitest";
@@ -12,15 +11,14 @@ const exifReader = require(
) as typeof import("exif-reader").default;
import { editMetadata } from "@snapotter/image-engine";
const FIXTURES_DIR = path.resolve(__dirname, "../../fixtures");
import { fixtures, readFixture } from "../../fixtures/index.js";
let jpgWithExif: Buffer;
let png200x150: Buffer;
beforeAll(() => {
jpgWithExif = readFileSync(path.join(FIXTURES_DIR, "test-with-exif.jpg"));
png200x150 = readFileSync(path.join(FIXTURES_DIR, "test-200x150.png"));
jpgWithExif = readFixture(fixtures.image.exifGps);
png200x150 = readFixture(fixtures.image.base.png200);
});
async function getExif(img: sharp.Sharp) {
@@ -7,7 +7,7 @@
* - Queued operation ordering
* - processImage with ico/jp2/qoi/bmp/heic in OutputFormat (unsupported check)
*/
import { readFileSync } from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";
import { beforeAll, describe, expect, it } from "vitest";
@@ -18,15 +18,14 @@ const require = createRequire(
const _sharp = require("sharp") as typeof import("sharp").default;
import { processImage } from "@snapotter/image-engine";
const FIXTURES_DIR = path.resolve(__dirname, "../../fixtures");
import { fixtures, readFixture } from "../../fixtures/index.js";
let png200x150: Buffer;
let jpg100x100: Buffer;
beforeAll(() => {
png200x150 = readFileSync(path.join(FIXTURES_DIR, "test-200x150.png"));
jpg100x100 = readFileSync(path.join(FIXTURES_DIR, "test-100x100.jpg"));
png200x150 = readFixture(fixtures.image.base.png200);
jpg100x100 = readFixture(fixtures.image.base.jpg100);
});
describe("processImage FORMAT_MAP coverage", () => {
+8 -10
View File
@@ -7,7 +7,7 @@
* - Edge cases around operation ordering
* - The Operation type contract
*/
import { readFileSync } from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";
import { beforeAll, describe, expect, it } from "vitest";
@@ -18,18 +18,16 @@ const require = createRequire(
const _sharp = require("sharp") as typeof import("sharp").default;
import { processImage } from "@snapotter/image-engine";
const FIXTURES_DIR = path.resolve(__dirname, "../../fixtures");
const FORMATS_DIR = path.resolve(__dirname, "../../fixtures/formats");
import { fixtures, readFixture } from "../../fixtures/index.js";
let png200x150: Buffer;
let jpg100x100: Buffer;
let webp50x50: Buffer;
beforeAll(() => {
png200x150 = readFileSync(path.join(FIXTURES_DIR, "test-200x150.png"));
jpg100x100 = readFileSync(path.join(FIXTURES_DIR, "test-100x100.jpg"));
webp50x50 = readFileSync(path.join(FIXTURES_DIR, "test-50x50.webp"));
png200x150 = readFixture(fixtures.image.base.png200);
jpg100x100 = readFixture(fixtures.image.base.jpg100);
webp50x50 = readFixture(fixtures.image.base.webp50);
});
// ---------------------------------------------------------------------------
@@ -213,21 +211,21 @@ describe("processImage with different input formats", () => {
});
it("processes AVIF input from fixture", async () => {
const avif = readFileSync(path.join(FORMATS_DIR, "sample.avif"));
const avif = readFixture(fixtures.image.formats("avif"));
const result = await processImage(avif, [{ type: "resize", options: { width: 40 } }]);
expect(result.info.width).toBe(40);
expect(result.buffer.length).toBeGreaterThan(0);
});
it("processes GIF input from fixture", async () => {
const gif = readFileSync(path.join(FORMATS_DIR, "sample.gif"));
const gif = readFixture(fixtures.image.formats("gif"));
const result = await processImage(gif, [{ type: "resize", options: { width: 30 } }]);
expect(result.info.width).toBe(30);
expect(result.buffer.length).toBeGreaterThan(0);
});
it("processes TIFF input from fixture", async () => {
const tiff = readFileSync(path.join(FORMATS_DIR, "sample.tiff"));
const tiff = readFixture(fixtures.image.formats("tiff"));
const result = await processImage(tiff, [{ type: "resize", options: { width: 30 } }]);
expect(result.info.width).toBe(30);
expect(result.buffer.length).toBeGreaterThan(0);
+5 -7
View File
@@ -1,4 +1,3 @@
import { readFileSync } from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";
import { beforeAll, describe, expect, it } from "vitest";
@@ -16,8 +15,7 @@ import {
parseXmp,
sanitizeValue,
} from "@snapotter/image-engine";
const FIXTURES_DIR = path.resolve(__dirname, "../../fixtures");
import { fixtures, readFixture } from "../../fixtures/index.js";
let png200x150: Buffer;
let jpg100x100: Buffer;
@@ -25,10 +23,10 @@ let webp50x50: Buffer;
let jpgWithExif: Buffer;
beforeAll(() => {
png200x150 = readFileSync(path.join(FIXTURES_DIR, "test-200x150.png"));
jpg100x100 = readFileSync(path.join(FIXTURES_DIR, "test-100x100.jpg"));
webp50x50 = readFileSync(path.join(FIXTURES_DIR, "test-50x50.webp"));
jpgWithExif = readFileSync(path.join(FIXTURES_DIR, "test-with-exif.jpg"));
png200x150 = readFixture(fixtures.image.base.png200);
jpg100x100 = readFixture(fixtures.image.base.jpg100);
webp50x50 = readFixture(fixtures.image.base.webp50);
jpgWithExif = readFixture(fixtures.image.exifGps);
});
// ---------------------------------------------------------------------------
+8 -10
View File
@@ -1,4 +1,3 @@
import { readFileSync } from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";
import { beforeAll, describe, expect, it } from "vitest";
@@ -37,8 +36,7 @@ import {
sharpenAdvanced,
stripMetadata,
} from "@snapotter/image-engine";
const FIXTURES_DIR = path.resolve(__dirname, "../../fixtures");
import { fixtures, readFixture } from "../../fixtures/index.js";
// Helper to get metadata from a sharp pipeline result
async function getMeta(img: sharp.Sharp) {
@@ -58,11 +56,11 @@ let webp50x50: Buffer;
let jpgWithExif: Buffer;
beforeAll(() => {
png200x150 = readFileSync(path.join(FIXTURES_DIR, "test-200x150.png"));
png1x1 = readFileSync(path.join(FIXTURES_DIR, "test-1x1.png"));
jpg100x100 = readFileSync(path.join(FIXTURES_DIR, "test-100x100.jpg"));
webp50x50 = readFileSync(path.join(FIXTURES_DIR, "test-50x50.webp"));
jpgWithExif = readFileSync(path.join(FIXTURES_DIR, "test-with-exif.jpg"));
png200x150 = readFixture(fixtures.image.base.png200);
png1x1 = readFixture(fixtures.image.edge.px1);
jpg100x100 = readFixture(fixtures.image.base.jpg100);
webp50x50 = readFixture(fixtures.image.base.webp50);
jpgWithExif = readFixture(fixtures.image.exifGps);
});
// ---------------------------------------------------------------------------
@@ -736,7 +734,7 @@ describe("compress", () => {
});
it("falls back to PNG for SVG input (NO_ENCODER format)", async () => {
const svgBuf = readFileSync(path.join(FIXTURES_DIR, "formats/sample.svg"));
const svgBuf = readFixture(fixtures.image.formats("svg"));
const img = sharp(svgBuf);
const result = await compress(img, { quality: 80 });
const meta = await getMeta(result);
@@ -2254,7 +2252,7 @@ describe("optimizeForWeb", () => {
// -- Quality impact --
it("lower quality produces smaller file for webp", async () => {
const buf = readFileSync(path.join(FIXTURES_DIR, "test-100x100.jpg"));
const buf = readFixture(fixtures.image.base.jpg100);
const bufHigh = await (
await optimizeForWeb(sharp(buf), { format: "webp", quality: 95 })
).toBuffer();
+3 -5
View File
@@ -1,9 +1,7 @@
import { readFileSync } from "node:fs";
import path from "node:path";
import { qoiDecode, qoiEncode } from "@snapotter/image-engine";
import { describe, expect, it } from "vitest";
const FORMATS_DIR = path.resolve(__dirname, "../../fixtures/formats");
import { fixtures, readFixture } from "../../fixtures/index.js";
describe("QOI codec", () => {
it("round-trips RGBA pixel data", () => {
@@ -157,7 +155,7 @@ describe("QOI codec", () => {
});
it("decodes real fixture with correct header", () => {
const data = readFileSync(path.join(FORMATS_DIR, "sample.qoi"));
const data = readFixture(fixtures.image.formats("qoi"));
const { header } = qoiDecode(new Uint8Array(data));
expect(header.width).toBe(10);
expect(header.height).toBe(10);
@@ -165,7 +163,7 @@ describe("QOI codec", () => {
});
it("re-encodes real fixture with matching pixels", () => {
const data = readFileSync(path.join(FORMATS_DIR, "sample.qoi"));
const data = readFixture(fixtures.image.formats("qoi"));
const { header, pixels } = qoiDecode(new Uint8Array(data));
const reEncoded = qoiEncode(pixels, header.width, header.height, 4);
const { pixels: reDecoded } = qoiDecode(reEncoded);
+3 -5
View File
@@ -1,4 +1,3 @@
import { readFileSync } from "node:fs";
import { createRequire } from "node:module";
import path from "node:path";
import { beforeAll, describe, expect, it } from "vitest";
@@ -9,13 +8,12 @@ const require = createRequire(
const sharp = require("sharp") as typeof import("sharp").default;
import { saturation } from "@snapotter/image-engine";
const FIXTURES_DIR = path.resolve(__dirname, "../../fixtures");
import { fixtures, readFixture } from "../../fixtures/index.js";
let png200x150: Buffer;
beforeAll(() => {
png200x150 = readFileSync(path.join(FIXTURES_DIR, "test-200x150.png"));
png200x150 = readFixture(fixtures.image.base.png200);
});
async function getMeta(img: sharp.Sharp) {
@@ -97,7 +95,7 @@ describe("saturation", () => {
});
it("works with JPEG input", async () => {
const jpg = readFileSync(path.join(FIXTURES_DIR, "test-100x100.jpg"));
const jpg = readFixture(fixtures.image.base.jpg100);
const img = sharp(jpg);
const result = await saturation(img, { value: 25 });
const meta = await getMeta(result);
@@ -8,20 +8,20 @@
*/
import { describe, expect, it, vi } from "vitest";
vi.mock("../../apps/api/src/db/index.js", () => ({
vi.mock("../../../apps/api/src/db/index.js", () => ({
db: {},
pool: {},
closeDb: async () => {},
schema: {},
}));
import { loadEnv } from "../../apps/api/src/lib/env.js";
import { loadEnv } from "../../../apps/api/src/lib/env.js";
import {
changePasswordSchema,
loginSchema,
registerSchema,
resetPasswordSchema,
} from "../../apps/api/src/plugins/auth.js";
} from "../../../apps/api/src/plugins/auth.js";
// ── Env defaults ─────────────────────────────────────────────────────────────
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { stripInternalPaths } from "../../apps/api/src/lib/errors.js";
import { stripInternalPaths } from "../../../apps/api/src/lib/errors.js";
describe("stripInternalPaths", () => {
it("removes /tmp paths from error messages", () => {
@@ -1,10 +1,10 @@
import { describe, expect, it } from "vitest";
import { stripInternalPaths } from "../../apps/api/src/lib/errors.js";
import { stripInternalPaths } from "../../../apps/api/src/lib/errors.js";
import {
buildTagArgs,
sanitizeTagValue,
validateTagName,
} from "../../apps/api/src/lib/exiftool.js";
} from "../../../apps/api/src/lib/exiftool.js";
describe("ExifTool security: tag value validation", () => {
it("rejects tag values exceeding 10,000 characters", () => {
@@ -8,8 +8,8 @@
* - CSP directives are present and correctly configured
*/
import { describe, expect, it } from "vitest";
import { buildCsp } from "../../apps/api/src/lib/csp.js";
import { validateFetchUrl } from "../../apps/api/src/lib/ssrf.js";
import { buildCsp } from "../../../apps/api/src/lib/csp.js";
import { validateFetchUrl } from "../../../apps/api/src/lib/ssrf.js";
describe("SSRF: blocks private IPv4 addresses", () => {
it("blocks 127.0.0.1 (loopback)", async () => {
@@ -5,7 +5,7 @@
* obfuscation, animation injection, and filter-based SSRF.
*/
import { describe, expect, it } from "vitest";
import { sanitizeSvg } from "../../apps/api/src/lib/svg-sanitize.js";
import { sanitizeSvg } from "../../../apps/api/src/lib/svg-sanitize.js";
/** Wrap a payload fragment inside a minimal valid SVG. */
function wrapSvg(inner: string, attrs = ""): string {
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { pairwise } from "../helpers/pairwise.js";
import { pairwise } from "../../helpers/pairwise.js";
describe("pairwise covering-array generator", () => {
it("covers every pair of values across all axis pairs", () => {
+105
View File
@@ -0,0 +1,105 @@
/**
* i18n cross-locale key parity guard.
*
* Asserts that every locale in SUPPORTED_LOCALES has exactly the same key set
* as en.ts (the reference locale). Missing or extra keys in any locale fail
* the test.
*
* Runtime behavior: loadTranslations() falls back to `en` when a locale file
* fails to load or the named export is not found (no crash). So missing keys
* do not crash the app, but they cause untranslated English text to appear for
* users of that locale. This guard catches that drift at PR time.
*
* Real bug found and fixed: zh-CN and pt-BR exported only a camelCase named
* export (e.g. `zhCN`) with no `export default`. loadTranslations looks up
* `mod[locale]` (e.g. `mod["zh-CN"]`), which fails for dashed locale codes.
* Without a default export fallback, these two locales silently returned
* English. Fixed by adding `export default` to both files.
*/
import { en, loadTranslations, SUPPORTED_LOCALES } from "@snapotter/shared";
import { describe, expect, it } from "vitest";
/** Recursively collect all dot-separated key paths from an object tree. */
function getKeyPaths(obj: Record<string, unknown>, prefix = ""): string[] {
const keys: string[] = [];
for (const [k, v] of Object.entries(obj)) {
const path = prefix ? `${prefix}.${k}` : k;
if (v && typeof v === "object" && !Array.isArray(v)) {
keys.push(...getKeyPaths(v as Record<string, unknown>, path));
} else {
keys.push(path);
}
}
return keys;
}
/** Recursively collect dot-separated key paths for array-valued leaves too. */
function getStructuralKeys(obj: Record<string, unknown>, prefix = ""): string[] {
const keys: string[] = [];
for (const [k, v] of Object.entries(obj)) {
const path = prefix ? `${prefix}.${k}` : k;
if (v && typeof v === "object" && !Array.isArray(v)) {
keys.push(...getStructuralKeys(v as Record<string, unknown>, path));
} else {
// Leaf: string, number, array, etc.
keys.push(path);
}
}
return keys;
}
describe("i18n cross-locale parity", () => {
const enKeys = new Set(getStructuralKeys(en as unknown as Record<string, unknown>));
it("en reference locale has keys", () => {
expect(enKeys.size).toBeGreaterThan(100);
});
it.each(
SUPPORTED_LOCALES.filter((l) => l.code !== "en").map((l) => [l.code, l.name]),
)("%s (%s) has the same key set as en", async (code) => {
const translations = await loadTranslations(code);
// If loadTranslations fell back to en, we get en back. That is a real
// problem (the locale file failed to load). Detect this by checking
// whether the returned object is literally the en reference.
expect(
translations !== en || code === "en",
`locale "${code}" fell back to English -- check the file exports a default or named export matching the locale code`,
).toBe(true);
const localeKeys = new Set(
getStructuralKeys(translations as unknown as Record<string, unknown>),
);
const missing = [...enKeys].filter((k) => !localeKeys.has(k));
const extra = [...localeKeys].filter((k) => !enKeys.has(k));
expect(
missing,
`locale "${code}" is missing ${missing.length} keys from en:\n ${missing.slice(0, 20).join("\n ")}${missing.length > 20 ? `\n ... and ${missing.length - 20} more` : ""}`,
).toEqual([]);
expect(
extra,
`locale "${code}" has ${extra.length} extra keys not in en:\n ${extra.slice(0, 20).join("\n ")}${extra.length > 20 ? `\n ... and ${extra.length - 20} more` : ""}`,
).toEqual([]);
});
it("all dashed locales load their own translations (not en fallback)", async () => {
const dashedLocales = SUPPORTED_LOCALES.filter((l) => l.code.includes("-"));
for (const locale of dashedLocales) {
const translations = await loadTranslations(locale.code);
expect(
translations !== en,
`locale "${locale.code}" (${locale.name}) fell back to English -- this means ${locale.nativeName} users see untranslated UI`,
).toBe(true);
}
});
it("loadTranslations returns en for unknown locale (graceful fallback)", async () => {
const result = await loadTranslations("xx-FAKE");
expect(result).toBe(en);
});
});
+93
View File
@@ -0,0 +1,93 @@
// @vitest-environment jsdom
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";
import { afterEach, describe, expect, it, vi } from "vitest";
import { MobileBottomNav } from "@/components/layout/mobile-bottom-nav";
afterEach(() => {
cleanup();
vi.restoreAllMocks();
});
function renderNav(onSettingsClick?: () => void) {
return render(
<MemoryRouter>
<MobileBottomNav onSettingsClick={onSettingsClick} />
</MemoryRouter>,
);
}
describe("MobileBottomNav", () => {
it("renders all navigation items", () => {
renderNav(() => {});
// Navigation links (i18n keys render their English values)
expect(screen.getByText("Tools")).toBeDefined();
expect(screen.getByText("Automate")).toBeDefined();
expect(screen.getByText("Editor")).toBeDefined();
expect(screen.getByText("Files")).toBeDefined();
expect(screen.getByText("Settings")).toBeDefined();
});
it("renders as a nav element with fixed positioning", () => {
renderNav(() => {});
const nav = document.querySelector("nav");
expect(nav).not.toBeNull();
expect(nav!.className).toContain("fixed");
expect(nav!.className).toContain("bottom-0");
});
it("navigation links have correct href targets", () => {
renderNav(() => {});
const links = document.querySelectorAll("a");
const hrefs = Array.from(links).map((a) => a.getAttribute("href"));
expect(hrefs).toContain("/");
expect(hrefs).toContain("/automate");
expect(hrefs).toContain("/editor");
expect(hrefs).toContain("/files");
});
it("settings button calls onSettingsClick when clicked", () => {
const onClick = vi.fn();
renderNav(onClick);
const settingsBtn = screen.getByText("Settings");
fireEvent.click(settingsBtn);
expect(onClick).toHaveBeenCalledTimes(1);
});
it("settings button is hidden when onSettingsClick is not provided", () => {
renderNav();
// Without onSettingsClick, the settings button should not render
expect(screen.queryByText("Settings")).toBeNull();
});
it("renders icons for each navigation item", () => {
renderNav(() => {});
// 4 items use lucide SVG icons (Tools, Automate, Files, Settings)
// Editor uses ImageEditIcon which is a CSS-masked <span>, not SVG
const svgs = document.querySelectorAll("nav svg");
expect(svgs.length).toBe(4);
// The Editor icon is a span with a mask-image
const spans = document.querySelectorAll("nav span");
const maskedSpan = Array.from(spans).find((s) => (s as HTMLElement).style.maskImage !== "");
expect(maskedSpan).toBeDefined();
});
it("nav has backdrop blur and border-top styling", () => {
renderNav(() => {});
const nav = document.querySelector("nav") as HTMLElement;
expect(nav).not.toBeNull();
// The component applies bg-background/95 backdrop-blur-sm border-t
expect(nav.className).toContain("backdrop-blur");
expect(nav.className).toContain("border-t");
expect(nav.className).toContain("z-30");
});
});
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { getSettingsSummary } from "../../apps/web/src/components/tools/pipeline-step-summary";
import { getSettingsSummary } from "../../../apps/web/src/components/tools/pipeline-step-summary";
describe("getSettingsSummary", () => {
it("returns dimensions for resize with width and height", () => {
@@ -1,5 +1,5 @@
import { afterEach, describe, expect, it } from "vitest";
import { usePipelineStore } from "../../apps/web/src/stores/pipeline-store";
import { usePipelineStore } from "../../../apps/web/src/stores/pipeline-store";
describe("usePipelineStore", () => {
afterEach(() => {
@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
describe("useFocusTrap", () => {
it("should be importable", async () => {
const mod = await import("../../apps/web/src/hooks/use-focus-trap");
const mod = await import("../../../apps/web/src/hooks/use-focus-trap");
expect(mod.useFocusTrap).toBeDefined();
});
});
+162
View File
@@ -0,0 +1,162 @@
// @vitest-environment jsdom
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
// ---------------------------------------------------------------------------
// Mock window.matchMedia for controlled breakpoint testing
// ---------------------------------------------------------------------------
let currentMatches = false;
const listeners = new Map<string, Set<(e: MediaQueryListEvent) => void>>();
function createMockMql(query: string): MediaQueryList {
if (!listeners.has(query)) {
listeners.set(query, new Set());
}
return {
matches: currentMatches,
media: query,
onchange: null,
addEventListener: (_: string, handler: (e: MediaQueryListEvent) => void) => {
listeners.get(query)!.add(handler);
},
removeEventListener: (_: string, handler: (e: MediaQueryListEvent) => void) => {
listeners.get(query)!.delete(handler);
},
addListener: vi.fn(),
removeListener: vi.fn(),
dispatchEvent: vi.fn(),
} as unknown as MediaQueryList;
}
function fireMediaChange(query: string, matches: boolean) {
currentMatches = matches;
const set = listeners.get(query);
if (set) {
for (const handler of set) {
handler({ matches, media: query } as MediaQueryListEvent);
}
}
}
describe("use-mobile hook", () => {
beforeEach(() => {
currentMatches = false;
listeners.clear();
vi.stubGlobal(
"matchMedia",
vi.fn((q: string) => createMockMql(q)),
);
});
afterEach(() => {
vi.restoreAllMocks();
});
it("exports MOBILE_BREAKPOINT at 768", async () => {
// The hook uses max-width: 767px (MOBILE_BREAKPOINT - 1)
const mod = await import("@/hooks/use-mobile");
// useMobile calls useMediaQuery with `(max-width: 767px)`
expect(mod.useMobile).toBeDefined();
});
it("useMobile returns false when viewport >= 768px (desktop)", async () => {
// Viewport wider than breakpoint: matchMedia returns false
currentMatches = false;
const { renderHook } = await import("@testing-library/react");
const { useMobile } = await import("@/hooks/use-mobile");
const { result } = renderHook(() => useMobile());
expect(result.current).toBe(false);
});
it("useMobile returns true when viewport < 768px (mobile)", async () => {
// Viewport narrower than breakpoint: matchMedia returns true
currentMatches = true;
const { renderHook } = await import("@testing-library/react");
const { useMobile } = await import("@/hooks/use-mobile");
const { result } = renderHook(() => useMobile());
expect(result.current).toBe(true);
});
it("useMediaQuery updates when media query changes", async () => {
currentMatches = false;
const { renderHook, act } = await import("@testing-library/react");
const { useMediaQuery } = await import("@/hooks/use-mobile");
const query = "(max-width: 767px)";
const { result } = renderHook(() => useMediaQuery(query));
expect(result.current).toBe(false);
// Simulate viewport shrinking below breakpoint
await act(() => {
fireMediaChange(query, true);
});
expect(result.current).toBe(true);
// Simulate viewport growing above breakpoint
await act(() => {
fireMediaChange(query, false);
});
expect(result.current).toBe(false);
});
it("useTouchDevice returns true for coarse pointer", async () => {
currentMatches = true;
const { renderHook } = await import("@testing-library/react");
const { useTouchDevice } = await import("@/hooks/use-mobile");
const { result } = renderHook(() => useTouchDevice());
expect(result.current).toBe(true);
});
it("useTouchDevice returns false for fine pointer (desktop)", async () => {
currentMatches = false;
const { renderHook } = await import("@testing-library/react");
const { useTouchDevice } = await import("@/hooks/use-mobile");
const { result } = renderHook(() => useTouchDevice());
expect(result.current).toBe(false);
});
// Device-specific breakpoint validation
describe("device breakpoint classification", () => {
it("Pixel 7 (412px) is classified as mobile", async () => {
// 412 < 768, so max-width:767px matches
currentMatches = true;
const { renderHook } = await import("@testing-library/react");
const { useMobile } = await import("@/hooks/use-mobile");
const { result } = renderHook(() => useMobile());
expect(result.current).toBe(true);
});
it("iPhone 14 (390px) is classified as mobile", async () => {
currentMatches = true;
const { renderHook } = await import("@testing-library/react");
const { useMobile } = await import("@/hooks/use-mobile");
const { result } = renderHook(() => useMobile());
expect(result.current).toBe(true);
});
it("Galaxy Tab S9 (640px) is classified as mobile", async () => {
// 640 < 768, so max-width:767px matches -> mobile!
currentMatches = true;
const { renderHook } = await import("@testing-library/react");
const { useMobile } = await import("@/hooks/use-mobile");
const { result } = renderHook(() => useMobile());
expect(result.current).toBe(true);
});
it("iPad gen 7 (810px) is NOT classified as mobile", async () => {
// 810 >= 768, so max-width:767px does NOT match -> not mobile
currentMatches = false;
const { renderHook } = await import("@testing-library/react");
const { useMobile } = await import("@/hooks/use-mobile");
const { result } = renderHook(() => useMobile());
expect(result.current).toBe(false);
});
});
});