test(effects): add unit tests for chromatic aberration, glitch, sharpen, and vignette effects

- Implement tests for chromatic aberration effect definition, verifying type, name, category, parameters, and renderer behavior.
- Add tests for glitch effect definition, covering type, name, parameters, and rendering logic.
- Create tests for sharpen effect definition, ensuring correct parameter defaults and rendering.
- Introduce tests for vignette effect definition, validating parameters and rendering behavior.

These tests enhance coverage for the new effects introduced in the @opencut/effects package.
This commit is contained in:
anh doan 2026-03-23 14:36:05 +07:00
parent 81adac6e1e
commit 6cbc8d9d1b
No known key found for this signature in database
5 changed files with 521 additions and 1 deletions

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,122 @@
import { describe, test, expect } from "bun:test";
import { chromaticAberrationEffectDefinition } from "./index";
import { EffectCategory } from "../../categories";
describe("Chromatic Aberration Effect Definition", () => {
test("has correct type and name", () => {
expect(chromaticAberrationEffectDefinition.type).toBe("chromatic-aberration");
expect(chromaticAberrationEffectDefinition.name).toBe("Chromatic Aberration");
});
test("is categorized as artistic", () => {
expect(chromaticAberrationEffectDefinition.category).toBe(EffectCategory.ARTISTIC);
});
test("has searchable keywords", () => {
expect(chromaticAberrationEffectDefinition.keywords).toContain("chromatic");
expect(chromaticAberrationEffectDefinition.keywords).toContain("aberration");
expect(chromaticAberrationEffectDefinition.keywords).toContain("rgb split");
expect(chromaticAberrationEffectDefinition.keywords).toContain("prism");
});
test("has 2 params: intensity and angle", () => {
expect(chromaticAberrationEffectDefinition.params.map((p) => p.key)).toEqual([
"intensity",
"angle",
]);
});
test("intensity param has correct defaults", () => {
const param = chromaticAberrationEffectDefinition.params[0];
expect(param.key).toBe("intensity");
expect(param.type).toBe("number");
if (param.type === "number") {
expect(param.default).toBe(20);
expect(param.min).toBe(0);
expect(param.max).toBe(100);
expect(param.step).toBe(1);
}
});
test("angle param has correct defaults", () => {
const param = chromaticAberrationEffectDefinition.params[1];
expect(param.key).toBe("angle");
expect(param.type).toBe("number");
if (param.type === "number") {
expect(param.default).toBe(0);
expect(param.min).toBe(0);
expect(param.max).toBe(360);
expect(param.step).toBe(1);
}
});
test("renderer has single pass", () => {
expect(chromaticAberrationEffectDefinition.renderer.type).toBe("webgl");
expect(chromaticAberrationEffectDefinition.renderer.passes).toHaveLength(1);
});
test("uniforms scale intensity by factor of 10", () => {
const pass = chromaticAberrationEffectDefinition.renderer.passes[0];
const at0 = pass.uniforms({
effectParams: { intensity: 0, angle: 0 },
width: 1920,
height: 1080,
});
const at100 = pass.uniforms({
effectParams: { intensity: 100, angle: 0 },
width: 1920,
height: 1080,
});
expect(at0.u_intensity).toBe(0);
expect(at100.u_intensity).toBe(10); // 100 / 10 = 10
});
test("uniforms convert angle from degrees to radians", () => {
const pass = chromaticAberrationEffectDefinition.renderer.passes[0];
const at0 = pass.uniforms({
effectParams: { intensity: 50, angle: 0 },
width: 1920,
height: 1080,
});
const at90 = pass.uniforms({
effectParams: { intensity: 50, angle: 90 },
width: 1920,
height: 1080,
});
const at180 = pass.uniforms({
effectParams: { intensity: 50, angle: 180 },
width: 1920,
height: 1080,
});
const at360 = pass.uniforms({
effectParams: { intensity: 50, angle: 360 },
width: 1920,
height: 1080,
});
expect(at0.u_angle).toBe(0);
expect(at90.u_angle).toBeCloseTo(Math.PI / 2, 5);
expect(at180.u_angle).toBeCloseTo(Math.PI, 5);
expect(at360.u_angle).toBeCloseTo(2 * Math.PI, 5);
});
test("angle 45 degrees converts correctly", () => {
const pass = chromaticAberrationEffectDefinition.renderer.passes[0];
const result = pass.uniforms({
effectParams: { intensity: 50, angle: 45 },
width: 1920,
height: 1080,
});
expect(result.u_angle).toBeCloseTo(Math.PI / 4, 5);
});
test("uniforms with default values", () => {
const pass = chromaticAberrationEffectDefinition.renderer.passes[0];
const result = pass.uniforms({
effectParams: { intensity: 20, angle: 0 },
width: 1920,
height: 1080,
});
expect(result.u_intensity).toBe(2); // 20 / 10 = 2
expect(result.u_angle).toBe(0);
});
});

