@qwickapps/react-framework 1.3.1 → 1.3.3
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 +123 -1
- 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/index.d.ts +3 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/index.bundled.css +12 -0
- package/dist/index.esm.js +910 -44
- package/dist/index.js +916 -47
- package/dist/templates/TemplateResolver.d.ts.map +1 -1
- package/dist/utils/htmlTransform.d.ts.map +1 -1
- package/dist/utils/logger.d.ts +15 -3
- package/dist/utils/logger.d.ts.map +1 -1
- package/package.json +4 -2
- 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/index.ts +3 -0
- package/src/stories/AccessibilityProvider.stories.tsx +284 -0
- package/src/stories/Breadcrumbs.stories.tsx +304 -0
- package/src/stories/ErrorBoundary.stories.tsx +159 -0
- package/src/stories/{form/FormComponents.stories.tsx → FormComponents.stories.tsx} +8 -8
- package/src/templates/TemplateResolver.ts +2 -6
- package/src/utils/__tests__/nested-dom-fix.test.tsx +53 -0
- package/src/utils/__tests__/optional-logging.test.ts +83 -0
- package/src/utils/htmlTransform.tsx +69 -3
- package/src/utils/logger.ts +60 -5
- 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,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
|
+
});
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
3
|
+
import { Breadcrumbs, useBreadcrumbs, type BreadcrumbItem } from '../Breadcrumbs';
|
|
4
|
+
|
|
5
|
+
// Test component for useBreadcrumbs hook
|
|
6
|
+
const BreadcrumbHookTest = () => {
|
|
7
|
+
const { breadcrumbs, addBreadcrumb, removeBreadcrumb, setBreadcrumbs, clearBreadcrumbs } = useBreadcrumbs();
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<div>
|
|
11
|
+
<div data-testid="breadcrumb-count">{breadcrumbs.length}</div>
|
|
12
|
+
<button onClick={() => addBreadcrumb({ label: 'Test', href: '/test' })}>
|
|
13
|
+
Add Breadcrumb
|
|
14
|
+
</button>
|
|
15
|
+
<button onClick={() => removeBreadcrumb(0)}>
|
|
16
|
+
Remove First
|
|
17
|
+
</button>
|
|
18
|
+
<button onClick={() => setBreadcrumbs([{ label: 'Set', href: '/set' }])}>
|
|
19
|
+
Set Breadcrumbs
|
|
20
|
+
</button>
|
|
21
|
+
<button onClick={clearBreadcrumbs}>
|
|
22
|
+
Clear All
|
|
23
|
+
</button>
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Sample breadcrumb data
|
|
29
|
+
const sampleBreadcrumbs: BreadcrumbItem[] = [
|
|
30
|
+
{ label: 'Home', href: '/' },
|
|
31
|
+
{ label: 'Products', href: '/products' },
|
|
32
|
+
{ label: 'Electronics', href: '/products/electronics', current: true },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
describe('Breadcrumbs', () => {
|
|
36
|
+
it('renders breadcrumb items correctly', () => {
|
|
37
|
+
render(<Breadcrumbs items={sampleBreadcrumbs} />);
|
|
38
|
+
|
|
39
|
+
expect(screen.getByText('Home')).toBeInTheDocument();
|
|
40
|
+
expect(screen.getByText('Products')).toBeInTheDocument();
|
|
41
|
+
expect(screen.getByText('Electronics')).toBeInTheDocument();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('renders with proper ARIA attributes', () => {
|
|
45
|
+
render(<Breadcrumbs items={sampleBreadcrumbs} />);
|
|
46
|
+
|
|
47
|
+
const nav = screen.getByRole('navigation');
|
|
48
|
+
expect(nav).toHaveAttribute('aria-label', 'Breadcrumb navigation');
|
|
49
|
+
|
|
50
|
+
// Electronics has current: true, so it should be wrapped in a span with aria-current="page"
|
|
51
|
+
const currentItemText = screen.getByText('Electronics');
|
|
52
|
+
const currentItem = currentItemText.parentElement;
|
|
53
|
+
expect(currentItem?.tagName).toBe('SPAN');
|
|
54
|
+
expect(currentItem).toHaveAttribute('aria-current', 'page');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('renders custom separator', () => {
|
|
58
|
+
render(<Breadcrumbs items={sampleBreadcrumbs} separator=">" />);
|
|
59
|
+
|
|
60
|
+
const separators = screen.getAllByText('>');
|
|
61
|
+
expect(separators).toHaveLength(2); // Should have 2 separators for 3 items
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('applies custom className', () => {
|
|
65
|
+
render(<Breadcrumbs items={sampleBreadcrumbs} className="custom-breadcrumbs" />);
|
|
66
|
+
|
|
67
|
+
const nav = screen.getByRole('navigation');
|
|
68
|
+
expect(nav).toHaveClass('breadcrumbs', 'custom-breadcrumbs');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('handles navigation clicks with onNavigate', () => {
|
|
72
|
+
const onNavigate = jest.fn();
|
|
73
|
+
render(<Breadcrumbs items={sampleBreadcrumbs} onNavigate={onNavigate} />);
|
|
74
|
+
|
|
75
|
+
const homeLink = screen.getByText('Home');
|
|
76
|
+
fireEvent.click(homeLink);
|
|
77
|
+
|
|
78
|
+
expect(onNavigate).toHaveBeenCalledWith(
|
|
79
|
+
sampleBreadcrumbs[0],
|
|
80
|
+
0
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('handles keyboard navigation', () => {
|
|
85
|
+
const onNavigate = jest.fn();
|
|
86
|
+
render(<Breadcrumbs items={sampleBreadcrumbs} onNavigate={onNavigate} />);
|
|
87
|
+
|
|
88
|
+
const homeLink = screen.getByText('Home');
|
|
89
|
+
|
|
90
|
+
// Test Enter key
|
|
91
|
+
fireEvent.keyDown(homeLink, { key: 'Enter' });
|
|
92
|
+
expect(onNavigate).toHaveBeenCalledWith(sampleBreadcrumbs[0], 0);
|
|
93
|
+
|
|
94
|
+
// Test Space key
|
|
95
|
+
fireEvent.keyDown(homeLink, { key: ' ' });
|
|
96
|
+
expect(onNavigate).toHaveBeenCalledTimes(2);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('truncates items when maxItems is set', () => {
|
|
100
|
+
const manyItems: BreadcrumbItem[] = [
|
|
101
|
+
{ label: 'Home', href: '/' },
|
|
102
|
+
{ label: 'Level1', href: '/level1' },
|
|
103
|
+
{ label: 'Level2', href: '/level2' },
|
|
104
|
+
{ label: 'Level3', href: '/level3' },
|
|
105
|
+
{ label: 'Level4', href: '/level4' },
|
|
106
|
+
{ label: 'Current', href: '/current', current: true },
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
render(<Breadcrumbs items={manyItems} maxItems={4} />);
|
|
110
|
+
|
|
111
|
+
expect(screen.getByText('Home')).toBeInTheDocument();
|
|
112
|
+
expect(screen.getByText('...')).toBeInTheDocument();
|
|
113
|
+
expect(screen.getByText('Current')).toBeInTheDocument();
|
|
114
|
+
|
|
115
|
+
// Should not show middle items
|
|
116
|
+
expect(screen.queryByText('Level2')).not.toBeInTheDocument();
|
|
117
|
+
expect(screen.queryByText('Level3')).not.toBeInTheDocument();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('hides root item when showRoot is false', () => {
|
|
121
|
+
render(<Breadcrumbs items={sampleBreadcrumbs} showRoot={false} />);
|
|
122
|
+
|
|
123
|
+
expect(screen.queryByText('Home')).not.toBeInTheDocument();
|
|
124
|
+
expect(screen.getByText('Products')).toBeInTheDocument();
|
|
125
|
+
expect(screen.getByText('Electronics')).toBeInTheDocument();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('renders with icons when provided', () => {
|
|
129
|
+
const itemsWithIcons: BreadcrumbItem[] = [
|
|
130
|
+
{
|
|
131
|
+
label: 'Home',
|
|
132
|
+
href: '/',
|
|
133
|
+
icon: <span data-testid="home-icon">🏠</span>
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
label: 'Products',
|
|
137
|
+
href: '/products',
|
|
138
|
+
icon: <span data-testid="products-icon">📦</span>
|
|
139
|
+
},
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
render(<Breadcrumbs items={itemsWithIcons} />);
|
|
143
|
+
|
|
144
|
+
expect(screen.getByTestId('home-icon')).toBeInTheDocument();
|
|
145
|
+
expect(screen.getByTestId('products-icon')).toBeInTheDocument();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('does not render when only one item or less', () => {
|
|
149
|
+
const { container } = render(<Breadcrumbs items={[{ label: 'Home', href: '/' }]} />);
|
|
150
|
+
expect(container.firstChild).toBeNull();
|
|
151
|
+
|
|
152
|
+
const { container: emptyContainer } = render(<Breadcrumbs items={[]} />);
|
|
153
|
+
expect(emptyContainer.firstChild).toBeNull();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('renders non-clickable items correctly', () => {
|
|
157
|
+
const nonClickableItems: BreadcrumbItem[] = [
|
|
158
|
+
{ label: 'Home', href: '/' }, // Clickable: has href, not current
|
|
159
|
+
{ label: 'Current', current: true }, // Non-clickable: current is true
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
render(<Breadcrumbs items={nonClickableItems} />);
|
|
163
|
+
|
|
164
|
+
// Home should be clickable - find the parent element (the <a> tag)
|
|
165
|
+
const homeElement = screen.getByText('Home').parentElement;
|
|
166
|
+
expect(homeElement?.tagName).toBe('A');
|
|
167
|
+
|
|
168
|
+
// Current should not be clickable - find the parent element (should be <span>)
|
|
169
|
+
const currentElement = screen.getByText('Current').parentElement;
|
|
170
|
+
expect(currentElement?.tagName).toBe('SPAN');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('prevents navigation for ellipsis items', () => {
|
|
174
|
+
const onNavigate = jest.fn();
|
|
175
|
+
const manyItems: BreadcrumbItem[] = [
|
|
176
|
+
{ label: 'Home', href: '/' },
|
|
177
|
+
{ label: 'Level1', href: '/level1' },
|
|
178
|
+
{ label: 'Level2', href: '/level2' },
|
|
179
|
+
{ label: 'Level3', href: '/level3' },
|
|
180
|
+
{ label: 'Current', href: '/current', current: true },
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
render(<Breadcrumbs items={manyItems} maxItems={3} onNavigate={onNavigate} />);
|
|
184
|
+
|
|
185
|
+
const ellipsis = screen.getByText('...');
|
|
186
|
+
fireEvent.click(ellipsis);
|
|
187
|
+
|
|
188
|
+
expect(onNavigate).not.toHaveBeenCalled();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('handles default link behavior when no onNavigate is provided', () => {
|
|
192
|
+
// Create a mock for window.location.href
|
|
193
|
+
const originalLocation = window.location;
|
|
194
|
+
delete (window as any).location;
|
|
195
|
+
window.location = { ...originalLocation, href: '' } as any;
|
|
196
|
+
|
|
197
|
+
const itemsWithHref: BreadcrumbItem[] = [
|
|
198
|
+
{ label: 'Home', href: '/' },
|
|
199
|
+
{ label: 'Current', href: '/current' }, // Need at least 2 items for breadcrumbs to render
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
render(<Breadcrumbs items={itemsWithHref} />);
|
|
203
|
+
|
|
204
|
+
const homeLink = screen.getByText('Home');
|
|
205
|
+
fireEvent.keyDown(homeLink, { key: 'Enter' });
|
|
206
|
+
|
|
207
|
+
// Should attempt to navigate using href
|
|
208
|
+
expect(window.location.href).toBe('/');
|
|
209
|
+
|
|
210
|
+
// Restore original location
|
|
211
|
+
window.location = originalLocation;
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe('useBreadcrumbs hook', () => {
|
|
216
|
+
it('provides initial empty breadcrumbs', () => {
|
|
217
|
+
render(<BreadcrumbHookTest />);
|
|
218
|
+
|
|
219
|
+
expect(screen.getByTestId('breadcrumb-count')).toHaveTextContent('0');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('allows adding breadcrumbs', () => {
|
|
223
|
+
render(<BreadcrumbHookTest />);
|
|
224
|
+
|
|
225
|
+
const addButton = screen.getByText('Add Breadcrumb');
|
|
226
|
+
fireEvent.click(addButton);
|
|
227
|
+
|
|
228
|
+
expect(screen.getByTestId('breadcrumb-count')).toHaveTextContent('1');
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('allows removing breadcrumbs', () => {
|
|
232
|
+
render(<BreadcrumbHookTest />);
|
|
233
|
+
|
|
234
|
+
// Add a breadcrumb first
|
|
235
|
+
const addButton = screen.getByText('Add Breadcrumb');
|
|
236
|
+
fireEvent.click(addButton);
|
|
237
|
+
expect(screen.getByTestId('breadcrumb-count')).toHaveTextContent('1');
|
|
238
|
+
|
|
239
|
+
// Remove it
|
|
240
|
+
const removeButton = screen.getByText('Remove First');
|
|
241
|
+
fireEvent.click(removeButton);
|
|
242
|
+
expect(screen.getByTestId('breadcrumb-count')).toHaveTextContent('0');
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('allows setting breadcrumbs', () => {
|
|
246
|
+
render(<BreadcrumbHookTest />);
|
|
247
|
+
|
|
248
|
+
const setButton = screen.getByText('Set Breadcrumbs');
|
|
249
|
+
fireEvent.click(setButton);
|
|
250
|
+
|
|
251
|
+
expect(screen.getByTestId('breadcrumb-count')).toHaveTextContent('1');
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('allows clearing all breadcrumbs', () => {
|
|
255
|
+
render(<BreadcrumbHookTest />);
|
|
256
|
+
|
|
257
|
+
// Add multiple breadcrumbs
|
|
258
|
+
const addButton = screen.getByText('Add Breadcrumb');
|
|
259
|
+
fireEvent.click(addButton);
|
|
260
|
+
fireEvent.click(addButton);
|
|
261
|
+
expect(screen.getByTestId('breadcrumb-count')).toHaveTextContent('2');
|
|
262
|
+
|
|
263
|
+
// Clear all
|
|
264
|
+
const clearButton = screen.getByText('Clear All');
|
|
265
|
+
fireEvent.click(clearButton);
|
|
266
|
+
expect(screen.getByTestId('breadcrumb-count')).toHaveTextContent('0');
|
|
267
|
+
});
|
|
268
|
+
});
|