From 3d103d5092f8cdcf2f09eeeb20b483c935736adc Mon Sep 17 00:00:00 2001 From: SnapOtter Date: Fri, 8 May 2026 16:03:21 +0800 Subject: [PATCH] test(meme-generator): add template manifest validation tests --- tests/unit/meme-templates.test.ts | 188 ++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 tests/unit/meme-templates.test.ts diff --git a/tests/unit/meme-templates.test.ts b/tests/unit/meme-templates.test.ts new file mode 100644 index 00000000..1d2571ed --- /dev/null +++ b/tests/unit/meme-templates.test.ts @@ -0,0 +1,188 @@ +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 MANIFEST_PATH = join(TEMPLATES_DIR, "meme-templates.json"); +const VALID_CATEGORIES = ["reaction", "comparison", "opinion", "animals", "classic"]; + +interface TextBox { + id: string; + x: number; + y: number; + width: number; + height: number; + defaultText: string; +} + +interface Template { + id: string; + name: string; + aliases: string[]; + tags: string[]; + category: string; + filename: string; + width: number; + height: number; + popularity: number; + textBoxes: TextBox[]; +} + +interface Manifest { + version: number; + categories: string[]; + templates: Template[]; +} + +function loadManifest(): Manifest { + const raw = readFileSync(MANIFEST_PATH, "utf-8"); + return JSON.parse(raw); +} + +describe("meme template manifest validation", () => { + it("manifest file exists and is valid JSON with version 1 and non-empty templates array", () => { + expect(existsSync(MANIFEST_PATH)).toBe(true); + + const raw = readFileSync(MANIFEST_PATH, "utf-8"); + let manifest: Manifest; + expect(() => { + manifest = JSON.parse(raw); + }).not.toThrow(); + + manifest = JSON.parse(raw); + expect(manifest.version).toBe(1); + expect(Array.isArray(manifest.templates)).toBe(true); + expect(manifest.templates.length).toBeGreaterThan(0); + }); + + it("has no duplicate template IDs", () => { + const manifest = loadManifest(); + const ids = manifest.templates.map((t) => t.id); + const uniqueIds = new Set(ids); + expect(uniqueIds.size).toBe(ids.length); + + // Identify duplicates for a useful error message + const seen = new Set(); + const duplicates: string[] = []; + for (const id of ids) { + if (seen.has(id)) { + duplicates.push(id); + } + seen.add(id); + } + expect(duplicates, `Duplicate template IDs: ${duplicates.join(", ")}`).toHaveLength(0); + }); + + it("every template has required fields with correct types", () => { + const manifest = loadManifest(); + + for (const template of manifest.templates) { + const label = `template "${template.id || template.name || "unknown"}"`; + + // Required string fields + expect(typeof template.id, `${label}: id must be a string`).toBe("string"); + expect(template.id.length, `${label}: id must not be empty`).toBeGreaterThan(0); + expect(typeof template.name, `${label}: name must be a string`).toBe("string"); + expect(template.name.length, `${label}: name must not be empty`).toBeGreaterThan(0); + + // aliases must be an array + expect(Array.isArray(template.aliases), `${label}: aliases must be an array`).toBe(true); + + // tags must be an array + expect(Array.isArray(template.tags), `${label}: tags must be an array`).toBe(true); + + // category must be valid + expect( + VALID_CATEGORIES.includes(template.category), + `${label}: category "${template.category}" is not one of ${VALID_CATEGORIES.join(", ")}`, + ).toBe(true); + + // filename + expect(typeof template.filename, `${label}: filename must be a string`).toBe("string"); + expect(template.filename.length, `${label}: filename must not be empty`).toBeGreaterThan(0); + + // width and height must be positive numbers + expect(typeof template.width, `${label}: width must be a number`).toBe("number"); + expect(template.width, `${label}: width must be positive`).toBeGreaterThan(0); + expect(typeof template.height, `${label}: height must be a number`).toBe("number"); + expect(template.height, `${label}: height must be positive`).toBeGreaterThan(0); + + // popularity must be non-negative + expect(typeof template.popularity, `${label}: popularity must be a number`).toBe("number"); + expect( + template.popularity, + `${label}: popularity must be non-negative`, + ).toBeGreaterThanOrEqual(0); + + // textBoxes must be a non-empty array + expect(Array.isArray(template.textBoxes), `${label}: textBoxes must be an array`).toBe(true); + expect( + template.textBoxes.length, + `${label}: must have at least one textBox`, + ).toBeGreaterThanOrEqual(1); + } + }); + + it("text box coordinates are in valid percentage range (0-100)", () => { + const manifest = loadManifest(); + + for (const template of manifest.templates) { + for (const box of template.textBoxes) { + const label = `template "${template.id}" textBox "${box.id}"`; + + expect(box.x, `${label}: x must be >= 0`).toBeGreaterThanOrEqual(0); + expect(box.x, `${label}: x must be <= 100`).toBeLessThanOrEqual(100); + + expect(box.y, `${label}: y must be >= 0`).toBeGreaterThanOrEqual(0); + expect(box.y, `${label}: y must be <= 100`).toBeLessThanOrEqual(100); + + expect(box.width, `${label}: width must be >= 0`).toBeGreaterThanOrEqual(0); + expect(box.width, `${label}: width must be <= 100`).toBeLessThanOrEqual(100); + + expect(box.height, `${label}: height must be >= 0`).toBeGreaterThanOrEqual(0); + expect(box.height, `${label}: height must be <= 100`).toBeLessThanOrEqual(100); + } + } + }); + + it("no duplicate text box IDs within a template", () => { + const manifest = loadManifest(); + + for (const template of manifest.templates) { + const boxIds = template.textBoxes.map((b) => b.id); + const uniqueBoxIds = new Set(boxIds); + expect(uniqueBoxIds.size, `template "${template.id}" has duplicate textBox IDs`).toBe( + boxIds.length, + ); + } + }); + + it("every template has a corresponding full-size image in full/ directory", () => { + const manifest = loadManifest(); + const fullDir = join(TEMPLATES_DIR, "full"); + + for (const template of manifest.templates) { + const imagePath = join(fullDir, template.filename); + expect( + existsSync(imagePath), + `template "${template.id}": missing full image at full/${template.filename}`, + ).toBe(true); + } + }); + + it("every template has a corresponding thumbnail in thumbs/ directory", () => { + const manifest = loadManifest(); + const thumbsDir = join(TEMPLATES_DIR, "thumbs"); + + for (const template of manifest.templates) { + // Thumbnail uses the same base name but with .webp extension + const baseName = template.filename.replace(/\.[^.]+$/, ""); + const thumbFilename = `${baseName}.webp`; + const thumbPath = join(thumbsDir, thumbFilename); + expect( + existsSync(thumbPath), + `template "${template.id}": missing thumbnail at thumbs/${thumbFilename}`, + ).toBe(true); + } + }); +});