@jmruthers/pace-core 0.5.117 → 0.5.119
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/dist/{DataTable-ZOAKQ3SU.js → DataTable-BQYGKVHR.js} +6 -6
- package/dist/{UnifiedAuthProvider-YFN7YGVN.js → UnifiedAuthProvider-UACKFATV.js} +3 -3
- package/dist/{chunk-XN2LYHDI.js → chunk-B4GZ2BXO.js} +27 -8
- package/dist/{chunk-XN2LYHDI.js.map → chunk-B4GZ2BXO.js.map} +1 -1
- package/dist/{chunk-KA3PSVNV.js → chunk-BHWIUEYH.js} +2 -1
- package/dist/chunk-BHWIUEYH.js.map +1 -0
- package/dist/{chunk-LFS45U62.js → chunk-CGURJ27Z.js} +2 -2
- package/dist/{chunk-PHDAXDHB.js → chunk-D6BOFXYR.js} +3 -3
- package/dist/{chunk-2LM4QQGH.js → chunk-F7COHU5B.js} +8 -8
- package/dist/{chunk-P3PUOL6B.js → chunk-FKFHZUGF.js} +4 -4
- package/dist/{chunk-UKZWNQMB.js → chunk-NP5VABFV.js} +4 -4
- package/dist/{chunk-O3FTRYEU.js → chunk-NZ32EONV.js} +2 -2
- package/dist/{chunk-ECOVPXYS.js → chunk-RIEJGKD3.js} +4 -4
- package/dist/{chunk-IZXS7RZK.js → chunk-TDNI6ZWL.js} +5 -5
- package/dist/{chunk-VN3OOE35.js → chunk-ZYJ6O5CA.js} +2 -2
- package/dist/components.js +8 -8
- package/dist/hooks.js +7 -7
- package/dist/index.js +11 -11
- package/dist/providers.js +2 -2
- package/dist/rbac/index.js +7 -7
- package/dist/utils.js +1 -1
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +1 -1
- package/docs/api/classes/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +1 -1
- package/docs/api/classes/PublicErrorBoundary.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +1 -1
- package/docs/api/classes/RBACCache.md +1 -1
- package/docs/api/classes/RBACEngine.md +1 -1
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +1 -1
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataRecord.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventAppRoleData.md +1 -1
- package/docs/api/interfaces/FileDisplayProps.md +1 -1
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
- package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
- package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +1 -1
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RoleManagementResult.md +1 -1
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +1 -1
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +2 -2
- package/package.json +1 -1
- package/src/components/DataTable/__tests__/DataTableCore.test.tsx +697 -0
- package/src/components/DataTable/components/__tests__/EditableRow.test.tsx +544 -9
- package/src/components/DataTable/components/__tests__/UnifiedTableBody.test.tsx +1004 -0
- package/src/components/DataTable/utils/__tests__/a11yUtils.test.ts +612 -0
- package/src/components/DataTable/utils/__tests__/errorHandling.test.ts +266 -0
- package/src/components/DataTable/utils/__tests__/exportUtils.test.ts +455 -1
- package/src/hooks/__tests__/index.unit.test.ts +223 -0
- package/src/hooks/__tests__/useDataTablePerformance.unit.test.ts +748 -0
- package/src/hooks/__tests__/useEvents.unit.test.ts +249 -0
- package/src/hooks/__tests__/useFileDisplay.unit.test.ts +1060 -0
- package/src/hooks/__tests__/useFileUrl.unit.test.ts +958 -0
- package/src/hooks/__tests__/useFocusTrap.unit.test.tsx +540 -1
- package/src/hooks/__tests__/useIsMobile.unit.test.ts +205 -5
- package/src/hooks/__tests__/useKeyboardShortcuts.unit.test.ts +616 -1
- package/src/hooks/__tests__/useOrganisations.unit.test.ts +369 -0
- package/src/hooks/__tests__/usePerformanceMonitor.unit.test.ts +608 -0
- package/src/hooks/__tests__/useSecureDataAccess.unit.test.tsx +2 -0
- package/src/hooks/__tests__/useSessionRestoration.unit.test.tsx +372 -0
- package/src/hooks/__tests__/useToast.unit.test.tsx +431 -30
- package/src/hooks/useSecureDataAccess.test.ts +1 -0
- package/src/hooks/useSecureDataAccess.ts +43 -5
- package/src/rbac/audit-enhanced.ts +339 -0
- package/src/services/EventService.ts +1 -0
- package/src/services/__tests__/AuthService.test.ts +473 -0
- package/src/services/__tests__/EventService.test.ts +390 -0
- package/src/services/__tests__/InactivityService.test.ts +217 -0
- package/src/services/__tests__/OrganisationService.test.ts +371 -0
- package/dist/chunk-KA3PSVNV.js.map +0 -1
- package/src/components/DataTable/utils/debugTools.ts +0 -609
- package/src/rbac/testing/index.tsx +0 -340
- /package/dist/{DataTable-ZOAKQ3SU.js.map → DataTable-BQYGKVHR.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-YFN7YGVN.js.map → UnifiedAuthProvider-UACKFATV.js.map} +0 -0
- /package/dist/{chunk-LFS45U62.js.map → chunk-CGURJ27Z.js.map} +0 -0
- /package/dist/{chunk-PHDAXDHB.js.map → chunk-D6BOFXYR.js.map} +0 -0
- /package/dist/{chunk-2LM4QQGH.js.map → chunk-F7COHU5B.js.map} +0 -0
- /package/dist/{chunk-P3PUOL6B.js.map → chunk-FKFHZUGF.js.map} +0 -0
- /package/dist/{chunk-UKZWNQMB.js.map → chunk-NP5VABFV.js.map} +0 -0
- /package/dist/{chunk-O3FTRYEU.js.map → chunk-NZ32EONV.js.map} +0 -0
- /package/dist/{chunk-ECOVPXYS.js.map → chunk-RIEJGKD3.js.map} +0 -0
- /package/dist/{chunk-IZXS7RZK.js.map → chunk-TDNI6ZWL.js.map} +0 -0
- /package/dist/{chunk-VN3OOE35.js.map → chunk-ZYJ6O5CA.js.map} +0 -0
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Accessibility Utils Tests
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Components/DataTable/Utils/__tests__
|
|
5
|
+
* @since 0.4.0
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
9
|
+
import {
|
|
10
|
+
initializeLiveRegion,
|
|
11
|
+
cleanupLiveRegion,
|
|
12
|
+
announce,
|
|
13
|
+
getSortButtonLabel,
|
|
14
|
+
announceSortChange,
|
|
15
|
+
announceFilterChange,
|
|
16
|
+
announceSearchResults,
|
|
17
|
+
announcePaginationChange,
|
|
18
|
+
announceSelectionChange,
|
|
19
|
+
announceLoadingState,
|
|
20
|
+
announceBulkOperation,
|
|
21
|
+
getAriaSortValue,
|
|
22
|
+
getRowDescription,
|
|
23
|
+
} from '../a11yUtils';
|
|
24
|
+
|
|
25
|
+
describe('[unit] a11yUtils', () => {
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
// Clear any existing live region
|
|
28
|
+
if (document.body.querySelector('.sr-only[aria-live]')) {
|
|
29
|
+
const existing = document.body.querySelector('.sr-only[aria-live]');
|
|
30
|
+
if (existing) {
|
|
31
|
+
document.body.removeChild(existing);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
vi.clearAllMocks();
|
|
35
|
+
vi.useFakeTimers();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
cleanupLiveRegion();
|
|
40
|
+
vi.useRealTimers();
|
|
41
|
+
vi.clearAllMocks();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('initializeLiveRegion', () => {
|
|
45
|
+
it('creates live region element when none exists', () => {
|
|
46
|
+
const region = initializeLiveRegion();
|
|
47
|
+
|
|
48
|
+
expect(region).not.toBeNull();
|
|
49
|
+
expect(region).toBeInstanceOf(HTMLElement);
|
|
50
|
+
expect(region?.getAttribute('aria-live')).toBe('polite');
|
|
51
|
+
expect(region?.getAttribute('aria-atomic')).toBe('true');
|
|
52
|
+
expect(region?.getAttribute('class')).toBe('sr-only');
|
|
53
|
+
expect(region?.style.position).toBe('absolute');
|
|
54
|
+
expect(region?.style.left).toBe('-10000px');
|
|
55
|
+
expect(region?.style.width).toBe('1px');
|
|
56
|
+
expect(region?.style.height).toBe('1px');
|
|
57
|
+
expect(region?.style.overflow).toBe('hidden');
|
|
58
|
+
expect(document.body.contains(region!)).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('returns existing live region if already created', () => {
|
|
62
|
+
const firstRegion = initializeLiveRegion();
|
|
63
|
+
const secondRegion = initializeLiveRegion();
|
|
64
|
+
|
|
65
|
+
expect(firstRegion).toBe(secondRegion);
|
|
66
|
+
expect(document.body.querySelectorAll('.sr-only[aria-live]')).toHaveLength(1);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('creates new region if existing one is not in document', () => {
|
|
70
|
+
const firstRegion = initializeLiveRegion();
|
|
71
|
+
document.body.removeChild(firstRegion!);
|
|
72
|
+
|
|
73
|
+
const secondRegion = initializeLiveRegion();
|
|
74
|
+
expect(secondRegion).not.toBe(firstRegion);
|
|
75
|
+
expect(document.body.contains(secondRegion!)).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('returns null in SSR environment', () => {
|
|
79
|
+
const originalWindow = global.window;
|
|
80
|
+
// @ts-expect-error - Testing SSR scenario
|
|
81
|
+
delete global.window;
|
|
82
|
+
|
|
83
|
+
const region = initializeLiveRegion();
|
|
84
|
+
expect(region).toBeNull();
|
|
85
|
+
|
|
86
|
+
global.window = originalWindow;
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('cleanupLiveRegion', () => {
|
|
91
|
+
it('removes live region from document', () => {
|
|
92
|
+
const region = initializeLiveRegion();
|
|
93
|
+
expect(document.body.contains(region!)).toBe(true);
|
|
94
|
+
|
|
95
|
+
cleanupLiveRegion();
|
|
96
|
+
expect(document.body.contains(region!)).toBe(false);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('sets live region to null after cleanup', () => {
|
|
100
|
+
initializeLiveRegion();
|
|
101
|
+
cleanupLiveRegion();
|
|
102
|
+
|
|
103
|
+
// Second call should not throw
|
|
104
|
+
cleanupLiveRegion();
|
|
105
|
+
expect(document.body.querySelector('.sr-only[aria-live]')).toBeNull();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('handles cleanup when region does not exist', () => {
|
|
109
|
+
expect(() => cleanupLiveRegion()).not.toThrow();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('handles cleanup when region is not in document', () => {
|
|
113
|
+
const region = initializeLiveRegion();
|
|
114
|
+
document.body.removeChild(region!);
|
|
115
|
+
|
|
116
|
+
expect(() => cleanupLiveRegion()).not.toThrow();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('returns early in SSR environment', () => {
|
|
120
|
+
const originalWindow = global.window;
|
|
121
|
+
// @ts-expect-error - Testing SSR scenario
|
|
122
|
+
delete global.window;
|
|
123
|
+
|
|
124
|
+
expect(() => cleanupLiveRegion()).not.toThrow();
|
|
125
|
+
|
|
126
|
+
global.window = originalWindow;
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('announce', () => {
|
|
131
|
+
it('announces message with polite priority by default', () => {
|
|
132
|
+
initializeLiveRegion();
|
|
133
|
+
announce('Test message');
|
|
134
|
+
|
|
135
|
+
vi.advanceTimersByTime(100);
|
|
136
|
+
const region = document.body.querySelector('.sr-only[aria-live]') as HTMLElement;
|
|
137
|
+
expect(region.getAttribute('aria-live')).toBe('polite');
|
|
138
|
+
expect(region.textContent).toBe('Test message');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('announces message with assertive priority', () => {
|
|
142
|
+
initializeLiveRegion();
|
|
143
|
+
announce('Urgent message', 'assertive');
|
|
144
|
+
|
|
145
|
+
vi.advanceTimersByTime(100);
|
|
146
|
+
const region = document.body.querySelector('.sr-only[aria-live]') as HTMLElement;
|
|
147
|
+
expect(region.getAttribute('aria-live')).toBe('assertive');
|
|
148
|
+
expect(region.textContent).toBe('Urgent message');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('updates aria-live attribute when priority changes', () => {
|
|
152
|
+
initializeLiveRegion();
|
|
153
|
+
announce('First message', 'polite');
|
|
154
|
+
vi.advanceTimersByTime(100);
|
|
155
|
+
|
|
156
|
+
announce('Second message', 'assertive');
|
|
157
|
+
vi.advanceTimersByTime(100);
|
|
158
|
+
|
|
159
|
+
const region = document.body.querySelector('.sr-only[aria-live]') as HTMLElement;
|
|
160
|
+
expect(region.getAttribute('aria-live')).toBe('assertive');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('clears message after delay to avoid repetition', () => {
|
|
164
|
+
initializeLiveRegion();
|
|
165
|
+
announce('Test message');
|
|
166
|
+
|
|
167
|
+
vi.advanceTimersByTime(100);
|
|
168
|
+
const region = document.body.querySelector('.sr-only[aria-live]') as HTMLElement;
|
|
169
|
+
expect(region.textContent).toBe('Test message');
|
|
170
|
+
|
|
171
|
+
vi.advanceTimersByTime(1000);
|
|
172
|
+
expect(region.textContent).toBe('');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('does not clear message if content changed during delay', () => {
|
|
176
|
+
initializeLiveRegion();
|
|
177
|
+
announce('First message');
|
|
178
|
+
vi.advanceTimersByTime(100);
|
|
179
|
+
|
|
180
|
+
const region = document.body.querySelector('.sr-only[aria-live]') as HTMLElement;
|
|
181
|
+
expect(region.textContent).toBe('First message');
|
|
182
|
+
|
|
183
|
+
// Announce new message before first timeout clears
|
|
184
|
+
announce('Second message');
|
|
185
|
+
vi.advanceTimersByTime(100);
|
|
186
|
+
expect(region.textContent).toBe('Second message');
|
|
187
|
+
|
|
188
|
+
// First timeout should not clear since content changed (it was 'Second message', not 'First message')
|
|
189
|
+
vi.advanceTimersByTime(900);
|
|
190
|
+
// The second timeout should clear it after 1000ms, but we need to advance past the first timeout
|
|
191
|
+
// The first timeout (1000ms after first announce) checks if textContent === 'First message'
|
|
192
|
+
// Since it's now 'Second message', it won't clear
|
|
193
|
+
// Then we advance to the second timeout (1000ms after second announce)
|
|
194
|
+
vi.advanceTimersByTime(100);
|
|
195
|
+
// Now the second timeout should check if textContent === 'Second message' and clear it
|
|
196
|
+
expect(region.textContent).toBe('');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('does not announce empty or whitespace-only messages', () => {
|
|
200
|
+
initializeLiveRegion();
|
|
201
|
+
const region = document.body.querySelector('.sr-only[aria-live]') as HTMLElement;
|
|
202
|
+
|
|
203
|
+
announce('');
|
|
204
|
+
announce(' ');
|
|
205
|
+
announce('\n\t ');
|
|
206
|
+
|
|
207
|
+
vi.advanceTimersByTime(100);
|
|
208
|
+
expect(region.textContent).toBe('');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('returns early in SSR environment', () => {
|
|
212
|
+
const originalWindow = global.window;
|
|
213
|
+
// @ts-expect-error - Testing SSR scenario
|
|
214
|
+
delete global.window;
|
|
215
|
+
|
|
216
|
+
expect(() => announce('Test message')).not.toThrow();
|
|
217
|
+
|
|
218
|
+
global.window = originalWindow;
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('initializes live region if not already initialized', () => {
|
|
222
|
+
announce('Auto-init message');
|
|
223
|
+
vi.advanceTimersByTime(100);
|
|
224
|
+
|
|
225
|
+
const region = document.body.querySelector('.sr-only[aria-live]') as HTMLElement;
|
|
226
|
+
expect(region).not.toBeNull();
|
|
227
|
+
expect(region.textContent).toBe('Auto-init message');
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe('getSortButtonLabel', () => {
|
|
232
|
+
it('returns ascending label when no sort is applied', () => {
|
|
233
|
+
expect(getSortButtonLabel('Name', false)).toBe('Sort Name ascending');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('returns descending label when ascending sort is applied', () => {
|
|
237
|
+
expect(getSortButtonLabel('Name', 'asc')).toBe('Sort Name descending');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('returns remove sort label when descending sort is applied', () => {
|
|
241
|
+
expect(getSortButtonLabel('Name', 'desc')).toBe('Remove sort from Name');
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('handles column names with special characters', () => {
|
|
245
|
+
expect(getSortButtonLabel('User Name', false)).toBe('Sort User Name ascending');
|
|
246
|
+
expect(getSortButtonLabel('Email Address', 'asc')).toBe('Sort Email Address descending');
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe('announceSortChange', () => {
|
|
251
|
+
beforeEach(() => {
|
|
252
|
+
initializeLiveRegion();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('announces ascending sort', () => {
|
|
256
|
+
announceSortChange('Name', 'asc');
|
|
257
|
+
vi.advanceTimersByTime(100);
|
|
258
|
+
|
|
259
|
+
const region = document.body.querySelector('.sr-only[aria-live]') as HTMLElement;
|
|
260
|
+
expect(region.textContent).toBe('Table sorted by Name in ascending order');
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('announces descending sort', () => {
|
|
264
|
+
announceSortChange('Name', 'desc');
|
|
265
|
+
vi.advanceTimersByTime(100);
|
|
266
|
+
|
|
267
|
+
const region = document.body.querySelector('.sr-only[aria-live]') as HTMLElement;
|
|
268
|
+
expect(region.textContent).toBe('Table sorted by Name in descending order');
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('announces sort removal', () => {
|
|
272
|
+
announceSortChange('Name', null);
|
|
273
|
+
vi.advanceTimersByTime(100);
|
|
274
|
+
|
|
275
|
+
const region = document.body.querySelector('.sr-only[aria-live]') as HTMLElement;
|
|
276
|
+
expect(region.textContent).toBe('Sort removed from Name');
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('handles column names with special characters', () => {
|
|
280
|
+
announceSortChange('Email Address', 'asc');
|
|
281
|
+
vi.advanceTimersByTime(100);
|
|
282
|
+
|
|
283
|
+
const region = document.body.querySelector('.sr-only[aria-live]') as HTMLElement;
|
|
284
|
+
expect(region.textContent).toBe('Table sorted by Email Address in ascending order');
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
describe('announceFilterChange', () => {
|
|
289
|
+
beforeEach(() => {
|
|
290
|
+
initializeLiveRegion();
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('announces filter with single result', () => {
|
|
294
|
+
announceFilterChange('Name', 'John', 1);
|
|
295
|
+
vi.advanceTimersByTime(100);
|
|
296
|
+
|
|
297
|
+
const region = document.body.querySelector('.sr-only[aria-live]') as HTMLElement;
|
|
298
|
+
expect(region.textContent).toBe('Filtered Name by "John". 1 result found.');
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('announces filter with multiple results', () => {
|
|
302
|
+
announceFilterChange('Name', 'John', 5);
|
|
303
|
+
vi.advanceTimersByTime(100);
|
|
304
|
+
|
|
305
|
+
const region = document.body.querySelector('.sr-only[aria-live]') as HTMLElement;
|
|
306
|
+
expect(region.textContent).toBe('Filtered Name by "John". 5 results found.');
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('announces filter removal when filter value is empty', () => {
|
|
310
|
+
announceFilterChange('Name', '', 0);
|
|
311
|
+
vi.advanceTimersByTime(100);
|
|
312
|
+
|
|
313
|
+
const region = document.body.querySelector('.sr-only[aria-live]') as HTMLElement;
|
|
314
|
+
expect(region.textContent).toBe('Filter removed from Name');
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('announces filter removal when filter value is whitespace', () => {
|
|
318
|
+
announceFilterChange('Name', ' ', 0);
|
|
319
|
+
vi.advanceTimersByTime(100);
|
|
320
|
+
|
|
321
|
+
const region = document.body.querySelector('.sr-only[aria-live]') as HTMLElement;
|
|
322
|
+
expect(region.textContent).toBe('Filter removed from Name');
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('handles filter values with special characters', () => {
|
|
326
|
+
announceFilterChange('Email', 'test@example.com', 2);
|
|
327
|
+
vi.advanceTimersByTime(100);
|
|
328
|
+
|
|
329
|
+
const region = document.body.querySelector('.sr-only[aria-live]') as HTMLElement;
|
|
330
|
+
expect(region.textContent).toBe('Filtered Email by "test@example.com". 2 results found.');
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
describe('announceSearchResults', () => {
|
|
335
|
+
beforeEach(() => {
|
|
336
|
+
initializeLiveRegion();
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('announces search with single result', () => {
|
|
340
|
+
announceSearchResults('test', 1);
|
|
341
|
+
vi.advanceTimersByTime(100);
|
|
342
|
+
|
|
343
|
+
const region = document.body.querySelector('.sr-only[aria-live]') as HTMLElement;
|
|
344
|
+
expect(region.textContent).toBe('Search for "test" returned 1 result.');
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('announces search with multiple results', () => {
|
|
348
|
+
announceSearchResults('test', 10);
|
|
349
|
+
vi.advanceTimersByTime(100);
|
|
350
|
+
|
|
351
|
+
const region = document.body.querySelector('.sr-only[aria-live]') as HTMLElement;
|
|
352
|
+
expect(region.textContent).toBe('Search for "test" returned 10 results.');
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('announces search cleared when query is empty', () => {
|
|
356
|
+
announceSearchResults('', 0);
|
|
357
|
+
vi.advanceTimersByTime(100);
|
|
358
|
+
|
|
359
|
+
const region = document.body.querySelector('.sr-only[aria-live]') as HTMLElement;
|
|
360
|
+
expect(region.textContent).toBe('Search cleared');
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('announces search cleared when query is whitespace', () => {
|
|
364
|
+
announceSearchResults(' ', 0);
|
|
365
|
+
vi.advanceTimersByTime(100);
|
|
366
|
+
|
|
367
|
+
const region = document.body.querySelector('.sr-only[aria-live]') as HTMLElement;
|
|
368
|
+
expect(region.textContent).toBe('Search cleared');
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('handles search queries with special characters', () => {
|
|
372
|
+
announceSearchResults('test@example.com', 3);
|
|
373
|
+
vi.advanceTimersByTime(100);
|
|
374
|
+
|
|
375
|
+
const region = document.body.querySelector('.sr-only[aria-live]') as HTMLElement;
|
|
376
|
+
expect(region.textContent).toBe('Search for "test@example.com" returned 3 results.');
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
describe('announcePaginationChange', () => {
|
|
381
|
+
beforeEach(() => {
|
|
382
|
+
initializeLiveRegion();
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('announces first page', () => {
|
|
386
|
+
announcePaginationChange(1, 5, 10, 50);
|
|
387
|
+
vi.advanceTimersByTime(100);
|
|
388
|
+
|
|
389
|
+
const region = document.body.querySelector('.sr-only[aria-live]') as HTMLElement;
|
|
390
|
+
expect(region.textContent).toBe('Page 1 of 5. Showing items 1 to 10 of 50.');
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('announces middle page', () => {
|
|
394
|
+
announcePaginationChange(3, 5, 10, 50);
|
|
395
|
+
vi.advanceTimersByTime(100);
|
|
396
|
+
|
|
397
|
+
const region = document.body.querySelector('.sr-only[aria-live]') as HTMLElement;
|
|
398
|
+
expect(region.textContent).toBe('Page 3 of 5. Showing items 21 to 30 of 50.');
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('announces last page', () => {
|
|
402
|
+
announcePaginationChange(5, 5, 10, 50);
|
|
403
|
+
vi.advanceTimersByTime(100);
|
|
404
|
+
|
|
405
|
+
const region = document.body.querySelector('.sr-only[aria-live]') as HTMLElement;
|
|
406
|
+
expect(region.textContent).toBe('Page 5 of 5. Showing items 41 to 50 of 50.');
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('announces single page', () => {
|
|
410
|
+
announcePaginationChange(1, 1, 10, 5);
|
|
411
|
+
vi.advanceTimersByTime(100);
|
|
412
|
+
|
|
413
|
+
const region = document.body.querySelector('.sr-only[aria-live]') as HTMLElement;
|
|
414
|
+
expect(region.textContent).toBe('Page 1 of 1. Showing items 1 to 5 of 5.');
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('handles page where last item exceeds total', () => {
|
|
418
|
+
announcePaginationChange(2, 2, 10, 15);
|
|
419
|
+
vi.advanceTimersByTime(100);
|
|
420
|
+
|
|
421
|
+
const region = document.body.querySelector('.sr-only[aria-live]') as HTMLElement;
|
|
422
|
+
expect(region.textContent).toBe('Page 2 of 2. Showing items 11 to 15 of 15.');
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it('handles edge case with single item', () => {
|
|
426
|
+
announcePaginationChange(1, 1, 1, 1);
|
|
427
|
+
vi.advanceTimersByTime(100);
|
|
428
|
+
|
|
429
|
+
const region = document.body.querySelector('.sr-only[aria-live]') as HTMLElement;
|
|
430
|
+
expect(region.textContent).toBe('Page 1 of 1. Showing items 1 to 1 of 1.');
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
describe('announceSelectionChange', () => {
|
|
435
|
+
beforeEach(() => {
|
|
436
|
+
initializeLiveRegion();
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it('announces all rows deselected', () => {
|
|
440
|
+
announceSelectionChange(0, 10);
|
|
441
|
+
vi.advanceTimersByTime(100);
|
|
442
|
+
|
|
443
|
+
const region = document.body.querySelector('.sr-only[aria-live]') as HTMLElement;
|
|
444
|
+
expect(region.textContent).toBe('All rows deselected');
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it('announces all rows selected', () => {
|
|
448
|
+
announceSelectionChange(10, 10);
|
|
449
|
+
vi.advanceTimersByTime(100);
|
|
450
|
+
|
|
451
|
+
const region = document.body.querySelector('.sr-only[aria-live]') as HTMLElement;
|
|
452
|
+
expect(region.textContent).toBe('All 10 rows selected');
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it('announces partial selection', () => {
|
|
456
|
+
announceSelectionChange(5, 10);
|
|
457
|
+
vi.advanceTimersByTime(100);
|
|
458
|
+
|
|
459
|
+
const region = document.body.querySelector('.sr-only[aria-live]') as HTMLElement;
|
|
460
|
+
expect(region.textContent).toBe('5 of 10 rows selected');
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it('handles single row selection', () => {
|
|
464
|
+
announceSelectionChange(1, 1);
|
|
465
|
+
vi.advanceTimersByTime(100);
|
|
466
|
+
|
|
467
|
+
const region = document.body.querySelector('.sr-only[aria-live]') as HTMLElement;
|
|
468
|
+
expect(region.textContent).toBe('All 1 rows selected');
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it('handles large numbers', () => {
|
|
472
|
+
announceSelectionChange(500, 1000);
|
|
473
|
+
vi.advanceTimersByTime(100);
|
|
474
|
+
|
|
475
|
+
const region = document.body.querySelector('.sr-only[aria-live]') as HTMLElement;
|
|
476
|
+
expect(region.textContent).toBe('500 of 1000 rows selected');
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
describe('announceLoadingState', () => {
|
|
481
|
+
beforeEach(() => {
|
|
482
|
+
initializeLiveRegion();
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it('announces loading state', () => {
|
|
486
|
+
announceLoadingState(true, false);
|
|
487
|
+
vi.advanceTimersByTime(100);
|
|
488
|
+
|
|
489
|
+
const region = document.body.querySelector('.sr-only[aria-live]') as HTMLElement;
|
|
490
|
+
expect(region.textContent).toBe('Loading data...');
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it('announces loaded state', () => {
|
|
494
|
+
announceLoadingState(false, false);
|
|
495
|
+
vi.advanceTimersByTime(100);
|
|
496
|
+
|
|
497
|
+
const region = document.body.querySelector('.sr-only[aria-live]') as HTMLElement;
|
|
498
|
+
expect(region.textContent).toBe('Data loaded');
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it('announces error state with assertive priority', () => {
|
|
502
|
+
announceLoadingState(false, true);
|
|
503
|
+
vi.advanceTimersByTime(100);
|
|
504
|
+
|
|
505
|
+
const region = document.body.querySelector('.sr-only[aria-live]') as HTMLElement;
|
|
506
|
+
expect(region.getAttribute('aria-live')).toBe('assertive');
|
|
507
|
+
expect(region.textContent).toBe('Error loading data');
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it('prioritizes error over loading state', () => {
|
|
511
|
+
announceLoadingState(true, true);
|
|
512
|
+
vi.advanceTimersByTime(100);
|
|
513
|
+
|
|
514
|
+
const region = document.body.querySelector('.sr-only[aria-live]') as HTMLElement;
|
|
515
|
+
expect(region.getAttribute('aria-live')).toBe('assertive');
|
|
516
|
+
expect(region.textContent).toBe('Error loading data');
|
|
517
|
+
});
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
describe('announceBulkOperation', () => {
|
|
521
|
+
beforeEach(() => {
|
|
522
|
+
initializeLiveRegion();
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it('announces bulk operation with single item', () => {
|
|
526
|
+
announceBulkOperation('delete', 1);
|
|
527
|
+
vi.advanceTimersByTime(100);
|
|
528
|
+
|
|
529
|
+
const region = document.body.querySelector('.sr-only[aria-live]') as HTMLElement;
|
|
530
|
+
expect(region.textContent).toBe('delete completed for 1 item');
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it('announces bulk operation with multiple items', () => {
|
|
534
|
+
announceBulkOperation('delete', 5);
|
|
535
|
+
vi.advanceTimersByTime(100);
|
|
536
|
+
|
|
537
|
+
const region = document.body.querySelector('.sr-only[aria-live]') as HTMLElement;
|
|
538
|
+
expect(region.textContent).toBe('delete completed for 5 items');
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
it('handles different operation types', () => {
|
|
542
|
+
announceBulkOperation('export', 10);
|
|
543
|
+
vi.advanceTimersByTime(100);
|
|
544
|
+
|
|
545
|
+
const region = document.body.querySelector('.sr-only[aria-live]') as HTMLElement;
|
|
546
|
+
expect(region.textContent).toBe('export completed for 10 items');
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it('handles zero items', () => {
|
|
550
|
+
announceBulkOperation('delete', 0);
|
|
551
|
+
vi.advanceTimersByTime(100);
|
|
552
|
+
|
|
553
|
+
const region = document.body.querySelector('.sr-only[aria-live]') as HTMLElement;
|
|
554
|
+
expect(region.textContent).toBe('delete completed for 0 items');
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
describe('getAriaSortValue', () => {
|
|
559
|
+
it('returns ascending for asc sort', () => {
|
|
560
|
+
expect(getAriaSortValue('asc')).toBe('ascending');
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
it('returns descending for desc sort', () => {
|
|
564
|
+
expect(getAriaSortValue('desc')).toBe('descending');
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
it('returns none when no sort is applied', () => {
|
|
568
|
+
expect(getAriaSortValue(false)).toBe('none');
|
|
569
|
+
});
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
describe('getRowDescription', () => {
|
|
573
|
+
it('returns basic row description', () => {
|
|
574
|
+
expect(getRowDescription(0, 10)).toBe('Row 1 of 10');
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
it('returns description with selected state', () => {
|
|
578
|
+
expect(getRowDescription(0, 10, true)).toBe('Row 1 of 10, selected');
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
it('returns description with editing state', () => {
|
|
582
|
+
expect(getRowDescription(0, 10, false, true)).toBe('Row 1 of 10, editing mode');
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
it('returns description with both selected and editing states', () => {
|
|
586
|
+
expect(getRowDescription(0, 10, true, true)).toBe('Row 1 of 10, selected, editing mode');
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
it('handles middle row', () => {
|
|
590
|
+
expect(getRowDescription(5, 10)).toBe('Row 6 of 10');
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
it('handles last row', () => {
|
|
594
|
+
expect(getRowDescription(9, 10)).toBe('Row 10 of 10');
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it('handles single row', () => {
|
|
598
|
+
expect(getRowDescription(0, 1)).toBe('Row 1 of 1');
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
it('handles large row counts', () => {
|
|
602
|
+
expect(getRowDescription(999, 1000)).toBe('Row 1000 of 1000');
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
it('handles zero-based index correctly', () => {
|
|
606
|
+
expect(getRowDescription(0, 5)).toBe('Row 1 of 5');
|
|
607
|
+
expect(getRowDescription(1, 5)).toBe('Row 2 of 5');
|
|
608
|
+
expect(getRowDescription(4, 5)).toBe('Row 5 of 5');
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
});
|
|
612
|
+
|