@reshape-biotech/design-system 2.3.1 → 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.
Files changed (60) hide show
  1. package/README.md +0 -3
  2. package/dist/components/activity/Activity.stories.svelte +8 -0
  3. package/dist/components/activity/Activity.test.d.ts +1 -0
  4. package/dist/components/activity/Activity.test.js +89 -0
  5. package/dist/components/avatar/Avatar.test.d.ts +1 -0
  6. package/dist/components/avatar/Avatar.test.js +55 -0
  7. package/dist/components/button/Button.test.d.ts +1 -0
  8. package/dist/components/button/Button.test.js +117 -0
  9. package/dist/components/button/Button.test.svelte +5 -0
  10. package/dist/components/button/Button.test.svelte.d.ts +19 -0
  11. package/dist/components/checkbox/Checkbox.test.d.ts +1 -0
  12. package/dist/components/checkbox/Checkbox.test.js +51 -0
  13. package/dist/components/graphs/matrix/Matrix.test.d.ts +1 -0
  14. package/dist/components/graphs/matrix/Matrix.test.js +256 -0
  15. package/dist/components/graphs/matrix/Matrix.test.svelte +49 -0
  16. package/dist/components/graphs/matrix/Matrix.test.svelte.d.ts +4 -0
  17. package/dist/components/graphs/utils/tooltipFormatter.d.ts +1 -0
  18. package/dist/components/graphs/utils/tooltipFormatter.js +18 -4
  19. package/dist/components/icons/index.d.ts +1 -6
  20. package/dist/components/icons/index.js +7 -0
  21. package/dist/components/input/Input.test.d.ts +1 -0
  22. package/dist/components/input/Input.test.js +35 -0
  23. package/dist/components/label/Label.test.d.ts +1 -0
  24. package/dist/components/label/Label.test.js +29 -0
  25. package/dist/components/manual-cfu-counter/ManualCFUCounter.svelte +1 -1
  26. package/dist/components/manual-cfu-counter/test/ManualCFUCounter.test.d.ts +1 -0
  27. package/dist/components/manual-cfu-counter/test/ManualCFUCounter.test.js +319 -0
  28. package/dist/components/modal/components/modal-overlay.svelte +1 -5
  29. package/dist/components/multi-cfu-counter/MultiCFUCounter.svelte +1 -1
  30. package/dist/components/multi-cfu-counter/test/MultiCFUCounter.test.d.ts +1 -0
  31. package/dist/components/multi-cfu-counter/test/MultiCFUCounter.test.js +320 -0
  32. package/dist/components/progress-circle/ProgressCircle.svelte +1 -1
  33. package/dist/components/select/components/SelectItem.svelte +2 -2
  34. package/dist/components/sjsf-wrappers/SjsfNumberInputWrapper.svelte +1 -1
  35. package/dist/components/sjsf-wrappers/SjsfNumberInputWrapper.svelte.d.ts +1 -1
  36. package/dist/components/sjsf-wrappers/SjsfSelectWidgetWrapper.svelte +77 -0
  37. package/dist/components/sjsf-wrappers/SjsfSelectWidgetWrapper.svelte.d.ts +3 -0
  38. package/dist/components/sjsf-wrappers/SjsfTextInputWrapper.svelte.d.ts +1 -1
  39. package/dist/components/sjsf-wrappers/sjsfCustomTheme.js +4 -0
  40. package/dist/components/slider/Slider.stories.svelte +5 -0
  41. package/dist/components/slider/Slider.svelte +7 -1
  42. package/dist/components/slider/Slider.svelte.d.ts +1 -0
  43. package/dist/components/stat-card/StatCard.test.d.ts +1 -0
  44. package/dist/components/stat-card/StatCard.test.js +54 -0
  45. package/dist/components/table/Table.test.d.ts +1 -0
  46. package/dist/components/table/Table.test.js +97 -0
  47. package/dist/components/table/Table.test.svelte +37 -0
  48. package/dist/components/table/Table.test.svelte.d.ts +12 -0
  49. package/dist/components/textarea/Textarea.test.d.ts +1 -0
  50. package/dist/components/textarea/Textarea.test.js +90 -0
  51. package/dist/components/toggle-icon-button/ToggleIconButton.test.d.ts +1 -0
  52. package/dist/components/toggle-icon-button/ToggleIconButton.test.js +100 -0
  53. package/dist/components/tooltip/Tooltip.test.d.ts +1 -0
  54. package/dist/components/tooltip/Tooltip.test.js +81 -0
  55. package/dist/notifications.d.ts +4 -1
  56. package/dist/tailwind.preset.d.ts +3 -0
  57. package/dist/tailwind.preset.js +1 -0
  58. package/dist/tokens.d.ts +8 -0
  59. package/dist/tokens.js +4 -0
  60. package/package.json +197 -197
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
@@ -116,4 +116,12 @@
116
116
  author: 'Dwight',
117
117
  }}
118
118
  />
119
+ <Activity
120
+ activity={{
121
+ icon: 'add',
122
+ label: 'Project exploded',
123
+ timestamp: '2 Oct, 1999',
124
+ author: 'Dwight',
125
+ }}
126
+ />
119
127
  </Story>
@@ -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,5 @@
1
+ <script lang="ts">
2
+ import Button from './Button.svelte';
3
+ </script>
4
+
5
+ <Button>Test Children</Button>
@@ -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
+ });