Skip to content

Commit 40bab06

Browse files
committed
feat(image-loader): add comprehensive unit tests for loaders
Introduced detailed unit tests for `BaseImageLoader` and all specific loaders (`AWSCloudFrontLoader`, `CloudinaryLoader`, `ImgixLoader`, and `SupabaseLoader`) to ensure robust validation, transformation, and error handling. Refactored `default-loaders.ts` and `types.ts` for improved readability, consistency, and alignment with test implementation. Enhanced testing coverage significantly to validate cases like malformed URLs, default parameters, and query handling.
1 parent c86ed9b commit 40bab06

File tree

8 files changed

+770
-13
lines changed

8 files changed

+770
-13
lines changed

.junie/guidelines.md

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -99,16 +99,10 @@ pnpm test
9999

100100
1. Create test files with `.test.ts` or `.spec.ts` extension
101101
2. Place tests alongside source files in `src/` directory
102-
3. Import test utilities from `@jest/globals`:
103-
```typescript
104-
import { describe, expect, it } from "@jest/globals";
105-
```
106102

107103
### Test Example
108104

109105
```typescript
110-
import { describe, expect, it } from "@jest/globals";
111-
112106
describe("Feature Name", () => {
113107
it("should demonstrate basic functionality", () => {
114108
const result = someFunction();
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
import type { ImageLoaderProps } from "next/image";
2+
3+
import { BaseImageLoader } from "@/base-loader";
4+
5+
// Concrete test implementation of BaseImageLoader for testing
6+
class TestImageLoader extends BaseImageLoader {
7+
public getName(): string {
8+
return "test";
9+
}
10+
11+
public canHandle(source: string): boolean {
12+
return source.includes("test.com");
13+
}
14+
15+
// Expose protected methods for testing
16+
public testBuildQueryParams(params: Record<string, number | string | undefined>): string {
17+
return this.buildQueryParams(params);
18+
}
19+
20+
public testEnsureProtocol(url: string, defaultProtocol?: string): string {
21+
return this.ensureProtocol(url, defaultProtocol);
22+
}
23+
24+
public testExtractDomain(url: string): string {
25+
return this.extractDomain(url);
26+
}
27+
28+
public testValidateConfig(config: ImageLoaderProps): void {
29+
this.validateConfig(config);
30+
}
31+
32+
public testNormalizeConfig(config: ImageLoaderProps): ImageLoaderProps {
33+
return this.normalizeConfig(config);
34+
}
35+
36+
protected transformUrl(config: ImageLoaderProps): string {
37+
return config.src;
38+
}
39+
}
40+
41+
describe("BaseImageLoader", () => {
42+
const loader = new TestImageLoader();
43+
const loaderWithCustomQuality = new TestImageLoader({ defaultQuality: 90 });
44+
45+
describe("constructor", () => {
46+
it("should use default quality of 75 when no config provided", () => {
47+
const result = loader.testNormalizeConfig({
48+
src: "https://test.com/image.jpg",
49+
width: 800,
50+
});
51+
52+
expect(result.quality).toBe(75);
53+
});
54+
55+
it("should use custom default quality when provided in config", () => {
56+
const result = loaderWithCustomQuality.testNormalizeConfig({
57+
src: "https://test.com/image.jpg",
58+
width: 800,
59+
});
60+
61+
expect(result.quality).toBe(90);
62+
});
63+
});
64+
65+
describe("buildQueryParams", () => {
66+
it("should build query string from parameters", () => {
67+
const result = loader.testBuildQueryParams({
68+
format: "auto",
69+
quality: 80,
70+
width: 800,
71+
});
72+
73+
expect(result).toBe("?format=auto&quality=80&width=800");
74+
});
75+
76+
it("should handle undefined parameters", () => {
77+
const result = loader.testBuildQueryParams({
78+
format: "auto",
79+
quality: undefined,
80+
width: 800,
81+
});
82+
83+
expect(result).toBe("?format=auto&width=800");
84+
});
85+
86+
it("should return empty string when no parameters", () => {
87+
const result = loader.testBuildQueryParams({});
88+
89+
expect(result).toBe("");
90+
});
91+
92+
it("should return empty string when all parameters are undefined", () => {
93+
const result = loader.testBuildQueryParams({
94+
quality: undefined,
95+
width: undefined,
96+
});
97+
98+
expect(result).toBe("");
99+
});
100+
});
101+
102+
describe("ensureProtocol", () => {
103+
it("should add https protocol to URLs starting with //", () => {
104+
const result = loader.testEnsureProtocol("//example.com/image.jpg");
105+
106+
expect(result).toBe("https://example.com/image.jpg");
107+
});
108+
109+
it("should add custom protocol to URLs starting with //", () => {
110+
const result = loader.testEnsureProtocol("//example.com/image.jpg", "http");
111+
112+
expect(result).toBe("http://example.com/image.jpg");
113+
});
114+
115+
it("should add https protocol to URLs without protocol", () => {
116+
const result = loader.testEnsureProtocol("example.com/image.jpg");
117+
118+
expect(result).toBe("https://example.com/image.jpg");
119+
});
120+
121+
it("should add custom protocol to URLs without protocol", () => {
122+
const result = loader.testEnsureProtocol("example.com/image.jpg", "http");
123+
124+
expect(result).toBe("http://example.com/image.jpg");
125+
});
126+
127+
it("should not modify URLs that already have http protocol", () => {
128+
const url = "http://example.com/image.jpg";
129+
const result = loader.testEnsureProtocol(url);
130+
131+
expect(result).toBe(url);
132+
});
133+
134+
it("should not modify URLs that already have https protocol", () => {
135+
const url = "https://example.com/image.jpg";
136+
const result = loader.testEnsureProtocol(url);
137+
138+
expect(result).toBe(url);
139+
});
140+
});
141+
142+
describe("extractDomain", () => {
143+
it("should extract domain from valid URL", () => {
144+
const result = loader.testExtractDomain("https://example.com/path/image.jpg");
145+
146+
expect(result).toBe("example.com");
147+
});
148+
149+
it("should extract domain and convert to lowercase", () => {
150+
const result = loader.testExtractDomain("https://EXAMPLE.COM/path/image.jpg");
151+
152+
expect(result).toBe("example.com");
153+
});
154+
155+
it("should return empty string for invalid URL", () => {
156+
const result = loader.testExtractDomain("not-a-valid-url");
157+
158+
expect(result).toBe("");
159+
});
160+
});
161+
162+
describe("validateConfig", () => {
163+
it("should pass validation for valid config", () => {
164+
expect(() => {
165+
loader.testValidateConfig({
166+
quality: 80,
167+
src: "https://test.com/image.jpg",
168+
width: 800,
169+
});
170+
}).not.toThrow();
171+
});
172+
173+
it("should throw error for empty src", () => {
174+
expect(() => {
175+
loader.testValidateConfig({
176+
src: "",
177+
width: 800,
178+
});
179+
}).toThrow("Image source URL is required");
180+
});
181+
182+
it("should throw error for zero width", () => {
183+
expect(() => {
184+
loader.testValidateConfig({
185+
src: "https://test.com/image.jpg",
186+
width: 0,
187+
});
188+
}).toThrow("Image width must be a positive number");
189+
});
190+
191+
it("should throw error for negative width", () => {
192+
expect(() => {
193+
loader.testValidateConfig({
194+
src: "https://test.com/image.jpg",
195+
width: -100,
196+
});
197+
}).toThrow("Image width must be a positive number");
198+
});
199+
200+
it("should throw error for quality below 1", () => {
201+
expect(() => {
202+
loader.testValidateConfig({
203+
quality: 0,
204+
src: "https://test.com/image.jpg",
205+
width: 800,
206+
});
207+
}).toThrow("Image quality must be between 1 and 100");
208+
});
209+
210+
it("should throw error for quality above 100", () => {
211+
expect(() => {
212+
loader.testValidateConfig({
213+
quality: 101,
214+
src: "https://test.com/image.jpg",
215+
width: 800,
216+
});
217+
}).toThrow("Image quality must be between 1 and 100");
218+
});
219+
});
220+
221+
describe("normalizeConfig", () => {
222+
it("should apply default quality when not specified", () => {
223+
const result = loader.testNormalizeConfig({
224+
src: "https://test.com/image.jpg",
225+
width: 800,
226+
});
227+
228+
expect(result.quality).toBe(75);
229+
});
230+
231+
it("should preserve specified quality", () => {
232+
const result = loader.testNormalizeConfig({
233+
quality: 90,
234+
src: "https://test.com/image.jpg",
235+
width: 800,
236+
});
237+
238+
expect(result.quality).toBe(90);
239+
});
240+
241+
it("should preserve other config properties", () => {
242+
const config = {
243+
quality: 85,
244+
src: "https://test.com/image.jpg",
245+
width: 800,
246+
};
247+
const result = loader.testNormalizeConfig(config);
248+
249+
expect(result.src).toBe(config.src);
250+
expect(result.width).toBe(config.width);
251+
expect(result.quality).toBe(config.quality);
252+
});
253+
});
254+
255+
describe("load", () => {
256+
it("should call validation, normalization, and transformation", () => {
257+
const result = loader.load({
258+
quality: 80,
259+
src: "https://test.com/image.jpg",
260+
width: 800,
261+
});
262+
263+
expect(result).toBe("https://test.com/image.jpg");
264+
});
265+
});
266+
});

packages/image-loader/src/default-loaders.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ImageLoaderFactory } from "@/loader-factory";
2-
import { UnsplashLoader, CloudinaryLoader, ImgixLoader, AWSCloudFrontLoader, SupabaseLoader } from "@/loaders";
2+
import { AWSCloudFrontLoader, CloudinaryLoader, ImgixLoader, SupabaseLoader, UnsplashLoader } from "@/loaders";
33

44
/**
55
* Registers all default image loaders to the factory

0 commit comments

Comments
 (0)