Skip to content

Commit 35246fa

Browse files
committed
test: imgdl function
1 parent 4985282 commit 35246fa

File tree

3 files changed

+180
-96
lines changed

3 files changed

+180
-96
lines changed

src/index.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,8 @@ async function imgdl(
105105
const images: Image[] = [];
106106
const countNames = new Map<string, number>();
107107

108-
url.forEach((u) => {
109-
const img = parseImageParams(u, options);
108+
url.forEach((_url) => {
109+
const img = parseImageParams(_url, options);
110110

111111
// Make sure the name is unique
112112
const nameKey = `${img.name}.${img.extension}`;
@@ -130,11 +130,13 @@ async function imgdl(
130130
signal,
131131
});
132132
} catch (error) {
133-
if (error instanceof Error) {
134-
options?.onError?.(error, u);
135-
return undefined;
136-
}
137-
throw error;
133+
options?.onError?.(
134+
error instanceof Error
135+
? error
136+
: new Error('Unknown error', { cause: error }),
137+
_url,
138+
);
139+
return undefined;
138140
}
139141
},
140142
{ signal: options?.signal },
@@ -164,6 +166,7 @@ async function imgdl(
164166
});
165167
}
166168

169+
// TODO: implement `onSuccess` and `onError` for single download
167170
return download(parseImageParams(url, options), {
168171
...options,
169172
signal: options?.signal,

test/fixtures/mocks/handlers.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import fs from 'fs';
22
import { http, HttpResponse } from 'msw';
33
import path from 'path';
44

5-
const DEFAULT_IMAGE_NAME = '200x300';
5+
const DEFAULT_IMAGE_NAME = 'image';
66
const DEFAULT_IMAGE_EXTENSION = 'jpg';
77

88
export const BASE_URL = 'https://example.com';
@@ -19,8 +19,6 @@ export const handlers = [
1919
imageName = imageName[0];
2020
}
2121

22-
imageName = path.basename(imageName);
23-
2422
// Use DEFAULT_IMAGE_NAME if the image name begins with 'img-' followed by a number.
2523
// either with or without an extension. For example, 'img-1', 'img-1.jpg', 'img-20.webp' are valid.
2624
const regex = /^img-[0-9]+(\.[a-zA-Z]+)?$/;

test/index.spec.ts

Lines changed: 169 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,106 +1,189 @@
1-
import fs from 'node:fs';
2-
import { describe, expect, test, vi } from 'vitest';
3-
import { DEFAULT_EXTENSION, DEFAULT_NAME } from '~/constanta.js';
4-
import imgdl from '~/index.js';
1+
import fs from 'node:fs/promises';
2+
import {
3+
afterAll,
4+
afterEach,
5+
beforeAll,
6+
describe,
7+
expect,
8+
it,
9+
vi,
10+
} from 'vitest';
11+
import imgdl, { Options } from '~/index.js';
12+
import { server } from './fixtures/mocks/node.js';
13+
import { BASE_URL } from './fixtures/mocks/handlers.js';
14+
import * as downloader from '~/downloader.js';
15+
import path from 'node:path';
516

6-
describe.skip('`imgdl()`', () => {
7-
test('single', async () => {
8-
const url = 'https://picsum.photos/200/300.webp';
9-
const expectedFilePath = `${process.cwd()}/300.webp`;
17+
type OnError = Exclude<Options['onError'], undefined>;
1018

11-
expect((await imgdl(url)).path).toEqual(expectedFilePath);
12-
expect(fs.existsSync(expectedFilePath)).toBe(true); // Ensure the image is actually exists
19+
describe('`imgdl`', () => {
20+
/**
21+
* The directory to save the downloaded images.
22+
*/
23+
const directory = 'test/tmp';
1324

14-
// Cleanup
15-
fs.unlinkSync(expectedFilePath);
25+
beforeAll(() => server.listen());
26+
27+
afterEach(async () => {
28+
server.resetHandlers();
29+
30+
// Clean up downloaded images
31+
await fs.rm(directory, { force: true, recursive: true });
1632
});
1733

18-
describe('multiple', () => {
19-
const testUrls = [
20-
'https://picsum.photos/200/300.webp',
21-
'https://picsum.photos/200/300',
22-
];
23-
const expectedNames = ['300.webp', `${DEFAULT_NAME}.${DEFAULT_EXTENSION}`];
24-
25-
test('only array of `url`s', async () => {
26-
const expectedFilePaths = expectedNames.map(
27-
(n) => `${process.cwd()}/${n}`,
28-
);
29-
const images = await imgdl(testUrls);
30-
31-
expect(images.map((img) => img.path).sort()).toEqual(
32-
expectedFilePaths.sort(),
33-
);
34-
expectedFilePaths.forEach((filepath) => {
35-
expect(fs.existsSync(filepath)).toBe(true); // Ensure the image is actually exists
36-
fs.unlinkSync(filepath); // Cleanup
37-
});
34+
afterAll(() => server.close());
35+
36+
it('should download an image if single URL is provided', async () => {
37+
const url = `${BASE_URL}/image.jpg`;
38+
const image = await imgdl(url, { directory });
39+
40+
expect(image).toStrictEqual({
41+
url,
42+
name: 'image',
43+
extension: 'jpg',
44+
directory,
45+
originalName: 'image',
46+
originalExtension: 'jpg',
47+
path: path.resolve(directory, 'image.jpg'),
3848
});
49+
await expect(fs.access(image.path)).resolves.not.toThrow();
50+
});
3951

40-
test('with `directory` argument', async () => {
41-
const directory = 'test/tmp';
42-
const expectedFilePaths = expectedNames.map(
43-
(n) => `${process.cwd()}/${directory}/${n}`,
44-
);
45-
const images = await imgdl(testUrls, { directory });
46-
47-
expect(images.map((img) => img.path).sort()).toEqual(
48-
expectedFilePaths.sort(),
49-
);
50-
expectedFilePaths.forEach((filepath) => {
51-
expect(fs.existsSync(filepath)).toBe(true); // Ensure the image is actually exists
52-
fs.unlinkSync(filepath); // Cleanup
53-
});
52+
it('should download an image if single URL is provided with options', async () => {
53+
const parseImageParamsSpy = vi.spyOn(downloader, 'parseImageParams');
54+
const url = `${BASE_URL}/image.jpg`;
55+
const options = {
56+
directory: directory + '/images',
57+
extension: 'png',
58+
name: 'myimage',
59+
};
60+
const image = await imgdl(url, options);
61+
62+
expect(parseImageParamsSpy).toHaveBeenCalledOnce();
63+
expect(parseImageParamsSpy).toHaveBeenCalledWith(url, options);
64+
expect(image).toStrictEqual({
65+
...options,
66+
url,
67+
originalName: 'image',
68+
originalExtension: 'jpg',
69+
path: path.resolve(options.directory, 'myimage.png'),
5470
});
71+
await expect(fs.access(image.path)).resolves.not.toThrow();
72+
});
73+
74+
// Update the `imgdl` function first before activate this test!
75+
it.todo(
76+
'should not throw any error if URL is invalid, call onError instead',
77+
async () => {
78+
const url = `${BASE_URL}/unknown`;
79+
const onError = vi.fn<Parameters<OnError>>();
80+
81+
await expect(imgdl(url, { onError })).resolves.not.toThrow();
82+
expect(onError).toHaveBeenCalledTimes(1);
83+
},
84+
);
85+
86+
it('should download multiple images if array of URLs is provided', async () => {
87+
const urls = [`${BASE_URL}/img-1.jpg`, `${BASE_URL}/img-2.jpg`];
88+
const downloadSpy = vi.spyOn(downloader, 'download');
89+
const onError = vi
90+
.fn<Parameters<OnError>>()
91+
.mockImplementation((err, url) => console.error(url, err.message));
92+
93+
const images = await imgdl(urls, { directory, onError });
94+
95+
expect(downloadSpy).toHaveBeenCalledTimes(2);
96+
expect(onError).toHaveBeenCalledTimes(0);
97+
expect(images).is.an('array').and.toHaveLength(2);
5598

56-
test('with `name` argument', async () => {
57-
const expectedFilePaths = [
58-
'asset.webp',
59-
`asset.${DEFAULT_EXTENSION}`,
60-
].map((n) => `${process.cwd()}/${n}`);
61-
const images = await imgdl(testUrls, { name: 'asset' });
62-
63-
expect(images.map((img) => img.path).sort()).toEqual(
64-
expectedFilePaths.sort(),
65-
);
66-
expectedFilePaths.forEach((filepath) => {
67-
expect(fs.existsSync(filepath)).toBe(true); // Ensure the image is actually exists
68-
fs.unlinkSync(filepath); // Cleanup
99+
images.forEach(async (img, i) => {
100+
expect(img).toStrictEqual({
101+
url: urls[i],
102+
originalName: `img-${i + 1}`,
103+
originalExtension: 'jpg',
104+
directory,
105+
name: `img-${i + 1}`,
106+
extension: 'jpg',
107+
path: path.resolve(directory, `img-${i + 1}.jpg`),
69108
});
109+
await expect(fs.access(img.path)).resolves.not.toThrow();
70110
});
111+
});
71112

72-
test('with `onSuccess` argument', async () => {
73-
const expectedFilePaths = expectedNames.map(
74-
(n) => `${process.cwd()}/${n}`,
75-
);
76-
let downloadCount = 0;
77-
const onSuccess = vi.fn().mockImplementation(() => {
78-
downloadCount += 1;
79-
});
80-
const images = await imgdl(testUrls, { onSuccess });
113+
it('should not throw any error if one of the URLs is invalid, call onError instead', async () => {
114+
const urls = [`${BASE_URL}/img-1.jpg`, `${BASE_URL}/unknown`];
115+
const onError = vi.fn<Parameters<OnError>>();
116+
117+
await expect(imgdl(urls, { directory, onError })).resolves.toHaveLength(1);
118+
expect(onError).toHaveBeenCalledTimes(1);
119+
120+
// The first image should be downloaded
121+
await expect(
122+
fs.access(path.resolve(directory, 'img-1.jpg')),
123+
).resolves.not.toThrow();
124+
});
125+
126+
it('should download multiple images if array of URLs is provided with options', async () => {
127+
const urls = [`${BASE_URL}/img-1.jpg`, `${BASE_URL}/img-2.jpg`];
128+
const parseImageParamsSpy = vi.spyOn(downloader, 'parseImageParams');
129+
const onError = vi
130+
.fn<Parameters<OnError>>()
131+
.mockImplementation((err, url) => console.error(url, err.message));
132+
const onSuccess = vi.fn();
133+
const options = {
134+
directory,
135+
extension: 'png',
136+
name: 'myimage',
137+
onError,
138+
onSuccess,
139+
};
140+
const images = await imgdl(urls, options);
141+
142+
expect(parseImageParamsSpy).toHaveBeenCalledTimes(2);
143+
expect(onError).toHaveBeenCalledTimes(0);
144+
expect(onSuccess).toHaveBeenCalledTimes(2);
145+
expect(images).is.an('array').and.toHaveLength(2);
81146

82-
expect(images.map((img) => img.path).sort()).toEqual(
83-
expectedFilePaths.sort(),
84-
);
85-
expect(onSuccess).toHaveBeenCalledTimes(2);
86-
expect(downloadCount).toEqual(2);
147+
images.forEach(async (img, i) => {
148+
expect(parseImageParamsSpy).toHaveBeenCalledWith(urls[i], options);
87149

88-
expectedFilePaths.forEach((filepath) => {
89-
expect(fs.existsSync(filepath)).toBe(true); // Ensure the image is actually exists
90-
fs.unlinkSync(filepath); // Cleanup
150+
const name = `${options.name}${i === 0 ? '' : ` (${i})`}`;
151+
expect(img).toStrictEqual({
152+
url: urls[i],
153+
originalName: `img-${i + 1}`,
154+
originalExtension: 'jpg',
155+
directory,
156+
name,
157+
extension: options.extension,
158+
path: path.resolve(directory, `${name}.png`),
91159
});
160+
await expect(fs.access(img.path)).resolves.not.toThrow();
92161
});
162+
});
93163

94-
test('with `onError` argument', async () => {
95-
let errorCount = 0;
96-
const onError = vi.fn().mockImplementation(() => {
97-
errorCount += 1;
98-
});
99-
const images = await imgdl(['invalid-url1', 'invalid-url2'], { onError });
164+
it('should abort download if signal is aborted', async () => {
165+
// 30 images
166+
const urls = Array.from(
167+
{ length: 30 },
168+
(_, i) => `${BASE_URL}/img-${i}.jpg`,
169+
);
170+
const controller = new AbortController();
100171

101-
expect(onError).toHaveBeenCalledTimes(2);
102-
expect(errorCount).toEqual(2);
103-
expect(images).toEqual([]);
104-
});
172+
// Abort after 100ms
173+
setTimeout(() => controller.abort(), 100);
174+
175+
await expect(
176+
imgdl(urls, { directory, signal: controller.signal }),
177+
).rejects.toThrow(/aborted/);
178+
179+
// First image should be downloaded
180+
await expect(
181+
fs.access(path.resolve(directory, 'img-0.jpg')),
182+
).resolves.not.toThrow();
183+
184+
// The last image should not be downloaded
185+
await expect(
186+
fs.access(path.resolve(directory, 'img-30.jpg')),
187+
).rejects.toThrow();
105188
});
106189
});

0 commit comments

Comments
 (0)