Skip to content

Commit b3551a1

Browse files
authored
Merge pull request #44 from DouglasNeuroInformatics/fix-bugs
bug fixes
2 parents 92ad86a + 234d854 commit b3551a1

File tree

5 files changed

+48
-36
lines changed

5 files changed

+48
-36
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
"zod": "^3.23.6"
7070
},
7171
"dependencies": {
72-
"@douglasneuroinformatics/libjs": "^1.1.0",
72+
"@douglasneuroinformatics/libjs": "^1.2.0",
7373
"@douglasneuroinformatics/libui-form-types": "^0.11.0",
7474
"@radix-ui/react-accordion": "^1.2.1",
7575
"@radix-ui/react-alert-dialog": "^1.1.2",

pnpm-lock.yaml

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/Form/DateField/DateField.spec.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { toBasicISOString } from '@douglasneuroinformatics/libjs';
1+
import { toBasicISOString, toLocalISOString } from '@douglasneuroinformatics/libjs';
22
import { getByText, render, screen } from '@testing-library/react';
33
import { userEvent } from '@testing-library/user-event';
4-
import { beforeEach, describe, expect, it, vi } from 'vitest';
4+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
55

66
import { DateField } from './DateField';
77

@@ -10,6 +10,12 @@ describe('DateField', () => {
1010
const setValue = vi.fn();
1111

1212
beforeEach(() => {
13+
vi.useFakeTimers({ toFake: ['Date'] });
14+
vi.setSystemTime(new Date(2025, 0, 1, 22));
15+
});
16+
17+
afterEach(() => {
18+
vi.useRealTimers();
1319
vi.clearAllMocks();
1420
});
1521

@@ -78,14 +84,14 @@ describe('DateField', () => {
7884
datepicker = screen.getByTestId('datepicker');
7985
await userEvent.click(getByText(datepicker, '1'));
8086
expectedDate = new Date(new Date().setDate(1));
81-
expectedDateString = toBasicISOString(expectedDate);
87+
expectedDateString = toLocalISOString(expectedDate).split('T')[0]!;
8288
expect(toBasicISOString(setValue.mock.lastCall?.[0])).toBe(expectedDateString);
8389

8490
await userEvent.click(input);
8591
datepicker = screen.getByTestId('datepicker');
8692
await userEvent.click(getByText(datepicker, '2'));
8793
expectedDate = new Date(new Date().setDate(2));
88-
expectedDateString = toBasicISOString(expectedDate);
94+
expectedDateString = toLocalISOString(expectedDate).split('T')[0]!;
8995
expect(toBasicISOString(setValue.mock.lastCall?.[0])).toBe(expectedDateString);
9096
});
9197
it('should render the value provided as a prop', () => {

src/hooks/useDownload/useDownload.test.ts

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import React from 'react';
2-
31
import { act, renderHook } from '@testing-library/react';
42
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
53

@@ -17,8 +15,6 @@ describe('useDownload', () => {
1715
let download: ReturnType<typeof useDownload>;
1816

1917
beforeEach(() => {
20-
vi.spyOn(document, 'createElement');
21-
vi.spyOn(React, 'useState');
2218
const { result } = renderHook(() => useDownload());
2319
download = result.current;
2420
});
@@ -27,15 +23,12 @@ describe('useDownload', () => {
2723
vi.clearAllMocks();
2824
});
2925

30-
it('should render', () => {
31-
expect(download).toBeDefined();
32-
});
33-
3426
it('should invoke the fetch data function', async () => {
3527
const fetchData = vi.fn(() => 'hello world');
3628
await download('hello.txt', fetchData);
3729
expect(fetchData).toHaveBeenCalledOnce();
3830
});
31+
3932
it('should attempt at add a notification if the fetch data function throws an error', async () => {
4033
await act(() =>
4134
download('hello.txt', () => {
@@ -45,6 +38,7 @@ describe('useDownload', () => {
4538
expect(mockNotificationsStore.addNotification).toHaveBeenCalledOnce();
4639
expect(mockNotificationsStore.addNotification.mock.lastCall?.[0]).toMatchObject({ message: 'An error occurred!' });
4740
});
41+
4842
it('should attempt at add a notification if the fetch data function throws a non-error', async () => {
4943
await act(() =>
5044
download('hello.txt', () => {
@@ -54,11 +48,19 @@ describe('useDownload', () => {
5448
);
5549
expect(mockNotificationsStore.addNotification).toHaveBeenCalledOnce();
5650
});
57-
it('should attempt to create one anchor element', async () => {
58-
await act(() => download('hello.txt', () => 'hello world'));
59-
// eslint-disable-next-line @typescript-eslint/unbound-method
60-
expect(document.createElement).toHaveBeenLastCalledWith('a');
51+
52+
it('should invoke HTMLAnchorElement.prototype.click once', async () => {
53+
const click = vi.spyOn(HTMLAnchorElement.prototype, 'click');
54+
await act(() => download('hello.txt', 'hello world'));
55+
expect(click).toHaveBeenCalledOnce();
56+
});
57+
58+
it('should allow multiple simultaneous downloads', async () => {
59+
const click = vi.spyOn(HTMLAnchorElement.prototype, 'click');
60+
await act(() => Promise.all([download('foo.txt', 'foo'), download('bar.txt', 'bar')]));
61+
expect(click).toHaveBeenCalledTimes(2);
6162
});
63+
6264
it('should invoke the fetch data a gather an image', async () => {
6365
const fetchData = vi.fn(() => new Blob());
6466
await download('testdiv.png', fetchData, { blobType: 'image/png' });

src/hooks/useDownload/useDownload.ts

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,43 +20,47 @@ interface DownloadFunction {
2020
(filename: string, data: () => Promisable<string>, options?: DownloadTextOptions): Promise<void>;
2121
}
2222

23+
type Downloadable = {
24+
blobType: string;
25+
data: Blob | string;
26+
filename: string;
27+
id: string;
28+
};
29+
2330
/**
2431
* Used to trigger downloads of arbitrary data to the client
2532
* @returns A function to invoke the download
2633
*/
2734
export function useDownload(): DownloadFunction {
2835
const notifications = useNotificationsStore();
29-
const [state, setState] = useState<{
30-
blobType: string;
31-
data: Blob | string;
32-
filename: string;
33-
} | null>(null);
36+
const [downloads, setDownloads] = useState<Downloadable[]>([]);
3437

3538
useEffect(() => {
36-
if (state) {
37-
const { blobType, data, filename } = state;
39+
if (downloads.length) {
40+
const { blobType, data, filename, id } = downloads.at(-1)!;
3841
const anchor = document.createElement('a');
3942
document.body.appendChild(anchor);
40-
4143
const blob = new Blob([data], { type: blobType });
42-
4344
const url = URL.createObjectURL(blob);
4445
anchor.href = url;
4546
anchor.download = filename;
4647
anchor.click();
4748
URL.revokeObjectURL(url);
4849
anchor.remove();
49-
setState(null);
50+
setDownloads((prevDownloads) => prevDownloads.filter((item) => item.id !== id));
5051
}
51-
}, [state]);
52+
}, [downloads]);
5253

5354
return async (filename, _data, options) => {
5455
try {
5556
const data = typeof _data === 'function' ? await _data() : _data;
5657
if (typeof data !== 'string' && !options?.blobType) {
5758
throw new Error("argument 'blobType' must be defined when download is called with a Blob object");
5859
}
59-
setState({ blobType: options?.blobType ?? 'text/plain', data, filename });
60+
setDownloads((prevDownloads) => [
61+
...prevDownloads,
62+
{ blobType: options?.blobType ?? 'text/plain', data, filename, id: crypto.randomUUID() }
63+
]);
6064
} catch (error) {
6165
const message = error instanceof Error ? error.message : 'An unknown error occurred';
6266
notifications.addNotification({

0 commit comments

Comments
 (0)