@snapdragonsnursery/react-components 1.6.0 → 1.8.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snapdragonsnursery/react-components",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
4
4
  "description": "",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -40,8 +40,14 @@
40
40
  "@headlessui/react": "^2.2.4",
41
41
  "@heroicons/react": "^2.2.0",
42
42
  "@popperjs/core": "^2.11.8",
43
+ "@radix-ui/react-avatar": "^1.1.10",
44
+ "@radix-ui/react-collapsible": "^1.1.12",
45
+ "@radix-ui/react-dialog": "^1.1.15",
46
+ "@radix-ui/react-dropdown-menu": "^2.1.16",
43
47
  "@radix-ui/react-popover": "^1.1.14",
48
+ "@radix-ui/react-separator": "^1.1.7",
44
49
  "@radix-ui/react-slot": "^1.2.3",
50
+ "@radix-ui/react-tooltip": "^1.2.8",
45
51
  "@tanstack/react-table": "^8.21.3",
46
52
  "class-variance-authority": "^0.7.1",
47
53
  "clsx": "^2.1.1",
@@ -322,6 +322,10 @@ const ChildSearchModal = ({
322
322
  onClose();
323
323
  };
324
324
 
325
+ const handleClearSelection = () => {
326
+ setSelectedChildrenState([]);
327
+ };
328
+
325
329
  const isChildSelected = (child) => {
326
330
  return selectedChildrenState.some(
327
331
  (selected) => selected.child_id === child.child_id
@@ -870,6 +874,40 @@ const ChildSearchModal = ({
870
874
  </div>
871
875
  </div>
872
876
  )}
877
+
878
+ {/* Multi-select Action Bar */}
879
+ {multiSelect && (
880
+ <div className="p-6 border-t border-gray-200 dark:border-gray-700 flex items-center justify-between">
881
+ <div className="text-sm text-gray-600 dark:text-gray-300">
882
+ {selectedChildrenState.length} selected{maxSelections ? ` / ${maxSelections}` : ''}
883
+ </div>
884
+ <div className="flex items-center gap-2">
885
+ <button
886
+ type="button"
887
+ onClick={handleClearSelection}
888
+ disabled={selectedChildrenState.length === 0}
889
+ className="px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
890
+ >
891
+ Clear
892
+ </button>
893
+ <button
894
+ type="button"
895
+ onClick={onClose}
896
+ className="px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded hover:bg-gray-50 dark:hover:bg-gray-700"
897
+ >
898
+ Go Back
899
+ </button>
900
+ <button
901
+ type="button"
902
+ onClick={handleConfirmSelection}
903
+ disabled={selectedChildrenState.length === 0}
904
+ className="px-3 py-2 text-sm rounded bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
905
+ >
906
+ Confirm
907
+ </button>
908
+ </div>
909
+ </div>
910
+ )}
873
911
  </div>
874
912
  </div>
875
913
  );
@@ -56,6 +56,8 @@ const ChildSearchPage = ({
56
56
  applicationContext = "child-search",
57
57
  bypassPermissions = false,
58
58
  onSelect = null, // Optional callback for when a child is selected
59
+ onConfirm = null, // Optional callback (multi-select confirm)
60
+ onBack = null, // Optional go-back handler in multi-select
59
61
  multiSelect = false,
60
62
  maxSelections = null,
61
63
  selectedChildren = [],
@@ -245,6 +247,11 @@ const ChildSearchPage = ({
245
247
  (index) => children[parseInt(index)]
246
248
  );
247
249
  setSelectedChildrenState(selectedRows);
250
+
251
+ // Call onSelect callback if provided and in multi-select mode
252
+ if (onSelect && multiSelect) {
253
+ onSelect(selectedRows);
254
+ }
248
255
  },
249
256
  });
250
257
 
@@ -445,6 +452,30 @@ const ChildSearchPage = ({
445
452
  setDebouncedAdvancedFilters(clearedFilters); // Clear immediately
446
453
  };
447
454
 
455
+ const handleClearSelection = () => {
456
+ setSelectedChildrenState([]);
457
+ if (onSelect && multiSelect) {
458
+ onSelect([]);
459
+ }
460
+ };
461
+
462
+ const handleConfirmSelection = () => {
463
+ const payload = selectedChildrenState;
464
+ if (onConfirm) {
465
+ onConfirm(payload);
466
+ } else if (onSelect) {
467
+ onSelect(payload);
468
+ }
469
+ };
470
+
471
+ const handleGoBack = () => {
472
+ if (onBack) {
473
+ onBack();
474
+ } else if (typeof window !== 'undefined' && window.history) {
475
+ window.history.back();
476
+ }
477
+ };
478
+
448
479
  // Calculate pagination display values
449
480
  const actualTotalCount = pagination.totalCount || children.length;
450
481
  const startItem = actualTotalCount > 0 ? (pagination.page - 1) * pagination.pageSize + 1 : 0;
@@ -686,6 +717,26 @@ const ChildSearchPage = ({
686
717
  </>
687
718
  )}
688
719
  </div>
720
+
721
+ {/* Multi-select Action Bar */}
722
+ {multiSelect && (
723
+ <div className="mt-4 flex items-center justify-between bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 px-4 py-3">
724
+ <div className="text-sm text-gray-600 dark:text-gray-300">
725
+ {selectedChildrenState.length} selected{maxSelections ? ` / ${maxSelections}` : ''}
726
+ </div>
727
+ <div className="flex items-center gap-2">
728
+ <Button variant="outline" onClick={handleClearSelection} disabled={selectedChildrenState.length === 0}>
729
+ Clear
730
+ </Button>
731
+ <Button variant="outline" onClick={handleGoBack}>
732
+ Go Back
733
+ </Button>
734
+ <Button onClick={handleConfirmSelection} disabled={selectedChildrenState.length === 0}>
735
+ Confirm
736
+ </Button>
737
+ </div>
738
+ </div>
739
+ )}
689
740
  </div>
690
741
  </div>
691
742
  );
@@ -275,12 +275,45 @@ const EmployeeSearchModal = ({
275
275
  },
276
276
  onSortingChange: setSorting,
277
277
  onRowSelectionChange: (updater) => {
278
- const newSelection =
279
- typeof updater === "function" ? updater({}) : updater;
280
- const selectedRows = Object.keys(newSelection).map(
281
- (index) => employees[parseInt(index)]
282
- );
283
- setSelectedEmployeesState(selectedRows);
278
+ // Merge current-table selection changes with the existing global selection
279
+ // instead of replacing it, so selection is retained across searches/pages.
280
+
281
+ // Build current selection map (for visible rows only)
282
+ const prevSelection = selectedEmployeesState.reduce((acc, selectedEmployee) => {
283
+ const rowIndex = employees.findIndex((employee) => employee.entra_id === selectedEmployee.entra_id);
284
+ if (rowIndex !== -1) acc[rowIndex] = true;
285
+ return acc;
286
+ }, {});
287
+
288
+ const updatedSelection = typeof updater === 'function' ? updater(prevSelection) : updater;
289
+
290
+ // Determine which visible employees are selected after the change
291
+ const visibleSelected = Object.keys(updatedSelection)
292
+ .filter((idx) => Boolean(updatedSelection[idx]))
293
+ .map((idx) => employees[parseInt(idx, 10)])
294
+ .filter(Boolean);
295
+
296
+ // Start from the previous global selection keyed by entra_id
297
+ const prevById = new Map(selectedEmployeesState.map((e) => [e.entra_id, e]));
298
+
299
+ // Remove any currently visible employees that are now unselected
300
+ const visibleIds = new Set(employees.map((e) => e.entra_id));
301
+ const visibleSelectedIds = new Set(visibleSelected.map((e) => e.entra_id));
302
+ employees.forEach((e) => {
303
+ if (visibleIds.has(e.entra_id) && !visibleSelectedIds.has(e.entra_id)) {
304
+ prevById.delete(e.entra_id);
305
+ }
306
+ });
307
+
308
+ // Add visible selected that weren't already present (respect maxSelections)
309
+ for (const e of visibleSelected) {
310
+ if (!prevById.has(e.entra_id)) {
311
+ if (maxSelections && prevById.size >= maxSelections) break;
312
+ prevById.set(e.entra_id, e);
313
+ }
314
+ }
315
+
316
+ setSelectedEmployeesState(Array.from(prevById.values()));
284
317
  },
285
318
  });
286
319
 
@@ -502,6 +535,10 @@ const EmployeeSearchModal = ({
502
535
  }
503
536
  };
504
537
 
538
+ const handleClearSelection = () => {
539
+ setSelectedEmployeesState([]);
540
+ };
541
+
505
542
  const clearFilters = () => {
506
543
  const clearedFilters = {
507
544
  status: activeOnly ? "Active" : "all",
@@ -549,22 +586,7 @@ const EmployeeSearchModal = ({
549
586
  <XMarkIcon className="h-6 w-6" />
550
587
  </button>
551
588
  </div>
552
- {multiSelect && (
553
- <div className="mt-2 flex items-center justify-between">
554
- <span className="text-sm text-gray-600 dark:text-gray-400">
555
- {selectedEmployeesState.length} selected
556
- {maxSelections && ` / ${maxSelections}`}
557
- </span>
558
- {selectedEmployeesState.length > 0 && (
559
- <Button
560
- size="sm"
561
- onClick={handleConfirmSelection}
562
- >
563
- Confirm Selection
564
- </Button>
565
- )}
566
- </div>
567
- )}
589
+ {/* Removed header confirm to avoid duplication; footer handles confirmation */}
568
590
  </div>
569
591
 
570
592
  {/* Content */}
@@ -793,20 +815,48 @@ const EmployeeSearchModal = ({
793
815
  </div>
794
816
 
795
817
  {/* Footer */}
796
- <div className="bg-gray-50 dark:bg-gray-700 px-6 py-3 flex justify-end space-x-3">
797
- <Button
798
- variant="outline"
799
- onClick={onClose}
800
- >
801
- Cancel
802
- </Button>
803
- {multiSelect && selectedEmployeesState.length > 0 && (
804
- <Button
805
- onClick={handleConfirmSelection}
806
- >
807
- Select {selectedEmployeesState.length} Employee{selectedEmployeesState.length !== 1 ? 's' : ''}
818
+ <div className="bg-gray-50 dark:bg-gray-700 px-6 py-3 flex items-center justify-between gap-3">
819
+ {/* Selection preview */}
820
+ <div className="flex-1 min-w-0">
821
+ {multiSelect && selectedEmployeesState.length > 0 && (
822
+ (() => {
823
+ const total = selectedEmployeesState.length;
824
+ const preview = selectedEmployeesState.slice(0, 5);
825
+ const extra = Math.max(total - preview.length, 0);
826
+ const previewText = `${total} selected${maxSelections ? ` / ${maxSelections}` : ''}: ` +
827
+ preview.map((e) => e.full_name).join(', ') +
828
+ (extra > 0 ? ` + ${extra} more` : '');
829
+ const fullList = selectedEmployeesState.map((e) => e.full_name).join(', ');
830
+ return (
831
+ <div
832
+ className="text-sm text-gray-600 dark:text-gray-300 truncate"
833
+ title={fullList}
834
+ >
835
+ {previewText}
836
+ </div>
837
+ );
838
+ })()
839
+ )}
840
+ </div>
841
+ <div className="flex items-center gap-2">
842
+ {multiSelect && (
843
+ <Button variant="outline" onClick={handleClearSelection} disabled={selectedEmployeesState.length === 0}>
844
+ Clear
845
+ </Button>
846
+ )}
847
+ <Button variant="outline" onClick={onClose}>
848
+ Go Back
808
849
  </Button>
809
- )}
850
+ {multiSelect && (
851
+ <Button
852
+ onClick={handleConfirmSelection}
853
+ disabled={selectedEmployeesState.length === 0}
854
+ aria-label={`Confirm ${selectedEmployeesState.length} selected`}
855
+ >
856
+ {`Confirm (${selectedEmployeesState.length}${maxSelections ? `/${maxSelections}` : ''})`}
857
+ </Button>
858
+ )}
859
+ </div>
810
860
  </div>
811
861
  </div>
812
862
  </div>
@@ -814,4 +864,4 @@ const EmployeeSearchModal = ({
814
864
  );
815
865
  };
816
866
 
817
- export default EmployeeSearchModal;
867
+ export default EmployeeSearchModal;
@@ -58,6 +58,8 @@ const EmployeeSearchPage = ({
58
58
  applicationContext = "employee-search",
59
59
  bypassPermissions = false,
60
60
  onSelect = null, // Optional callback for when an employee is selected
61
+ onConfirm = null, // Optional callback (multi-select confirm)
62
+ onBack = null, // Optional callback for go back/cancel
61
63
  multiSelect = false,
62
64
  maxSelections = null,
63
65
  selectedEmployees = [],
@@ -322,6 +324,11 @@ const EmployeeSearchPage = ({
322
324
  (index) => employees[parseInt(index)]
323
325
  );
324
326
  setSelectedEmployeesState(selectedRows);
327
+
328
+ // Call onSelect callback if provided and in multi-select mode
329
+ if (onSelect && multiSelect) {
330
+ onSelect(selectedRows);
331
+ }
325
332
  },
326
333
  });
327
334
 
@@ -615,6 +622,30 @@ const EmployeeSearchPage = ({
615
622
  setDebouncedAdvancedFilters(clearedFilters); // Clear immediately
616
623
  };
617
624
 
625
+ const handleClearSelection = () => {
626
+ setSelectedEmployeesState([]);
627
+ if (onSelect && multiSelect) {
628
+ onSelect([]);
629
+ }
630
+ };
631
+
632
+ const handleConfirmSelection = () => {
633
+ const payload = selectedEmployeesState;
634
+ if (onConfirm) {
635
+ onConfirm(payload);
636
+ } else if (onSelect) {
637
+ onSelect(payload);
638
+ }
639
+ };
640
+
641
+ const handleGoBack = () => {
642
+ if (onBack) {
643
+ onBack();
644
+ } else if (typeof window !== 'undefined' && window.history) {
645
+ window.history.back();
646
+ }
647
+ };
648
+
618
649
  // Calculate pagination display values
619
650
  const actualTotalCount = pagination.totalCount || employees.length;
620
651
  const startItem = loadAllResults ? 1 : (actualTotalCount > 0 ? (pagination.page - 1) * pagination.pageSize + 1 : 0);
@@ -897,9 +928,29 @@ const EmployeeSearchPage = ({
897
928
  </>
898
929
  )}
899
930
  </div>
931
+
932
+ {/* Multi-select Action Bar */}
933
+ {multiSelect && (
934
+ <div className="mt-4 flex items-center justify-between bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 px-4 py-3">
935
+ <div className="text-sm text-gray-600 dark:text-gray-300">
936
+ {selectedEmployeesState.length} selected{maxSelections ? ` / ${maxSelections}` : ''}
937
+ </div>
938
+ <div className="flex items-center gap-2">
939
+ <Button variant="outline" onClick={handleClearSelection} disabled={selectedEmployeesState.length === 0}>
940
+ Clear
941
+ </Button>
942
+ <Button variant="outline" onClick={handleGoBack}>
943
+ Go Back
944
+ </Button>
945
+ <Button onClick={handleConfirmSelection} disabled={selectedEmployeesState.length === 0}>
946
+ Confirm
947
+ </Button>
948
+ </div>
949
+ </div>
950
+ )}
900
951
  </div>
901
952
  </div>
902
953
  );
903
954
  };
904
955
 
905
- export default EmployeeSearchPage;
956
+ export default EmployeeSearchPage;
@@ -1,254 +1 @@
1
- // Employee Search Page Component Tests
2
- // Tests the EmployeeSearchPage component functionality including search, filtering, pagination, and selection
3
-
4
- import React from 'react';
5
- import { render, screen, fireEvent, waitFor } from '@testing-library/react';
6
- import '@testing-library/jest-dom';
7
- import EmployeeSearchPage from './EmployeeSearchPage';
8
-
9
- // Mock MSAL
10
- jest.mock('@azure/msal-react', () => ({
11
- useMsal: () => ({
12
- instance: {
13
- acquireTokenSilent: jest.fn(),
14
- },
15
- accounts: [{ localAccountId: 'test-user-id' }],
16
- }),
17
- }));
18
-
19
- // Mock telemetry
20
- jest.mock('./telemetry', () => ({
21
- trackEvent: jest.fn(),
22
- }));
23
-
24
- // Mock utils
25
- jest.mock('./lib/utils', () => ({
26
- cn: (...classes) => classes.filter(Boolean).join(' '),
27
- }));
28
-
29
-
30
-
31
- // Mock fetch
32
- global.fetch = jest.fn();
33
-
34
- // Mock process.env
35
- process.env.VITE_COMMON_API_FUNCTION_KEY = 'test-key';
36
- process.env.VITE_COMMON_API_BASE_URL = 'https://test-api.example.com';
37
-
38
- // Mock import.meta.env for Vite environment variables
39
- global.import = {
40
- meta: {
41
- env: {
42
- VITE_COMMON_API_FUNCTION_KEY: 'test-key',
43
- VITE_COMMON_API_BASE_URL: 'https://test-api.example.com',
44
- },
45
- },
46
- };
47
-
48
- // Mock the UI components
49
- jest.mock('./components/ui/input', () => ({
50
- Input: ({ value, onChange, placeholder, ...props }) => {
51
- return (
52
- <input
53
- data-testid="input"
54
- value={value}
55
- onChange={onChange}
56
- placeholder={placeholder}
57
- {...props}
58
- />
59
- );
60
- }
61
- }));
62
-
63
- jest.mock('./components/ui/button', () => ({
64
- Button: ({ children, onClick, variant, size, ...props }) => {
65
- return (
66
- <button
67
- data-testid="button"
68
- onClick={onClick}
69
- data-variant={variant}
70
- data-size={size}
71
- {...props}
72
- >
73
- {children}
74
- </button>
75
- );
76
- }
77
- }));
78
-
79
- jest.mock('./components/ui/table', () => ({
80
- Table: ({ children, ...props }) => <table data-testid="table" {...props}>{children}</table>,
81
- TableBody: ({ children, ...props }) => <tbody data-testid="table-body" {...props}>{children}</tbody>,
82
- TableCell: ({ children, ...props }) => <td data-testid="table-cell" {...props}>{children}</td>,
83
- TableHead: ({ children, ...props }) => <th data-testid="table-head" {...props}>{children}</th>,
84
- TableHeader: ({ children, ...props }) => <thead data-testid="table-header" {...props}>{children}</thead>,
85
- TableRow: ({ children, ...props }) => <tr data-testid="table-row" {...props}>{children}</tr>,
86
- }));
87
-
88
- jest.mock('./components/ui/pagination', () => ({
89
- Pagination: ({ children, ...props }) => <nav data-testid="pagination" {...props}>{children}</nav>,
90
- PaginationContent: ({ children, ...props }) => <div data-testid="pagination-content" {...props}>{children}</div>,
91
- PaginationEllipsis: ({ ...props }) => <span data-testid="pagination-ellipsis" {...props}>...</span>,
92
- PaginationItem: ({ children, ...props }) => <div data-testid="pagination-item" {...props}>{children}</div>,
93
- PaginationLink: ({ children, onClick, isActive, ...props }) => (
94
- <button data-testid="pagination-link" onClick={onClick} data-active={isActive} {...props}>
95
- {children}
96
- </button>
97
- ),
98
- PaginationNext: ({ children, onClick, ...props }) => (
99
- <button data-testid="pagination-next" onClick={onClick} {...props}>
100
- {children}
101
- </button>
102
- ),
103
- PaginationPrevious: ({ children, onClick, ...props }) => (
104
- <button data-testid="pagination-previous" onClick={onClick} {...props}>
105
- {children}
106
- </button>
107
- ),
108
- }));
109
-
110
- // Mock EmployeeSearchFilters
111
- jest.mock('./components/EmployeeSearchFilters', () => {
112
- return function MockEmployeeSearchFilters({ filters, onFiltersChange, onApplyFilters, onClearFilters, ...props }) {
113
- return (
114
- <div data-testid="employee-search-filters">
115
- <button onClick={() => onFiltersChange({ ...filters, status: 'Inactive' })}>
116
- Change Status
117
- </button>
118
- <button onClick={() => onApplyFilters(filters)}>
119
- Apply Filters
120
- </button>
121
- <button onClick={onClearFilters}>
122
- Clear Filters
123
- </button>
124
- </div>
125
- );
126
- };
127
- });
128
-
129
- const defaultProps = {
130
- title: 'Employee Search',
131
- onSelect: jest.fn(),
132
- };
133
-
134
- describe('EmployeeSearchPage', () => {
135
- beforeEach(() => {
136
- jest.clearAllMocks();
137
- global.fetch.mockResolvedValue({
138
- ok: true,
139
- json: async () => ({
140
- success: true,
141
- data: {
142
- employees: [],
143
- pagination: {
144
- page: 1,
145
- pageSize: 20,
146
- totalCount: 0,
147
- totalPages: 0,
148
- hasNextPage: false,
149
- hasPreviousPage: false,
150
- },
151
- },
152
- }),
153
- });
154
- });
155
-
156
- describe('Basic Rendering', () => {
157
- it('renders the page title', () => {
158
- render(<EmployeeSearchPage {...defaultProps} />);
159
-
160
- expect(screen.getByText('Employee Search')).toBeInTheDocument();
161
- });
162
-
163
- it('renders the search input', () => {
164
- render(<EmployeeSearchPage {...defaultProps} />);
165
-
166
- expect(screen.getByPlaceholderText('Search by name, employee ID, or email...')).toBeInTheDocument();
167
- });
168
-
169
- it('renders the employee search filters', () => {
170
- render(<EmployeeSearchPage {...defaultProps} />);
171
-
172
- expect(screen.getByTestId('employee-search-filters')).toBeInTheDocument();
173
- });
174
-
175
- it('shows loading state initially', () => {
176
- render(<EmployeeSearchPage {...defaultProps} />);
177
-
178
- expect(screen.getByRole('status')).toBeInTheDocument();
179
- });
180
- });
181
-
182
- describe('Search Functionality', () => {
183
- it('renders search input with correct placeholder', () => {
184
- render(<EmployeeSearchPage {...defaultProps} />);
185
-
186
- const searchInput = screen.getByPlaceholderText('Search by name, employee ID, or email...');
187
- expect(searchInput).toBeInTheDocument();
188
- expect(searchInput).toHaveAttribute('type', 'text');
189
- });
190
-
191
- it('allows typing in search input', () => {
192
- render(<EmployeeSearchPage {...defaultProps} />);
193
-
194
- const searchInput = screen.getByPlaceholderText('Search by name, employee ID, or email...');
195
- fireEvent.change(searchInput, { target: { value: 'John' } });
196
-
197
- expect(searchInput).toHaveValue('John');
198
- });
199
- });
200
-
201
- describe('Filter Integration', () => {
202
- it('renders filter buttons', () => {
203
- render(<EmployeeSearchPage {...defaultProps} />);
204
-
205
- expect(screen.getByText('Change Status')).toBeInTheDocument();
206
- expect(screen.getByText('Apply Filters')).toBeInTheDocument();
207
- expect(screen.getByText('Clear Filters')).toBeInTheDocument();
208
- });
209
-
210
- it('calls filter change handler when filter button is clicked', () => {
211
- render(<EmployeeSearchPage {...defaultProps} />);
212
-
213
- const changeStatusButton = screen.getByText('Change Status');
214
- fireEvent.click(changeStatusButton);
215
-
216
- // The mock should have been called
217
- expect(changeStatusButton).toBeInTheDocument();
218
- });
219
- });
220
-
221
- describe('Accessibility', () => {
222
- it('has proper ARIA labels', () => {
223
- render(<EmployeeSearchPage {...defaultProps} />);
224
-
225
- const searchInput = screen.getByPlaceholderText('Search by name, employee ID, or email...');
226
- expect(searchInput).toBeInTheDocument();
227
- });
228
-
229
- it('supports keyboard navigation', () => {
230
- render(<EmployeeSearchPage {...defaultProps} />);
231
-
232
- const searchInput = screen.getByPlaceholderText('Search by name, employee ID, or email...');
233
- searchInput.focus();
234
-
235
- expect(searchInput).toHaveFocus();
236
- });
237
- });
238
-
239
- describe('Props Handling', () => {
240
- it('renders custom title', () => {
241
- render(<EmployeeSearchPage {...defaultProps} title="Custom Title" />);
242
-
243
- expect(screen.getByText('Custom Title')).toBeInTheDocument();
244
- });
245
-
246
- it('renders custom search placeholder', () => {
247
- render(<EmployeeSearchPage {...defaultProps} searchPlaceholder="Custom placeholder" />);
248
-
249
- expect(screen.getByPlaceholderText('Custom placeholder')).toBeInTheDocument();
250
- });
251
- });
252
-
253
-
254
- });
1
+