@qwickapps/react-framework 1.3.2 → 1.3.4
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 +326 -0
- package/dist/components/AccessibilityProvider.d.ts +64 -0
- package/dist/components/AccessibilityProvider.d.ts.map +1 -0
- package/dist/components/Breadcrumbs.d.ts +39 -0
- package/dist/components/Breadcrumbs.d.ts.map +1 -0
- package/dist/components/ErrorBoundary.d.ts +39 -0
- package/dist/components/ErrorBoundary.d.ts.map +1 -0
- package/dist/components/QwickApp.d.ts.map +1 -1
- package/dist/components/forms/FormBlock.d.ts +1 -1
- package/dist/components/forms/FormBlock.d.ts.map +1 -1
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/input/SwitchInputField.d.ts +28 -0
- package/dist/components/input/SwitchInputField.d.ts.map +1 -0
- package/dist/components/input/index.d.ts +2 -0
- package/dist/components/input/index.d.ts.map +1 -1
- package/dist/components/layout/CollapsibleLayout/CollapsibleLayout.d.ts +34 -0
- package/dist/components/layout/CollapsibleLayout/CollapsibleLayout.d.ts.map +1 -0
- package/dist/components/layout/CollapsibleLayout/index.d.ts +9 -0
- package/dist/components/layout/CollapsibleLayout/index.d.ts.map +1 -0
- package/dist/components/layout/index.d.ts +2 -0
- package/dist/components/layout/index.d.ts.map +1 -1
- package/dist/index.bundled.css +12 -0
- package/dist/index.esm.js +1678 -25
- package/dist/index.js +1689 -21
- package/dist/schemas/CollapsibleLayoutSchema.d.ts +31 -0
- package/dist/schemas/CollapsibleLayoutSchema.d.ts.map +1 -0
- package/dist/schemas/SwitchInputFieldSchema.d.ts +18 -0
- package/dist/schemas/SwitchInputFieldSchema.d.ts.map +1 -0
- package/dist/types/CollapsibleLayout.d.ts +142 -0
- package/dist/types/CollapsibleLayout.d.ts.map +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/AccessibilityProvider.tsx +466 -0
- package/src/components/Breadcrumbs.tsx +223 -0
- package/src/components/ErrorBoundary.tsx +216 -0
- package/src/components/QwickApp.tsx +17 -11
- package/src/components/__tests__/AccessibilityProvider.test.tsx +330 -0
- package/src/components/__tests__/Breadcrumbs.test.tsx +268 -0
- package/src/components/__tests__/ErrorBoundary.test.tsx +163 -0
- package/src/components/forms/FormBlock.tsx +2 -2
- package/src/components/index.ts +3 -0
- package/src/components/input/SwitchInputField.tsx +165 -0
- package/src/components/input/index.ts +2 -0
- package/src/components/layout/CollapsibleLayout/CollapsibleLayout.tsx +554 -0
- package/src/components/layout/CollapsibleLayout/__tests__/CollapsibleLayout.test.tsx +1469 -0
- package/src/components/layout/CollapsibleLayout/index.tsx +17 -0
- package/src/components/layout/index.ts +4 -1
- package/src/components/pages/FormPage.tsx +1 -1
- package/src/schemas/CollapsibleLayoutSchema.ts +276 -0
- package/src/schemas/SwitchInputFieldSchema.ts +99 -0
- package/src/stories/AccessibilityProvider.stories.tsx +284 -0
- package/src/stories/Breadcrumbs.stories.tsx +304 -0
- package/src/stories/CollapsibleLayout.stories.tsx +1566 -0
- package/src/stories/ErrorBoundary.stories.tsx +159 -0
- package/src/types/CollapsibleLayout.ts +231 -0
- package/src/types/index.ts +1 -0
- package/dist/schemas/Builders.d.ts +0 -7
- package/dist/schemas/Builders.d.ts.map +0 -1
- package/dist/schemas/types.d.ts +0 -7
- package/dist/schemas/types.d.ts.map +0 -1
- package/dist/types/DataBinding.d.ts +0 -7
- package/dist/types/DataBinding.d.ts.map +0 -1
- package/dist/types/DataProvider.d.ts +0 -7
- package/dist/types/DataProvider.d.ts.map +0 -1
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import React, { Component, ErrorInfo, ReactNode } from 'react';
|
|
2
|
+
import { Button } from './buttons/Button';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
fallback?: ReactNode;
|
|
7
|
+
onError?: (error: Error, errorInfo: ErrorInfo) => void;
|
|
8
|
+
showErrorDetails?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface State {
|
|
12
|
+
hasError: boolean;
|
|
13
|
+
error: Error | null;
|
|
14
|
+
errorInfo: ErrorInfo | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Generic ErrorBoundary component for catching and handling React errors
|
|
19
|
+
*
|
|
20
|
+
* Features:
|
|
21
|
+
* - Catches JavaScript errors anywhere in child component tree
|
|
22
|
+
* - Displays fallback UI with retry functionality
|
|
23
|
+
* - Shows error details in development mode
|
|
24
|
+
* - Customizable error handling and fallback UI
|
|
25
|
+
* - Automatic error logging
|
|
26
|
+
*/
|
|
27
|
+
export class ErrorBoundary extends Component<Props, State> {
|
|
28
|
+
constructor(props: Props) {
|
|
29
|
+
super(props);
|
|
30
|
+
this.state = {
|
|
31
|
+
hasError: false,
|
|
32
|
+
error: null,
|
|
33
|
+
errorInfo: null
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
static getDerivedStateFromError(error: Error): State {
|
|
38
|
+
// Update state so the next render will show the fallback UI
|
|
39
|
+
return {
|
|
40
|
+
hasError: true,
|
|
41
|
+
error,
|
|
42
|
+
errorInfo: null
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
|
47
|
+
// Log error details
|
|
48
|
+
this.setState({
|
|
49
|
+
error,
|
|
50
|
+
errorInfo
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Log to console for debugging
|
|
54
|
+
console.error('ErrorBoundary caught an error:', error, errorInfo);
|
|
55
|
+
|
|
56
|
+
// Custom error handler
|
|
57
|
+
if (this.props.onError) {
|
|
58
|
+
this.props.onError(error, errorInfo);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Send error to logging service if available
|
|
62
|
+
if (typeof window !== 'undefined') {
|
|
63
|
+
// @ts-ignore - Global error logging service
|
|
64
|
+
if (window.qwickapps?.logError) {
|
|
65
|
+
window.qwickapps.logError(error, errorInfo);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
handleRetry = () => {
|
|
71
|
+
this.setState({
|
|
72
|
+
hasError: false,
|
|
73
|
+
error: null,
|
|
74
|
+
errorInfo: null
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
handleRefresh = () => {
|
|
79
|
+
if (typeof window !== 'undefined') {
|
|
80
|
+
window.location.reload();
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
render() {
|
|
85
|
+
if (this.state.hasError) {
|
|
86
|
+
// Custom fallback UI
|
|
87
|
+
if (this.props.fallback) {
|
|
88
|
+
return this.props.fallback;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Default error UI
|
|
92
|
+
return (
|
|
93
|
+
<div
|
|
94
|
+
className="error-boundary"
|
|
95
|
+
role="alert"
|
|
96
|
+
style={{
|
|
97
|
+
padding: '2rem',
|
|
98
|
+
textAlign: 'center',
|
|
99
|
+
backgroundColor: '#fef2f2',
|
|
100
|
+
border: '1px solid #fecaca',
|
|
101
|
+
borderRadius: '8px',
|
|
102
|
+
margin: '1rem',
|
|
103
|
+
color: '#991b1b'
|
|
104
|
+
}}
|
|
105
|
+
>
|
|
106
|
+
<div style={{ marginBottom: '1.5rem' }}>
|
|
107
|
+
<h2 style={{
|
|
108
|
+
fontSize: '1.5rem',
|
|
109
|
+
fontWeight: 'bold',
|
|
110
|
+
marginBottom: '0.5rem',
|
|
111
|
+
color: '#991b1b'
|
|
112
|
+
}}>
|
|
113
|
+
Something went wrong
|
|
114
|
+
</h2>
|
|
115
|
+
<p style={{
|
|
116
|
+
color: '#7f1d1d',
|
|
117
|
+
marginBottom: '1rem'
|
|
118
|
+
}}>
|
|
119
|
+
An unexpected error occurred in the application. Please try again or refresh the page.
|
|
120
|
+
</p>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<div style={{
|
|
124
|
+
display: 'flex',
|
|
125
|
+
gap: '0.75rem',
|
|
126
|
+
justifyContent: 'center',
|
|
127
|
+
marginBottom: '1rem'
|
|
128
|
+
}}>
|
|
129
|
+
<Button
|
|
130
|
+
variant="contained"
|
|
131
|
+
onClick={this.handleRetry}
|
|
132
|
+
style={{
|
|
133
|
+
backgroundColor: '#dc2626',
|
|
134
|
+
color: 'white'
|
|
135
|
+
}}
|
|
136
|
+
>
|
|
137
|
+
Try Again
|
|
138
|
+
</Button>
|
|
139
|
+
|
|
140
|
+
<Button
|
|
141
|
+
variant="outlined"
|
|
142
|
+
onClick={this.handleRefresh}
|
|
143
|
+
style={{
|
|
144
|
+
borderColor: '#dc2626',
|
|
145
|
+
color: '#dc2626'
|
|
146
|
+
}}
|
|
147
|
+
>
|
|
148
|
+
Refresh Page
|
|
149
|
+
</Button>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
{/* Show error details in development or when explicitly enabled */}
|
|
153
|
+
{(process.env.NODE_ENV === 'development' || this.props.showErrorDetails) && this.state.error && (
|
|
154
|
+
<details
|
|
155
|
+
style={{
|
|
156
|
+
textAlign: 'left',
|
|
157
|
+
marginTop: '1rem',
|
|
158
|
+
padding: '1rem',
|
|
159
|
+
backgroundColor: '#f9fafb',
|
|
160
|
+
border: '1px solid #d1d5db',
|
|
161
|
+
borderRadius: '6px'
|
|
162
|
+
}}
|
|
163
|
+
>
|
|
164
|
+
<summary style={{
|
|
165
|
+
cursor: 'pointer',
|
|
166
|
+
fontWeight: 'bold',
|
|
167
|
+
marginBottom: '0.5rem',
|
|
168
|
+
color: '#374151'
|
|
169
|
+
}}>
|
|
170
|
+
Error Details (Development Mode)
|
|
171
|
+
</summary>
|
|
172
|
+
<pre style={{
|
|
173
|
+
fontSize: '0.75rem',
|
|
174
|
+
color: '#374151',
|
|
175
|
+
whiteSpace: 'pre-wrap',
|
|
176
|
+
overflow: 'auto'
|
|
177
|
+
}}>
|
|
178
|
+
{this.state.error.toString()}
|
|
179
|
+
{this.state.errorInfo?.componentStack && (
|
|
180
|
+
<>
|
|
181
|
+
<br />
|
|
182
|
+
<br />
|
|
183
|
+
Component Stack:
|
|
184
|
+
{this.state.errorInfo.componentStack}
|
|
185
|
+
</>
|
|
186
|
+
)}
|
|
187
|
+
</pre>
|
|
188
|
+
</details>
|
|
189
|
+
)}
|
|
190
|
+
</div>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return this.props.children;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Higher-order component that wraps a component with ErrorBoundary
|
|
200
|
+
*/
|
|
201
|
+
export function withErrorBoundary<P extends object>(
|
|
202
|
+
WrappedComponent: React.ComponentType<P>,
|
|
203
|
+
errorBoundaryProps?: Omit<Props, 'children'>
|
|
204
|
+
) {
|
|
205
|
+
const WithErrorBoundaryComponent = (props: P) => (
|
|
206
|
+
<ErrorBoundary {...errorBoundaryProps}>
|
|
207
|
+
<WrappedComponent {...props} />
|
|
208
|
+
</ErrorBoundary>
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
WithErrorBoundaryComponent.displayName = `withErrorBoundary(${WrappedComponent.displayName || WrappedComponent.name || 'Component'})`;
|
|
212
|
+
|
|
213
|
+
return WithErrorBoundaryComponent;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export default ErrorBoundary;
|
|
@@ -35,6 +35,8 @@ import { QwickAppContext, type QwickAppContextValue, type QwickAppProps } from '
|
|
|
35
35
|
import { type TemplateResolverConfig } from '../types';
|
|
36
36
|
import './QwickApp.css';
|
|
37
37
|
import Scaffold from './Scaffold';
|
|
38
|
+
import { ErrorBoundary } from './ErrorBoundary';
|
|
39
|
+
import { AccessibilityProvider } from './AccessibilityProvider';
|
|
38
40
|
// Auth logic moved to AuthProvider - QwickApp now focuses on app infrastructure
|
|
39
41
|
|
|
40
42
|
// RouteConfig moved to AuthProvider for auth-specific routing
|
|
@@ -125,17 +127,21 @@ export const QwickApp: React.FC<QwickAppComponentProps> = ({
|
|
|
125
127
|
) : content;
|
|
126
128
|
|
|
127
129
|
const appContent = (
|
|
128
|
-
<
|
|
129
|
-
<
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
130
|
+
<ErrorBoundary>
|
|
131
|
+
<AccessibilityProvider>
|
|
132
|
+
<div className={`qwick-app ${className || ''}`} style={style}>
|
|
133
|
+
<ThemeProvider
|
|
134
|
+
appId={appId}
|
|
135
|
+
defaultTheme={defaultTheme}
|
|
136
|
+
defaultPalette={defaultPalette}
|
|
137
|
+
>
|
|
138
|
+
<QwickAppContext.Provider value={contextValue}>
|
|
139
|
+
{wrappedContent}
|
|
140
|
+
</QwickAppContext.Provider>
|
|
141
|
+
</ThemeProvider>
|
|
142
|
+
</div>
|
|
143
|
+
</AccessibilityProvider>
|
|
144
|
+
</ErrorBoundary>
|
|
139
145
|
);
|
|
140
146
|
|
|
141
147
|
// If router is provided, wrap the entire app with it
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, fireEvent, act, waitFor } from '@testing-library/react';
|
|
3
|
+
import { AccessibilityProvider, useAccessibility } from '../AccessibilityProvider';
|
|
4
|
+
|
|
5
|
+
// Test component that uses accessibility hook
|
|
6
|
+
const TestComponent = () => {
|
|
7
|
+
const {
|
|
8
|
+
highContrast,
|
|
9
|
+
reducedMotion,
|
|
10
|
+
largeText,
|
|
11
|
+
focusVisible,
|
|
12
|
+
isKeyboardUser,
|
|
13
|
+
issues,
|
|
14
|
+
setHighContrast,
|
|
15
|
+
setReducedMotion,
|
|
16
|
+
setLargeText,
|
|
17
|
+
announce,
|
|
18
|
+
announcePolite,
|
|
19
|
+
announceAssertive,
|
|
20
|
+
addIssue,
|
|
21
|
+
clearIssues,
|
|
22
|
+
runAudit
|
|
23
|
+
} = useAccessibility();
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div>
|
|
27
|
+
<div data-testid="high-contrast">{highContrast.toString()}</div>
|
|
28
|
+
<div data-testid="reduced-motion">{reducedMotion.toString()}</div>
|
|
29
|
+
<div data-testid="large-text">{largeText.toString()}</div>
|
|
30
|
+
<div data-testid="focus-visible">{focusVisible.toString()}</div>
|
|
31
|
+
<div data-testid="keyboard-user">{isKeyboardUser.toString()}</div>
|
|
32
|
+
<div data-testid="issues-count">{issues.length}</div>
|
|
33
|
+
|
|
34
|
+
<button onClick={() => setHighContrast(!highContrast)}>Toggle High Contrast</button>
|
|
35
|
+
<button onClick={() => setReducedMotion(!reducedMotion)}>Toggle Reduced Motion</button>
|
|
36
|
+
<button onClick={() => setLargeText(!largeText)}>Toggle Large Text</button>
|
|
37
|
+
<button onClick={() => announce('Test message')}>Announce</button>
|
|
38
|
+
<button onClick={() => announcePolite('Polite message')}>Announce Polite</button>
|
|
39
|
+
<button onClick={() => announceAssertive('Assertive message')}>Announce Assertive</button>
|
|
40
|
+
<button onClick={() => addIssue({ type: 'test', message: 'Test issue', level: 'error' })}>Add Issue</button>
|
|
41
|
+
<button onClick={clearIssues}>Clear Issues</button>
|
|
42
|
+
<button onClick={runAudit}>Run Audit</button>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Mock matchMedia
|
|
48
|
+
const mockMatchMedia = (matches: boolean) => {
|
|
49
|
+
return jest.fn(() => ({
|
|
50
|
+
matches,
|
|
51
|
+
addListener: jest.fn(),
|
|
52
|
+
removeListener: jest.fn(),
|
|
53
|
+
addEventListener: jest.fn(),
|
|
54
|
+
removeEventListener: jest.fn(),
|
|
55
|
+
}));
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Mock console methods
|
|
59
|
+
const originalError = console.error;
|
|
60
|
+
const originalGroup = console.group;
|
|
61
|
+
const originalGroupEnd = console.groupEnd;
|
|
62
|
+
const originalWarn = console.warn;
|
|
63
|
+
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
// Mock window.matchMedia
|
|
66
|
+
Object.defineProperty(window, 'matchMedia', {
|
|
67
|
+
writable: true,
|
|
68
|
+
value: mockMatchMedia(false),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Mock console methods
|
|
72
|
+
console.error = jest.fn();
|
|
73
|
+
console.group = jest.fn();
|
|
74
|
+
console.groupEnd = jest.fn();
|
|
75
|
+
console.warn = jest.fn();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
afterEach(() => {
|
|
79
|
+
// Restore console methods
|
|
80
|
+
console.error = originalError;
|
|
81
|
+
console.group = originalGroup;
|
|
82
|
+
console.groupEnd = originalGroupEnd;
|
|
83
|
+
console.warn = originalWarn;
|
|
84
|
+
|
|
85
|
+
// Clean up DOM
|
|
86
|
+
document.body.classList.remove('keyboard-user', 'high-contrast', 'reduced-motion', 'large-text');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('AccessibilityProvider', () => {
|
|
90
|
+
it('renders children correctly', () => {
|
|
91
|
+
render(
|
|
92
|
+
<AccessibilityProvider>
|
|
93
|
+
<div data-testid="child">Test content</div>
|
|
94
|
+
</AccessibilityProvider>
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
expect(screen.getByTestId('child')).toBeInTheDocument();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('provides initial accessibility state', () => {
|
|
101
|
+
render(
|
|
102
|
+
<AccessibilityProvider>
|
|
103
|
+
<TestComponent />
|
|
104
|
+
</AccessibilityProvider>
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
expect(screen.getByTestId('high-contrast')).toHaveTextContent('false');
|
|
108
|
+
expect(screen.getByTestId('reduced-motion')).toHaveTextContent('false');
|
|
109
|
+
expect(screen.getByTestId('large-text')).toHaveTextContent('false');
|
|
110
|
+
expect(screen.getByTestId('focus-visible')).toHaveTextContent('true');
|
|
111
|
+
expect(screen.getByTestId('keyboard-user')).toHaveTextContent('false');
|
|
112
|
+
expect(screen.getByTestId('issues-count')).toHaveTextContent('0');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('allows toggling high contrast mode', () => {
|
|
116
|
+
render(
|
|
117
|
+
<AccessibilityProvider>
|
|
118
|
+
<TestComponent />
|
|
119
|
+
</AccessibilityProvider>
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const button = screen.getByText('Toggle High Contrast');
|
|
123
|
+
fireEvent.click(button);
|
|
124
|
+
|
|
125
|
+
expect(screen.getByTestId('high-contrast')).toHaveTextContent('true');
|
|
126
|
+
expect(document.body).toHaveClass('high-contrast');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('allows toggling reduced motion mode', () => {
|
|
130
|
+
render(
|
|
131
|
+
<AccessibilityProvider>
|
|
132
|
+
<TestComponent />
|
|
133
|
+
</AccessibilityProvider>
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const button = screen.getByText('Toggle Reduced Motion');
|
|
137
|
+
fireEvent.click(button);
|
|
138
|
+
|
|
139
|
+
expect(screen.getByTestId('reduced-motion')).toHaveTextContent('true');
|
|
140
|
+
expect(document.body).toHaveClass('reduced-motion');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('allows toggling large text mode', () => {
|
|
144
|
+
render(
|
|
145
|
+
<AccessibilityProvider>
|
|
146
|
+
<TestComponent />
|
|
147
|
+
</AccessibilityProvider>
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const button = screen.getByText('Toggle Large Text');
|
|
151
|
+
fireEvent.click(button);
|
|
152
|
+
|
|
153
|
+
expect(screen.getByTestId('large-text')).toHaveTextContent('true');
|
|
154
|
+
expect(document.body).toHaveClass('large-text');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('detects keyboard usage', () => {
|
|
158
|
+
render(
|
|
159
|
+
<AccessibilityProvider>
|
|
160
|
+
<TestComponent />
|
|
161
|
+
</AccessibilityProvider>
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
// Simulate Tab key press
|
|
165
|
+
act(() => {
|
|
166
|
+
fireEvent.keyDown(document, { key: 'Tab' });
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
expect(screen.getByTestId('keyboard-user')).toHaveTextContent('true');
|
|
170
|
+
expect(document.body).toHaveClass('keyboard-user');
|
|
171
|
+
|
|
172
|
+
// Simulate mouse click
|
|
173
|
+
act(() => {
|
|
174
|
+
fireEvent.mouseDown(document);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
expect(screen.getByTestId('keyboard-user')).toHaveTextContent('false');
|
|
178
|
+
expect(document.body).not.toHaveClass('keyboard-user');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('creates ARIA live regions', () => {
|
|
182
|
+
render(
|
|
183
|
+
<AccessibilityProvider>
|
|
184
|
+
<TestComponent />
|
|
185
|
+
</AccessibilityProvider>
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const politeRegion = document.getElementById('qwickapps-aria-live-polite');
|
|
189
|
+
const assertiveRegion = document.getElementById('qwickapps-aria-live-assertive');
|
|
190
|
+
|
|
191
|
+
expect(politeRegion).toBeInTheDocument();
|
|
192
|
+
expect(politeRegion).toHaveAttribute('aria-live', 'polite');
|
|
193
|
+
expect(assertiveRegion).toBeInTheDocument();
|
|
194
|
+
expect(assertiveRegion).toHaveAttribute('aria-live', 'assertive');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('handles polite announcements', async () => {
|
|
198
|
+
render(
|
|
199
|
+
<AccessibilityProvider>
|
|
200
|
+
<TestComponent />
|
|
201
|
+
</AccessibilityProvider>
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const button = screen.getByText('Announce Polite');
|
|
205
|
+
fireEvent.click(button);
|
|
206
|
+
|
|
207
|
+
await waitFor(() => {
|
|
208
|
+
const politeRegion = document.getElementById('qwickapps-aria-live-polite');
|
|
209
|
+
expect(politeRegion).toHaveTextContent('Polite message');
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('handles assertive announcements', async () => {
|
|
214
|
+
render(
|
|
215
|
+
<AccessibilityProvider>
|
|
216
|
+
<TestComponent />
|
|
217
|
+
</AccessibilityProvider>
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
const button = screen.getByText('Announce Assertive');
|
|
221
|
+
fireEvent.click(button);
|
|
222
|
+
|
|
223
|
+
await waitFor(() => {
|
|
224
|
+
const assertiveRegion = document.getElementById('qwickapps-aria-live-assertive');
|
|
225
|
+
expect(assertiveRegion).toHaveTextContent('Assertive message');
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('manages accessibility issues', () => {
|
|
230
|
+
render(
|
|
231
|
+
<AccessibilityProvider>
|
|
232
|
+
<TestComponent />
|
|
233
|
+
</AccessibilityProvider>
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
// Add an issue
|
|
237
|
+
const addButton = screen.getByText('Add Issue');
|
|
238
|
+
fireEvent.click(addButton);
|
|
239
|
+
expect(screen.getByTestId('issues-count')).toHaveTextContent('1');
|
|
240
|
+
|
|
241
|
+
// Clear issues
|
|
242
|
+
const clearButton = screen.getByText('Clear Issues');
|
|
243
|
+
fireEvent.click(clearButton);
|
|
244
|
+
expect(screen.getByTestId('issues-count')).toHaveTextContent('0');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('runs accessibility audit when enabled', async () => {
|
|
248
|
+
render(
|
|
249
|
+
<AccessibilityProvider enableAudit={true}>
|
|
250
|
+
<TestComponent />
|
|
251
|
+
{/* Add elements that will trigger audit issues */}
|
|
252
|
+
<img src="test.jpg" />
|
|
253
|
+
<button></button>
|
|
254
|
+
<input type="text" />
|
|
255
|
+
</AccessibilityProvider>
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
await waitFor(() => {
|
|
259
|
+
expect(screen.getByTestId('issues-count')).not.toHaveTextContent('0');
|
|
260
|
+
}, { timeout: 2000 });
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('does not run audit when disabled', () => {
|
|
264
|
+
render(
|
|
265
|
+
<AccessibilityProvider enableAudit={false}>
|
|
266
|
+
<TestComponent />
|
|
267
|
+
<img src="test.jpg" />
|
|
268
|
+
<button></button>
|
|
269
|
+
<input type="text" />
|
|
270
|
+
</AccessibilityProvider>
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
expect(screen.getByTestId('issues-count')).toHaveTextContent('0');
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('detects system high contrast preference', () => {
|
|
277
|
+
// Mock high contrast preference
|
|
278
|
+
Object.defineProperty(window, 'matchMedia', {
|
|
279
|
+
writable: true,
|
|
280
|
+
value: jest.fn((query) => ({
|
|
281
|
+
matches: query === '(prefers-contrast: high)',
|
|
282
|
+
addListener: jest.fn(),
|
|
283
|
+
removeListener: jest.fn(),
|
|
284
|
+
addEventListener: jest.fn(),
|
|
285
|
+
removeEventListener: jest.fn(),
|
|
286
|
+
})),
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
render(
|
|
290
|
+
<AccessibilityProvider>
|
|
291
|
+
<TestComponent />
|
|
292
|
+
</AccessibilityProvider>
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
expect(screen.getByTestId('high-contrast')).toHaveTextContent('true');
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('detects system reduced motion preference', () => {
|
|
299
|
+
// Mock reduced motion preference
|
|
300
|
+
Object.defineProperty(window, 'matchMedia', {
|
|
301
|
+
writable: true,
|
|
302
|
+
value: jest.fn((query) => ({
|
|
303
|
+
matches: query === '(prefers-reduced-motion: reduce)',
|
|
304
|
+
addListener: jest.fn(),
|
|
305
|
+
removeListener: jest.fn(),
|
|
306
|
+
addEventListener: jest.fn(),
|
|
307
|
+
removeEventListener: jest.fn(),
|
|
308
|
+
})),
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
render(
|
|
312
|
+
<AccessibilityProvider>
|
|
313
|
+
<TestComponent />
|
|
314
|
+
</AccessibilityProvider>
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
expect(screen.getByTestId('reduced-motion')).toHaveTextContent('true');
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('throws error when hook is used outside provider', () => {
|
|
321
|
+
// Suppress console.error for this test
|
|
322
|
+
const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
323
|
+
|
|
324
|
+
expect(() => {
|
|
325
|
+
render(<TestComponent />);
|
|
326
|
+
}).toThrow('useAccessibility must be used within an AccessibilityProvider');
|
|
327
|
+
|
|
328
|
+
spy.mockRestore();
|
|
329
|
+
});
|
|
330
|
+
});
|