mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
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:
+1
-1
@@ -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) =>
|
||||
@@ -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.";
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"];
|
||||
|
||||
@@ -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'", () => {
|
||||
@@ -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 () => {
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
+3
-3
@@ -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
-1
@@ -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", () => {
|
||||
+2
-2
@@ -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 () => {
|
||||
+1
-1
@@ -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", () => {
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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
-1
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user