View File

@ -0,0 +1,183 @@
import { describe, test, expect } from "bun:test";
import { glitchEffectDefinition } from "./index";
import { EffectCategory } from "../../categories";
describe("Glitch Effect Definition", () => {
test("has correct type and name", () => {
expect(glitchEffectDefinition.type).toBe("glitch");
expect(glitchEffectDefinition.name).toBe("Glitch");
});
test("is categorized as artistic", () => {
expect(glitchEffectDefinition.category).toBe(EffectCategory.ARTISTIC);
});
test("has searchable keywords", () => {
expect(glitchEffectDefinition.keywords).toContain("glitch");
expect(glitchEffectDefinition.keywords).toContain("digital");
expect(glitchEffectDefinition.keywords).toContain("vhs");
expect(glitchEffectDefinition.keywords).toContain("corrupt");
});
test("has 3 params: intensity, speed, colorSplit", () => {
expect(glitchEffectDefinition.params.map((p) => p.key)).toEqual([
"intensity",
"speed",
"colorSplit",
]);
});
test("intensity param has correct defaults", () => {
const param = glitchEffectDefinition.params[0];
expect(param.key).toBe("intensity");
expect(param.type).toBe("number");
if (param.type === "number") {
expect(param.default).toBe(50);
expect(param.min).toBe(0);
expect(param.max).toBe(100);
expect(param.step).toBe(1);
}
});
test("speed param has correct defaults", () => {
const param = glitchEffectDefinition.params[1];
expect(param.key).toBe("speed");
expect(param.type).toBe("number");
if (param.type === "number") {
expect(param.default).toBe(50);
expect(param.min).toBe(0);
expect(param.max).toBe(100);
expect(param.step).toBe(1);
}
});
test("colorSplit param has correct defaults", () => {
const param = glitchEffectDefinition.params[2];
expect(param.key).toBe("colorSplit");
expect(param.type).toBe("number");
if (param.type === "number") {
expect(param.default).toBe(30);
expect(param.min).toBe(0);
expect(param.max).toBe(100);
expect(param.step).toBe(1);
}
});
test("renderer has 2 passes (block displacement + scanlines)", () => {
expect(glitchEffectDefinition.renderer.type).toBe("webgl");
expect(glitchEffectDefinition.renderer.passes).toHaveLength(2);
});
test("pass 1 uniforms scale intensity correctly", () => {
const pass1 = glitchEffectDefinition.renderer.passes[0];
const result = pass1.uniforms({
effectParams: { intensity: 50, speed: 50, colorSplit: 30 },
width: 1920,
height: 1080,
time: 1,
});
expect(result.u_intensity).toBe(0.05); // 50 / 1000 = 0.05
});
test("pass 1 colorSplit scales with width", () => {
const pass1 = glitchEffectDefinition.renderer.passes[0];
const at1920 = pass1.uniforms({
effectParams: { intensity: 50, speed: 50, colorSplit: 30 },
width: 1920,
height: 1080,
time: 1,
});
const at3840 = pass1.uniforms({
effectParams: { intensity: 50, speed: 50, colorSplit: 30 },
width: 3840,
height: 2160,
time: 1,
});
// colorSplit: 30 / (width * 10)
expect(at1920.u_colorSplit).toBeCloseTo(30 / 19200, 6);
expect(at3840.u_colorSplit).toBeCloseTo(30 / 38400, 6);
// Higher resolution = smaller colorSplit value
expect(at3840.u_colorSplit).toBeLessThan(at1920.u_colorSplit as number);
});
test("pass 1 time scales with speed", () => {
const pass1 = glitchEffectDefinition.renderer.passes[0];
const atSpeed0 = pass1.uniforms({
effectParams: { intensity: 50, speed: 0, colorSplit: 30 },
width: 1920,
height: 1080,
time: 2,
});
const atSpeed50 = pass1.uniforms({
effectParams: { intensity: 50, speed: 50, colorSplit: 30 },
width: 1920,
height: 1080,
time: 2,
});
const atSpeed100 = pass1.uniforms({
effectParams: { intensity: 50, speed: 100, colorSplit: 30 },
width: 1920,
height: 1080,
time: 2,
});
// time * (speed / 50)
expect(atSpeed0.u_time).toBe(0);
expect(atSpeed50.u_time).toBe(2); // 2 * (50 / 50) = 2
expect(atSpeed100.u_time).toBe(4); // 2 * (100 / 50) = 4
});
test("pass 2 uniforms scale intensity correctly", () => {
const pass2 = glitchEffectDefinition.renderer.passes[1];
const result = pass2.uniforms({
effectParams: { intensity: 50, speed: 50, colorSplit: 30 },
width: 1920,
height: 1080,
time: 1,
});
expect(result.u_intensity).toBe(0.5); // 50 / 100 = 0.5
});
test("pass 2 time scales with speed", () => {
const pass2 = glitchEffectDefinition.renderer.passes[1];
const atSpeed25 = pass2.uniforms({
effectParams: { intensity: 50, speed: 25, colorSplit: 30 },
width: 1920,
height: 1080,
time: 4,
});
expect(atSpeed25.u_time).toBe(2); // 4 * (25 / 50) = 2
});
test("both passes use time uniformly", () => {
const pass1 = glitchEffectDefinition.renderer.passes[0];
const pass2 = glitchEffectDefinition.renderer.passes[1];
const params = { intensity: 50, speed: 50, colorSplit: 30 };
const args = { effectParams: params, width: 1920, height: 1080, time: 3 };
const result1 = pass1.uniforms(args);
const result2 = pass2.uniforms(args);
// Both passes should have the same time value when speed is 50
expect(result1.u_time).toBe(3);
expect(result2.u_time).toBe(3);
});
test("handles undefined time gracefully", () => {
const pass1 = glitchEffectDefinition.renderer.passes[0];
const result = pass1.uniforms({
effectParams: { intensity: 50, speed: 50, colorSplit: 30 },
width: 1920,
height: 1080,
});
expect(result.u_time).toBe(0);
});
test("zero speed results in zero time", () => {
const pass1 = glitchEffectDefinition.renderer.passes[0];
const result = pass1.uniforms({
effectParams: { intensity: 50, speed: 0, colorSplit: 30 },
width: 1920,
height: 1080,
time: 100,
});
expect(result.u_time).toBe(0);
});
});

