This project is a palette creator tool that allows users to generate and customize color palettes for their design projects.
1// oxlint-disable max-lines
2// oxlint-disable no-magic-numbers
3
4import {
5 generateAnalogous,
6 generateComplement,
7 generateHsl,
8 generateMono,
9 generateSaturations,
10 generateTriad,
11 getSaturation,
12 hexToHsl,
13 hslToRgb,
14 isAchromatic,
15 isLowSaturation,
16 rgbToHex,
17 rgbToHsl,
18 toHslString,
19} from './utils';
20
21// oxlint-disable-next-line max-lines-per-function
22describe('utility functions', () => {
23 describe('toHslString utility', () => {
24 it('formats h,s,l into hsl string', () => {
25 expect(toHslString(180, 50, 50)).toBe('hsl(180, 50%, 50%)');
26 expect(toHslString(0, 0, 100)).toBe('hsl(0, 0%, 100%)');
27 expect(toHslString(360, 100, 0)).toBe('hsl(360, 100%, 0%)');
28 });
29 });
30
31 describe('generateHsl utility', () => {
32 it('returns a valid hsl string', () => {
33 expect(generateHsl()).toMatch(/^hsl\(\d+, \d+%, \d+%\)$/);
34 });
35 });
36
37 describe('hslToRgb utility', () => {
38 it('converts hsl to rgb', () => {
39 expect(hslToRgb('hsl(0, 0%, 0%)')).toBe('rgb(0, 0, 0)');
40 expect(hslToRgb('hsl(0, 0%, 100%)')).toBe('rgb(255, 255, 255)');
41 expect(hslToRgb('hsl(0, 100%, 50%)')).toBe('rgb(255, 0, 0)');
42 expect(hslToRgb('hsl(120, 100%, 50%)')).toBe('rgb(0, 255, 0)');
43 expect(hslToRgb('hsl(240, 100%, 50%)')).toBe('rgb(0, 0, 255)');
44 });
45
46 it('handles hue > 240 (triggers t > 1 in hue2rgb)', () => {
47 expect(hslToRgb('hsl(300, 100%, 50%)')).toBe('rgb(255, 0, 255)');
48 });
49
50 it('returns black for invalid input', () => {
51 expect(hslToRgb('invalid')).toBe('rgb(0, 0, 0)');
52 });
53 });
54
55 describe('rgbToHsl utility', () => {
56 it('converts rgb to hsl', () => {
57 expect(rgbToHsl('rgb(0, 0, 0)')).toBe('hsl(0, 0%, 0%)');
58 expect(rgbToHsl('rgb(255, 255, 255)')).toBe('hsl(0, 0%, 100%)');
59 expect(rgbToHsl('rgb(255, 0, 0)')).toBe('hsl(0, 100%, 50%)');
60 expect(rgbToHsl('rgb(0, 255, 0)')).toBe('hsl(120, 100%, 50%)');
61 expect(rgbToHsl('rgb(0, 0, 255)')).toBe('hsl(240, 100%, 50%)');
62 });
63
64 it('handles high-lightness colors (l > 0.5 saturation branch)', () => {
65 expect(rgbToHsl('rgb(255, 128, 128)')).toBe('hsl(0, 100%, 75%)');
66 });
67
68 it('returns black for invalid input', () => {
69 expect(rgbToHsl('invalid')).toBe('hsl(0, 0%, 0%)');
70 });
71 });
72
73 describe('rgbToHex utility', () => {
74 it('converts rgb to hex', () => {
75 expect(rgbToHex('rgb(0, 0, 0)')).toBe('#000000');
76 expect(rgbToHex('rgb(255, 255, 255)')).toBe('#ffffff');
77 expect(rgbToHex('rgb(255, 0, 0)')).toBe('#ff0000');
78 expect(rgbToHex('rgb(0, 255, 0)')).toBe('#00ff00');
79 expect(rgbToHex('rgb(0, 0, 255)')).toBe('#0000ff');
80 });
81
82 it('returns black for invalid input', () => {
83 expect(rgbToHex('invalid')).toBe('#000000');
84 });
85 });
86
87 describe('hexToHsl utility', () => {
88 it('converts hex to hsl', () => {
89 expect(hexToHsl('000000')).toBe('hsl(0, 0%, 0%)');
90 expect(hexToHsl('ffffff')).toBe('hsl(0, 0%, 100%)');
91 expect(hexToHsl('ff0000')).toBe('hsl(0, 100%, 50%)');
92 expect(hexToHsl('00ff00')).toBe('hsl(120, 100%, 50%)');
93 expect(hexToHsl('0000ff')).toBe('hsl(240, 100%, 50%)');
94 });
95 });
96
97 describe('generateComplement utility', () => {
98 it('returns array of 7 colors', () => {
99 expect(generateComplement('hsl(180, 50%, 50%)')).toHaveLength(7);
100 });
101
102 it('generates complementary hue (180 degrees)', () => {
103 const result = generateComplement('hsl(0, 50%, 50%)');
104
105 expect(result[0]).toBe('hsl(180, 50%, 50%)');
106 });
107
108 it('returns empty array for invalid input', () => {
109 expect(generateComplement('invalid')).toEqual([]);
110 });
111 });
112
113 describe('generateMono utility', () => {
114 it('returns array of 8 colors', () => {
115 expect(generateMono('hsl(180, 50%, 50%)')).toHaveLength(8);
116 });
117
118 it('varies lightness while keeping hue constant', () => {
119 const result = generateMono('hsl(180, 50%, 50%)');
120
121 expect(result[0]).toBe('hsl(180, 50%, 8%)');
122 expect(result[7]).toBe('hsl(180, 50%, 95%)');
123 });
124
125 it('returns empty array for invalid input', () => {
126 expect(generateMono('invalid')).toEqual([]);
127 });
128 });
129
130 describe('generateTriad utility', () => {
131 it('returns array of 6 colors', () => {
132 expect(generateTriad('hsl(180, 50%, 50%)')).toHaveLength(6);
133 });
134
135 it('generates triadic hues (120 degrees apart)', () => {
136 const result = generateTriad('hsl(0, 50%, 50%)');
137
138 expect(result[0]).toBe('hsl(120, 50%, 50%)');
139 expect(result[1]).toBe('hsl(240, 50%, 50%)');
140 });
141
142 it('clamps lightness below 0 when l - 20 < 0', () => {
143 const result = generateTriad('hsl(0, 50%, 10%)');
144
145 expect(result[2]).toBe('hsl(120, 50%, 0%)');
146 expect(result[3]).toBe('hsl(240, 50%, 0%)');
147 });
148
149 it('clamps lightness above 100 when l + 20 > 100', () => {
150 const result = generateTriad('hsl(0, 50%, 90%)');
151
152 expect(result[4]).toBe('hsl(120, 50%, 100%)');
153 expect(result[5]).toBe('hsl(240, 50%, 100%)');
154 });
155
156 it('returns empty array for invalid input', () => {
157 expect(generateTriad('invalid')).toEqual([]);
158 });
159 });
160
161 describe('generateAnalogous utility', () => {
162 it('returns array of 6 colors', () => {
163 expect(generateAnalogous('hsl(180, 50%, 50%)')).toHaveLength(6);
164 });
165
166 it('generates analogous hues (30 degree steps)', () => {
167 const result = generateAnalogous('hsl(180, 50%, 50%)');
168
169 expect(result[0]).toBe('hsl(120, 50%, 50%)');
170 expect(result[2]).toBe('hsl(210, 50%, 50%)');
171 });
172
173 it('returns empty array for invalid input', () => {
174 expect(generateAnalogous('invalid')).toEqual([]);
175 });
176 });
177
178 describe('generateSaturations utility', () => {
179 it('returns array of 8 colors', () => {
180 expect(generateSaturations('hsl(180, 50%, 50%)')).toHaveLength(8);
181 });
182
183 it('varies saturation while keeping hue and lightness constant', () => {
184 const result = generateSaturations('hsl(180, 50%, 50%)');
185
186 for (const color of result) {
187 expect(color).toMatch(/^hsl\(180, \d+%, 50%\)$/);
188 }
189 });
190
191 it('returns empty array for invalid input', () => {
192 expect(generateSaturations('invalid')).toEqual([]);
193 });
194 });
195
196 describe('isLowSaturation utility', () => {
197 it('returns true for s=0', () => {
198 expect(isLowSaturation(0)).toBeTruthy();
199 });
200
201 it('returns true at the threshold s=10', () => {
202 expect(isLowSaturation(10)).toBeTruthy();
203 });
204
205 it('returns false just above the threshold s=11', () => {
206 expect(isLowSaturation(11)).toBeFalsy();
207 });
208
209 it('returns false for s=50', () => {
210 expect(isLowSaturation(50)).toBeFalsy();
211 });
212 });
213
214 describe('isAchromatic utility', () => {
215 it('returns true for pure black (s=0, l=0)', () => {
216 expect(isAchromatic(0, 0)).toBeTruthy();
217 });
218
219 it('returns true for pure white (s=0, l=100)', () => {
220 expect(isAchromatic(0, 100)).toBeTruthy();
221 });
222
223 it('returns true at the dark threshold boundary (s=5, l=8)', () => {
224 expect(isAchromatic(5, 8)).toBeTruthy();
225 });
226
227 it('returns true at the light threshold boundary (s=5, l=95)', () => {
228 expect(isAchromatic(5, 95)).toBeTruthy();
229 });
230
231 it('returns false when lightness is mid-range even at s=0', () => {
232 expect(isAchromatic(0, 50)).toBeFalsy();
233 });
234
235 it('returns false when saturation is above threshold', () => {
236 expect(isAchromatic(15, 0)).toBeFalsy();
237 });
238 });
239
240 // oxlint-disable-next-line max-lines-per-function
241 describe('achromatic variation generation', () => {
242 const BLACK = 'hsl(0, 0%, 0%)';
243 const WHITE = 'hsl(0, 0%, 100%)';
244 const HSL_PATTERN = /^hsl\(\d+, \d+%, \d+%\)$/;
245
246 it('generateComplement returns 7 chromatic colors for black', () => {
247 const result = generateComplement(BLACK);
248 expect(result).toHaveLength(7);
249 for (const color of result) {
250 expect(color).toMatch(HSL_PATTERN);
251 expect(getSaturation(color)).toBeGreaterThan(0);
252 }
253 });
254
255 it('generateComplement returns 7 chromatic colors for white', () => {
256 const result = generateComplement(WHITE);
257 expect(result).toHaveLength(7);
258 for (const color of result) {
259 expect(getSaturation(color)).toBeGreaterThan(0);
260 }
261 });
262
263 it('generateComplement produces different results on repeated calls', () => {
264 const results = new Set(
265 Array.from({ length: 10 }, () => generateComplement(BLACK)[0]),
266 );
267 expect(results.size).toBeGreaterThan(1);
268 });
269
270 it('generateTriad returns 6 chromatic colors for black', () => {
271 const result = generateTriad(BLACK);
272 expect(result).toHaveLength(6);
273 for (const color of result) {
274 expect(color).toMatch(HSL_PATTERN);
275 expect(getSaturation(color)).toBeGreaterThan(0);
276 }
277 });
278
279 it('generateTriad produces different results on repeated calls', () => {
280 const results = new Set(
281 Array.from({ length: 10 }, () => generateTriad(BLACK)[0]),
282 );
283 expect(results.size).toBeGreaterThan(1);
284 });
285
286 it('generateAnalogous returns 6 chromatic colors for black', () => {
287 const result = generateAnalogous(BLACK);
288 expect(result).toHaveLength(6);
289 for (const color of result) {
290 expect(color).toMatch(HSL_PATTERN);
291 expect(getSaturation(color)).toBeGreaterThan(0);
292 }
293 });
294
295 it('generateAnalogous produces different results on repeated calls', () => {
296 const results = new Set(
297 Array.from({ length: 10 }, () => generateAnalogous(BLACK)[0]),
298 );
299 expect(results.size).toBeGreaterThan(1);
300 });
301
302 it('generateSaturations returns 8 chromatic colors for black', () => {
303 const result = generateSaturations(BLACK);
304 expect(result).toHaveLength(8);
305 for (const color of result) {
306 expect(color).toMatch(HSL_PATTERN);
307 expect(getSaturation(color)).toBeGreaterThan(0);
308 }
309 });
310
311 it('generateSaturations uses normal behavior for mid-lightness low-sat gray', () => {
312 const result = generateSaturations('hsl(0, 0%, 50%)');
313 expect(result).toHaveLength(8);
314 for (const color of result) {
315 expect(color).toMatch(/^hsl\(0, \d+%, 50%\)$/);
316 }
317 });
318
319 it('generateSaturations produces different results on repeated calls for black', () => {
320 const results = new Set(
321 Array.from({ length: 10 }, () => generateSaturations(BLACK)[0]),
322 );
323 expect(results.size).toBeGreaterThan(1);
324 });
325 });
326});