@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.
- package/README.md +0 -3
- package/dist/components/activity/Activity.stories.svelte +8 -0
- 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 -4
- package/dist/components/icons/index.d.ts +1 -6
- package/dist/components/icons/index.js +7 -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/ManualCFUCounter.svelte +1 -1
- 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/modal/components/modal-overlay.svelte +1 -5
- package/dist/components/multi-cfu-counter/MultiCFUCounter.svelte +1 -1
- 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/progress-circle/ProgressCircle.svelte +1 -1
- package/dist/components/select/components/SelectItem.svelte +2 -2
- package/dist/components/sjsf-wrappers/SjsfNumberInputWrapper.svelte +1 -1
- 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/dist/tailwind.preset.d.ts +3 -0
- package/dist/tailwind.preset.js +1 -0
- package/dist/tokens.d.ts +8 -0
- package/dist/tokens.js +4 -0
- package/package.json +197 -197
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Matrix, { type MatrixChartProps } from './Matrix.svelte';
|
|
3
|
+
|
|
4
|
+
// Default test data
|
|
5
|
+
const confusionMatrix = [
|
|
6
|
+
[
|
|
7
|
+
{ value: 8, row: 0, col: 0 }, // TP (TNTC/TNTC)
|
|
8
|
+
{ value: 27, row: 0, col: 1 }, // FN (TNTC/Not TNTC)
|
|
9
|
+
],
|
|
10
|
+
[
|
|
11
|
+
{ value: 2, row: 1, col: 0 }, // FP (Not TNTC/TNTC)
|
|
12
|
+
{ value: 123, row: 1, col: 1 }, // TN (Not TNTC/Not TNTC)
|
|
13
|
+
],
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const defaultRowLabels = ['TNTC', 'Not TNTC'];
|
|
17
|
+
const defaultColLabels = ['TNTC', 'Not TNTC'];
|
|
18
|
+
|
|
19
|
+
// Props with default values
|
|
20
|
+
const {
|
|
21
|
+
data = confusionMatrix,
|
|
22
|
+
rowLabels = defaultRowLabels,
|
|
23
|
+
colLabels = defaultColLabels,
|
|
24
|
+
xAxisName = 'Predicted',
|
|
25
|
+
yAxisName = 'Actual',
|
|
26
|
+
width = '500px',
|
|
27
|
+
height = '400px',
|
|
28
|
+
onCellClick = (cell) => console.log('Cell clicked:', cell),
|
|
29
|
+
onCellHover = (cell) => console.log('Cell hover:', cell),
|
|
30
|
+
loading = false,
|
|
31
|
+
invertYAxis = true,
|
|
32
|
+
}: MatrixChartProps = $props();
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<div data-testid="matrix-wrapper" style="width: 100%; height: 400px;">
|
|
36
|
+
<Matrix
|
|
37
|
+
{data}
|
|
38
|
+
{rowLabels}
|
|
39
|
+
{colLabels}
|
|
40
|
+
{xAxisName}
|
|
41
|
+
{yAxisName}
|
|
42
|
+
{width}
|
|
43
|
+
{height}
|
|
44
|
+
{onCellClick}
|
|
45
|
+
{onCellHover}
|
|
46
|
+
{loading}
|
|
47
|
+
{invertYAxis}
|
|
48
|
+
/>
|
|
49
|
+
</div>
|
|
@@ -10,5 +10,6 @@ interface TooltipFormatterConfig {
|
|
|
10
10
|
getSeriesColor?: (seriesName: string, seriesIndex: number) => string;
|
|
11
11
|
maxSeriesToShow?: number;
|
|
12
12
|
}
|
|
13
|
+
export declare function toFixedLocaleString(value: number | undefined | null, fractionDigits?: number, locale?: string | undefined): string;
|
|
13
14
|
export declare const createTooltipFormatter: (config: TooltipFormatterConfig) => (params: ExtendedCallbackDataParams | ExtendedCallbackDataParams[]) => string;
|
|
14
15
|
export {};
|
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
import { textColor } from '../../../tokens';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
}
|
|
2
|
+
export function toFixedLocaleString(value, fractionDigits = 0, locale = undefined) {
|
|
3
|
+
if (value === undefined || value === null) {
|
|
4
|
+
return '';
|
|
5
|
+
}
|
|
6
|
+
const formatted = value.toLocaleString(locale, {
|
|
7
|
+
minimumFractionDigits: fractionDigits,
|
|
8
|
+
maximumFractionDigits: fractionDigits,
|
|
9
|
+
});
|
|
10
|
+
const decimalPart = formatted.split('.')[1];
|
|
11
|
+
// preserve meaningful zeros like 5.04
|
|
12
|
+
if (decimalPart && decimalPart.split('').every((digit) => digit === '0')) {
|
|
13
|
+
return value.toLocaleString(locale, {
|
|
14
|
+
minimumFractionDigits: 0,
|
|
15
|
+
maximumFractionDigits: 0,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
return formatted;
|
|
19
|
+
}
|
|
6
20
|
export const createTooltipFormatter = (config) => {
|
|
7
21
|
const { maxSeriesToShow = 10 } = config;
|
|
8
22
|
return (params) => {
|
|
@@ -2,18 +2,13 @@ import type { IconComponentProps } from 'phosphor-svelte';
|
|
|
2
2
|
import type { Component } from 'svelte';
|
|
3
3
|
import type { textColor, backgroundColor } from './../../tokens';
|
|
4
4
|
export type PhosphorIconProps = Component<IconComponentProps, object, ''>;
|
|
5
|
-
export type IconName = 'Aperture' | 'ArrowFatLineRight' | 'ArrowCounterClockwise' | 'ArrowRight' | 'ArrowUpRight' | 'ArrowUpLeft' | 'ArrowUUpLeft' | 'Barcode' | 'Bell' | 'BookOpen' | 'BookOpenText' | 'BowlingBall' | 'BugBeetle' | 'Calendar' | 'CalendarBlank' | 'Camera' | 'CameraSlash' | 'CaretDown' | 'CaretLeft' | 'CaretRight' | 'CaretUp' | 'CaretUpDown' | 'ChatTeardropDots' | 'Check' | 'CheckCircle' | 'CheckFat' | 'Circle' | 'CircleDashed' | 'CircleHalf' | 'CirclesFour' | 'CirclesThreePlus' | 'Clock' | 'ClockCountdown' | 'Copy' | 'Crop' | 'Cube' | 'CursorClick' | 'Database' | 'Dna' | 'DotsThree' | 'DotsThreeOutlineVertical' | 'DownloadSimple' | 'Drop' | 'EnvelopeSimple' | 'Eye' | 'Eyedropper' | 'FileCsv' | 'Flag' | 'Flask' | 'Folder' | 'FolderDashed' | 'FolderSimplePlus' | 'FunnelSimple' | 'Gear' | 'GlobeSimple' | 'GlobeSimpleX' | 'GridFour' | 'Hash' | 'House' | 'ImageSquare' | 'ImagesSquare' | 'Info' | 'Lock' | 'LineSegments' | 'List' | 'Link' | 'ListMagnifyingGlass' | 'MagnifyingGlass' | 'MegaphoneSimple' | 'MicrosoftExcelLogo' | 'Moon' | 'Minus' | 'Palette' | 'Pause' | 'Pencil' | 'PencilSimple' | 'Percent' | 'Phone' | 'Plant' | 'Play' | 'Plus' | 'PlusMinus' | 'Ruler' | 'Question' | 'SealCheck' | 'RadioButton' | 'SealQuestion' | 'SealWarning' | 'SelectionAll' | 'Share' | 'SidebarSimple' | 'SkipBack' | 'SkipForward' | 'SignIn' | 'SignOut' | 'SortAscending' | 'Sparkle' | 'SpinnerGap' | 'SquaresFour' | 'Star' | 'Stop' | 'StopCircle' | 'Sun' | 'Table' | 'Tag' | 'Target' | 'TestTube' | 'Trash' | 'TrashSimple' | 'TreeStructure' | 'UserCircle' | 'UserPlus' | 'Video' | 'Warning' | 'WarningCircle' | 'WifiSlash' | 'X';
|
|
5
|
+
export type IconName = 'Aperture' | 'Archive' | 'ArrowFatLineRight' | 'ArrowCounterClockwise' | 'ArrowRight' | 'ArrowUpRight' | 'ArrowUpLeft' | 'ArrowUUpLeft' | 'Barcode' | 'Bell' | 'BookOpen' | 'BookOpenText' | 'BowlingBall' | 'BugBeetle' | 'Calendar' | 'CalendarBlank' | 'Camera' | 'CameraSlash' | 'CaretDown' | 'CaretLeft' | 'CaretRight' | 'CaretUp' | 'CaretUpDown' | 'ChatTeardropDots' | 'Check' | 'CheckCircle' | 'CheckFat' | 'Circle' | 'CircleDashed' | 'CircleHalf' | 'CirclesFour' | 'CirclesThreePlus' | 'Clock' | 'ClockCountdown' | 'Copy' | 'Crop' | 'Cube' | 'CursorClick' | 'CraneTower' | 'Database' | 'Dna' | 'DotsThree' | 'DotsThreeOutlineVertical' | 'DownloadSimple' | 'Drop' | 'EnvelopeSimple' | 'Eye' | 'Eyedropper' | 'EyeSlash' | 'FileCsv' | 'Flag' | 'Flask' | 'Folder' | 'FolderDashed' | 'FolderSimplePlus' | 'FunnelSimple' | 'Gear' | 'GlobeSimple' | 'GlobeSimpleX' | 'GridFour' | 'Hash' | 'House' | 'ImageSquare' | 'ImagesSquare' | 'Info' | 'Lock' | 'LineSegments' | 'List' | 'Link' | 'ListMagnifyingGlass' | 'MagnifyingGlass' | 'MegaphoneSimple' | 'MicrosoftExcelLogo' | 'Moon' | 'Minus' | 'Palette' | 'Pause' | 'Pencil' | 'PencilSimple' | 'Percent' | 'Phone' | 'Plant' | 'Play' | 'Plus' | 'PlusMinus' | 'Ruler' | 'Question' | 'SealCheck' | 'RadioButton' | 'SealQuestion' | 'SealWarning' | 'SelectionAll' | 'Share' | 'SidebarSimple' | 'SkipBack' | 'SkipForward' | 'SignIn' | 'SignOut' | 'SortAscending' | 'Sparkle' | 'SpinnerGap' | 'SquaresFour' | 'Star' | 'Stop' | 'StopCircle' | 'Sun' | 'Table' | 'Tag' | 'Target' | 'TestTube' | 'Trash' | 'TrashSimple' | 'TreeStructure' | 'UserCircle' | 'UserPlus' | 'Video' | 'Warning' | 'WarningCircle' | 'WifiSlash' | 'X';
|
|
6
6
|
export declare const iconMap: Record<IconName, PhosphorIconProps>;
|
|
7
7
|
export declare function renderIcon(iconName: keyof typeof iconMap): PhosphorIconProps;
|
|
8
8
|
export type BackgroundClass = `bg-${keyof typeof backgroundColor}`;
|
|
9
9
|
export type IconColor = keyof typeof textColor;
|
|
10
10
|
export type SupportedAnalysisModelIcons = 'pipeline_halos' | 'pipeline_large_colonies' | 'pipeline_microbial_colonies' | 'pipeline_small_colonies' | 'sgs_enteros' | 'general_germination_rate_with_tracking' | 'general_germination_rate_without_tracking' | 'pipeline_insects' | 'pipeline_colony_formation' | 'pipeline_radial_growth' | 'syngenta_health_score_crw' | 'pipeline_seed_germination' | 'syngenta_health_score_faw' | 'syngenta_health_score_sbl' | 'unilever_cfu_count_lowres';
|
|
11
11
|
export type SupportedPrincipalIcons = 'user' | 'group' | 'organization';
|
|
12
|
-
export interface IconComponentProps {
|
|
13
|
-
size?: number | string;
|
|
14
|
-
color?: string;
|
|
15
|
-
weight?: 'thin' | 'light' | 'regular' | 'bold' | 'fill' | 'duotone';
|
|
16
|
-
}
|
|
17
12
|
export { default as Icon } from './Icon.svelte';
|
|
18
13
|
export type IconProps = {
|
|
19
14
|
iconName: IconName;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Aperture from 'phosphor-svelte/lib/Aperture';
|
|
2
|
+
import Archive from 'phosphor-svelte/lib/Archive';
|
|
2
3
|
import ArrowFatLineRight from 'phosphor-svelte/lib/ArrowFatLineRight';
|
|
3
4
|
import ArrowCounterClockwise from 'phosphor-svelte/lib/ArrowCounterClockwise';
|
|
4
5
|
import ArrowRight from 'phosphor-svelte/lib/ArrowRight';
|
|
@@ -35,6 +36,7 @@ import Copy from 'phosphor-svelte/lib/Copy';
|
|
|
35
36
|
import Crop from 'phosphor-svelte/lib/Crop';
|
|
36
37
|
import Cube from 'phosphor-svelte/lib/Cube';
|
|
37
38
|
import CursorClick from 'phosphor-svelte/lib/CursorClick';
|
|
39
|
+
import CraneTower from 'phosphor-svelte/lib/CraneTower';
|
|
38
40
|
import Database from 'phosphor-svelte/lib/Database';
|
|
39
41
|
import Dna from 'phosphor-svelte/lib/Dna';
|
|
40
42
|
import DotsThree from 'phosphor-svelte/lib/DotsThree';
|
|
@@ -44,6 +46,7 @@ import Drop from 'phosphor-svelte/lib/Drop';
|
|
|
44
46
|
import EnvelopeSimple from 'phosphor-svelte/lib/EnvelopeSimple';
|
|
45
47
|
import Eye from 'phosphor-svelte/lib/Eye';
|
|
46
48
|
import Eyedropper from 'phosphor-svelte/lib/Eyedropper';
|
|
49
|
+
import EyeSlash from 'phosphor-svelte/lib/EyeSlash';
|
|
47
50
|
import FileCsv from 'phosphor-svelte/lib/FileCsv';
|
|
48
51
|
import Flag from 'phosphor-svelte/lib/Flag';
|
|
49
52
|
import Flask from 'phosphor-svelte/lib/Flask';
|
|
@@ -110,6 +113,7 @@ import TrashSimple from 'phosphor-svelte/lib/TrashSimple';
|
|
|
110
113
|
import TreeStructure from 'phosphor-svelte/lib/TreeStructure';
|
|
111
114
|
import UserCircle from 'phosphor-svelte/lib/UserCircle';
|
|
112
115
|
import UserPlus from 'phosphor-svelte/lib/UserPlus';
|
|
116
|
+
import User from 'phosphor-svelte/lib/User';
|
|
113
117
|
import Video from 'phosphor-svelte/lib/Video';
|
|
114
118
|
import Warning from 'phosphor-svelte/lib/Warning';
|
|
115
119
|
import WarningCircle from 'phosphor-svelte/lib/WarningCircle';
|
|
@@ -117,6 +121,7 @@ import WifiSlash from 'phosphor-svelte/lib/WifiSlash';
|
|
|
117
121
|
import X from 'phosphor-svelte/lib/X';
|
|
118
122
|
export const iconMap = {
|
|
119
123
|
Aperture,
|
|
124
|
+
Archive,
|
|
120
125
|
ArrowFatLineRight,
|
|
121
126
|
ArrowCounterClockwise,
|
|
122
127
|
ArrowRight,
|
|
@@ -153,6 +158,7 @@ export const iconMap = {
|
|
|
153
158
|
Crop,
|
|
154
159
|
Cube,
|
|
155
160
|
CursorClick,
|
|
161
|
+
CraneTower,
|
|
156
162
|
Database,
|
|
157
163
|
Dna,
|
|
158
164
|
DotsThree,
|
|
@@ -162,6 +168,7 @@ export const iconMap = {
|
|
|
162
168
|
EnvelopeSimple,
|
|
163
169
|
Eye,
|
|
164
170
|
Eyedropper,
|
|
171
|
+
EyeSlash,
|
|
165
172
|
FileCsv,
|
|
166
173
|
Flag,
|
|
167
174
|
Flask,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { render } from '@testing-library/svelte';
|
|
2
|
+
import { describe, it, expect } from 'vitest';
|
|
3
|
+
import Input from './Input.svelte';
|
|
4
|
+
describe('Input', () => {
|
|
5
|
+
it('should render input with text type when no type is provided', () => {
|
|
6
|
+
const { getByRole } = render(Input, {
|
|
7
|
+
props: {
|
|
8
|
+
value: 'foo',
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
const input = getByRole('textbox');
|
|
12
|
+
expect(input.type).toBe('text');
|
|
13
|
+
});
|
|
14
|
+
it('should render label when provided', () => {
|
|
15
|
+
const { getByLabelText } = render(Input, {
|
|
16
|
+
props: {
|
|
17
|
+
value: 'foo',
|
|
18
|
+
label: 'bar',
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
const input = getByLabelText('bar');
|
|
22
|
+
expect(input.value).toBe('foo');
|
|
23
|
+
});
|
|
24
|
+
it('should include placeholder when provided, and contain placeholder class', () => {
|
|
25
|
+
const { getByRole } = render(Input, {
|
|
26
|
+
props: {
|
|
27
|
+
placeholder: 'foo',
|
|
28
|
+
value: 'foo',
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
const input = getByRole('textbox');
|
|
32
|
+
expect(input.placeholder).toBe('foo');
|
|
33
|
+
expect(input.classList).toContain('has-placeholder');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/svelte';
|
|
2
|
+
import Label from './Label.svelte';
|
|
3
|
+
import { describe, it, expect } from 'vitest';
|
|
4
|
+
describe('Label component', () => {
|
|
5
|
+
it('renders the label text', () => {
|
|
6
|
+
const labelText = 'Test Label';
|
|
7
|
+
render(Label, { props: { text: labelText, forId: 'testId' } });
|
|
8
|
+
expect(screen.getByText(labelText)).toBeInTheDocument();
|
|
9
|
+
});
|
|
10
|
+
it('renders the required indicator when required is true', () => {
|
|
11
|
+
render(Label, { props: { text: 'Required Label', forId: 'reqId', required: true } });
|
|
12
|
+
expect(screen.getByText('*')).toBeInTheDocument();
|
|
13
|
+
expect(screen.getByText('*')).toHaveClass('ml-0.5 text-danger');
|
|
14
|
+
});
|
|
15
|
+
it('does not render the required indicator when required is false or not provided', () => {
|
|
16
|
+
render(Label, { props: { text: 'Optional Label', forId: 'optId' } });
|
|
17
|
+
expect(screen.queryByText('*')).not.toBeInTheDocument();
|
|
18
|
+
});
|
|
19
|
+
it('applies the forId to the for attribute', () => {
|
|
20
|
+
const testId = 'my-input';
|
|
21
|
+
render(Label, { props: { text: 'Label for Input', forId: testId } });
|
|
22
|
+
expect(screen.getByText('Label for Input').closest('label')).toHaveAttribute('for', testId);
|
|
23
|
+
});
|
|
24
|
+
it('applies custom classes', () => {
|
|
25
|
+
const customClass = 'my-custom-class';
|
|
26
|
+
render(Label, { props: { text: 'Custom Class Label', forId: 'customId', class: customClass } });
|
|
27
|
+
expect(screen.getByText('Custom Class Label').closest('label')).toHaveClass(customClass);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -534,7 +534,7 @@
|
|
|
534
534
|
{@render ZoomControls()}
|
|
535
535
|
{/if}
|
|
536
536
|
|
|
537
|
-
<!--
|
|
537
|
+
<!--
|
|
538
538
|
We need to use SVG for this interactive component.
|
|
539
539
|
The SVG element is treated as a canvas for clicking, panning, and zooming.
|
|
540
540
|
We add accessibility attributes to make it more accessible despite the interactive nature.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { render, fireEvent, waitFor, findByText, findByRole, getByTestId, } from '@testing-library/svelte';
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
3
|
+
import { tick } from 'svelte';
|
|
4
|
+
import ManualCFUCounter from '../ManualCFUCounter.svelte';
|
|
5
|
+
import ManualCFUCounterTestWrapper from './ManualCFUCounterTestWrapper.svelte';
|
|
6
|
+
class MockImage {
|
|
7
|
+
width = 464;
|
|
8
|
+
height = 464;
|
|
9
|
+
src = '';
|
|
10
|
+
onload = null;
|
|
11
|
+
complete = false;
|
|
12
|
+
constructor() {
|
|
13
|
+
// Set a timeout to simulate async image loading
|
|
14
|
+
setTimeout(() => {
|
|
15
|
+
this.complete = true;
|
|
16
|
+
if (this.onload)
|
|
17
|
+
this.onload();
|
|
18
|
+
}, 0);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
if (typeof global !== 'undefined') {
|
|
22
|
+
global.Image = MockImage;
|
|
23
|
+
// mock resizeobserver which isn't available in gh actions environment
|
|
24
|
+
class MockResizeObserver {
|
|
25
|
+
observe = vi.fn();
|
|
26
|
+
unobserve = vi.fn();
|
|
27
|
+
disconnect = vi.fn();
|
|
28
|
+
}
|
|
29
|
+
global.ResizeObserver = MockResizeObserver;
|
|
30
|
+
}
|
|
31
|
+
const simulateImageLoad = async () => {
|
|
32
|
+
await tick();
|
|
33
|
+
};
|
|
34
|
+
describe('ManualCFUCounter Component', () => {
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
vi.clearAllMocks();
|
|
37
|
+
});
|
|
38
|
+
it('renders with default props', async () => {
|
|
39
|
+
const { container } = render(ManualCFUCounter, {
|
|
40
|
+
props: {
|
|
41
|
+
imageUrl: 'test-image.jpg',
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
await simulateImageLoad();
|
|
45
|
+
const svg = container.querySelector('svg');
|
|
46
|
+
const image = container.querySelector('image');
|
|
47
|
+
const dotsGroup = container.querySelector('#dots');
|
|
48
|
+
expect(svg).toBeTruthy();
|
|
49
|
+
expect(image).toBeTruthy();
|
|
50
|
+
expect(dotsGroup).toBeTruthy();
|
|
51
|
+
const mainSvg = container.querySelector('svg[role="application"]');
|
|
52
|
+
expect(mainSvg).toBeTruthy();
|
|
53
|
+
expect(mainSvg?.getAttribute('role')).toBe('application');
|
|
54
|
+
expect(mainSvg?.getAttribute('aria-label')).toContain('CFU Counter');
|
|
55
|
+
});
|
|
56
|
+
it('displays action buttons when marks are present', async () => {
|
|
57
|
+
const { container } = render(ManualCFUCounter, {
|
|
58
|
+
props: {
|
|
59
|
+
imageUrl: 'test-image.jpg',
|
|
60
|
+
marks: [{ x: 10, y: 10 }],
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
await simulateImageLoad();
|
|
64
|
+
const undoButton = await findByRole(container, 'button', { name: /Undo last mark/i });
|
|
65
|
+
const clearButton = await findByText(container, 'Clear all');
|
|
66
|
+
expect(undoButton).toBeTruthy();
|
|
67
|
+
expect(clearButton).toBeTruthy();
|
|
68
|
+
});
|
|
69
|
+
it('does not display action buttons when hideMarkers is true', async () => {
|
|
70
|
+
const { queryByAltText, queryByText } = render(ManualCFUCounter, {
|
|
71
|
+
props: {
|
|
72
|
+
imageUrl: 'test-image.jpg',
|
|
73
|
+
hideMarkers: true,
|
|
74
|
+
marks: [{ x: 10, y: 10 }],
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
await simulateImageLoad();
|
|
78
|
+
const undoButton = queryByAltText('Undo last mark');
|
|
79
|
+
const clearButton = queryByText('Clear all');
|
|
80
|
+
expect(undoButton).toBeFalsy();
|
|
81
|
+
expect(clearButton).toBeFalsy();
|
|
82
|
+
});
|
|
83
|
+
it('does not display action buttons when disabled is true', async () => {
|
|
84
|
+
const { queryByAltText, queryByText } = render(ManualCFUCounter, {
|
|
85
|
+
props: {
|
|
86
|
+
imageUrl: 'test-image.jpg',
|
|
87
|
+
disabled: true,
|
|
88
|
+
marks: [{ x: 10, y: 10 }],
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
await simulateImageLoad();
|
|
92
|
+
const undoButton = queryByAltText('Undo last mark');
|
|
93
|
+
const clearButton = queryByText('Clear all');
|
|
94
|
+
expect(undoButton).toBeFalsy();
|
|
95
|
+
expect(clearButton).toBeFalsy();
|
|
96
|
+
});
|
|
97
|
+
it('adds cursor-not-allowed class when disabled', async () => {
|
|
98
|
+
const { container } = render(ManualCFUCounter, {
|
|
99
|
+
props: {
|
|
100
|
+
imageUrl: 'test-image.jpg',
|
|
101
|
+
disabled: true,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
await simulateImageLoad();
|
|
105
|
+
const svg = container.querySelector('svg');
|
|
106
|
+
expect(svg?.classList.contains('cursor-not-allowed')).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
it('renders with initial marks provided via marks prop', async () => {
|
|
109
|
+
const initialMarksData = [
|
|
110
|
+
{ x: 100, y: 100 },
|
|
111
|
+
{ x: 200, y: 200 },
|
|
112
|
+
];
|
|
113
|
+
const { container } = render(ManualCFUCounter, {
|
|
114
|
+
props: {
|
|
115
|
+
imageUrl: 'test-image.jpg',
|
|
116
|
+
marks: initialMarksData,
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
await simulateImageLoad();
|
|
120
|
+
await tick();
|
|
121
|
+
const marksCount = container.querySelector('[data-testid="marks-count"]');
|
|
122
|
+
expect(marksCount?.textContent).toBe('2');
|
|
123
|
+
const markers = container.querySelectorAll('[data-testid^="marker-"]');
|
|
124
|
+
expect(markers.length).toBe(2);
|
|
125
|
+
const circles = container.querySelectorAll('circle');
|
|
126
|
+
expect(circles.length).toBe(2);
|
|
127
|
+
expect(circles[0].getAttribute('cx')).toBe('100');
|
|
128
|
+
expect(circles[0].getAttribute('cy')).toBe('100');
|
|
129
|
+
expect(circles[1].getAttribute('cx')).toBe('200');
|
|
130
|
+
expect(circles[1].getAttribute('cy')).toBe('200');
|
|
131
|
+
});
|
|
132
|
+
it('simulates adding a marker via direct click', async () => {
|
|
133
|
+
const mockOnClick = vi.fn();
|
|
134
|
+
const { container } = render(ManualCFUCounter, {
|
|
135
|
+
props: {
|
|
136
|
+
imageUrl: 'test-image.jpg',
|
|
137
|
+
onclick: mockOnClick,
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
await simulateImageLoad();
|
|
141
|
+
const marksCountBefore = container.querySelector('[data-testid="marks-count"]');
|
|
142
|
+
expect(marksCountBefore?.textContent).toBe('0');
|
|
143
|
+
const svg = container.querySelector('svg');
|
|
144
|
+
expect(svg).toBeTruthy();
|
|
145
|
+
if (svg) {
|
|
146
|
+
const viewportElement = container.querySelector('#viewport');
|
|
147
|
+
expect(viewportElement).toBeTruthy();
|
|
148
|
+
if (viewportElement) {
|
|
149
|
+
await fireEvent.click(viewportElement, {
|
|
150
|
+
clientX: 100,
|
|
151
|
+
clientY: 100,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
await waitFor(() => {
|
|
155
|
+
expect(mockOnClick).toHaveBeenCalledTimes(1);
|
|
156
|
+
});
|
|
157
|
+
const callArgs = mockOnClick.mock.calls[0];
|
|
158
|
+
const addedMarks = callArgs[1];
|
|
159
|
+
expect(addedMarks.length).toBe(1);
|
|
160
|
+
expect(addedMarks[0].x).toBeCloseTo(100);
|
|
161
|
+
expect(addedMarks[0].y).toBeCloseTo(100);
|
|
162
|
+
await waitFor(() => {
|
|
163
|
+
const marksCountAfter = container.querySelector('[data-testid="marks-count"]');
|
|
164
|
+
expect(marksCountAfter?.textContent).toBe('1');
|
|
165
|
+
});
|
|
166
|
+
const marker = container.querySelector('[data-testid="marker-1"]');
|
|
167
|
+
expect(marker).toBeTruthy();
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
it('simulates adding multiple markers via clicks', async () => {
|
|
171
|
+
const mockOnClick = vi.fn();
|
|
172
|
+
const { container } = render(ManualCFUCounter, {
|
|
173
|
+
props: {
|
|
174
|
+
imageUrl: 'test-image.jpg',
|
|
175
|
+
onclick: mockOnClick,
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
await simulateImageLoad();
|
|
179
|
+
let marksCount = container.querySelector('[data-testid="marks-count"]');
|
|
180
|
+
expect(marksCount?.textContent).toBe('0');
|
|
181
|
+
const svg = container.querySelector('svg');
|
|
182
|
+
expect(svg).toBeTruthy();
|
|
183
|
+
if (svg) {
|
|
184
|
+
const viewportElement = container.querySelector('#viewport');
|
|
185
|
+
expect(viewportElement).toBeTruthy();
|
|
186
|
+
if (viewportElement) {
|
|
187
|
+
await fireEvent.click(viewportElement, {
|
|
188
|
+
clientX: 100,
|
|
189
|
+
clientY: 100,
|
|
190
|
+
});
|
|
191
|
+
await waitFor(() => {
|
|
192
|
+
expect(mockOnClick).toHaveBeenCalledTimes(1);
|
|
193
|
+
});
|
|
194
|
+
let firstCallArgs = mockOnClick.mock.calls[0];
|
|
195
|
+
let firstMarks = firstCallArgs[1];
|
|
196
|
+
expect(firstMarks.length).toBe(1);
|
|
197
|
+
await fireEvent.click(viewportElement, {
|
|
198
|
+
clientX: 200,
|
|
199
|
+
clientY: 200,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
await waitFor(() => {
|
|
203
|
+
expect(mockOnClick).toHaveBeenCalledTimes(2);
|
|
204
|
+
});
|
|
205
|
+
let secondCallArgs = mockOnClick.mock.calls[1];
|
|
206
|
+
let secondMarks = secondCallArgs[1];
|
|
207
|
+
expect(secondMarks.length).toBe(2);
|
|
208
|
+
const markers = container.querySelectorAll('[data-testid^="marker-"]');
|
|
209
|
+
expect(markers.length).toBe(2);
|
|
210
|
+
const circles = container.querySelectorAll('circle');
|
|
211
|
+
expect(circles.length).toBe(2);
|
|
212
|
+
expect(mockOnClick).toHaveBeenCalledTimes(2);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
it('simulates clearing marks through the reset button', async () => {
|
|
216
|
+
const mockOnClick = vi.fn();
|
|
217
|
+
const initialMarksData = [
|
|
218
|
+
{ x: 100, y: 100 },
|
|
219
|
+
{ x: 200, y: 200 },
|
|
220
|
+
];
|
|
221
|
+
const { container, component } = render(ManualCFUCounterTestWrapper, {
|
|
222
|
+
props: {
|
|
223
|
+
initialTestMarks: initialMarksData,
|
|
224
|
+
onclick: mockOnClick,
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
await simulateImageLoad();
|
|
228
|
+
await tick();
|
|
229
|
+
let marksCount = getByTestId(container, 'marks-count');
|
|
230
|
+
expect(marksCount?.textContent).toBe('2');
|
|
231
|
+
const clearButton = await findByText(container, 'Clear all');
|
|
232
|
+
expect(clearButton).toBeTruthy();
|
|
233
|
+
if (clearButton) {
|
|
234
|
+
await fireEvent.click(clearButton);
|
|
235
|
+
await tick();
|
|
236
|
+
const wrapperInstance = component;
|
|
237
|
+
const currentWrapperMarks = wrapperInstance.getCurrentMarks();
|
|
238
|
+
expect(currentWrapperMarks.length).toBe(0);
|
|
239
|
+
marksCount = getByTestId(container, 'wrapper-marks-count');
|
|
240
|
+
expect(marksCount?.textContent).toBe('0');
|
|
241
|
+
const innerMarksCount = getByTestId(container, 'marks-count');
|
|
242
|
+
expect(innerMarksCount?.textContent).toBe('0');
|
|
243
|
+
const markers = container.querySelectorAll('[data-testid^="marker-"]');
|
|
244
|
+
expect(markers.length).toBe(0);
|
|
245
|
+
expect(mockOnClick).not.toHaveBeenCalledWith();
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
it('properly constrains pan to prevent moving outside container', async () => {
|
|
249
|
+
const { container } = render(ManualCFUCounter, {
|
|
250
|
+
props: {
|
|
251
|
+
imageUrl: 'test-image.jpg',
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
await simulateImageLoad();
|
|
255
|
+
const viewport = container.querySelector('#viewport');
|
|
256
|
+
expect(viewport).toBeTruthy();
|
|
257
|
+
expect(viewport?.getAttribute('transform')).toBe('translate(0, 0) scale(1)');
|
|
258
|
+
});
|
|
259
|
+
it('properly sets up keyboard accessibility attributes', async () => {
|
|
260
|
+
const { container } = render(ManualCFUCounter, {
|
|
261
|
+
props: {
|
|
262
|
+
imageUrl: 'test-image.jpg',
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
await simulateImageLoad();
|
|
266
|
+
const svg = container.querySelector('svg[role="application"]');
|
|
267
|
+
expect(svg?.getAttribute('tabindex')).toBe('0');
|
|
268
|
+
expect(svg?.getAttribute('role')).toBe('application');
|
|
269
|
+
expect(svg?.getAttribute('aria-label')).toContain('CFU Counter');
|
|
270
|
+
});
|
|
271
|
+
it('simulates undoing a marker addition via the undo button', async () => {
|
|
272
|
+
const mockOnClick = vi.fn();
|
|
273
|
+
const initialMarksData = [{ x: 50, y: 50 }];
|
|
274
|
+
const { container, component } = render(ManualCFUCounterTestWrapper, {
|
|
275
|
+
props: {
|
|
276
|
+
initialTestMarks: initialMarksData,
|
|
277
|
+
onclick: mockOnClick,
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
await simulateImageLoad();
|
|
281
|
+
await tick();
|
|
282
|
+
let marksCount = getByTestId(container, 'marks-count');
|
|
283
|
+
expect(marksCount?.textContent).toBe('1');
|
|
284
|
+
const svg = container.querySelector('svg');
|
|
285
|
+
expect(svg).toBeTruthy();
|
|
286
|
+
if (svg) {
|
|
287
|
+
const viewportElement = container.querySelector('#viewport');
|
|
288
|
+
expect(viewportElement).toBeTruthy();
|
|
289
|
+
if (viewportElement) {
|
|
290
|
+
await fireEvent.click(viewportElement, { clientX: 100, clientY: 100 });
|
|
291
|
+
}
|
|
292
|
+
await tick();
|
|
293
|
+
const wrapperInstance = component;
|
|
294
|
+
const marksAfterClick = wrapperInstance.getCurrentMarks();
|
|
295
|
+
expect(marksAfterClick.length).toBe(2);
|
|
296
|
+
mockOnClick.mockClear();
|
|
297
|
+
}
|
|
298
|
+
const undoButton = await findByRole(container, 'button', { name: /Undo last mark/i });
|
|
299
|
+
expect(undoButton).toBeTruthy();
|
|
300
|
+
if (undoButton) {
|
|
301
|
+
await fireEvent.click(undoButton);
|
|
302
|
+
await tick();
|
|
303
|
+
const wrapperInstance = component;
|
|
304
|
+
const marksAfterUndo = wrapperInstance.getCurrentMarks();
|
|
305
|
+
expect(marksAfterUndo.length).toBe(1);
|
|
306
|
+
expect(marksAfterUndo[0].x).toBeCloseTo(50);
|
|
307
|
+
expect(marksAfterUndo[0].y).toBeCloseTo(50);
|
|
308
|
+
await waitFor(() => {
|
|
309
|
+
const innerMarksCount = getByTestId(container, 'marks-count');
|
|
310
|
+
expect(innerMarksCount?.textContent).toBe('1');
|
|
311
|
+
const wrapperMarksCount = getByTestId(container, 'wrapper-marks-count');
|
|
312
|
+
expect(wrapperMarksCount?.textContent).toBe('1');
|
|
313
|
+
});
|
|
314
|
+
const markers = container.querySelectorAll('[data-testid^="marker-"]');
|
|
315
|
+
expect(markers.length).toBe(1);
|
|
316
|
+
expect(mockOnClick).not.toHaveBeenCalled();
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
});
|
|
@@ -9,11 +9,7 @@
|
|
|
9
9
|
<Dialog.Overlay forceMount {...restProps}>
|
|
10
10
|
{#snippet child({ props, open })}
|
|
11
11
|
{#if open}
|
|
12
|
-
<div
|
|
13
|
-
{...props}
|
|
14
|
-
class="fixed inset-0 z-50 bg-overlay backdrop-blur-sm"
|
|
15
|
-
transition:fade={{ duration: 200 }}
|
|
16
|
-
>
|
|
12
|
+
<div {...props} class="fixed inset-0 z-50 bg-overlay" transition:fade={{ duration: 200 }}>
|
|
17
13
|
{#if children}
|
|
18
14
|
{@render children()}
|
|
19
15
|
{/if}
|
|
@@ -623,7 +623,7 @@
|
|
|
623
623
|
{@render ZoomControls()}
|
|
624
624
|
{/if}
|
|
625
625
|
|
|
626
|
-
<!--
|
|
626
|
+
<!--
|
|
627
627
|
We need to use SVG for this interactive component.
|
|
628
628
|
The SVG element is treated as a canvas for clicking, panning, and zooming.
|
|
629
629
|
We add accessibility attributes to make it more accessible despite the interactive nature.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|