@snapdragonsnursery/react-components 1.3.0 → 1.3.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.
@@ -0,0 +1,546 @@
1
+ // Employee Search Filters Component Tests
2
+ // Tests the EmployeeSearchFilters component functionality including filter changes, validation, and UI interactions
3
+
4
+ import React from 'react';
5
+ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
6
+ import '@testing-library/jest-dom';
7
+ import EmployeeSearchFilters from './EmployeeSearchFilters';
8
+
9
+ // Mock the UI components
10
+ jest.mock('./ui/input', () => ({
11
+ Input: ({ value, onChange, placeholder, ...props }) => {
12
+ return (
13
+ <input
14
+ data-testid="input"
15
+ value={value}
16
+ onChange={onChange}
17
+ placeholder={placeholder}
18
+ {...props}
19
+ />
20
+ );
21
+ }
22
+ }));
23
+
24
+ jest.mock('./ui/select', () => {
25
+ const MockSelect = ({ value, onChange, children, ...props }) => {
26
+ return (
27
+ <select data-testid="select" value={value} onChange={onChange} {...props}>
28
+ {children}
29
+ </select>
30
+ );
31
+ };
32
+
33
+ const MockSelectOption = ({ value, children, ...props }) => {
34
+ return (
35
+ <option value={value} {...props}>
36
+ {children}
37
+ </option>
38
+ );
39
+ };
40
+
41
+ return {
42
+ Select: MockSelect,
43
+ SelectOption: MockSelectOption,
44
+ };
45
+ });
46
+
47
+ jest.mock('./ui/date-range-picker', () => ({
48
+ DateRangePicker: ({ selectedRange, onSelect, placeholder, ...props }) => {
49
+ return (
50
+ <div data-testid="date-range-picker">
51
+ <input
52
+ data-testid="date-range-input"
53
+ placeholder={placeholder}
54
+ value={selectedRange ? `${selectedRange.from?.toISOString() || ''} - ${selectedRange.to?.toISOString() || ''}` : ''}
55
+ onChange={(e) => {
56
+ // Simulate date range selection
57
+ if (e.target.value.includes('2024-01-01')) {
58
+ onSelect({
59
+ from: new Date('2024-01-01'),
60
+ to: new Date('2024-01-31')
61
+ });
62
+ } else {
63
+ onSelect(null);
64
+ }
65
+ }}
66
+ {...props}
67
+ />
68
+ </div>
69
+ );
70
+ }
71
+ }));
72
+
73
+ jest.mock('./ui/button', () => ({
74
+ Button: ({ children, onClick, variant, size, ...props }) => {
75
+ return (
76
+ <button
77
+ data-testid="button"
78
+ onClick={onClick}
79
+ data-variant={variant}
80
+ data-size={size}
81
+ {...props}
82
+ >
83
+ {children}
84
+ </button>
85
+ );
86
+ }
87
+ }));
88
+
89
+ const mockSites = [
90
+ { site_id: 1, site_name: "Nursery A" },
91
+ { site_id: 2, site_name: "Nursery B" },
92
+ { site_id: 3, site_name: "Nursery C" },
93
+ ];
94
+
95
+ const mockRoles = [
96
+ { role_id: 1, role_name: "Manager" },
97
+ { role_id: 2, role_name: "Deputy Manager" },
98
+ { role_id: 3, role_name: "Room Leader" },
99
+ ];
100
+
101
+ const mockManagers = [
102
+ { entra_id: "manager1", full_name: "John Smith" },
103
+ { entra_id: "manager2", full_name: "Jane Doe" },
104
+ ];
105
+
106
+ const defaultFilters = {
107
+ status: "Active",
108
+ selectedSiteId: "",
109
+ roleId: "",
110
+ managerId: "",
111
+ startDateFrom: "",
112
+ startDateTo: "",
113
+ endDateFrom: "",
114
+ endDateTo: "",
115
+ termTimeOnly: "",
116
+ onMaternityLeave: "",
117
+ dbsNumber: "",
118
+ minHoursPerWeek: "",
119
+ sortBy: "surname",
120
+ sortOrder: "asc",
121
+ };
122
+
123
+ const defaultProps = {
124
+ filters: defaultFilters,
125
+ onFiltersChange: jest.fn(),
126
+ onApplyFilters: jest.fn(),
127
+ onClearFilters: jest.fn(),
128
+ onCancelChanges: jest.fn(),
129
+ sites: mockSites,
130
+ roles: mockRoles,
131
+ managers: mockManagers,
132
+ activeOnly: true,
133
+ isAdvancedFiltersOpen: false,
134
+ onToggleAdvancedFilters: jest.fn(),
135
+ };
136
+
137
+ describe('EmployeeSearchFilters', () => {
138
+ beforeEach(() => {
139
+ jest.clearAllMocks();
140
+ });
141
+
142
+ describe('Basic Rendering', () => {
143
+ it('renders the advanced filters toggle button', () => {
144
+ render(<EmployeeSearchFilters {...defaultProps} />);
145
+
146
+ expect(screen.getByText('Advanced Filters')).toBeInTheDocument();
147
+ expect(screen.getByText('Clear Filters')).toBeInTheDocument();
148
+ });
149
+
150
+ it('shows advanced filters when isAdvancedFiltersOpen is true', () => {
151
+ render(<EmployeeSearchFilters {...defaultProps} isAdvancedFiltersOpen={true} />);
152
+
153
+ // Use getAllByText to handle multiple elements with same text
154
+ const statusLabels = screen.getAllByText('Status');
155
+ expect(statusLabels.length).toBeGreaterThan(0);
156
+ const siteLabels = screen.getAllByText('Site');
157
+ expect(siteLabels.length).toBeGreaterThan(0);
158
+ });
159
+
160
+ it('hides advanced filters when isAdvancedFiltersOpen is false', () => {
161
+ render(<EmployeeSearchFilters {...defaultProps} isAdvancedFiltersOpen={false} />);
162
+
163
+ expect(screen.queryByText('Status')).not.toBeInTheDocument();
164
+ expect(screen.queryByText('Site')).not.toBeInTheDocument();
165
+ });
166
+ });
167
+
168
+ describe('Filter Interactions', () => {
169
+ it('calls onToggleAdvancedFilters when toggle button is clicked', () => {
170
+ render(<EmployeeSearchFilters {...defaultProps} />);
171
+
172
+ fireEvent.click(screen.getByText('Advanced Filters'));
173
+
174
+ expect(defaultProps.onToggleAdvancedFilters).toHaveBeenCalledTimes(1);
175
+ });
176
+
177
+ it('calls onClearFilters when clear filters button is clicked', () => {
178
+ render(<EmployeeSearchFilters {...defaultProps} />);
179
+
180
+ fireEvent.click(screen.getByText('Clear Filters'));
181
+
182
+ expect(defaultProps.onClearFilters).toHaveBeenCalledTimes(1);
183
+ });
184
+
185
+ it('updates status filter when changed', () => {
186
+ render(<EmployeeSearchFilters {...defaultProps} isAdvancedFiltersOpen={true} />);
187
+
188
+ const statusSelect = screen.getAllByTestId('select')[0];
189
+ fireEvent.change(statusSelect, { target: { value: 'Inactive' } });
190
+
191
+ expect(defaultProps.onFiltersChange).toHaveBeenCalledWith({
192
+ ...defaultFilters,
193
+ status: 'Inactive',
194
+ });
195
+ });
196
+
197
+ it('updates site filter when changed', () => {
198
+ render(<EmployeeSearchFilters {...defaultProps} isAdvancedFiltersOpen={true} />);
199
+
200
+ const siteSelect = screen.getAllByTestId('select')[1];
201
+ fireEvent.change(siteSelect, { target: { value: '2' } });
202
+
203
+ expect(defaultProps.onFiltersChange).toHaveBeenCalledWith({
204
+ ...defaultFilters,
205
+ selectedSiteId: '2',
206
+ });
207
+ });
208
+
209
+ it('updates role filter when changed', () => {
210
+ render(<EmployeeSearchFilters {...defaultProps} isAdvancedFiltersOpen={true} />);
211
+
212
+ const roleSelect = screen.getAllByTestId('select')[2];
213
+ fireEvent.change(roleSelect, { target: { value: '3' } });
214
+
215
+ expect(defaultProps.onFiltersChange).toHaveBeenCalledWith({
216
+ ...defaultFilters,
217
+ roleId: '3',
218
+ });
219
+ });
220
+
221
+ it('updates manager filter when changed', () => {
222
+ render(<EmployeeSearchFilters {...defaultProps} isAdvancedFiltersOpen={true} />);
223
+
224
+ const managerSelect = screen.getAllByTestId('select')[3];
225
+ fireEvent.change(managerSelect, { target: { value: 'manager2' } });
226
+
227
+ expect(defaultProps.onFiltersChange).toHaveBeenCalledWith({
228
+ ...defaultFilters,
229
+ managerId: 'manager2',
230
+ });
231
+ });
232
+
233
+ it('updates DBS number filter when changed', () => {
234
+ render(<EmployeeSearchFilters {...defaultProps} isAdvancedFiltersOpen={true} />);
235
+
236
+ const dbsInput = screen.getAllByTestId('input')[0];
237
+ fireEvent.change(dbsInput, { target: { value: 'DBS123456' } });
238
+
239
+ expect(defaultProps.onFiltersChange).toHaveBeenCalledWith({
240
+ ...defaultFilters,
241
+ dbsNumber: 'DBS123456',
242
+ });
243
+ });
244
+
245
+ it('updates working hours filter when changed', () => {
246
+ render(<EmployeeSearchFilters {...defaultProps} isAdvancedFiltersOpen={true} />);
247
+
248
+ const hoursInput = screen.getAllByTestId('input')[1];
249
+ fireEvent.change(hoursInput, { target: { value: '30' } });
250
+
251
+ expect(defaultProps.onFiltersChange).toHaveBeenCalledWith({
252
+ ...defaultFilters,
253
+ minHoursPerWeek: '30',
254
+ });
255
+ });
256
+ });
257
+
258
+ describe('Date Range Filters', () => {
259
+ it('handles start date range selection', async () => {
260
+ render(<EmployeeSearchFilters {...defaultProps} isAdvancedFiltersOpen={true} />);
261
+
262
+ const startDatePicker = screen.getAllByTestId('date-range-input')[0];
263
+ fireEvent.change(startDatePicker, { target: { value: '2024-01-01T00:00:00.000Z - 2024-01-31T00:00:00.000Z' } });
264
+
265
+ // The component uses local state, so we just check that the unsaved changes indicator appears
266
+ await waitFor(() => {
267
+ expect(screen.getByText('You have unsaved filter changes')).toBeInTheDocument();
268
+ });
269
+ });
270
+
271
+ it('handles end date range selection', async () => {
272
+ render(<EmployeeSearchFilters {...defaultProps} isAdvancedFiltersOpen={true} />);
273
+
274
+ const endDatePicker = screen.getAllByTestId('date-range-input')[1];
275
+ fireEvent.change(endDatePicker, { target: { value: '2024-01-01T00:00:00.000Z - 2024-01-31T00:00:00.000Z' } });
276
+
277
+ // The component uses local state, so we just check that the unsaved changes indicator appears
278
+ await waitFor(() => {
279
+ expect(screen.getByText('You have unsaved filter changes')).toBeInTheDocument();
280
+ });
281
+ });
282
+ });
283
+
284
+ describe('Filter Visibility', () => {
285
+ it('shows all filters by default', () => {
286
+ render(<EmployeeSearchFilters {...defaultProps} isAdvancedFiltersOpen={true} />);
287
+
288
+ // Use getAllByText to get all elements with "Role" text and check the label specifically
289
+ const roleLabels = screen.getAllByText('Role');
290
+ expect(roleLabels.length).toBeGreaterThan(0);
291
+
292
+ // Use getAllByText to get all elements with "Manager" text and check the label specifically
293
+ const managerLabels = screen.getAllByText('Manager');
294
+ expect(managerLabels.length).toBeGreaterThan(0);
295
+
296
+ // Use getAllByText for other filters that might have conflicts
297
+ const termTimeLabels = screen.getAllByText('Term Time Only');
298
+ expect(termTimeLabels.length).toBeGreaterThan(0);
299
+
300
+ const maternityLabels = screen.getAllByText('Maternity Leave');
301
+ expect(maternityLabels.length).toBeGreaterThan(0);
302
+
303
+ expect(screen.getByText('Start Date Range')).toBeInTheDocument();
304
+ expect(screen.getByText('End Date Range')).toBeInTheDocument();
305
+ expect(screen.getByText('DBS Number')).toBeInTheDocument();
306
+ expect(screen.getByText('Min Hours Per Week')).toBeInTheDocument();
307
+ });
308
+
309
+ it('hides role filter when showRoleFilter is false', () => {
310
+ render(
311
+ <EmployeeSearchFilters
312
+ {...defaultProps}
313
+ isAdvancedFiltersOpen={true}
314
+ showRoleFilter={false}
315
+ />
316
+ );
317
+
318
+ // Check that the role label is not present (but the sort option might still be there)
319
+ const roleLabels = screen.queryAllByText('Role');
320
+ const roleLabel = roleLabels.find(element => element.tagName === 'LABEL');
321
+ expect(roleLabel).toBeUndefined();
322
+ });
323
+
324
+ it('hides manager filter when showManagerFilter is false', () => {
325
+ render(
326
+ <EmployeeSearchFilters
327
+ {...defaultProps}
328
+ isAdvancedFiltersOpen={true}
329
+ showManagerFilter={false}
330
+ />
331
+ );
332
+
333
+ // Check that the manager label is not present (but role options might still be there)
334
+ const managerLabels = screen.queryAllByText('Manager');
335
+ const managerLabel = managerLabels.find(element => element.tagName === 'LABEL');
336
+ expect(managerLabel).toBeUndefined();
337
+ });
338
+
339
+ it('hides term time filter when showTermTimeFilter is false', () => {
340
+ render(
341
+ <EmployeeSearchFilters
342
+ {...defaultProps}
343
+ isAdvancedFiltersOpen={true}
344
+ showTermTimeFilter={false}
345
+ />
346
+ );
347
+
348
+ const termTimeLabels = screen.queryAllByText('Term Time Only');
349
+ const termTimeLabel = termTimeLabels.find(element => element.tagName === 'LABEL');
350
+ expect(termTimeLabel).toBeUndefined();
351
+ });
352
+
353
+ it('hides maternity filter when showMaternityFilter is false', () => {
354
+ render(
355
+ <EmployeeSearchFilters
356
+ {...defaultProps}
357
+ isAdvancedFiltersOpen={true}
358
+ showMaternityFilter={false}
359
+ />
360
+ );
361
+
362
+ const maternityLabels = screen.queryAllByText('Maternity Leave');
363
+ const maternityLabel = maternityLabels.find(element => element.tagName === 'LABEL');
364
+ expect(maternityLabel).toBeUndefined();
365
+ });
366
+
367
+ it('hides start date filter when showStartDateFilter is false', () => {
368
+ render(
369
+ <EmployeeSearchFilters
370
+ {...defaultProps}
371
+ isAdvancedFiltersOpen={true}
372
+ showStartDateFilter={false}
373
+ />
374
+ );
375
+
376
+ expect(screen.queryByText('Start Date Range')).not.toBeInTheDocument();
377
+ });
378
+
379
+ it('hides end date filter when showEndDateFilter is false', () => {
380
+ render(
381
+ <EmployeeSearchFilters
382
+ {...defaultProps}
383
+ isAdvancedFiltersOpen={true}
384
+ showEndDateFilter={false}
385
+ />
386
+ );
387
+
388
+ expect(screen.queryByText('End Date Range')).not.toBeInTheDocument();
389
+ });
390
+
391
+ it('hides DBS filter when showDbsFilter is false', () => {
392
+ render(
393
+ <EmployeeSearchFilters
394
+ {...defaultProps}
395
+ isAdvancedFiltersOpen={true}
396
+ showDbsFilter={false}
397
+ />
398
+ );
399
+
400
+ expect(screen.queryByText('DBS Number')).not.toBeInTheDocument();
401
+ });
402
+
403
+ it('hides working hours filter when showWorkingHoursFilter is false', () => {
404
+ render(
405
+ <EmployeeSearchFilters
406
+ {...defaultProps}
407
+ isAdvancedFiltersOpen={true}
408
+ showWorkingHoursFilter={false}
409
+ />
410
+ );
411
+
412
+ expect(screen.queryByText('Min Hours Per Week')).not.toBeInTheDocument();
413
+ });
414
+ });
415
+
416
+ describe('Apply/Cancel Buttons', () => {
417
+ it('shows apply/cancel buttons when there are unsaved changes', () => {
418
+ render(<EmployeeSearchFilters {...defaultProps} isAdvancedFiltersOpen={true} />);
419
+
420
+ // Change a filter to trigger unsaved changes
421
+ const statusSelect = screen.getAllByTestId('select')[0];
422
+ fireEvent.change(statusSelect, { target: { value: 'Inactive' } });
423
+
424
+ expect(screen.getByText('You have unsaved filter changes')).toBeInTheDocument();
425
+ expect(screen.getByText('Cancel')).toBeInTheDocument();
426
+ expect(screen.getByText('Apply Filters')).toBeInTheDocument();
427
+ });
428
+
429
+ it('calls onApplyFilters when apply button is clicked', () => {
430
+ render(<EmployeeSearchFilters {...defaultProps} isAdvancedFiltersOpen={true} />);
431
+
432
+ // Change a filter to trigger unsaved changes
433
+ const statusSelect = screen.getAllByTestId('select')[0];
434
+ fireEvent.change(statusSelect, { target: { value: 'Inactive' } });
435
+
436
+ fireEvent.click(screen.getByText('Apply Filters'));
437
+
438
+ expect(defaultProps.onApplyFilters).toHaveBeenCalledWith({
439
+ ...defaultFilters,
440
+ status: 'Inactive',
441
+ });
442
+ });
443
+
444
+ it('calls onCancelChanges when cancel button is clicked', () => {
445
+ render(<EmployeeSearchFilters {...defaultProps} isAdvancedFiltersOpen={true} />);
446
+
447
+ // Change a filter to trigger unsaved changes
448
+ const statusSelect = screen.getAllByTestId('select')[0];
449
+ fireEvent.change(statusSelect, { target: { value: 'Inactive' } });
450
+
451
+ fireEvent.click(screen.getByText('Cancel'));
452
+
453
+ expect(defaultProps.onCancelChanges).toHaveBeenCalledTimes(1);
454
+ });
455
+ });
456
+
457
+ describe('Sorting Options', () => {
458
+ it('updates sort by field when changed', () => {
459
+ render(<EmployeeSearchFilters {...defaultProps} isAdvancedFiltersOpen={true} />);
460
+
461
+ const sortBySelect = screen.getAllByTestId('select').find(select =>
462
+ select.querySelector('option[value="first_name"]')
463
+ );
464
+ fireEvent.change(sortBySelect, { target: { value: 'first_name' } });
465
+
466
+ // The component should show unsaved changes indicator
467
+ expect(screen.getByText('You have unsaved filter changes')).toBeInTheDocument();
468
+ });
469
+
470
+ it('updates sort order when changed', () => {
471
+ render(<EmployeeSearchFilters {...defaultProps} isAdvancedFiltersOpen={true} />);
472
+
473
+ const sortOrderSelect = screen.getAllByTestId('select').find(select =>
474
+ select.querySelector('option[value="desc"]')
475
+ );
476
+ fireEvent.change(sortOrderSelect, { target: { value: 'desc' } });
477
+
478
+ // The component should show unsaved changes indicator
479
+ expect(screen.getByText('You have unsaved filter changes')).toBeInTheDocument();
480
+ });
481
+ });
482
+
483
+ describe('Edge Cases', () => {
484
+ it('handles empty sites array', () => {
485
+ render(
486
+ <EmployeeSearchFilters
487
+ {...defaultProps}
488
+ sites={[]}
489
+ isAdvancedFiltersOpen={true}
490
+ />
491
+ );
492
+
493
+ // When sites array is empty, the site filter should not be rendered
494
+ const siteLabels = screen.queryAllByText('Site');
495
+ const siteLabel = siteLabels.find(element => element.tagName === 'LABEL');
496
+ expect(siteLabel).toBeUndefined();
497
+ });
498
+
499
+ it('handles empty roles array', () => {
500
+ render(
501
+ <EmployeeSearchFilters
502
+ {...defaultProps}
503
+ roles={[]}
504
+ isAdvancedFiltersOpen={true}
505
+ />
506
+ );
507
+
508
+ // When roles array is empty, the role filter should not be rendered
509
+ const roleLabels = screen.queryAllByText('Role');
510
+ const roleLabel = roleLabels.find(element => element.tagName === 'LABEL');
511
+ expect(roleLabel).toBeUndefined();
512
+ });
513
+
514
+ it('handles empty managers array', () => {
515
+ render(
516
+ <EmployeeSearchFilters
517
+ {...defaultProps}
518
+ managers={[]}
519
+ isAdvancedFiltersOpen={true}
520
+ />
521
+ );
522
+
523
+ // When managers array is empty, the manager filter should not be rendered
524
+ const managerLabels = screen.queryAllByText('Manager');
525
+ const managerLabel = managerLabels.find(element => element.tagName === 'LABEL');
526
+ expect(managerLabel).toBeUndefined();
527
+ });
528
+
529
+ it('handles null props gracefully', () => {
530
+ render(
531
+ <EmployeeSearchFilters
532
+ {...defaultProps}
533
+ sites={null}
534
+ roles={null}
535
+ managers={null}
536
+ isAdvancedFiltersOpen={true}
537
+ />
538
+ );
539
+
540
+ // Should still render basic filters
541
+ const statusLabels = screen.getAllByText('Status');
542
+ expect(statusLabels.length).toBeGreaterThan(0);
543
+ expect(screen.getByText('Sort By')).toBeInTheDocument();
544
+ });
545
+ });
546
+ });
package/src/index.js CHANGED
@@ -10,6 +10,13 @@ export { default as DateRangePickerDemo } from "./DateRangePickerDemo";
10
10
  export { default as CalendarDemo } from "./CalendarDemo";
11
11
  export { default as DateRangePickerTest } from "./DateRangePickerTest";
12
12
  export { default as ApplyButtonDemo } from "./ApplyButtonDemo";
13
+
14
+ // Employee Search Components
15
+ export { default as EmployeeSearchPage } from "./EmployeeSearchPage";
16
+ export { default as EmployeeSearchModal } from "./EmployeeSearchModal";
17
+ export { default as EmployeeSearchDemo } from "./EmployeeSearchDemo";
18
+ export { default as EmployeeSearchFilters } from "./components/EmployeeSearchFilters";
19
+
13
20
  export { configureTelemetry } from "./telemetry";
14
21
 
15
22
  // UI Components