Skip to content

Commit d3b6607

Browse files
[Test Improver] Add unit tests for svg-filter.js Solver utility (#17729)
* test: add unit tests for shell/utils/svg-filter.js Add 18 unit tests for the Solver class in svg-filter.js: - css(): 4 tests verifying CSS filter string formatting and hue-rotate multiplier - loss(): 4 tests verifying color distance computation and idempotency - constructor: 5 tests verifying target color storage, clamping, and HSL computation - solve(): 5 tests verifying output structure and invariants Coverage: 0% → 95% stmts, 96.4% branches, 92% fns Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ci: trigger checks --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent a7b6e91 commit d3b6607

1 file changed

Lines changed: 184 additions & 0 deletions

File tree

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import { Solver } from '@shell/utils/svg-filter';
2+
3+
describe('solver', () => {
4+
describe('css', () => {
5+
it.each([
6+
{
7+
desc: 'all-zero filter values produce zero percentages and zero hue-rotate',
8+
filters: [0, 0, 0, 0, 0, 0],
9+
expected: 'filter: invert(0%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(0%) contrast(0%);',
10+
},
11+
{
12+
desc: 'integer filter values are rounded and formatted correctly',
13+
filters: [50, 20, 3750, 50, 100, 100],
14+
expected: 'filter: invert(50%) sepia(20%) saturate(3750%) hue-rotate(180deg) brightness(100%) contrast(100%);',
15+
},
16+
{
17+
desc: 'fractional filter values are rounded to nearest integer',
18+
filters: [10.6, 30.4, 100.5, 25.2, 80.9, 120.1],
19+
expected: 'filter: invert(11%) sepia(30%) saturate(101%) hue-rotate(91deg) brightness(81%) contrast(120%);',
20+
},
21+
{
22+
desc: 'hue-rotate index (3) uses 3.6 multiplier',
23+
filters: [0, 0, 0, 100, 0, 0],
24+
expected: 'filter: invert(0%) sepia(0%) saturate(0%) hue-rotate(360deg) brightness(0%) contrast(0%);',
25+
},
26+
])('$desc', ({ filters, expected }) => {
27+
const solver = new Solver({
28+
r: 255, g: 0, b: 0
29+
});
30+
31+
expect(solver.css(filters)).toStrictEqual(expected);
32+
});
33+
});
34+
35+
describe('loss', () => {
36+
it('returns 0 for filters that reproduce the target color exactly', () => {
37+
// Target: black (0,0,0). Starting from black and applying:
38+
// invert(0%), sepia(0%), saturate(0%), hueRotate(0), brightness(100%), contrast(100%)
39+
// should leave color as (0,0,0) — perfect match.
40+
const solver = new Solver({
41+
r: 0, g: 0, b: 0
42+
});
43+
const loss = solver.loss([0, 0, 0, 0, 100, 100]);
44+
45+
expect(loss).toStrictEqual(0);
46+
});
47+
48+
it('returns a positive value when filters produce a color that differs from the target', () => {
49+
const solver = new Solver({
50+
r: 255, g: 0, b: 0
51+
});
52+
// Filters that are all zero → color stays black → far from red target
53+
const loss = solver.loss([0, 0, 0, 0, 0, 0]);
54+
55+
expect(loss).toBeGreaterThan(0);
56+
});
57+
58+
it('loss is smaller when color produced by filters is closer to target', () => {
59+
const solver = new Solver({
60+
r: 0, g: 0, b: 0
61+
});
62+
// filters [0,0,0,0,0,0]: contrast(0) pushes everything to mid-gray → far from black target
63+
const highLoss = solver.loss([0, 0, 0, 0, 0, 0]);
64+
// filters [0,0,0,0,100,100]: identity transforms → stays black → matches target
65+
const lowerLoss = solver.loss([0, 0, 0, 0, 100, 100]);
66+
67+
expect(lowerLoss).toBeLessThan(highLoss);
68+
});
69+
70+
it('uses the same reusedColor instance without side effects between calls', () => {
71+
const solver = new Solver({
72+
r: 100, g: 150, b: 200
73+
});
74+
const loss1 = solver.loss([10, 5, 200, 30, 90, 110]);
75+
const loss2 = solver.loss([10, 5, 200, 30, 90, 110]);
76+
77+
// Same inputs must produce the same loss regardless of call order
78+
expect(loss1).toStrictEqual(loss2);
79+
});
80+
});
81+
82+
describe('constructor', () => {
83+
it('stores target color components', () => {
84+
const solver = new Solver({
85+
r: 100, g: 150, b: 200
86+
});
87+
88+
expect(solver.target.r).toStrictEqual(100);
89+
expect(solver.target.g).toStrictEqual(150);
90+
expect(solver.target.b).toStrictEqual(200);
91+
});
92+
93+
it('clamps target components above 255 to 255', () => {
94+
const solver = new Solver({
95+
r: 300, g: 0, b: 0
96+
});
97+
98+
expect(solver.target.r).toStrictEqual(255);
99+
});
100+
101+
it('clamps target components below 0 to 0', () => {
102+
const solver = new Solver({
103+
r: 0, g: -10, b: 0
104+
});
105+
106+
expect(solver.target.g).toStrictEqual(0);
107+
});
108+
109+
it('computes targetHSL from the provided color', () => {
110+
// Pure white: hsl should be h=0, s=0, l=100
111+
const solver = new Solver({
112+
r: 255, g: 255, b: 255
113+
});
114+
115+
expect(solver.targetHSL.h).toStrictEqual(0);
116+
expect(solver.targetHSL.s).toStrictEqual(0);
117+
expect(solver.targetHSL.l).toBeCloseTo(100, 1);
118+
});
119+
120+
it('computes targetHSL for pure red', () => {
121+
// Pure red: r=255, g=0, b=0 → hsl h≈0 (normalised *100 → 0), s=100, l=50
122+
const solver = new Solver({
123+
r: 255, g: 0, b: 0
124+
});
125+
126+
expect(solver.targetHSL.h).toBeCloseTo(0, 1);
127+
expect(solver.targetHSL.s).toBeCloseTo(100, 1);
128+
expect(solver.targetHSL.l).toBeCloseTo(50, 1);
129+
});
130+
});
131+
132+
describe('solve', () => {
133+
it('returns an object with values, loss, filter and filterVal properties', () => {
134+
const solver = new Solver({
135+
r: 255, g: 0, b: 0
136+
});
137+
const result = solver.solve();
138+
139+
expect(result).toHaveProperty('values');
140+
expect(result).toHaveProperty('loss');
141+
expect(result).toHaveProperty('filter');
142+
expect(result).toHaveProperty('filterVal');
143+
});
144+
145+
it('filter starts with "filter: " and ends with ";"', () => {
146+
const solver = new Solver({
147+
r: 0, g: 128, b: 255
148+
});
149+
const result = solver.solve();
150+
151+
expect(result.filter.startsWith('filter: ')).toBe(true);
152+
expect(result.filter.endsWith(';')).toBe(true);
153+
});
154+
155+
it('filterVal is filter without the "filter: " prefix and trailing ";"', () => {
156+
const solver = new Solver({
157+
r: 0, g: 200, b: 100
158+
});
159+
const result = solver.solve();
160+
161+
expect(result.filterVal).toStrictEqual(
162+
result.filter.replace('filter: ', '').replace(';', '')
163+
);
164+
});
165+
166+
it('loss is a non-negative number', () => {
167+
const solver = new Solver({
168+
r: 50, g: 100, b: 150
169+
});
170+
const result = solver.solve();
171+
172+
expect(result.loss).toBeGreaterThanOrEqual(0);
173+
});
174+
175+
it('values array has exactly 6 elements', () => {
176+
const solver = new Solver({
177+
r: 10, g: 20, b: 30
178+
});
179+
const result = solver.solve();
180+
181+
expect(result.values).toHaveLength(6);
182+
});
183+
});
184+
});

0 commit comments

Comments
 (0)