/** * Accessibility tests for asset management components */ import React from 'react'; import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { axe, toHaveNoViolations } from 'jest-axe'; import userEvent from '@testing-library/user-event'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { BrowserRouter } from 'react-router-dom'; import AssetList from '../AssetList'; import AssetForm from '../AssetForm'; import AssetDetail from '../AssetDetail'; import ConfirmDialog from '../shared/ConfirmDialog'; import Pagination from '../shared/Pagination'; import { Asset, AssetAssignment, SystemDetails } from '@/types/asset-management'; // Extend Jest matchers expect.extend(toHaveNoViolations); // Mock data const mockAssets: Asset[] = [ { assetID: 1, assetType: 'Laptop', brandModel: 'Dell Latitude 7420', serialNumber: 'DL123456', assetCondition: 'Excellent' }, { assetID: 2, assetType: 'Monitor', brandModel: 'Samsung 27" 4K', serialNumber: 'SM789012', assetCondition: 'Good' } ]; const mockAssignments: AssetAssignment[] = [ { assignmentID: 1, assetID: 1, employeeID: 101, assignedDate: '2024-01-15T00:00:00Z', returnedDate: null, remarks: 'Assigned for development work' } ]; const mockSystemDetails: SystemDetails = { systemDetailsID: 1, assetID: 1, systemModel: 'Latitude 7420', processor: 'Intel i7-1185G7', installedRAM: '16GB DDR4', graphics: 'Intel Iris Xe', screenResolution: '1920x1080', diskModel: 'Samsung SSD 980', diskSize: '512GB', manufacturer: 'Dell' }; // Test wrapper component const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } } }); return ( {children} ); }; describe('Asset Management Accessibility Tests', () => { describe('AssetList Component', () => { it('should not have accessibility violations', async () => { const { container } = render( ); const results = await axe(container); expect(results).toHaveNoViolations(); }); it('should have proper ARIA labels for interactive elements', () => { render( ); // Check search input has proper labeling const searchInput = screen.getByRole('searchbox'); expect(searchInput).toHaveAttribute('aria-label'); // Check filter selects have proper labeling const typeFilter = screen.getByLabelText(/filter by asset type/i); expect(typeFilter).toBeInTheDocument(); const conditionFilter = screen.getByLabelText(/filter by asset condition/i); expect(conditionFilter).toBeInTheDocument(); const assignmentFilter = screen.getByLabelText(/filter by assignment status/i); expect(assignmentFilter).toBeInTheDocument(); }); it('should support keyboard navigation for sortable headers', async () => { const user = userEvent.setup(); render( ); // Find sortable header (desktop view) const idHeader = screen.getByRole('button', { name: /sort by id/i }); expect(idHeader).toBeInTheDocument(); // Test keyboard activation idHeader.focus(); await user.keyboard('{Enter}'); // Should have proper ARIA attributes expect(idHeader).toHaveAttribute('aria-label'); expect(idHeader).toHaveAttribute('tabIndex', '0'); }); it('should announce filter results to screen readers', async () => { render( ); // Check results count has live region const resultsCount = screen.getByText(/showing \d+ of \d+ assets/i); expect(resultsCount).toHaveAttribute('aria-live', 'polite'); expect(resultsCount).toHaveAttribute('aria-atomic', 'true'); }); it('should provide proper action button labels', () => { const mockOnView = jest.fn(); const mockOnEdit = jest.fn(); const mockOnDelete = jest.fn(); render( ); // Check action buttons have descriptive labels const viewButtons = screen.getAllByLabelText(/view asset/i); const editButtons = screen.getAllByLabelText(/edit asset/i); const deleteButtons = screen.getAllByLabelText(/delete asset/i); expect(viewButtons.length).toBeGreaterThan(0); expect(editButtons.length).toBeGreaterThan(0); expect(deleteButtons.length).toBeGreaterThan(0); // Each button should include asset information in the label viewButtons.forEach(button => { expect(button.getAttribute('aria-label')).toMatch(/Dell Latitude 7420|Samsung 27" 4K/); }); }); }); describe('AssetForm Component', () => { it('should not have accessibility violations', async () => { const mockOnSubmit = jest.fn(); const mockOnCancel = jest.fn(); const { container } = render( ); const results = await axe(container); expect(results).toHaveNoViolations(); }); it('should have proper form labels and error handling', async () => { const mockOnSubmit = jest.fn().mockRejectedValue(new Error('Serial number already exists')); const mockOnCancel = jest.fn(); render( ); // Check all form fields have proper labels expect(screen.getByLabelText(/asset type/i)).toBeInTheDocument(); expect(screen.getByLabelText(/brand\/model/i)).toBeInTheDocument(); expect(screen.getByLabelText(/serial number/i)).toBeInTheDocument(); expect(screen.getByLabelText(/asset condition/i)).toBeInTheDocument(); }); it('should announce form errors to screen readers', async () => { const mockOnSubmit = jest.fn(); const mockOnCancel = jest.fn(); render( ); // Submit form without filling required fields const submitButton = screen.getByRole('button', { name: /create asset/i }); fireEvent.click(submitButton); await waitFor(() => { // Check for error messages with proper ARIA attributes const errorMessages = screen.getAllByRole('alert'); expect(errorMessages.length).toBeGreaterThan(0); }); }); it('should provide loading state feedback', () => { const mockOnSubmit = jest.fn(); const mockOnCancel = jest.fn(); render( ); const submitButton = screen.getByRole('button', { name: /creating/i }); expect(submitButton).toBeDisabled(); expect(submitButton).toHaveAttribute('aria-describedby'); }); }); describe('ConfirmDialog Component', () => { it('should not have accessibility violations', async () => { const mockOnClose = jest.fn(); const mockOnConfirm = jest.fn(); const { container } = render( ); const results = await axe(container); expect(results).toHaveNoViolations(); }); it('should have proper dialog ARIA attributes', () => { const mockOnClose = jest.fn(); const mockOnConfirm = jest.fn(); render( ); const dialog = screen.getByRole('dialog'); expect(dialog).toHaveAttribute('aria-modal', 'true'); expect(dialog).toHaveAttribute('aria-labelledby'); expect(dialog).toHaveAttribute('aria-describedby'); }); it('should trap focus within the dialog', async () => { const user = userEvent.setup(); const mockOnClose = jest.fn(); const mockOnConfirm = jest.fn(); render( ); // Focus should be on the cancel button initially const cancelButton = screen.getByRole('button', { name: /cancel/i }); expect(cancelButton).toHaveFocus(); // Tab should move to confirm button await user.tab(); const confirmButton = screen.getByRole('button', { name: /confirm/i }); expect(confirmButton).toHaveFocus(); // Tab again should cycle back to cancel await user.tab(); expect(cancelButton).toHaveFocus(); }); it('should close on Escape key', async () => { const user = userEvent.setup(); const mockOnClose = jest.fn(); const mockOnConfirm = jest.fn(); render( ); await user.keyboard('{Escape}'); expect(mockOnClose).toHaveBeenCalled(); }); }); describe('Pagination Component', () => { it('should not have accessibility violations', async () => { const mockOnPageChange = jest.fn(); const mockOnItemsPerPageChange = jest.fn(); const { container } = render( ); const results = await axe(container); expect(results).toHaveNoViolations(); }); it('should have proper navigation structure', () => { const mockOnPageChange = jest.fn(); const mockOnItemsPerPageChange = jest.fn(); render( ); // Should have navigation landmark const nav = screen.getByRole('navigation'); expect(nav).toHaveAttribute('aria-label', 'Pagination Navigation'); // Should have proper button labels expect(screen.getByLabelText(/go to first page/i)).toBeInTheDocument(); expect(screen.getByLabelText(/go to previous page/i)).toBeInTheDocument(); expect(screen.getByLabelText(/go to next page/i)).toBeInTheDocument(); expect(screen.getByLabelText(/go to last page/i)).toBeInTheDocument(); }); it('should indicate current page', () => { const mockOnPageChange = jest.fn(); const mockOnItemsPerPageChange = jest.fn(); render( ); const currentPageButton = screen.getByRole('button', { name: /go to page 2/i }); expect(currentPageButton).toHaveAttribute('aria-current', 'page'); }); it('should announce page changes', () => { const mockOnPageChange = jest.fn(); const mockOnItemsPerPageChange = jest.fn(); render( ); // Page info should have live region const pageInfo = screen.getByText(/showing 1 to 10 of 50 items/i); expect(pageInfo).toHaveAttribute('aria-live', 'polite'); expect(pageInfo).toHaveAttribute('aria-atomic', 'true'); }); }); describe('AssetDetail Component', () => { it('should not have accessibility violations', async () => { const { container } = render( ); const results = await axe(container); expect(results).toHaveNoViolations(); }); it('should have proper tab structure', () => { render( ); // Check tab list and panels const tabList = screen.getByRole('tablist'); expect(tabList).toBeInTheDocument(); const systemDetailsTab = screen.getByRole('tab', { name: /system details/i }); const assignmentsTab = screen.getByRole('tab', { name: /assignment history/i }); expect(systemDetailsTab).toHaveAttribute('aria-controls'); expect(assignmentsTab).toHaveAttribute('aria-controls'); }); it('should have proper field labeling', () => { render( ); // Check that data fields have proper labeling expect(screen.getByLabelText(/asset id/i)).toBeInTheDocument(); expect(screen.getByLabelText(/condition/i)).toBeInTheDocument(); expect(screen.getByLabelText(/serial number/i)).toBeInTheDocument(); expect(screen.getByLabelText(/assignment status/i)).toBeInTheDocument(); }); }); describe('Responsive Design Tests', () => { beforeEach(() => { // Mock window.matchMedia for responsive tests Object.defineProperty(window, 'matchMedia', { writable: true, value: jest.fn().mockImplementation(query => ({ matches: query.includes('max-width: 767px'), // Mock mobile media: query, onchange: null, addListener: jest.fn(), removeListener: jest.fn(), addEventListener: jest.fn(), removeEventListener: jest.fn(), dispatchEvent: jest.fn(), })), }); }); it('should show mobile card view on small screens', () => { // Mock mobile viewport Object.defineProperty(window, 'innerWidth', { writable: true, configurable: true, value: 600, }); render( ); // Mobile view should show cards instead of table // The table should be hidden on mobile (md:block class) const mobileCards = document.querySelectorAll('.md\\:hidden'); expect(mobileCards.length).toBeGreaterThan(0); }); it('should have proper touch target sizes on mobile', () => { render( ); // Check that buttons have minimum touch target size const buttons = screen.getAllByRole('button'); buttons.forEach(button => { const styles = window.getComputedStyle(button); // Note: In a real test environment, you'd check computed styles // Here we're just ensuring the buttons exist and are accessible expect(button).toBeInTheDocument(); }); }); }); describe('Keyboard Navigation Tests', () => { it('should support full keyboard navigation in AssetList', async () => { const user = userEvent.setup(); render( ); // Tab through interactive elements await user.tab(); // Search input expect(screen.getByRole('searchbox')).toHaveFocus(); await user.tab(); // Type filter await user.tab(); // Condition filter await user.tab(); // Assignment filter await user.tab(); // Create button (if present) // Should be able to navigate to table headers const sortableHeaders = screen.getAllByRole('button', { name: /sort by/i }); if (sortableHeaders.length > 0) { sortableHeaders[0].focus(); expect(sortableHeaders[0]).toHaveFocus(); } }); it('should support keyboard activation of interactive elements', async () => { const user = userEvent.setup(); const mockOnView = jest.fn(); render( ); // Find and activate view button with keyboard const viewButtons = screen.getAllByLabelText(/view asset/i); if (viewButtons.length > 0) { viewButtons[0].focus(); await user.keyboard('{Enter}'); expect(mockOnView).toHaveBeenCalled(); } }); }); });