@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,320 @@
|
|
|
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 MultiCFUCounter from '../MultiCFUCounter.svelte';
|
|
5
|
+
import MultiCFUCounterTestWrapper from './MultiCFUCounterTestWrapper.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(MultiCFUCounter, {
|
|
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(MultiCFUCounter, {
|
|
58
|
+
props: {
|
|
59
|
+
imageUrl: 'test-image.jpg',
|
|
60
|
+
marks: [{ x: 10, y: 10 }],
|
|
61
|
+
activeMarkerName: 'Test Marker',
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
await simulateImageLoad();
|
|
65
|
+
const undoButton = await findByRole(container, 'button', { name: /Undo last mark/i });
|
|
66
|
+
const clearButton = await findByText(container, 'Clear all');
|
|
67
|
+
expect(undoButton).toBeTruthy();
|
|
68
|
+
expect(clearButton).toBeTruthy();
|
|
69
|
+
});
|
|
70
|
+
it('does not display action buttons when hideMarkers is true', async () => {
|
|
71
|
+
const { queryByAltText, queryByText } = render(MultiCFUCounter, {
|
|
72
|
+
props: {
|
|
73
|
+
imageUrl: 'test-image.jpg',
|
|
74
|
+
hideMarkers: true,
|
|
75
|
+
marks: [{ x: 10, y: 10 }],
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
await simulateImageLoad();
|
|
79
|
+
const undoButton = queryByAltText('Undo last mark');
|
|
80
|
+
const clearButton = queryByText('Clear all');
|
|
81
|
+
expect(undoButton).toBeFalsy();
|
|
82
|
+
expect(clearButton).toBeFalsy();
|
|
83
|
+
});
|
|
84
|
+
it('does not display action buttons when disabled is true', async () => {
|
|
85
|
+
const { queryByAltText, queryByText } = render(MultiCFUCounter, {
|
|
86
|
+
props: {
|
|
87
|
+
imageUrl: 'test-image.jpg',
|
|
88
|
+
disabled: true,
|
|
89
|
+
marks: [{ x: 10, y: 10 }],
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
await simulateImageLoad();
|
|
93
|
+
const undoButton = queryByAltText('Undo last mark');
|
|
94
|
+
const clearButton = queryByText('Clear all');
|
|
95
|
+
expect(undoButton).toBeFalsy();
|
|
96
|
+
expect(clearButton).toBeFalsy();
|
|
97
|
+
});
|
|
98
|
+
it('adds cursor-not-allowed class when disabled', async () => {
|
|
99
|
+
const { container } = render(MultiCFUCounter, {
|
|
100
|
+
props: {
|
|
101
|
+
imageUrl: 'test-image.jpg',
|
|
102
|
+
disabled: true,
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
await simulateImageLoad();
|
|
106
|
+
const svg = container.querySelector('svg');
|
|
107
|
+
expect(svg?.classList.contains('cursor-not-allowed')).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
it('renders with initial marks provided via marks prop', async () => {
|
|
110
|
+
const initialMarksData = [
|
|
111
|
+
{ x: 100, y: 100 },
|
|
112
|
+
{ x: 200, y: 200 },
|
|
113
|
+
];
|
|
114
|
+
const { container } = render(MultiCFUCounter, {
|
|
115
|
+
props: {
|
|
116
|
+
imageUrl: 'test-image.jpg',
|
|
117
|
+
marks: initialMarksData,
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
await simulateImageLoad();
|
|
121
|
+
await tick();
|
|
122
|
+
const marksCount = container.querySelector('[data-testid="marks-count"]');
|
|
123
|
+
expect(marksCount?.textContent).toBe('2');
|
|
124
|
+
const markers = container.querySelectorAll('[data-testid^="marker-"]');
|
|
125
|
+
expect(markers.length).toBe(2);
|
|
126
|
+
const circles = container.querySelectorAll('circle');
|
|
127
|
+
expect(circles.length).toBe(2);
|
|
128
|
+
expect(circles[0].getAttribute('cx')).toBe('100');
|
|
129
|
+
expect(circles[0].getAttribute('cy')).toBe('100');
|
|
130
|
+
expect(circles[1].getAttribute('cx')).toBe('200');
|
|
131
|
+
expect(circles[1].getAttribute('cy')).toBe('200');
|
|
132
|
+
});
|
|
133
|
+
it('simulates adding a marker via direct click', async () => {
|
|
134
|
+
const mockOnClick = vi.fn();
|
|
135
|
+
const { container } = render(MultiCFUCounter, {
|
|
136
|
+
props: {
|
|
137
|
+
imageUrl: 'test-image.jpg',
|
|
138
|
+
onclick: mockOnClick,
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
await simulateImageLoad();
|
|
142
|
+
const marksCountBefore = container.querySelector('[data-testid="marks-count"]');
|
|
143
|
+
expect(marksCountBefore?.textContent).toBe('0');
|
|
144
|
+
const svg = container.querySelector('svg');
|
|
145
|
+
expect(svg).toBeTruthy();
|
|
146
|
+
if (svg) {
|
|
147
|
+
const viewportElement = container.querySelector('#viewport');
|
|
148
|
+
expect(viewportElement).toBeTruthy();
|
|
149
|
+
if (viewportElement) {
|
|
150
|
+
await fireEvent.click(viewportElement, {
|
|
151
|
+
clientX: 100,
|
|
152
|
+
clientY: 100,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
await waitFor(() => {
|
|
156
|
+
expect(mockOnClick).toHaveBeenCalledTimes(1);
|
|
157
|
+
});
|
|
158
|
+
const callArgs = mockOnClick.mock.calls[0];
|
|
159
|
+
const addedMarks = callArgs[1];
|
|
160
|
+
expect(addedMarks.length).toBe(1);
|
|
161
|
+
expect(addedMarks[0].x).toBeCloseTo(100);
|
|
162
|
+
expect(addedMarks[0].y).toBeCloseTo(100);
|
|
163
|
+
await waitFor(() => {
|
|
164
|
+
const marksCountAfter = container.querySelector('[data-testid="marks-count"]');
|
|
165
|
+
expect(marksCountAfter?.textContent).toBe('1');
|
|
166
|
+
});
|
|
167
|
+
const marker = container.querySelector('[data-testid="marker-1"]');
|
|
168
|
+
expect(marker).toBeTruthy();
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
it('simulates adding multiple markers via clicks', async () => {
|
|
172
|
+
const mockOnClick = vi.fn();
|
|
173
|
+
const { container } = render(MultiCFUCounter, {
|
|
174
|
+
props: {
|
|
175
|
+
imageUrl: 'test-image.jpg',
|
|
176
|
+
onclick: mockOnClick,
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
await simulateImageLoad();
|
|
180
|
+
let marksCount = container.querySelector('[data-testid="marks-count"]');
|
|
181
|
+
expect(marksCount?.textContent).toBe('0');
|
|
182
|
+
const svg = container.querySelector('svg');
|
|
183
|
+
expect(svg).toBeTruthy();
|
|
184
|
+
if (svg) {
|
|
185
|
+
const viewportElement = container.querySelector('#viewport');
|
|
186
|
+
expect(viewportElement).toBeTruthy();
|
|
187
|
+
if (viewportElement) {
|
|
188
|
+
await fireEvent.click(viewportElement, {
|
|
189
|
+
clientX: 100,
|
|
190
|
+
clientY: 100,
|
|
191
|
+
});
|
|
192
|
+
await waitFor(() => {
|
|
193
|
+
expect(mockOnClick).toHaveBeenCalledTimes(1);
|
|
194
|
+
});
|
|
195
|
+
let firstCallArgs = mockOnClick.mock.calls[0];
|
|
196
|
+
let firstMarks = firstCallArgs[1];
|
|
197
|
+
expect(firstMarks.length).toBe(1);
|
|
198
|
+
await fireEvent.click(viewportElement, {
|
|
199
|
+
clientX: 200,
|
|
200
|
+
clientY: 200,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
await waitFor(() => {
|
|
204
|
+
expect(mockOnClick).toHaveBeenCalledTimes(2);
|
|
205
|
+
});
|
|
206
|
+
let secondCallArgs = mockOnClick.mock.calls[1];
|
|
207
|
+
let secondMarks = secondCallArgs[1];
|
|
208
|
+
expect(secondMarks.length).toBe(2);
|
|
209
|
+
const markers = container.querySelectorAll('[data-testid^="marker-"]');
|
|
210
|
+
expect(markers.length).toBe(2);
|
|
211
|
+
const circles = container.querySelectorAll('circle');
|
|
212
|
+
expect(circles.length).toBe(2);
|
|
213
|
+
expect(mockOnClick).toHaveBeenCalledTimes(2);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
it('simulates clearing marks through the reset button', async () => {
|
|
217
|
+
const mockOnClick = vi.fn();
|
|
218
|
+
const initialMarksData = [
|
|
219
|
+
{ x: 100, y: 100 },
|
|
220
|
+
{ x: 200, y: 200 },
|
|
221
|
+
];
|
|
222
|
+
const { container, component } = render(MultiCFUCounterTestWrapper, {
|
|
223
|
+
props: {
|
|
224
|
+
initialTestMarks: initialMarksData,
|
|
225
|
+
onclick: mockOnClick,
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
await simulateImageLoad();
|
|
229
|
+
await tick();
|
|
230
|
+
let marksCount = getByTestId(container, 'marks-count');
|
|
231
|
+
expect(marksCount?.textContent).toBe('2');
|
|
232
|
+
const clearButton = await findByText(container, 'Clear all');
|
|
233
|
+
expect(clearButton).toBeTruthy();
|
|
234
|
+
if (clearButton) {
|
|
235
|
+
await fireEvent.click(clearButton);
|
|
236
|
+
await tick();
|
|
237
|
+
const wrapperInstance = component;
|
|
238
|
+
const currentWrapperMarks = wrapperInstance.getCurrentMarks();
|
|
239
|
+
expect(currentWrapperMarks.length).toBe(0);
|
|
240
|
+
marksCount = getByTestId(container, 'wrapper-marks-count');
|
|
241
|
+
expect(marksCount?.textContent).toBe('0');
|
|
242
|
+
const innerMarksCount = getByTestId(container, 'marks-count');
|
|
243
|
+
expect(innerMarksCount?.textContent).toBe('0');
|
|
244
|
+
const markers = container.querySelectorAll('[data-testid^="marker-"]');
|
|
245
|
+
expect(markers.length).toBe(0);
|
|
246
|
+
expect(mockOnClick).not.toHaveBeenCalledWith();
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
it('properly constrains pan to prevent moving outside container', async () => {
|
|
250
|
+
const { container } = render(MultiCFUCounter, {
|
|
251
|
+
props: {
|
|
252
|
+
imageUrl: 'test-image.jpg',
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
await simulateImageLoad();
|
|
256
|
+
const viewport = container.querySelector('#viewport');
|
|
257
|
+
expect(viewport).toBeTruthy();
|
|
258
|
+
expect(viewport?.getAttribute('transform')).toBe('translate(0, 0) scale(1)');
|
|
259
|
+
});
|
|
260
|
+
it('properly sets up keyboard accessibility attributes', async () => {
|
|
261
|
+
const { container } = render(MultiCFUCounter, {
|
|
262
|
+
props: {
|
|
263
|
+
imageUrl: 'test-image.jpg',
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
await simulateImageLoad();
|
|
267
|
+
const svg = container.querySelector('svg[role="application"]');
|
|
268
|
+
expect(svg?.getAttribute('tabindex')).toBe('0');
|
|
269
|
+
expect(svg?.getAttribute('role')).toBe('application');
|
|
270
|
+
expect(svg?.getAttribute('aria-label')).toContain('CFU Counter');
|
|
271
|
+
});
|
|
272
|
+
it('simulates undoing a marker addition via the undo button', async () => {
|
|
273
|
+
const mockOnClick = vi.fn();
|
|
274
|
+
const initialMarksData = [{ x: 50, y: 50 }];
|
|
275
|
+
const { container, component } = render(MultiCFUCounterTestWrapper, {
|
|
276
|
+
props: {
|
|
277
|
+
initialTestMarks: initialMarksData,
|
|
278
|
+
onclick: mockOnClick,
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
await simulateImageLoad();
|
|
282
|
+
await tick();
|
|
283
|
+
let marksCount = getByTestId(container, 'marks-count');
|
|
284
|
+
expect(marksCount?.textContent).toBe('1');
|
|
285
|
+
const svg = container.querySelector('svg');
|
|
286
|
+
expect(svg).toBeTruthy();
|
|
287
|
+
if (svg) {
|
|
288
|
+
const viewportElement = container.querySelector('#viewport');
|
|
289
|
+
expect(viewportElement).toBeTruthy();
|
|
290
|
+
if (viewportElement) {
|
|
291
|
+
await fireEvent.click(viewportElement, { clientX: 100, clientY: 100 });
|
|
292
|
+
}
|
|
293
|
+
await tick();
|
|
294
|
+
const wrapperInstance = component;
|
|
295
|
+
const marksAfterClick = wrapperInstance.getCurrentMarks();
|
|
296
|
+
expect(marksAfterClick.length).toBe(2);
|
|
297
|
+
mockOnClick.mockClear();
|
|
298
|
+
}
|
|
299
|
+
const undoButton = await findByRole(container, 'button', { name: /Undo last mark/i });
|
|
300
|
+
expect(undoButton).toBeTruthy();
|
|
301
|
+
if (undoButton) {
|
|
302
|
+
await fireEvent.click(undoButton);
|
|
303
|
+
await tick();
|
|
304
|
+
const wrapperInstance = component;
|
|
305
|
+
const marksAfterUndo = wrapperInstance.getCurrentMarks();
|
|
306
|
+
expect(marksAfterUndo.length).toBe(1);
|
|
307
|
+
expect(marksAfterUndo[0].x).toBeCloseTo(50);
|
|
308
|
+
expect(marksAfterUndo[0].y).toBeCloseTo(50);
|
|
309
|
+
await waitFor(() => {
|
|
310
|
+
const innerMarksCount = getByTestId(container, 'marks-count');
|
|
311
|
+
expect(innerMarksCount?.textContent).toBe('1');
|
|
312
|
+
const wrapperMarksCount = getByTestId(container, 'wrapper-marks-count');
|
|
313
|
+
expect(wrapperMarksCount?.textContent).toBe('1');
|
|
314
|
+
});
|
|
315
|
+
const markers = container.querySelectorAll('[data-testid^="marker-"]');
|
|
316
|
+
expect(markers.length).toBe(1);
|
|
317
|
+
expect(mockOnClick).not.toHaveBeenCalled();
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
});
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
let { class: className = '', children, ...restProps }: Props = $props();
|
|
14
14
|
|
|
15
15
|
const baseClasses = `
|
|
16
|
-
relative flex w-full h-auto gap-2 cursor-default select-none items-center justify-between rounded-lg p-3 text-sm outline-none
|
|
17
|
-
focus:bg-neutral focus:text-accent-foreground
|
|
16
|
+
relative flex w-full h-auto gap-2 cursor-default select-none items-center justify-between rounded-lg p-3 text-sm outline-none
|
|
17
|
+
focus:bg-neutral focus:text-accent-foreground
|
|
18
18
|
data-[disabled]:pointer-events-none data-[disabled]:opacity-50
|
|
19
19
|
data-[highlighted]:bg-neutral data-[highlighted]:text-accent-foreground
|
|
20
20
|
`;
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
<Input
|
|
87
87
|
bind:value={() => getStringValue(), (v) => setNumericValue(v)}
|
|
88
88
|
placeholder={String(placeholder)}
|
|
89
|
-
type={'
|
|
89
|
+
type={'text'}
|
|
90
90
|
inputmode={'decimal'}
|
|
91
91
|
pattern={config?.schema?.type === 'integer' ? '-?\\d*' : '-?\\d*\\.?\\d*'}
|
|
92
92
|
{readonly}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
declare const SjsfNumberInputWrapper: import("svelte").Component<
|
|
1
|
+
declare const SjsfNumberInputWrapper: import("svelte").Component<any, {}, "value">;
|
|
2
2
|
type SjsfNumberInputWrapper = ReturnType<typeof SjsfNumberInputWrapper>;
|
|
3
3
|
export default SjsfNumberInputWrapper;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getFormContext, selectAttributes } from '@sjsf/form';
|
|
3
|
+
import type { ComponentProps } from '@sjsf/form';
|
|
4
|
+
import * as Select from '../select';
|
|
5
|
+
|
|
6
|
+
type Props = ComponentProps['selectWidget'];
|
|
7
|
+
let { handlers, value = $bindable(), options = [], config }: Props = $props();
|
|
8
|
+
|
|
9
|
+
const ctx = getFormContext();
|
|
10
|
+
const attributes = $derived(
|
|
11
|
+
selectAttributes(ctx, config, 'select', handlers, { style: 'flex-grow: 1' })
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
const isPrimitive = (v: unknown): v is string | number | boolean =>
|
|
15
|
+
['string', 'number', 'boolean'].includes(typeof v);
|
|
16
|
+
|
|
17
|
+
let domValue = $derived.by(() => {
|
|
18
|
+
const primitiveMatch = options.find((o) => isPrimitive(o.value) && o.value === value);
|
|
19
|
+
if (primitiveMatch) return String(primitiveMatch.value);
|
|
20
|
+
const idx = options.findIndex((o) => o.value === value);
|
|
21
|
+
return idx >= 0 ? String(idx) : '';
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const onSelectChange = (v: string | undefined) => {
|
|
25
|
+
if (v === undefined || v === '') {
|
|
26
|
+
value = undefined;
|
|
27
|
+
} else {
|
|
28
|
+
const primitiveMatch = options.find((o) => isPrimitive(o.value) && String(o.value) === v);
|
|
29
|
+
if (primitiveMatch) {
|
|
30
|
+
value = primitiveMatch.value;
|
|
31
|
+
} else {
|
|
32
|
+
const idx = Number(v);
|
|
33
|
+
if (!Number.isNaN(idx) && options[idx]) {
|
|
34
|
+
value = options[idx].value;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (typeof (attributes as any).onchange === 'function') {
|
|
39
|
+
(attributes as any).onchange(new Event('change'));
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<input
|
|
45
|
+
type="hidden"
|
|
46
|
+
name={String(attributes.name)}
|
|
47
|
+
id={String(attributes.id)}
|
|
48
|
+
value={domValue}
|
|
49
|
+
disabled={attributes.disabled}
|
|
50
|
+
/>
|
|
51
|
+
|
|
52
|
+
<Select.Root value={domValue} type="single" items={[]} onValueChange={onSelectChange}>
|
|
53
|
+
<Select.Trigger
|
|
54
|
+
placeholder={attributes.placeholder ?? undefined}
|
|
55
|
+
displayValue={options.find((o) => isPrimitive(o.value) && String(o.value) === domValue)
|
|
56
|
+
?.label || options.find((o, idx) => String(idx) === domValue && !isPrimitive(o.value))?.label}
|
|
57
|
+
disabled={attributes.disabled}
|
|
58
|
+
/>
|
|
59
|
+
<Select.Portal>
|
|
60
|
+
<Select.Content>
|
|
61
|
+
{#if config.schema.default === undefined}
|
|
62
|
+
<Select.Item value="" label={attributes.placeholder ?? undefined} />
|
|
63
|
+
{/if}
|
|
64
|
+
{#each options as option, index (option.id)}
|
|
65
|
+
{#if isPrimitive(option.value)}
|
|
66
|
+
<Select.Item
|
|
67
|
+
value={String(option.value)}
|
|
68
|
+
label={option.label}
|
|
69
|
+
disabled={option.disabled}
|
|
70
|
+
/>
|
|
71
|
+
{:else}
|
|
72
|
+
<Select.Item value={String(index)} label={option.label} disabled={option.disabled} />
|
|
73
|
+
{/if}
|
|
74
|
+
{/each}
|
|
75
|
+
</Select.Content>
|
|
76
|
+
</Select.Portal>
|
|
77
|
+
</Select.Root>
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
declare const SjsfTextInputWrapper: import("svelte").Component<
|
|
1
|
+
declare const SjsfTextInputWrapper: import("svelte").Component<any, {}, "value">;
|
|
2
2
|
type SjsfTextInputWrapper = ReturnType<typeof SjsfTextInputWrapper>;
|
|
3
3
|
export default SjsfTextInputWrapper;
|
|
@@ -2,7 +2,11 @@ import { theme as BasicTheme } from '@sjsf/basic-theme';
|
|
|
2
2
|
import { overrideByRecord } from '@sjsf/form/lib/resolver';
|
|
3
3
|
import SjsfNumberInputWrapper from './SjsfNumberInputWrapper.svelte';
|
|
4
4
|
import SjsfTextInputWrapper from './SjsfTextInputWrapper.svelte';
|
|
5
|
+
import EnumField from '@sjsf/form/fields/extra-fields/enum.svelte';
|
|
6
|
+
import SjsfSelectWidgetWrapper from './SjsfSelectWidgetWrapper.svelte';
|
|
5
7
|
export const sjsfCustomTheme = overrideByRecord(BasicTheme, {
|
|
6
8
|
numberWidget: SjsfNumberInputWrapper,
|
|
7
9
|
textWidget: SjsfTextInputWrapper,
|
|
10
|
+
enumField: EnumField,
|
|
11
|
+
selectWidget: SjsfSelectWidgetWrapper,
|
|
8
12
|
});
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
export let onMousedown: () => void = () => {};
|
|
15
15
|
export let onMouseup: () => void = () => {};
|
|
16
16
|
export let id: string | undefined = undefined;
|
|
17
|
+
export let disabled = false;
|
|
17
18
|
|
|
18
19
|
$: if (value > max) {
|
|
19
20
|
value = max;
|
|
@@ -29,7 +30,7 @@
|
|
|
29
30
|
}
|
|
30
31
|
</script>
|
|
31
32
|
|
|
32
|
-
<div class={`relative flex h-10 items-center ${className}`}>
|
|
33
|
+
<div class={`relative flex h-10 items-center ${className} ${disabled ? 'opacity-40' : ''}`}>
|
|
33
34
|
<div class="track-overlay"></div>
|
|
34
35
|
<div
|
|
35
36
|
class="pointer-events-none absolute h-2.5 rounded-full {bufferColorClass}"
|
|
@@ -62,6 +63,7 @@
|
|
|
62
63
|
bind:value
|
|
63
64
|
on:mousedown={onMousedown}
|
|
64
65
|
on:mouseup={onMouseup}
|
|
66
|
+
{disabled}
|
|
65
67
|
/>
|
|
66
68
|
<div class="thumb-overlay" style="left: {calculatePosition(value)} + 0.5rem)">
|
|
67
69
|
<Icon iconName="CaretUpDown" class="rotate-90" />
|
|
@@ -124,4 +126,8 @@
|
|
|
124
126
|
height: 0;
|
|
125
127
|
border: none;
|
|
126
128
|
}
|
|
129
|
+
|
|
130
|
+
input[type='range']:disabled {
|
|
131
|
+
cursor: not-allowed;
|
|
132
|
+
}
|
|
127
133
|
</style>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { render, fireEvent, screen } from '@testing-library/svelte';
|
|
2
|
+
import { tick } from 'svelte';
|
|
3
|
+
import StatCard from './StatCard.svelte';
|
|
4
|
+
import '@testing-library/jest-dom';
|
|
5
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
6
|
+
describe('StatCard', () => {
|
|
7
|
+
it('should focus the input field when the pencil edit icon is clicked', async () => {
|
|
8
|
+
render(StatCard, {
|
|
9
|
+
props: {
|
|
10
|
+
title: 'Test Stat',
|
|
11
|
+
value: 'Initial Value',
|
|
12
|
+
editable: true,
|
|
13
|
+
onsubmit: vi.fn(),
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
const pencilButton = screen.getByRole('button');
|
|
17
|
+
await fireEvent.click(pencilButton);
|
|
18
|
+
// Wait for Svelte's reactivity and the $effect with tick() to apply focus
|
|
19
|
+
await tick();
|
|
20
|
+
const inputField = screen.getByRole('textbox');
|
|
21
|
+
expect(inputField).toHaveFocus();
|
|
22
|
+
});
|
|
23
|
+
it('should focus the input field when the card body is clicked', async () => {
|
|
24
|
+
render(StatCard, {
|
|
25
|
+
props: {
|
|
26
|
+
title: 'Clickable Stat',
|
|
27
|
+
value: 'Initial Value',
|
|
28
|
+
editable: true,
|
|
29
|
+
onsubmit: vi.fn(),
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
const cardBody = screen.getByTestId('stat-card-body');
|
|
33
|
+
await fireEvent.click(cardBody);
|
|
34
|
+
await tick(); // Wait for Svelte's reactivity
|
|
35
|
+
const inputField = screen.getByRole('textbox');
|
|
36
|
+
expect(inputField).toHaveFocus();
|
|
37
|
+
});
|
|
38
|
+
it('should not focus the input field when the card body is clicked, out of editable state', async () => {
|
|
39
|
+
render(StatCard, {
|
|
40
|
+
props: {
|
|
41
|
+
title: 'Clickable Stat',
|
|
42
|
+
value: 'Initial Value',
|
|
43
|
+
editable: false,
|
|
44
|
+
onsubmit: vi.fn(),
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
const cardBody = screen.getByTestId('stat-card-body');
|
|
48
|
+
await fireEvent.click(cardBody);
|
|
49
|
+
await tick(); // Wait for Svelte's reactivity
|
|
50
|
+
// When not editable, the input field should not be rendered
|
|
51
|
+
const inputField = screen.queryByRole('textbox');
|
|
52
|
+
expect(inputField).toBeNull();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/svelte';
|
|
2
|
+
import Table from './Table.test.svelte';
|
|
3
|
+
const users = [
|
|
4
|
+
{
|
|
5
|
+
name: 'John Doe',
|
|
6
|
+
age: 25,
|
|
7
|
+
role: 'admin',
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
name: 'Jane Smith',
|
|
11
|
+
age: 32,
|
|
12
|
+
role: 'member',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: 'Michael Johnson',
|
|
16
|
+
age: 41,
|
|
17
|
+
role: 'deactivated',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'Emily Brown',
|
|
21
|
+
age: 28,
|
|
22
|
+
role: 'member',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'David Lee',
|
|
26
|
+
age: 37,
|
|
27
|
+
role: 'admin',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'Sarah Wilson',
|
|
31
|
+
age: 29,
|
|
32
|
+
role: 'member',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'Robert Taylor',
|
|
36
|
+
age: 45,
|
|
37
|
+
role: 'deactivated',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'Lisa Anderson',
|
|
41
|
+
age: 33,
|
|
42
|
+
role: 'member',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'James Martinez',
|
|
46
|
+
age: 39,
|
|
47
|
+
role: 'admin',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'Jennifer Garcia',
|
|
51
|
+
age: 31,
|
|
52
|
+
role: 'deactivated',
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
describe('Table Component', () => {
|
|
56
|
+
it('renders the table with correct headers', () => {
|
|
57
|
+
render(Table, { props: { users } });
|
|
58
|
+
expect(screen.getByText('Name')).toBeInTheDocument();
|
|
59
|
+
expect(screen.getByText('Age')).toBeInTheDocument();
|
|
60
|
+
expect(screen.getByText('Role')).toBeInTheDocument();
|
|
61
|
+
});
|
|
62
|
+
it('renders table rows with user data', () => {
|
|
63
|
+
render(Table, { props: { users } });
|
|
64
|
+
users.forEach((user) => {
|
|
65
|
+
expect(screen.getByTestId(`user-${user.name}`)).toBeInTheDocument();
|
|
66
|
+
expect(screen.getByTestId(`user-${user.name}-age`)).toBeInTheDocument();
|
|
67
|
+
expect(screen.getByTestId(`user-${user.name}-role`)).toBeInTheDocument();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
it('applies disabled class to deactivated users', () => {
|
|
71
|
+
render(Table, { props: { users } });
|
|
72
|
+
const deactivatedUser = screen.getByText('Michael Johnson');
|
|
73
|
+
expect(deactivatedUser.closest('tr')).toHaveClass('disabled');
|
|
74
|
+
});
|
|
75
|
+
it('renders phone icon button for admin users', () => {
|
|
76
|
+
render(Table, { props: { users, userRole: 'admin' } });
|
|
77
|
+
const phoneIcon = screen.getAllByTestId('phone-icon');
|
|
78
|
+
expect(phoneIcon).toHaveLength(users.length);
|
|
79
|
+
});
|
|
80
|
+
it('does not render phone icon button for non-admin users', () => {
|
|
81
|
+
render(Table, { props: { users, userRole: 'member' } });
|
|
82
|
+
const phoneIcon = screen.queryAllByTestId('phone-icon');
|
|
83
|
+
expect(phoneIcon).toHaveLength(0);
|
|
84
|
+
});
|
|
85
|
+
it('applies disabled styles to td elements when disabled', () => {
|
|
86
|
+
render(Table, { props: { users, userRole: 'member' } });
|
|
87
|
+
const deactivatedUser = screen.getByText('Michael Johnson');
|
|
88
|
+
const tr = deactivatedUser.closest('tr');
|
|
89
|
+
if (tr) {
|
|
90
|
+
expect(tr.classList.contains('disabled')).toBe(true);
|
|
91
|
+
expect(tr.classList.contains('[&.disabled]:text-tertiary')).toBe(true);
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
throw new Error('Tr element not found');
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
});
|