import { describe, test, expect } from "bun:test"; import { EffectCategory } from "../src/categories"; import type { EffectDefinition } from "../src/types"; import { vignetteEffectDefinition } from "../src/effects/vignette"; import { filmGrainEffectDefinition } from "../src/effects/film-grain"; import { sharpenEffectDefinition } from "../src/effects/sharpen"; import { pixelateEffectDefinition } from "../src/effects/pixelate"; import { chromaticAberrationEffectDefinition } from "../src/effects/chromatic-aberration"; import { glitchEffectDefinition } from "../src/effects/glitch"; import { neonGlowEffectDefinition } from "../src/effects/neon-glow"; import { sketchEffectDefinition } from "../src/effects/sketch"; import { oilPaintEffectDefinition } from "../src/effects/oil-paint"; const defaultArgs = { width: 1920, height: 1080, time: 1.5 }; /** Validate common artistic effect structure */ function validateArtisticEffect(def: EffectDefinition, expectedType: string, expectedPasses: number) { test(`${expectedType}: has correct type and category`, () => { expect(def.type).toBe(expectedType); expect(def.category).toBe(EffectCategory.ARTISTIC); }); test(`${expectedType}: has ${expectedPasses} pass(es)`, () => { expect(def.renderer.passes).toHaveLength(expectedPasses); }); test(`${expectedType}: uniforms factory works with defaults`, () => { const defaults: Record = {}; for (const p of def.params) defaults[p.key] = p.default; for (const pass of def.renderer.passes) { const result = pass.uniforms({ effectParams: defaults, ...defaultArgs }); expect(typeof result).toBe("object"); } }); } // --- Single-pass effects --- describe("Vignette", () => { validateArtisticEffect(vignetteEffectDefinition, "vignette", 1); test("has 3 params: intensity, radius, softness", () => { expect(vignetteEffectDefinition.params.map((p) => p.key)).toEqual(["intensity", "radius", "softness"]); }); }); describe("Film Grain", () => { validateArtisticEffect(filmGrainEffectDefinition, "film-grain", 1); test("has animated boolean param", () => { const animated = filmGrainEffectDefinition.params.find((p) => p.key === "animated"); expect(animated?.type).toBe("boolean"); }); test("uses time when animated", () => { const pass = filmGrainEffectDefinition.renderer.passes[0]; const withTime = pass.uniforms({ effectParams: { intensity: 30, size: 2, animated: true }, ...defaultArgs }); expect(withTime.u_time).toBe(1.5); }); test("ignores time when not animated", () => { const pass = filmGrainEffectDefinition.renderer.passes[0]; const noTime = pass.uniforms({ effectParams: { intensity: 30, size: 2, animated: false }, ...defaultArgs }); expect(noTime.u_time).toBe(0); }); }); describe("Sharpen", () => { validateArtisticEffect(sharpenEffectDefinition, "sharpen", 1); }); describe("Pixelate", () => { validateArtisticEffect(pixelateEffectDefinition, "pixelate", 1); test("blockSize default is 10", () => { expect(pixelateEffectDefinition.params[0].default).toBe(10); }); test("blockSize passed directly as uniform", () => { const pass = pixelateEffectDefinition.renderer.passes[0]; const result = pass.uniforms({ effectParams: { blockSize: 20 }, ...defaultArgs }); expect(result.u_blockSize).toBe(20); }); }); describe("Chromatic Aberration", () => { validateArtisticEffect(chromaticAberrationEffectDefinition, "chromatic-aberration", 1); test("angle converted to radians", () => { const pass = chromaticAberrationEffectDefinition.renderer.passes[0]; const result = pass.uniforms({ effectParams: { intensity: 50, angle: 180 }, ...defaultArgs }); expect(result.u_angle).toBeCloseTo(Math.PI, 5); }); }); // --- Multi-pass effects --- describe("Glitch", () => { validateArtisticEffect(glitchEffectDefinition, "glitch", 2); test("has 3 params: intensity, speed, colorSplit", () => { expect(glitchEffectDefinition.params.map((p) => p.key)).toEqual(["intensity", "speed", "colorSplit"]); }); test("both passes use time", () => { const defaults: Record = { intensity: 50, speed: 50, colorSplit: 30 }; for (const pass of glitchEffectDefinition.renderer.passes) { const result = pass.uniforms({ effectParams: defaults, ...defaultArgs }); expect(result.u_time).toBeGreaterThan(0); } }); }); describe("Neon Glow", () => { validateArtisticEffect(neonGlowEffectDefinition, "neon-glow", 2); test("has color param", () => { const colorParam = neonGlowEffectDefinition.params.find((p) => p.key === "color"); expect(colorParam?.type).toBe("color"); }); test("pass 2 converts hex color to RGB array", () => { const pass2 = neonGlowEffectDefinition.renderer.passes[1]; const result = pass2.uniforms({ effectParams: { intensity: 50, color: "#ff0000", threshold: 50 }, ...defaultArgs }); expect(result.u_glowColor).toEqual([1, 0, 0]); }); }); describe("Sketch", () => { validateArtisticEffect(sketchEffectDefinition, "sketch", 2); test("has threshold and lineWidth params", () => { expect(sketchEffectDefinition.params.map((p) => p.key)).toEqual(["threshold", "lineWidth"]); }); }); describe("Oil Paint", () => { validateArtisticEffect(oilPaintEffectDefinition, "oil-paint", 2); test("radius max is capped at 5 for performance", () => { const radiusParam = oilPaintEffectDefinition.params.find((p) => p.key === "radius"); if (radiusParam?.type === "number") { expect(radiusParam.max).toBeLessThanOrEqual(10); } }); });