View File

@ -0,0 +1,84 @@
import { describe, test, expect } from "bun:test";
import { sharpenEffectDefinition } from "./index";
import { EffectCategory } from "../../categories";
describe("Sharpen Effect Definition", () => {
test("has correct type and name", () => {
expect(sharpenEffectDefinition.type).toBe("sharpen");
expect(sharpenEffectDefinition.name).toBe("Sharpen");
});
test("is categorized as artistic", () => {
expect(sharpenEffectDefinition.category).toBe(EffectCategory.ARTISTIC);
});
test("has searchable keywords", () => {
expect(sharpenEffectDefinition.keywords).toContain("sharpen");
expect(sharpenEffectDefinition.keywords).toContain("crisp");
expect(sharpenEffectDefinition.keywords).toContain("detail");
expect(sharpenEffectDefinition.keywords).toContain("unsharp mask");
});
test("has single intensity param", () => {
expect(sharpenEffectDefinition.params).toHaveLength(1);
expect(sharpenEffectDefinition.params[0].key).toBe("intensity");
});
test("intensity param has correct defaults", () => {
const param = sharpenEffectDefinition.params[0];
expect(param.type).toBe("number");
if (param.type === "number") {
expect(param.default).toBe(50);
expect(param.min).toBe(0);
expect(param.max).toBe(100);
expect(param.step).toBe(1);
}
});
test("renderer has single pass", () => {
expect(sharpenEffectDefinition.renderer.type).toBe("webgl");
expect(sharpenEffectDefinition.renderer.passes).toHaveLength(1);
});
test("uniforms normalize intensity to 0..1 range", () => {
const pass = sharpenEffectDefinition.renderer.passes[0];
const at0 = pass.uniforms({
effectParams: { intensity: 0 },
width: 1920,
height: 1080,
});
const at100 = pass.uniforms({
effectParams: { intensity: 100 },
width: 1920,
height: 1080,
});
expect(at0.u_intensity).toBe(0);
expect(at100.u_intensity).toBe(1);
});
test("uniforms at 50% intensity", () => {
const pass = sharpenEffectDefinition.renderer.passes[0];
const result = pass.uniforms({
effectParams: { intensity: 50 },
width: 1920,
height: 1080,
});
expect(result.u_intensity).toBe(0.5);
});
test("uniforms at various intensity levels", () => {
const pass = sharpenEffectDefinition.renderer.passes[0];
const at25 = pass.uniforms({
effectParams: { intensity: 25 },
width: 1920,
height: 1080,
});
const at75 = pass.uniforms({
effectParams: { intensity: 75 },
width: 1920,
height: 1080,
});
expect(at25.u_intensity).toBe(0.25);
expect(at75.u_intensity).toBe(0.75);
});
});

