@reshape-biotech/design-system 2.4.0 → 2.4.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 +0 -3
- package/dist/components/activity/Activity.test.d.ts +1 -0
- package/dist/components/activity/Activity.test.js +89 -0
- package/dist/components/avatar/Avatar.test.d.ts +1 -0
- package/dist/components/avatar/Avatar.test.js +55 -0
- package/dist/components/button/Button.test.d.ts +1 -0
- package/dist/components/button/Button.test.js +117 -0
- package/dist/components/button/Button.test.svelte +5 -0
- package/dist/components/button/Button.test.svelte.d.ts +19 -0
- package/dist/components/checkbox/Checkbox.test.d.ts +1 -0
- package/dist/components/checkbox/Checkbox.test.js +51 -0
- package/dist/components/graphs/matrix/Matrix.test.d.ts +1 -0
- package/dist/components/graphs/matrix/Matrix.test.js +256 -0
- package/dist/components/graphs/matrix/Matrix.test.svelte +49 -0
- package/dist/components/graphs/matrix/Matrix.test.svelte.d.ts +4 -0
- package/dist/components/graphs/utils/tooltipFormatter.d.ts +1 -0
- package/dist/components/graphs/utils/tooltipFormatter.js +18 -1
- package/dist/components/icons/index.d.ts +1 -1
- package/dist/components/icons/index.js +4 -0
- package/dist/components/input/Input.test.d.ts +1 -0
- package/dist/components/input/Input.test.js +35 -0
- package/dist/components/label/Label.test.d.ts +1 -0
- package/dist/components/label/Label.test.js +29 -0
- package/dist/components/manual-cfu-counter/test/ManualCFUCounter.test.d.ts +1 -0
- package/dist/components/manual-cfu-counter/test/ManualCFUCounter.test.js +319 -0
- package/dist/components/multi-cfu-counter/test/MultiCFUCounter.test.d.ts +1 -0
- package/dist/components/multi-cfu-counter/test/MultiCFUCounter.test.js +320 -0
- package/dist/components/sjsf-wrappers/SjsfNumberInputWrapper.svelte.d.ts +1 -1
- package/dist/components/sjsf-wrappers/SjsfSelectWidgetWrapper.svelte +77 -0
- package/dist/components/sjsf-wrappers/SjsfSelectWidgetWrapper.svelte.d.ts +3 -0
- package/dist/components/sjsf-wrappers/SjsfTextInputWrapper.svelte.d.ts +1 -1
- package/dist/components/sjsf-wrappers/sjsfCustomTheme.js +4 -0
- package/dist/components/slider/Slider.stories.svelte +5 -0
- package/dist/components/slider/Slider.svelte +7 -1
- package/dist/components/slider/Slider.svelte.d.ts +1 -0
- package/dist/components/stat-card/StatCard.test.d.ts +1 -0
- package/dist/components/stat-card/StatCard.test.js +54 -0
- package/dist/components/table/Table.test.d.ts +1 -0
- package/dist/components/table/Table.test.js +97 -0
- package/dist/components/table/Table.test.svelte +37 -0
- package/dist/components/table/Table.test.svelte.d.ts +12 -0
- package/dist/components/textarea/Textarea.test.d.ts +1 -0
- package/dist/components/textarea/Textarea.test.js +90 -0
- package/dist/components/toggle-icon-button/ToggleIconButton.test.d.ts +1 -0
- package/dist/components/toggle-icon-button/ToggleIconButton.test.js +100 -0
- package/dist/components/tooltip/Tooltip.test.d.ts +1 -0
- package/dist/components/tooltip/Tooltip.test.js +81 -0
- package/dist/notifications.d.ts +4 -1
- package/package.json +189 -189
package/README.md
CHANGED
|
@@ -22,19 +22,16 @@ This package contains the core design system for Reshape Biotech frontend projec
|
|
|
22
22
|
### Contents
|
|
23
23
|
|
|
24
24
|
1. **Component Library**
|
|
25
|
-
|
|
26
25
|
- A collection of shared, reusable Svelte components
|
|
27
26
|
- Standardized UI elements following Reshape Biotech's design guidelines
|
|
28
27
|
- Fully typed components with TypeScript support
|
|
29
28
|
|
|
30
29
|
2. **Tailwind Configuration**
|
|
31
|
-
|
|
32
30
|
- Pre-configured Tailwind CSS setup
|
|
33
31
|
- Custom theme extensions
|
|
34
32
|
- Shared utility classes
|
|
35
33
|
|
|
36
34
|
3. **DaisyUI**
|
|
37
|
-
|
|
38
35
|
- Pre-configured DaisyUI setup
|
|
39
36
|
- Custom theme extensions
|
|
40
37
|
- Shared utility classes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { render, screen } from '@testing-library/svelte';
|
|
3
|
+
import Activity from './Activity.svelte';
|
|
4
|
+
import { Settings } from 'luxon';
|
|
5
|
+
describe('Activity Component', () => {
|
|
6
|
+
// Store original values to restore after tests
|
|
7
|
+
const originalNavigatorLanguage = navigator.language;
|
|
8
|
+
const originalDefaultZone = Settings.defaultZone;
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
// Reset navigator.language before each test
|
|
11
|
+
Object.defineProperty(navigator, 'language', {
|
|
12
|
+
value: 'en-US',
|
|
13
|
+
configurable: true,
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
// Restore original settings
|
|
18
|
+
Settings.defaultZone = originalDefaultZone;
|
|
19
|
+
Object.defineProperty(navigator, 'language', {
|
|
20
|
+
value: originalNavigatorLanguage,
|
|
21
|
+
configurable: true,
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
it('formats valid ISO timestamp correctly in UTC timezone with en-US locale', () => {
|
|
25
|
+
// Mock timezone to UTC
|
|
26
|
+
Settings.defaultZone = 'UTC';
|
|
27
|
+
const isoTimestamp = '2023-04-15T14:30:00Z';
|
|
28
|
+
const activity = {
|
|
29
|
+
icon: 'add',
|
|
30
|
+
label: 'Test',
|
|
31
|
+
timestamp: isoTimestamp,
|
|
32
|
+
};
|
|
33
|
+
render(Activity, { props: { activity } });
|
|
34
|
+
// For en-US locale in UTC timezone, Apr 15, 2023, 2:30 PM
|
|
35
|
+
expect(screen.getByText('Apr 15, 2023, 2:30 PM')).toBeInTheDocument();
|
|
36
|
+
});
|
|
37
|
+
it('formats valid ISO timestamp correctly in London timezone with en-GB locale', () => {
|
|
38
|
+
// Mock timezone to London
|
|
39
|
+
Settings.defaultZone = 'Europe/London';
|
|
40
|
+
// Mock locale to en-GB
|
|
41
|
+
Object.defineProperty(navigator, 'language', {
|
|
42
|
+
value: 'en-GB',
|
|
43
|
+
configurable: true,
|
|
44
|
+
});
|
|
45
|
+
const isoTimestamp = '2023-04-15T14:30:00Z';
|
|
46
|
+
const activity = {
|
|
47
|
+
icon: 'add',
|
|
48
|
+
label: 'Test',
|
|
49
|
+
timestamp: isoTimestamp,
|
|
50
|
+
};
|
|
51
|
+
render(Activity, { props: { activity } });
|
|
52
|
+
// For en-GB locale in London timezone (BST in April), 15 Apr 2023, 15:30
|
|
53
|
+
expect(screen.getByText('15 Apr 2023, 15:30')).toBeInTheDocument();
|
|
54
|
+
});
|
|
55
|
+
it('returns original string when timestamp is completely invalid', () => {
|
|
56
|
+
const invalidTimestamp = 'not-a-date-at-all';
|
|
57
|
+
const activity = {
|
|
58
|
+
icon: 'add',
|
|
59
|
+
label: 'Test',
|
|
60
|
+
timestamp: invalidTimestamp,
|
|
61
|
+
};
|
|
62
|
+
render(Activity, { props: { activity } });
|
|
63
|
+
expect(screen.getByText(invalidTimestamp)).toBeInTheDocument();
|
|
64
|
+
});
|
|
65
|
+
it('returns original string when timestamp looks like a date but is not ISO format', () => {
|
|
66
|
+
// This looks like a date but isn't in ISO format
|
|
67
|
+
const nonIsoTimestamp = '24 Feb 2025, 10:10';
|
|
68
|
+
const activity = {
|
|
69
|
+
icon: 'add',
|
|
70
|
+
label: 'Test',
|
|
71
|
+
timestamp: nonIsoTimestamp,
|
|
72
|
+
};
|
|
73
|
+
render(Activity, { props: { activity } });
|
|
74
|
+
// Should show the original string since our component only handles ISO format
|
|
75
|
+
expect(screen.getByText(nonIsoTimestamp)).toBeInTheDocument();
|
|
76
|
+
});
|
|
77
|
+
it('returns original string for malformed ISO-like timestamps', () => {
|
|
78
|
+
// This looks like ISO but is missing the T
|
|
79
|
+
const malformedIsoTimestamp = '2023-04-15 14:30:00Z';
|
|
80
|
+
const activity = {
|
|
81
|
+
icon: 'add',
|
|
82
|
+
label: 'Test',
|
|
83
|
+
timestamp: malformedIsoTimestamp,
|
|
84
|
+
};
|
|
85
|
+
render(Activity, { props: { activity } });
|
|
86
|
+
// Should show the original string since it's not valid ISO
|
|
87
|
+
expect(screen.getByText(malformedIsoTimestamp)).toBeInTheDocument();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { render, screen } from '@testing-library/svelte';
|
|
3
|
+
import Avatar from './Avatar.svelte';
|
|
4
|
+
describe('Avatar Component', () => {
|
|
5
|
+
it('renders with default props (md size, tooltip shown)', () => {
|
|
6
|
+
render(Avatar, { props: { name: 'Test User' } });
|
|
7
|
+
const avatarDiv = screen.getByText('TU').closest('div');
|
|
8
|
+
expect(avatarDiv).toBeInTheDocument();
|
|
9
|
+
expect(avatarDiv).toHaveClass('h-8 w-8'); // md size class
|
|
10
|
+
// Check for tooltip trigger (the avatar itself)
|
|
11
|
+
const tooltipTrigger = screen.getByText('TU');
|
|
12
|
+
expect(tooltipTrigger).toBeInTheDocument();
|
|
13
|
+
});
|
|
14
|
+
it('renders with sm size', () => {
|
|
15
|
+
render(Avatar, { props: { name: 'Test User', size: 'sm' } });
|
|
16
|
+
const avatarDiv = screen.getByText('TU').closest('div');
|
|
17
|
+
expect(avatarDiv).toBeInTheDocument();
|
|
18
|
+
expect(avatarDiv).toHaveClass('h-6 w-6'); // sm size class
|
|
19
|
+
});
|
|
20
|
+
it('renders without tooltip when showTooltip is false', () => {
|
|
21
|
+
render(Avatar, { props: { name: 'Test User', showTooltip: false } });
|
|
22
|
+
const avatarDiv = screen.getByText('TU').closest('div');
|
|
23
|
+
expect(avatarDiv).toBeInTheDocument();
|
|
24
|
+
const tooltipRoot = avatarDiv?.closest('rdp-tooltip');
|
|
25
|
+
expect(tooltipRoot).not.toBeInTheDocument();
|
|
26
|
+
});
|
|
27
|
+
it('calculates initials correctly for a single name', () => {
|
|
28
|
+
render(Avatar, { props: { name: 'Vilfred' } });
|
|
29
|
+
expect(screen.getByText('VI')).toBeInTheDocument();
|
|
30
|
+
});
|
|
31
|
+
it('calculates initials correctly for two names', () => {
|
|
32
|
+
render(Avatar, { props: { name: 'Vilfred Pedersen' } });
|
|
33
|
+
expect(screen.getByText('VP')).toBeInTheDocument();
|
|
34
|
+
});
|
|
35
|
+
it('calculates initials correctly for multiple names', () => {
|
|
36
|
+
render(Avatar, { props: { name: 'Vilfred Midjord Pedersen' } });
|
|
37
|
+
expect(screen.getByText('VP')).toBeInTheDocument();
|
|
38
|
+
});
|
|
39
|
+
it('calculates initials correctly for an email', () => {
|
|
40
|
+
render(Avatar, { props: { name: 'test@example.com' } });
|
|
41
|
+
expect(screen.getByText('TE')).toBeInTheDocument();
|
|
42
|
+
});
|
|
43
|
+
it('calculates initials correctly for short single name/initials', () => {
|
|
44
|
+
render(Avatar, { props: { name: 'VP' } });
|
|
45
|
+
expect(screen.getByText('VP')).toBeInTheDocument();
|
|
46
|
+
});
|
|
47
|
+
it('calculates initials correctly for null name', () => {
|
|
48
|
+
render(Avatar, { props: { name: null } });
|
|
49
|
+
expect(screen.getByText('?')).toBeInTheDocument();
|
|
50
|
+
});
|
|
51
|
+
it('calculates initials correctly for undefined name', () => {
|
|
52
|
+
render(Avatar, { props: { name: undefined } });
|
|
53
|
+
expect(screen.getByText('?')).toBeInTheDocument();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { render, fireEvent } from '@testing-library/svelte';
|
|
2
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
3
|
+
import Button from './Button.svelte';
|
|
4
|
+
// Helper component specifically for testing the default slot
|
|
5
|
+
import ButtonTest from './Button.test.svelte';
|
|
6
|
+
describe('Button Component', () => {
|
|
7
|
+
it('renders with default props', () => {
|
|
8
|
+
const { getByRole } = render(Button);
|
|
9
|
+
const button = getByRole('button');
|
|
10
|
+
expect(button).toBeInTheDocument();
|
|
11
|
+
expect(button).toHaveClass('button');
|
|
12
|
+
expect(button).toHaveClass('btn-primary'); // Default variant
|
|
13
|
+
expect(button).toHaveClass('btn-size-lg'); // Default size
|
|
14
|
+
expect(button).not.toHaveClass('rounded');
|
|
15
|
+
expect(button).not.toBeDisabled();
|
|
16
|
+
expect(button).not.toHaveClass('cursor-wait');
|
|
17
|
+
expect(button).not.toHaveClass('disabled'); // Ensure separate disabled class check
|
|
18
|
+
expect(button).toHaveAttribute('type', 'button');
|
|
19
|
+
});
|
|
20
|
+
it('renders children content provided via slot', () => {
|
|
21
|
+
// Use the helper component to render the button with slot content
|
|
22
|
+
const { getByText } = render(ButtonTest);
|
|
23
|
+
expect(getByText('Test Children')).toBeInTheDocument();
|
|
24
|
+
});
|
|
25
|
+
// Test variants
|
|
26
|
+
['primary', 'secondary', 'transparent', 'danger', 'outline'].forEach((variant) => {
|
|
27
|
+
it(`applies the correct class for variant "${variant}"`, () => {
|
|
28
|
+
const { getByRole } = render(Button, { props: { variant } });
|
|
29
|
+
const button = getByRole('button');
|
|
30
|
+
expect(button).toHaveClass(`btn-${variant}`);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
// Test inverse variants
|
|
34
|
+
['secondary-inverse', 'transparent-inverse'].forEach((variant) => {
|
|
35
|
+
it(`applies the correct class for variant "${variant}"`, () => {
|
|
36
|
+
const { getByRole } = render(Button, { props: { variant } });
|
|
37
|
+
const button = getByRole('button');
|
|
38
|
+
expect(button).toHaveClass(`btn-${variant}`);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
// Test sizes
|
|
42
|
+
['xs', 'sm', 'md', 'lg'].forEach((size) => {
|
|
43
|
+
it(`applies the correct class for size "${size}"`, () => {
|
|
44
|
+
const { getByRole } = render(Button, { props: { size } });
|
|
45
|
+
const button = getByRole('button');
|
|
46
|
+
expect(button).toHaveClass(`btn-size-${size}`);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
it('applies the rounded class when rounded is true', () => {
|
|
50
|
+
const { getByRole } = render(Button, { props: { rounded: true } });
|
|
51
|
+
const button = getByRole('button');
|
|
52
|
+
expect(button).toHaveClass('rounded');
|
|
53
|
+
});
|
|
54
|
+
it('handles click events', async () => {
|
|
55
|
+
const handleClick = vi.fn();
|
|
56
|
+
const { getByRole } = render(Button, { props: { onClick: handleClick } });
|
|
57
|
+
const button = getByRole('button');
|
|
58
|
+
await fireEvent.click(button);
|
|
59
|
+
expect(handleClick).toHaveBeenCalledTimes(1);
|
|
60
|
+
});
|
|
61
|
+
it('disables the button when disabled is true', async () => {
|
|
62
|
+
const handleClick = vi.fn();
|
|
63
|
+
const { getByRole } = render(Button, { props: { disabled: true, onClick: handleClick } });
|
|
64
|
+
const button = getByRole('button');
|
|
65
|
+
expect(button).toBeDisabled();
|
|
66
|
+
expect(button).toHaveClass('disabled'); // Check for the specific disabled class
|
|
67
|
+
await fireEvent.click(button);
|
|
68
|
+
expect(handleClick).not.toHaveBeenCalled();
|
|
69
|
+
});
|
|
70
|
+
it('shows spinner and prevents click when loading is true', async () => {
|
|
71
|
+
const handleClick = vi.fn();
|
|
72
|
+
const { getByRole, container } = render(Button, {
|
|
73
|
+
props: { loading: true, onClick: handleClick },
|
|
74
|
+
});
|
|
75
|
+
const button = getByRole('button');
|
|
76
|
+
expect(button).toHaveClass('cursor-wait');
|
|
77
|
+
// Check if Spinner is rendered by looking for the div with the 'spinner' class
|
|
78
|
+
expect(container.querySelector('div.spinner')).toBeInTheDocument();
|
|
79
|
+
await fireEvent.click(button);
|
|
80
|
+
expect(handleClick).not.toHaveBeenCalled();
|
|
81
|
+
});
|
|
82
|
+
it('applies accessibility label', () => {
|
|
83
|
+
const label = 'Click me button';
|
|
84
|
+
const { getByRole } = render(Button, { props: { accessibilityLabel: label } });
|
|
85
|
+
const button = getByRole('button');
|
|
86
|
+
expect(button).toHaveAttribute('aria-label', label);
|
|
87
|
+
});
|
|
88
|
+
it('applies custom class', () => {
|
|
89
|
+
const customClass = 'my-custom-class';
|
|
90
|
+
const { getByRole } = render(Button, { props: { class: customClass } });
|
|
91
|
+
const button = getByRole('button');
|
|
92
|
+
expect(button).toHaveClass(customClass);
|
|
93
|
+
});
|
|
94
|
+
it('applies data-testid', () => {
|
|
95
|
+
const testId = 'my-button-test-id';
|
|
96
|
+
const { getByTestId } = render(Button, { props: { dataTestId: testId } });
|
|
97
|
+
expect(getByTestId(testId)).toBeInTheDocument();
|
|
98
|
+
});
|
|
99
|
+
it('applies id', () => {
|
|
100
|
+
const testId = 'my-button-id';
|
|
101
|
+
const { getByRole } = render(Button, { props: { id: testId } });
|
|
102
|
+
const button = getByRole('button');
|
|
103
|
+
expect(button).toHaveAttribute('id', testId);
|
|
104
|
+
});
|
|
105
|
+
it('applies tabindex', () => {
|
|
106
|
+
const tabIndex = 0;
|
|
107
|
+
const { getByRole } = render(Button, { props: { tabindex: tabIndex } });
|
|
108
|
+
const button = getByRole('button');
|
|
109
|
+
expect(button).toHaveAttribute('tabindex', tabIndex.toString());
|
|
110
|
+
});
|
|
111
|
+
it('applies button type', () => {
|
|
112
|
+
const type = 'submit';
|
|
113
|
+
const { getByRole } = render(Button, { props: { type: type } });
|
|
114
|
+
const button = getByRole('button');
|
|
115
|
+
expect(button).toHaveAttribute('type', type);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import Button from './Button.svelte';
|
|
2
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
3
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
4
|
+
$$bindings?: Bindings;
|
|
5
|
+
} & Exports;
|
|
6
|
+
(internal: unknown, props: {
|
|
7
|
+
$$events?: Events;
|
|
8
|
+
$$slots?: Slots;
|
|
9
|
+
}): Exports & {
|
|
10
|
+
$set?: any;
|
|
11
|
+
$on?: any;
|
|
12
|
+
};
|
|
13
|
+
z_$$bindings?: Bindings;
|
|
14
|
+
}
|
|
15
|
+
declare const Button: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
16
|
+
[evt: string]: CustomEvent<any>;
|
|
17
|
+
}, {}, {}, string>;
|
|
18
|
+
type Button = InstanceType<typeof Button>;
|
|
19
|
+
export default Button;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { render, fireEvent } from '@testing-library/svelte';
|
|
2
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
3
|
+
import Checkbox from './Checkbox.svelte';
|
|
4
|
+
describe('Checkbox component', () => {
|
|
5
|
+
it('renders correctly', () => {
|
|
6
|
+
const { getByRole } = render(Checkbox, { props: { checked: false } });
|
|
7
|
+
expect(getByRole('checkbox')).toBeTruthy();
|
|
8
|
+
});
|
|
9
|
+
it('is unchecked by default', () => {
|
|
10
|
+
const { getByRole } = render(Checkbox);
|
|
11
|
+
expect(getByRole('checkbox')).not.toBeChecked();
|
|
12
|
+
});
|
|
13
|
+
it('is checked when checked prop is true', () => {
|
|
14
|
+
const { getByRole } = render(Checkbox, {
|
|
15
|
+
props: {
|
|
16
|
+
checked: true,
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
expect(getByRole('checkbox')).toBeChecked();
|
|
20
|
+
});
|
|
21
|
+
it('calls onChange handler when clicked', async () => {
|
|
22
|
+
const handleChange = vi.fn();
|
|
23
|
+
const { getByRole } = render(Checkbox, {
|
|
24
|
+
props: {
|
|
25
|
+
checked: false,
|
|
26
|
+
onCheckedChange: handleChange,
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
const checkbox = getByRole('checkbox');
|
|
30
|
+
await fireEvent.click(checkbox);
|
|
31
|
+
expect(handleChange).toHaveBeenCalledTimes(1);
|
|
32
|
+
});
|
|
33
|
+
it('is disabled when disabled prop is true', () => {
|
|
34
|
+
const { getByRole } = render(Checkbox, {
|
|
35
|
+
props: {
|
|
36
|
+
checked: false,
|
|
37
|
+
disabled: true,
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
const checkbox = getByRole('checkbox');
|
|
41
|
+
expect(checkbox.hasAttribute('disabled')).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
it('is indeterminate when indeterminate prop is true', () => {
|
|
44
|
+
const { getByRole } = render(Checkbox, {
|
|
45
|
+
props: {
|
|
46
|
+
indeterminate: true,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
expect(getByRole('checkbox')).toHaveAttribute('data-state', 'indeterminate');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/svelte';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import Matrix from './Matrix.svelte';
|
|
5
|
+
describe('Matrix Component', () => {
|
|
6
|
+
// Test data
|
|
7
|
+
const xAxisName = 'Predicted';
|
|
8
|
+
const yAxisName = 'Actual';
|
|
9
|
+
const confusionMatrix = [
|
|
10
|
+
[
|
|
11
|
+
{ value: 8, row: 0, col: 0 }, // TP (TNTC/TNTC)
|
|
12
|
+
{ value: 27, row: 0, col: 1 }, // FN (TNTC/Not TNTC)
|
|
13
|
+
],
|
|
14
|
+
[
|
|
15
|
+
{ value: 2, row: 1, col: 0 }, // FP (Not TNTC/TNTC)
|
|
16
|
+
{ value: 123, row: 1, col: 1 }, // TN (Not TNTC/Not TNTC)
|
|
17
|
+
],
|
|
18
|
+
];
|
|
19
|
+
const defaultRowLabels = ['TNTC', 'Not TNTC'];
|
|
20
|
+
const defaultColLabels = ['TNTC', 'Not TNTC'];
|
|
21
|
+
it('renders with default props', () => {
|
|
22
|
+
render(Matrix, {
|
|
23
|
+
props: {
|
|
24
|
+
data: confusionMatrix,
|
|
25
|
+
rowLabels: defaultRowLabels,
|
|
26
|
+
colLabels: defaultColLabels,
|
|
27
|
+
xAxisName,
|
|
28
|
+
yAxisName,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
// Check that all values are rendered
|
|
32
|
+
expect(screen.getByText('8')).toBeInTheDocument();
|
|
33
|
+
expect(screen.getByText('27')).toBeInTheDocument();
|
|
34
|
+
expect(screen.getByText('2')).toBeInTheDocument();
|
|
35
|
+
expect(screen.getByText('123')).toBeInTheDocument();
|
|
36
|
+
// Check that all labels are rendered
|
|
37
|
+
defaultRowLabels.forEach((label) => {
|
|
38
|
+
expect(screen.getAllByText(label).length).toBeGreaterThan(0);
|
|
39
|
+
});
|
|
40
|
+
defaultColLabels.forEach((label) => {
|
|
41
|
+
expect(screen.getAllByText(label).length).toBeGreaterThan(0);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
it('renders with axis titles', () => {
|
|
45
|
+
render(Matrix, {
|
|
46
|
+
props: {
|
|
47
|
+
data: confusionMatrix,
|
|
48
|
+
rowLabels: defaultRowLabels,
|
|
49
|
+
colLabels: defaultColLabels,
|
|
50
|
+
xAxisName,
|
|
51
|
+
yAxisName,
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
expect(screen.getByText(xAxisName)).toBeInTheDocument();
|
|
55
|
+
expect(screen.getByText(yAxisName)).toBeInTheDocument();
|
|
56
|
+
});
|
|
57
|
+
it('renders without axis titles when when hideAxisNames is true', () => {
|
|
58
|
+
render(Matrix, {
|
|
59
|
+
props: {
|
|
60
|
+
data: confusionMatrix,
|
|
61
|
+
rowLabels: defaultRowLabels,
|
|
62
|
+
colLabels: defaultColLabels,
|
|
63
|
+
xAxisName,
|
|
64
|
+
yAxisName,
|
|
65
|
+
hideAxisNames: true,
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
// The titles should not be in the document
|
|
69
|
+
expect(screen.queryByText('Predicted')).not.toBeInTheDocument();
|
|
70
|
+
expect(screen.queryByText('Actual')).not.toBeInTheDocument();
|
|
71
|
+
});
|
|
72
|
+
it('renders loading state correctly', () => {
|
|
73
|
+
render(Matrix, {
|
|
74
|
+
props: {
|
|
75
|
+
xAxisName,
|
|
76
|
+
yAxisName,
|
|
77
|
+
data: confusionMatrix,
|
|
78
|
+
rowLabels: defaultRowLabels,
|
|
79
|
+
colLabels: defaultColLabels,
|
|
80
|
+
loading: true,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
|
84
|
+
});
|
|
85
|
+
it('inverts y-axis when invertYAxis is true', () => {
|
|
86
|
+
// First render without inversion to capture original order
|
|
87
|
+
const { unmount } = render(Matrix, {
|
|
88
|
+
props: {
|
|
89
|
+
data: confusionMatrix,
|
|
90
|
+
rowLabels: defaultRowLabels,
|
|
91
|
+
colLabels: defaultColLabels,
|
|
92
|
+
xAxisName,
|
|
93
|
+
yAxisName,
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
// Query all cell values to determine their positions in the DOM
|
|
97
|
+
const originalCells = screen
|
|
98
|
+
.getAllByRole('button')
|
|
99
|
+
.map((button) => button.querySelector('span')?.textContent);
|
|
100
|
+
unmount();
|
|
101
|
+
// Now render with inversion
|
|
102
|
+
render(Matrix, {
|
|
103
|
+
props: {
|
|
104
|
+
data: confusionMatrix,
|
|
105
|
+
rowLabels: defaultRowLabels,
|
|
106
|
+
colLabels: defaultColLabels,
|
|
107
|
+
invertYAxis: true,
|
|
108
|
+
xAxisName,
|
|
109
|
+
yAxisName,
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
// Get the new order of cells
|
|
113
|
+
const invertedCells = screen
|
|
114
|
+
.getAllByRole('button')
|
|
115
|
+
.map((button) => button.querySelector('span')?.textContent);
|
|
116
|
+
// The first row and last row should be swapped when inverted
|
|
117
|
+
// This is an approximation as DOM order might not directly correspond to visual order
|
|
118
|
+
expect(invertedCells).not.toEqual(originalCells);
|
|
119
|
+
});
|
|
120
|
+
it('calls onCellClick when a cell is clicked', async () => {
|
|
121
|
+
const user = userEvent.setup();
|
|
122
|
+
const onCellClick = vi.fn();
|
|
123
|
+
render(Matrix, {
|
|
124
|
+
props: {
|
|
125
|
+
xAxisName,
|
|
126
|
+
yAxisName,
|
|
127
|
+
data: confusionMatrix,
|
|
128
|
+
rowLabels: defaultRowLabels,
|
|
129
|
+
colLabels: defaultColLabels,
|
|
130
|
+
onCellClick,
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
const buttons = screen.getAllByRole('button');
|
|
134
|
+
await user.click(buttons[0]); // Click the first cell
|
|
135
|
+
expect(onCellClick).toHaveBeenCalledTimes(1);
|
|
136
|
+
expect(onCellClick).toHaveBeenCalledWith(expect.objectContaining({
|
|
137
|
+
value: expect.any(Number),
|
|
138
|
+
row: expect.any(Number),
|
|
139
|
+
col: expect.any(Number),
|
|
140
|
+
}));
|
|
141
|
+
});
|
|
142
|
+
it('calls onCellHover when a cell is hovered', async () => {
|
|
143
|
+
const onCellHover = vi.fn();
|
|
144
|
+
render(Matrix, {
|
|
145
|
+
props: {
|
|
146
|
+
xAxisName,
|
|
147
|
+
yAxisName,
|
|
148
|
+
data: confusionMatrix,
|
|
149
|
+
rowLabels: defaultRowLabels,
|
|
150
|
+
colLabels: defaultColLabels,
|
|
151
|
+
onCellHover,
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
const buttons = screen.getAllByRole('button');
|
|
155
|
+
await fireEvent.mouseOver(buttons[0]); // Hover over the first cell
|
|
156
|
+
expect(onCellHover).toHaveBeenCalledTimes(1);
|
|
157
|
+
expect(onCellHover).toHaveBeenCalledWith(expect.objectContaining({
|
|
158
|
+
value: expect.any(Number),
|
|
159
|
+
row: expect.any(Number),
|
|
160
|
+
col: expect.any(Number),
|
|
161
|
+
}));
|
|
162
|
+
});
|
|
163
|
+
it('uses default background colors for 2x2 confusion matrix', () => {
|
|
164
|
+
// Create a test component that inspects cell background colors
|
|
165
|
+
const { container } = render(Matrix, {
|
|
166
|
+
props: {
|
|
167
|
+
xAxisName,
|
|
168
|
+
yAxisName,
|
|
169
|
+
data: confusionMatrix,
|
|
170
|
+
rowLabels: defaultRowLabels,
|
|
171
|
+
colLabels: defaultColLabels,
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
// Get all cell buttons
|
|
175
|
+
const buttons = screen.getAllByRole('button');
|
|
176
|
+
// In a 2x2 confusion matrix, diagonal cells should have success background color
|
|
177
|
+
// and off-diagonal cells should have neutral background color
|
|
178
|
+
const diagonalCells = [0, 3]; // Indices for top-left and bottom-right cells
|
|
179
|
+
const offDiagonalCells = [1, 2]; // Indices for top-right and bottom-left cells
|
|
180
|
+
for (const idx of diagonalCells) {
|
|
181
|
+
// Check that the inline style contains the success background color
|
|
182
|
+
expect(buttons[idx].style.backgroundColor).not.toBeUndefined();
|
|
183
|
+
}
|
|
184
|
+
for (const idx of offDiagonalCells) {
|
|
185
|
+
// Check that the inline style contains the neutral background color
|
|
186
|
+
expect(buttons[idx].style.backgroundColor).not.toBeUndefined();
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
it('respects custom background colors when provided', () => {
|
|
190
|
+
const customRedColor = 'rgb(255, 0, 0)';
|
|
191
|
+
const customGreenColor = 'rgb(0, 255, 0)';
|
|
192
|
+
const customColorMatrix = [
|
|
193
|
+
[
|
|
194
|
+
{ value: 8, row: 0, col: 0, backgroundColor: customRedColor }, // Custom red color
|
|
195
|
+
{ value: 27, row: 0, col: 1 }, // Default color
|
|
196
|
+
],
|
|
197
|
+
[
|
|
198
|
+
{ value: 2, row: 1, col: 0 }, // Default color
|
|
199
|
+
{ value: 123, row: 1, col: 1, backgroundColor: customGreenColor }, // Custom green color
|
|
200
|
+
],
|
|
201
|
+
];
|
|
202
|
+
render(Matrix, {
|
|
203
|
+
props: {
|
|
204
|
+
xAxisName,
|
|
205
|
+
yAxisName,
|
|
206
|
+
data: customColorMatrix,
|
|
207
|
+
rowLabels: defaultRowLabels,
|
|
208
|
+
colLabels: defaultColLabels,
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
const buttons = screen.getAllByRole('button');
|
|
212
|
+
// Check that custom colors are applied
|
|
213
|
+
expect(buttons[0].style.backgroundColor).toBe(customRedColor);
|
|
214
|
+
expect(buttons[3].style.backgroundColor).toBe(customGreenColor);
|
|
215
|
+
});
|
|
216
|
+
it('handles focus events for accessibility', async () => {
|
|
217
|
+
const onCellHover = vi.fn();
|
|
218
|
+
render(Matrix, {
|
|
219
|
+
props: {
|
|
220
|
+
xAxisName,
|
|
221
|
+
yAxisName,
|
|
222
|
+
data: confusionMatrix,
|
|
223
|
+
rowLabels: defaultRowLabels,
|
|
224
|
+
colLabels: defaultColLabels,
|
|
225
|
+
onCellHover,
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
const buttons = screen.getAllByRole('button');
|
|
229
|
+
await fireEvent.focus(buttons[0]); // Focus the first cell
|
|
230
|
+
expect(onCellHover).toHaveBeenCalledTimes(1);
|
|
231
|
+
expect(onCellHover).toHaveBeenCalledWith(expect.objectContaining({
|
|
232
|
+
value: expect.any(Number),
|
|
233
|
+
row: expect.any(Number),
|
|
234
|
+
col: expect.any(Number),
|
|
235
|
+
}));
|
|
236
|
+
});
|
|
237
|
+
it('renders with custom width and height', () => {
|
|
238
|
+
const customWidth = '600px';
|
|
239
|
+
const customHeight = '300px';
|
|
240
|
+
const { container } = render(Matrix, {
|
|
241
|
+
props: {
|
|
242
|
+
xAxisName,
|
|
243
|
+
yAxisName,
|
|
244
|
+
data: confusionMatrix,
|
|
245
|
+
rowLabels: defaultRowLabels,
|
|
246
|
+
colLabels: defaultColLabels,
|
|
247
|
+
width: customWidth,
|
|
248
|
+
height: customHeight,
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
// Check that the root div has the specified width and height
|
|
252
|
+
const rootElement = container.firstChild;
|
|
253
|
+
expect(rootElement.style.width).toBe(customWidth);
|
|
254
|
+
expect(rootElement.style.height).toBe(customHeight);
|
|
255
|
+
});
|
|
256
|
+
});
|