@douglasneuroinformatics/libui 2.5.3 → 2.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/components/Form/NumberField/NumberFieldRadio.d.ts +1 -1
- package/dist/components/Form/NumberField/NumberFieldRadio.d.ts.map +1 -1
- package/dist/components/Form/NumberField/NumberFieldRadio.js +7 -4
- package/dist/components/Form/NumberField/NumberFieldSelect.d.ts +1 -1
- package/dist/components/Form/NumberField/NumberFieldSelect.d.ts.map +1 -1
- package/dist/components/Form/NumberField/NumberFieldSelect.js +6 -2
- package/dist/douglasneuroinformatics-libui-2.6.1.tgz +0 -0
- package/dist/i18n.js +1 -1
- package/package.json +16 -14
- package/src/components/Accordion/Accordion.spec.tsx +37 -0
- package/src/components/Accordion/Accordion.stories.tsx +35 -0
- package/src/components/ActionDropdown/ActionDropdown.stories.tsx +17 -0
- package/src/components/AlertDialog/AlertDialog.stories.tsx +35 -0
- package/src/components/ArrowToggle/ArrowToggle.spec.tsx +49 -0
- package/src/components/ArrowToggle/ArrowToggle.stories.tsx +27 -0
- package/src/components/Avatar/Avatar.spec.tsx +26 -0
- package/src/components/Avatar/Avatar.stories.tsx +20 -0
- package/src/components/Badge/Badge.spec.tsx +19 -0
- package/src/components/Badge/Badge.stories.tsx +13 -0
- package/src/components/Breadcrumb/Breadcrumb.stories.tsx +44 -0
- package/src/components/Button/Button.spec.tsx +27 -0
- package/src/components/Button/Button.stories.tsx +63 -0
- package/src/components/Card/Card.spec.tsx +19 -0
- package/src/components/Card/Card.stories.tsx +56 -0
- package/src/components/Checkbox/Checkbox.spec.tsx +28 -0
- package/src/components/Checkbox/Checkbox.stories.tsx +34 -0
- package/src/components/ClientTable/ClientTable.stories.tsx +126 -0
- package/src/components/Collapsible/Collapsible.stories.tsx +45 -0
- package/src/components/Command/Command.stories.tsx +55 -0
- package/src/components/ContextMenu/ContextMenu.stories.tsx +61 -0
- package/src/components/DatePicker/DatePicker.stories.tsx +15 -0
- package/src/components/Dialog/Dialog.stories.tsx +44 -0
- package/src/components/Drawer/Drawer.stories.tsx +37 -0
- package/src/components/DropdownButton/DropdownButton.stories.tsx +13 -0
- package/src/components/DropdownMenu/DropdownMenu.stories.tsx +78 -0
- package/src/components/ErrorFallback/ErrorFallback.stories.tsx +9 -0
- package/src/components/Form/BooleanField/BooleanField.spec.tsx +35 -0
- package/src/components/Form/BooleanField/BooleanField.stories.tsx +49 -0
- package/src/components/Form/DateField/DateField.spec.tsx +99 -0
- package/src/components/Form/DateField/DateField.stories.tsx +28 -0
- package/src/components/Form/Form.stories.tsx +360 -0
- package/src/components/Form/Form.test.tsx +119 -0
- package/src/components/Form/NumberField/NumberField.stories.tsx +103 -0
- package/src/components/Form/NumberField/NumberFieldRadio.tsx +16 -8
- package/src/components/Form/NumberField/NumberFieldSelect.tsx +10 -6
- package/src/components/Form/SetField/SetField.stories.tsx +63 -0
- package/src/components/Form/StringField/StringField.stories.tsx +94 -0
- package/src/components/Heading/Heading.stories.tsx +37 -0
- package/src/components/HoverCard/HoverCard.stories.tsx +40 -0
- package/src/components/Input/Input.spec.tsx +23 -0
- package/src/components/Input/Input.stories.tsx +9 -0
- package/src/components/Label/Label.spec.tsx +17 -0
- package/src/components/Label/Label.stories.tsx +13 -0
- package/src/components/LanguageToggle/LanguageToggle.stories.tsx +17 -0
- package/src/components/LineGraph/LineGraph.stories.tsx +81 -0
- package/src/components/ListboxDropdown/ListboxDropdown.stories.tsx +44 -0
- package/src/components/MenuBar/MenuBar.stories.tsx +101 -0
- package/src/components/NotificationHub/NotificationHub.stories.tsx +41 -0
- package/src/components/Pagination/Pagination.stories.tsx +41 -0
- package/src/components/Popover/Popover.spec.tsx +24 -0
- package/src/components/Popover/Popover.stories.tsx +72 -0
- package/src/components/Progress/Progress.stories.tsx +24 -0
- package/src/components/RadioGroup/RadioGroup.spec.tsx +42 -0
- package/src/components/RadioGroup/RadioGroup.stories.tsx +35 -0
- package/src/components/Resizable/Resizable.stories.tsx +39 -0
- package/src/components/ScrollArea/ScrollArea.spec.tsx +19 -0
- package/src/components/ScrollArea/ScrollArea.stories.tsx +23 -0
- package/src/components/SearchBar/SearchBar.stories.tsx +11 -0
- package/src/components/Select/Select.stories.tsx +31 -0
- package/src/components/Separator/Separator.spec.tsx +19 -0
- package/src/components/Separator/Separator.stories.tsx +30 -0
- package/src/components/Sheet/Sheet.stories.tsx +49 -0
- package/src/components/Slider/Slider.stories.tsx +9 -0
- package/src/components/Spinner/Spinner.stories.tsx +14 -0
- package/src/components/SpinnerIcon/SpinnerIcon.stories.tsx +9 -0
- package/src/components/Switch/Switch.stories.tsx +21 -0
- package/src/components/Table/Table.stories.tsx +88 -0
- package/src/components/Tabs/Tabs.stories.tsx +70 -0
- package/src/components/TextArea/TextArea.spec.tsx +23 -0
- package/src/components/TextArea/TextArea.stories.tsx +13 -0
- package/src/components/ThemeToggle/ThemeToggle.stories.tsx +9 -0
- package/src/components/Tooltip/Tooltip.stories.tsx +44 -0
- package/src/hooks/useDownload.test.ts +66 -0
- package/src/hooks/useEventCallback.test.tsx +22 -0
- package/src/hooks/useEventListener.test.tsx +120 -0
- package/src/hooks/useInterval.test.ts +58 -0
- package/src/hooks/useIsomorphicLayoutEffect.test.ts +27 -0
- package/src/hooks/useMediaQuery.test.ts +33 -0
- package/src/hooks/useNotificationsStore.test.ts +30 -0
- package/src/hooks/useOnClickOutside.test.ts +59 -0
- package/src/hooks/useSessionStorage.test.ts +186 -0
- package/src/hooks/useTheme.test.ts +74 -0
- package/src/hooks/useWindowSize.test.ts +57 -0
- package/src/i18n.ts +1 -1
- package/tailwind.config.cjs +4 -1
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { Label } from '@radix-ui/react-label';
|
|
4
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
5
|
+
|
|
6
|
+
import { Switch } from './Switch.js';
|
|
7
|
+
|
|
8
|
+
type Story = StoryObj<typeof Switch>;
|
|
9
|
+
|
|
10
|
+
export default { component: Switch, tags: ['autodocs'] } as Meta<typeof Switch>;
|
|
11
|
+
|
|
12
|
+
export const Default: Story = {
|
|
13
|
+
decorators: [
|
|
14
|
+
(Story) => (
|
|
15
|
+
<div className="flex items-center space-x-2">
|
|
16
|
+
<Story args={{ id: 'airplane-mode' }} />
|
|
17
|
+
<Label htmlFor="airplane-mode">Airplane Mode</Label>
|
|
18
|
+
</div>
|
|
19
|
+
)
|
|
20
|
+
]
|
|
21
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
4
|
+
|
|
5
|
+
import { Table } from './Table.js';
|
|
6
|
+
|
|
7
|
+
const invoices = [
|
|
8
|
+
{
|
|
9
|
+
invoice: 'INV001',
|
|
10
|
+
paymentMethod: 'Credit Card',
|
|
11
|
+
paymentStatus: 'Paid',
|
|
12
|
+
totalAmount: '$250.00'
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
invoice: 'INV002',
|
|
16
|
+
paymentMethod: 'PayPal',
|
|
17
|
+
paymentStatus: 'Pending',
|
|
18
|
+
totalAmount: '$150.00'
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
invoice: 'INV003',
|
|
22
|
+
paymentMethod: 'Bank Transfer',
|
|
23
|
+
paymentStatus: 'Unpaid',
|
|
24
|
+
totalAmount: '$350.00'
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
invoice: 'INV004',
|
|
28
|
+
paymentMethod: 'Credit Card',
|
|
29
|
+
paymentStatus: 'Paid',
|
|
30
|
+
totalAmount: '$450.00'
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
invoice: 'INV005',
|
|
34
|
+
paymentMethod: 'PayPal',
|
|
35
|
+
paymentStatus: 'Paid',
|
|
36
|
+
totalAmount: '$550.00'
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
invoice: 'INV006',
|
|
40
|
+
paymentMethod: 'Bank Transfer',
|
|
41
|
+
paymentStatus: 'Pending',
|
|
42
|
+
totalAmount: '$200.00'
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
invoice: 'INV007',
|
|
46
|
+
paymentMethod: 'Credit Card',
|
|
47
|
+
paymentStatus: 'Unpaid',
|
|
48
|
+
totalAmount: '$300.00'
|
|
49
|
+
}
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
type Story = StoryObj<typeof Table>;
|
|
53
|
+
|
|
54
|
+
export default { component: Table } as Meta<typeof Table>;
|
|
55
|
+
|
|
56
|
+
export const Default: Story = {
|
|
57
|
+
args: {
|
|
58
|
+
children: (
|
|
59
|
+
<React.Fragment>
|
|
60
|
+
<Table.Caption>A list of your recent invoices.</Table.Caption>
|
|
61
|
+
<Table.Header>
|
|
62
|
+
<Table.Row>
|
|
63
|
+
<Table.Head className="w-[100px]">Invoice</Table.Head>
|
|
64
|
+
<Table.Head>Status</Table.Head>
|
|
65
|
+
<Table.Head>Method</Table.Head>
|
|
66
|
+
<Table.Head className="text-right">Amount</Table.Head>
|
|
67
|
+
</Table.Row>
|
|
68
|
+
</Table.Header>
|
|
69
|
+
<Table.Body>
|
|
70
|
+
{invoices.map((invoice) => (
|
|
71
|
+
<Table.Row key={invoice.invoice}>
|
|
72
|
+
<Table.Cell className="font-medium">{invoice.invoice}</Table.Cell>
|
|
73
|
+
<Table.Cell>{invoice.paymentStatus}</Table.Cell>
|
|
74
|
+
<Table.Cell>{invoice.paymentMethod}</Table.Cell>
|
|
75
|
+
<Table.Cell className="text-right">{invoice.totalAmount}</Table.Cell>
|
|
76
|
+
</Table.Row>
|
|
77
|
+
))}
|
|
78
|
+
</Table.Body>
|
|
79
|
+
<Table.Footer>
|
|
80
|
+
<Table.Row>
|
|
81
|
+
<Table.Cell colSpan={3}>Total</Table.Cell>
|
|
82
|
+
<Table.Cell className="text-right">$2,500.00</Table.Cell>
|
|
83
|
+
</Table.Row>
|
|
84
|
+
</Table.Footer>
|
|
85
|
+
</React.Fragment>
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
4
|
+
|
|
5
|
+
import { Button } from '../Button/Button.js';
|
|
6
|
+
import { Card } from '../Card/Card.js';
|
|
7
|
+
import { Input } from '../Input/Input.js';
|
|
8
|
+
import { Label } from '../Label/Label.js';
|
|
9
|
+
import { Tabs } from './Tabs.js';
|
|
10
|
+
|
|
11
|
+
type Story = StoryObj<typeof Tabs>;
|
|
12
|
+
|
|
13
|
+
export default { component: Tabs, tags: ['autodocs'] } as Meta<typeof Tabs>;
|
|
14
|
+
|
|
15
|
+
export const Default: Story = {
|
|
16
|
+
args: {
|
|
17
|
+
children: (
|
|
18
|
+
<>
|
|
19
|
+
<Tabs.List className="grid w-full grid-cols-2">
|
|
20
|
+
<Tabs.Trigger value="account">Account</Tabs.Trigger>
|
|
21
|
+
<Tabs.Trigger value="password">Password</Tabs.Trigger>
|
|
22
|
+
</Tabs.List>
|
|
23
|
+
<Tabs.Content value="account">
|
|
24
|
+
<Card>
|
|
25
|
+
<Card.Header>
|
|
26
|
+
<Card.Title>Account</Card.Title>
|
|
27
|
+
<Card.Description>Make changes to your account here. Click save when you are done.</Card.Description>
|
|
28
|
+
</Card.Header>
|
|
29
|
+
<Card.Content className="space-y-2">
|
|
30
|
+
<div className="space-y-1">
|
|
31
|
+
<Label htmlFor="name">Name</Label>
|
|
32
|
+
<Input defaultValue="Pedro Duarte" id="name" />
|
|
33
|
+
</div>
|
|
34
|
+
<div className="space-y-1">
|
|
35
|
+
<Label htmlFor="username">Username</Label>
|
|
36
|
+
<Input defaultValue="@peduarte" id="username" />
|
|
37
|
+
</div>
|
|
38
|
+
</Card.Content>
|
|
39
|
+
<Card.Footer>
|
|
40
|
+
<Button>Save changes</Button>
|
|
41
|
+
</Card.Footer>
|
|
42
|
+
</Card>
|
|
43
|
+
</Tabs.Content>
|
|
44
|
+
<Tabs.Content value="password">
|
|
45
|
+
<Card>
|
|
46
|
+
<Card.Header>
|
|
47
|
+
<Card.Title>Password</Card.Title>
|
|
48
|
+
<Card.Description>Change your password here. After saving, you will be logged out.</Card.Description>
|
|
49
|
+
</Card.Header>
|
|
50
|
+
<Card.Content className="space-y-2">
|
|
51
|
+
<div className="space-y-1">
|
|
52
|
+
<Label htmlFor="current">Current password</Label>
|
|
53
|
+
<Input id="current" type="password" />
|
|
54
|
+
</div>
|
|
55
|
+
<div className="space-y-1">
|
|
56
|
+
<Label htmlFor="new">New password</Label>
|
|
57
|
+
<Input id="new" type="password" />
|
|
58
|
+
</div>
|
|
59
|
+
</Card.Content>
|
|
60
|
+
<Card.Footer>
|
|
61
|
+
<Button>Save password</Button>
|
|
62
|
+
</Card.Footer>
|
|
63
|
+
</Card>
|
|
64
|
+
</Tabs.Content>
|
|
65
|
+
</>
|
|
66
|
+
),
|
|
67
|
+
className: 'w-[400px]',
|
|
68
|
+
defaultValue: 'account'
|
|
69
|
+
}
|
|
70
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { render, screen } from '@testing-library/react';
|
|
4
|
+
import { userEvent } from '@testing-library/user-event';
|
|
5
|
+
import { describe, expect, it } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { TextArea } from './TextArea.js';
|
|
8
|
+
|
|
9
|
+
const TEST_ID = 'text-area';
|
|
10
|
+
|
|
11
|
+
describe('TextArea ', () => {
|
|
12
|
+
it('should render', () => {
|
|
13
|
+
render(<TextArea />);
|
|
14
|
+
const input = screen.getByTestId(TEST_ID);
|
|
15
|
+
expect(input).toBeInTheDocument();
|
|
16
|
+
});
|
|
17
|
+
it('should allow typing', async () => {
|
|
18
|
+
render(<TextArea />);
|
|
19
|
+
const input = screen.getByTestId(TEST_ID);
|
|
20
|
+
await userEvent.type(input, 'hello world');
|
|
21
|
+
expect(input).toHaveDisplayValue('hello world');
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
|
|
3
|
+
import { TextArea } from './TextArea.js';
|
|
4
|
+
|
|
5
|
+
type Story = StoryObj<typeof TextArea>;
|
|
6
|
+
|
|
7
|
+
export default { component: TextArea, tags: ['autodocs'] } as Meta<typeof TextArea>;
|
|
8
|
+
|
|
9
|
+
export const Default: Story = {
|
|
10
|
+
args: {
|
|
11
|
+
placeholder: 'Type your message here.'
|
|
12
|
+
}
|
|
13
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
|
|
3
|
+
import { ThemeToggle } from './ThemeToggle.js';
|
|
4
|
+
|
|
5
|
+
type Story = StoryObj<typeof ThemeToggle>;
|
|
6
|
+
|
|
7
|
+
export default { component: ThemeToggle, tags: ['autodocs'] } as Meta<typeof ThemeToggle>;
|
|
8
|
+
|
|
9
|
+
export const Default: Story = {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
4
|
+
import { DownloadIcon } from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
import { Tooltip } from './Tooltip.js';
|
|
7
|
+
|
|
8
|
+
type Story = StoryObj<typeof Tooltip>;
|
|
9
|
+
|
|
10
|
+
export default {
|
|
11
|
+
component: Tooltip,
|
|
12
|
+
parameters: {
|
|
13
|
+
layout: 'centered'
|
|
14
|
+
},
|
|
15
|
+
tags: ['autodocs']
|
|
16
|
+
} as Meta<typeof Tooltip>;
|
|
17
|
+
|
|
18
|
+
export const Outline: Story = {
|
|
19
|
+
args: {
|
|
20
|
+
children: (
|
|
21
|
+
<React.Fragment>
|
|
22
|
+
<Tooltip.Trigger>Hover</Tooltip.Trigger>
|
|
23
|
+
<Tooltip.Content>
|
|
24
|
+
<p>Add to library</p>
|
|
25
|
+
</Tooltip.Content>
|
|
26
|
+
</React.Fragment>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const Icon: Story = {
|
|
32
|
+
args: {
|
|
33
|
+
children: (
|
|
34
|
+
<React.Fragment>
|
|
35
|
+
<Tooltip.Trigger size="icon">
|
|
36
|
+
<DownloadIcon />
|
|
37
|
+
</Tooltip.Trigger>
|
|
38
|
+
<Tooltip.Content>
|
|
39
|
+
<p>Add to library</p>
|
|
40
|
+
</Tooltip.Content>
|
|
41
|
+
</React.Fragment>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { act, renderHook } from '@testing-library/react';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
|
|
6
|
+
import { useDownload } from './useDownload.js';
|
|
7
|
+
|
|
8
|
+
const mockNotificationsStore = {
|
|
9
|
+
addNotification: vi.fn()
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
vi.mock('./useNotificationsStore', () => ({
|
|
13
|
+
useNotificationsStore: () => mockNotificationsStore
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
describe('useDownload', () => {
|
|
17
|
+
let download: ReturnType<typeof useDownload>;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
vi.spyOn(document, 'createElement');
|
|
21
|
+
vi.spyOn(React, 'useState');
|
|
22
|
+
const { result } = renderHook(() => useDownload());
|
|
23
|
+
download = result.current;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
vi.clearAllMocks();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should render', () => {
|
|
31
|
+
expect(download).toBeDefined();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should invoke the fetch data function', async () => {
|
|
35
|
+
const fetchData = vi.fn(() => 'hello world');
|
|
36
|
+
await download('hello.txt', fetchData);
|
|
37
|
+
expect(fetchData).toHaveBeenCalledOnce();
|
|
38
|
+
});
|
|
39
|
+
it('should attempt at add a notification if the fetch data function throws an error', async () => {
|
|
40
|
+
await act(() =>
|
|
41
|
+
download('hello.txt', () => {
|
|
42
|
+
throw new Error('An error occurred!');
|
|
43
|
+
})
|
|
44
|
+
);
|
|
45
|
+
expect(mockNotificationsStore.addNotification).toHaveBeenCalledOnce();
|
|
46
|
+
expect(mockNotificationsStore.addNotification.mock.lastCall[0]).toMatchObject({ message: 'An error occurred!' });
|
|
47
|
+
});
|
|
48
|
+
it('should attempt at add a notification if the fetch data function throws a non-error', async () => {
|
|
49
|
+
await act(() =>
|
|
50
|
+
download('hello.txt', () => {
|
|
51
|
+
throw NaN;
|
|
52
|
+
})
|
|
53
|
+
);
|
|
54
|
+
expect(mockNotificationsStore.addNotification).toHaveBeenCalledOnce();
|
|
55
|
+
});
|
|
56
|
+
it('should attempt to create one anchor element', async () => {
|
|
57
|
+
await act(() => download('hello.txt', () => 'hello world'));
|
|
58
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
59
|
+
expect(document.createElement).toHaveBeenLastCalledWith('a');
|
|
60
|
+
});
|
|
61
|
+
it('should invoke the fetch data a gather an image', async () => {
|
|
62
|
+
const fetchData = vi.fn(() => new Blob());
|
|
63
|
+
await download('testdiv.png', fetchData, { blobType: 'image/png' });
|
|
64
|
+
expect(fetchData).toHaveBeenCalledOnce();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { fireEvent, render, renderHook, screen } from '@testing-library/react';
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
5
|
+
|
|
6
|
+
import { useEventCallback } from './useEventCallback.js';
|
|
7
|
+
|
|
8
|
+
describe('useEventCallback', () => {
|
|
9
|
+
it('should not call the callback during render', () => {
|
|
10
|
+
const fn = vi.fn();
|
|
11
|
+
const { result } = renderHook(() => useEventCallback(fn));
|
|
12
|
+
render(<button onClick={result.current}>Click me</button>);
|
|
13
|
+
expect(fn).not.toHaveBeenCalled();
|
|
14
|
+
});
|
|
15
|
+
it('should call the callback when the event is triggered', () => {
|
|
16
|
+
const fn = vi.fn();
|
|
17
|
+
const { result } = renderHook(() => useEventCallback(fn));
|
|
18
|
+
render(<button onClick={result.current}>Click me</button>);
|
|
19
|
+
fireEvent.click(screen.getByText('Click me'));
|
|
20
|
+
expect(fn).toHaveBeenCalled();
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { fireEvent, renderHook } from '@testing-library/react';
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { useEventListener } from './useEventListener.js';
|
|
5
|
+
|
|
6
|
+
describe('useEventListener', () => {
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
vi.clearAllMocks();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should bind/unbind the event listener to the window when element is not provided', () => {
|
|
12
|
+
const eventName: keyof WindowEventMap = 'load';
|
|
13
|
+
const handler = vi.fn();
|
|
14
|
+
const options = undefined;
|
|
15
|
+
|
|
16
|
+
vi.spyOn(window, 'addEventListener');
|
|
17
|
+
vi.spyOn(window, 'removeEventListener');
|
|
18
|
+
|
|
19
|
+
const { unmount } = renderHook(() => {
|
|
20
|
+
useEventListener(eventName, handler);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
expect(window.addEventListener).toHaveBeenCalledWith(eventName, expect.any(Function), options);
|
|
24
|
+
unmount();
|
|
25
|
+
expect(window.removeEventListener).toHaveBeenCalledWith(eventName, expect.any(Function), options);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should bind/unbind the event listener to the element when element is provided', () => {
|
|
29
|
+
const eventName: keyof HTMLElementEventMap = 'mouseenter';
|
|
30
|
+
const handler = vi.fn();
|
|
31
|
+
const ref = { current: document.createElement('div') };
|
|
32
|
+
const options = undefined;
|
|
33
|
+
|
|
34
|
+
vi.spyOn(ref.current, 'addEventListener');
|
|
35
|
+
vi.spyOn(ref.current, 'removeEventListener');
|
|
36
|
+
|
|
37
|
+
const { unmount } = renderHook(() => {
|
|
38
|
+
useEventListener(eventName, handler, ref, options);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
42
|
+
expect(ref.current.addEventListener).toHaveBeenCalledWith(eventName, expect.any(Function), options);
|
|
43
|
+
unmount();
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
45
|
+
expect(ref.current.removeEventListener).toHaveBeenCalledWith(eventName, expect.any(Function), options);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should bind/unbind the event listener to the document when document is provided', () => {
|
|
49
|
+
const eventName: keyof DocumentEventMap = 'load';
|
|
50
|
+
const handler = vi.fn();
|
|
51
|
+
const ref = { current: document };
|
|
52
|
+
const options = undefined;
|
|
53
|
+
|
|
54
|
+
vi.spyOn(document, 'addEventListener');
|
|
55
|
+
vi.spyOn(document, 'removeEventListener');
|
|
56
|
+
|
|
57
|
+
const { unmount } = renderHook(() => {
|
|
58
|
+
useEventListener(eventName, handler, ref, options);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
62
|
+
expect(document.addEventListener).toHaveBeenCalledWith(eventName, expect.any(Function), options);
|
|
63
|
+
unmount();
|
|
64
|
+
// eslint-disable-next-line @typescript-eslint/unbound-method
|
|
65
|
+
expect(document.removeEventListener).toHaveBeenCalledWith(eventName, expect.any(Function), options);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should pass the options to the event listener', () => {
|
|
69
|
+
const eventName: keyof WindowEventMap = 'load';
|
|
70
|
+
const handler = vi.fn();
|
|
71
|
+
const options = {
|
|
72
|
+
capture: true,
|
|
73
|
+
once: true,
|
|
74
|
+
passive: true
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
vi.spyOn(window, 'addEventListener');
|
|
78
|
+
vi.spyOn(window, 'removeEventListener');
|
|
79
|
+
|
|
80
|
+
renderHook(() => {
|
|
81
|
+
useEventListener(eventName, handler, undefined, options);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
expect(window.addEventListener).toHaveBeenCalledWith(eventName, expect.any(Function), options);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should call the event listener handler when the event is triggered', () => {
|
|
88
|
+
const eventName: keyof HTMLElementEventMap = 'click';
|
|
89
|
+
const handler = vi.fn();
|
|
90
|
+
const ref = { current: document.createElement('button') };
|
|
91
|
+
|
|
92
|
+
renderHook(() => {
|
|
93
|
+
useEventListener(eventName, handler, ref);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
fireEvent.click(ref.current);
|
|
97
|
+
|
|
98
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should have the correct event type', () => {
|
|
102
|
+
const ref = { current: document.createElement('button') };
|
|
103
|
+
|
|
104
|
+
const clickHandler = vi.fn();
|
|
105
|
+
const keydownHandler = vi.fn();
|
|
106
|
+
|
|
107
|
+
renderHook(() => {
|
|
108
|
+
useEventListener('click', clickHandler, ref);
|
|
109
|
+
});
|
|
110
|
+
renderHook(() => {
|
|
111
|
+
useEventListener('keydown', keydownHandler, ref);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
fireEvent.click(ref.current);
|
|
115
|
+
fireEvent.keyDown(ref.current);
|
|
116
|
+
|
|
117
|
+
expect(clickHandler).toHaveBeenCalledWith(expect.any(MouseEvent));
|
|
118
|
+
expect(keydownHandler).toHaveBeenCalledWith(expect.any(KeyboardEvent));
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { renderHook } from '@testing-library/react';
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { useInterval } from './useInterval.js';
|
|
5
|
+
|
|
6
|
+
describe('useInterval()', () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
vi.clearAllMocks();
|
|
9
|
+
vi.useFakeTimers();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should fire the callback function (1)', () => {
|
|
13
|
+
const timeout = 500;
|
|
14
|
+
const callback = vi.fn();
|
|
15
|
+
renderHook(() => {
|
|
16
|
+
useInterval(callback, timeout);
|
|
17
|
+
});
|
|
18
|
+
vi.advanceTimersByTime(timeout);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should fire the callback function (2)', () => {
|
|
22
|
+
const timeout = 500;
|
|
23
|
+
const earlyTimeout = 400;
|
|
24
|
+
const callback = vi.fn();
|
|
25
|
+
renderHook(() => {
|
|
26
|
+
useInterval(callback, timeout);
|
|
27
|
+
});
|
|
28
|
+
vi.advanceTimersByTime(earlyTimeout);
|
|
29
|
+
expect(callback).not.toHaveBeenCalled();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should call set interval on start', () => {
|
|
33
|
+
mockSetInterval();
|
|
34
|
+
const timeout = 1200;
|
|
35
|
+
const callback = vi.fn();
|
|
36
|
+
renderHook(() => {
|
|
37
|
+
useInterval(callback, timeout);
|
|
38
|
+
});
|
|
39
|
+
expect(setInterval).toHaveBeenCalledWith(expect.any(Function), timeout);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should call clearTimeout on unmount', () => {
|
|
43
|
+
mockClearInterval();
|
|
44
|
+
const callback = vi.fn();
|
|
45
|
+
const { unmount } = renderHook(() => {
|
|
46
|
+
useInterval(callback, 1200);
|
|
47
|
+
});
|
|
48
|
+
unmount();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
function mockSetInterval() {
|
|
53
|
+
vi.spyOn(global, 'setInterval');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function mockClearInterval() {
|
|
57
|
+
vi.spyOn(global, 'clearInterval');
|
|
58
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useEffect, useLayoutEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
|
|
5
|
+
const isBrowser = vi.fn();
|
|
6
|
+
|
|
7
|
+
vi.mock('../utils.js', () => ({ isBrowser }));
|
|
8
|
+
|
|
9
|
+
describe('useIsomorphicLayoutEffect', () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
vi.resetModules();
|
|
12
|
+
});
|
|
13
|
+
describe('browser', () => {
|
|
14
|
+
it('should return useLayoutEffect', async () => {
|
|
15
|
+
isBrowser.mockReturnValueOnce(true);
|
|
16
|
+
const { useIsomorphicLayoutEffect } = await import('./useIsomorphicLayoutEffect.js');
|
|
17
|
+
expect(useIsomorphicLayoutEffect).toBe(useLayoutEffect);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
describe('server', () => {
|
|
21
|
+
it('should be useEffect', async () => {
|
|
22
|
+
isBrowser.mockReturnValueOnce(false);
|
|
23
|
+
const { useIsomorphicLayoutEffect } = await import('./useIsomorphicLayoutEffect.js');
|
|
24
|
+
expect(useIsomorphicLayoutEffect).toBe(useEffect);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { renderHook } from '@testing-library/react';
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { mockMatchMedia } from '../testing/mocks.js';
|
|
5
|
+
import { isBrowser } from '../utils.js';
|
|
6
|
+
import { useMediaQuery } from './useMediaQuery.js';
|
|
7
|
+
|
|
8
|
+
vi.mock('../utils.js', () => ({ isBrowser: vi.fn(() => true) }));
|
|
9
|
+
|
|
10
|
+
describe('useMediaQuery', () => {
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
vi.spyOn(window, 'matchMedia');
|
|
13
|
+
vi.restoreAllMocks();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should return false if running on the server', () => {
|
|
17
|
+
vi.mocked(isBrowser).mockReturnValue(false);
|
|
18
|
+
const { result } = renderHook(() => useMediaQuery('(min-width: 768px)'));
|
|
19
|
+
expect(result.current).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should return false if running in the browser, if the media query is false', () => {
|
|
23
|
+
mockMatchMedia(false);
|
|
24
|
+
const { result } = renderHook(() => useMediaQuery('(min-width: 768px)'));
|
|
25
|
+
expect(result.current).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should return true if running in the browser, if the media query is true', () => {
|
|
29
|
+
mockMatchMedia(true);
|
|
30
|
+
const { result } = renderHook(() => useMediaQuery('(min-width: 768px)'));
|
|
31
|
+
expect(result.current).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { act, renderHook } from '@testing-library/react';
|
|
2
|
+
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import * as zustand from 'zustand';
|
|
4
|
+
|
|
5
|
+
import { useNotificationsStore } from './useNotificationsStore.js';
|
|
6
|
+
|
|
7
|
+
describe('useNotificationsStore', () => {
|
|
8
|
+
beforeAll(() => {
|
|
9
|
+
vi.spyOn(zustand, 'create');
|
|
10
|
+
});
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
vi.clearAllMocks();
|
|
13
|
+
});
|
|
14
|
+
it('should render and return an object', () => {
|
|
15
|
+
const { result } = renderHook(() => useNotificationsStore());
|
|
16
|
+
expect(result.current).toBeTypeOf('object');
|
|
17
|
+
});
|
|
18
|
+
it('should add and dismiss notifications', () => {
|
|
19
|
+
const { result } = renderHook(() => useNotificationsStore());
|
|
20
|
+
act(() => {
|
|
21
|
+
result.current.addNotification({ message: 'test', type: 'info' });
|
|
22
|
+
});
|
|
23
|
+
expect(result.current.notifications.length).toBe(1);
|
|
24
|
+
expect(result.current.notifications[0]).toMatchObject({ message: 'test' });
|
|
25
|
+
act(() => {
|
|
26
|
+
result.current.dismissNotification(result.current.notifications[0].id);
|
|
27
|
+
});
|
|
28
|
+
expect(result.current.notifications.length).toBe(0);
|
|
29
|
+
});
|
|
30
|
+
});
|