View File

@ -0,0 +1,131 @@
import { describe, test, expect } from "bun:test";
import { vignetteEffectDefinition } from "./index";
import { EffectCategory } from "../../categories";
describe("Vignette Effect Definition", () => {
test("has correct type and name", () => {
expect(vignetteEffectDefinition.type).toBe("vignette");
expect(vignetteEffectDefinition.name).toBe("Vignette");
});
test("is categorized as artistic", () => {
expect(vignetteEffectDefinition.category).toBe(EffectCategory.ARTISTIC);
});
test("has searchable keywords", () => {
expect(vignetteEffectDefinition.keywords).toContain("vignette");
expect(vignetteEffectDefinition.keywords).toContain("cinematic");
expect(vignetteEffectDefinition.keywords).toContain("edges");
});
test("has 3 params: intensity, radius, softness", () => {
expect(vignetteEffectDefinition.params.map((p) => p.key)).toEqual([
"intensity",
"radius",
"softness",
]);
});
test("intensity param has correct defaults", () => {
const param = vignetteEffectDefinition.params[0];
expect(param.key).toBe("intensity");
expect(param.type).toBe("number");
if (param.type === "number") {
expect(param.default).toBe(50);
expect(param.min).toBe(0);
expect(param.max).toBe(100);
expect(param.step).toBe(1);
}
});
test("radius param has correct defaults", () => {
const param = vignetteEffectDefinition.params[1];
expect(param.key).toBe("radius");
expect(param.type).toBe("number");
if (param.type === "number") {
expect(param.default).toBe(80);
expect(param.min).toBe(0);
expect(param.max).toBe(100);
expect(param.step).toBe(1);
}
});
test("softness param has correct defaults", () => {
const param = vignetteEffectDefinition.params[2];
expect(param.key).toBe("softness");
expect(param.type).toBe("number");
if (param.type === "number") {
expect(param.default).toBe(50);
expect(param.min).toBe(0);
expect(param.max).toBe(100);
expect(param.step).toBe(1);
}
});
test("renderer has single pass", () => {
expect(vignetteEffectDefinition.renderer.type).toBe("webgl");
expect(vignetteEffectDefinition.renderer.passes).toHaveLength(1);
});
test("uniforms normalize intensity to 0..1 range", () => {
const pass = vignetteEffectDefinition.renderer.passes[0];
const at0 = pass.uniforms({
effectParams: { intensity: 0, radius: 50, softness: 50 },
width: 1920,
height: 1080,
});
const at100 = pass.uniforms({
effectParams: { intensity: 100, radius: 50, softness: 50 },
width: 1920,
height: 1080,
});
expect(at0.u_intensity).toBe(0);
expect(at100.u_intensity).toBe(1);
});
test("uniforms scale radius correctly", () => {
const pass = vignetteEffectDefinition.renderer.passes[0];
const at0 = pass.uniforms({
effectParams: { intensity: 50, radius: 0, softness: 50 },
width: 1920,
height: 1080,
});
const at100 = pass.uniforms({
effectParams: { intensity: 50, radius: 100, softness: 50 },
width: 1920,
height: 1080,
});
// radius: 100 / 100 * 0.707 = 0.707
expect(at0.u_radius).toBe(0);
expect(at100.u_radius).toBeCloseTo(0.707, 3);
});
test("uniforms scale softness correctly", () => {
const pass = vignetteEffectDefinition.renderer.passes[0];
const at0 = pass.uniforms({
effectParams: { intensity: 50, radius: 50, softness: 0 },
width: 1920,
height: 1080,
});
const at100 = pass.uniforms({
effectParams: { intensity: 50, radius: 50, softness: 100 },
width: 1920,
height: 1080,
});
// softness: 100 / 100 * 0.5 = 0.5
expect(at0.u_softness).toBe(0);
expect(at100.u_softness).toBe(0.5);
});
test("uniforms with default values", () => {
const pass = vignetteEffectDefinition.renderer.passes[0];
const result = pass.uniforms({
effectParams: { intensity: 50, radius: 80, softness: 50 },
width: 1920,
height: 1080,
});
expect(result.u_intensity).toBe(0.5);
expect(result.u_radius).toBeCloseTo(0.5656, 3); // 80 / 100 * 0.707
expect(result.u_softness).toBe(0.25); // 50 / 100 * 0.5
});
});