Spaces:
Sleeping
Sleeping
| /** | |
| * 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 ( | |
| <QueryClientProvider client={queryClient}> | |
| <BrowserRouter> | |
| {children} | |
| </BrowserRouter> | |
| </QueryClientProvider> | |
| ); | |
| }; | |
| describe('Asset Management Accessibility Tests', () => { | |
| describe('AssetList Component', () => { | |
| it('should not have accessibility violations', async () => { | |
| const { container } = render( | |
| <TestWrapper> | |
| <AssetList | |
| assets={mockAssets} | |
| assignments={mockAssignments} | |
| isLoading={false} | |
| showActions={true} | |
| /> | |
| </TestWrapper> | |
| ); | |
| const results = await axe(container); | |
| expect(results).toHaveNoViolations(); | |
| }); | |
| it('should have proper ARIA labels for interactive elements', () => { | |
| render( | |
| <TestWrapper> | |
| <AssetList | |
| assets={mockAssets} | |
| assignments={mockAssignments} | |
| isLoading={false} | |
| showActions={true} | |
| /> | |
| </TestWrapper> | |
| ); | |
| // 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( | |
| <TestWrapper> | |
| <AssetList | |
| assets={mockAssets} | |
| assignments={mockAssignments} | |
| isLoading={false} | |
| showActions={true} | |
| /> | |
| </TestWrapper> | |
| ); | |
| // 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( | |
| <TestWrapper> | |
| <AssetList | |
| assets={mockAssets} | |
| assignments={mockAssignments} | |
| isLoading={false} | |
| showActions={true} | |
| /> | |
| </TestWrapper> | |
| ); | |
| // 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( | |
| <TestWrapper> | |
| <AssetList | |
| assets={mockAssets} | |
| assignments={mockAssignments} | |
| isLoading={false} | |
| onView={mockOnView} | |
| onEdit={mockOnEdit} | |
| onDelete={mockOnDelete} | |
| showActions={true} | |
| /> | |
| </TestWrapper> | |
| ); | |
| // 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( | |
| <TestWrapper> | |
| <AssetForm | |
| onSubmit={mockOnSubmit} | |
| onCancel={mockOnCancel} | |
| isSubmitting={false} | |
| /> | |
| </TestWrapper> | |
| ); | |
| 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( | |
| <TestWrapper> | |
| <AssetForm | |
| onSubmit={mockOnSubmit} | |
| onCancel={mockOnCancel} | |
| isSubmitting={false} | |
| /> | |
| </TestWrapper> | |
| ); | |
| // 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( | |
| <TestWrapper> | |
| <AssetForm | |
| onSubmit={mockOnSubmit} | |
| onCancel={mockOnCancel} | |
| isSubmitting={false} | |
| /> | |
| </TestWrapper> | |
| ); | |
| // 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( | |
| <TestWrapper> | |
| <AssetForm | |
| onSubmit={mockOnSubmit} | |
| onCancel={mockOnCancel} | |
| isSubmitting={true} | |
| /> | |
| </TestWrapper> | |
| ); | |
| 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( | |
| <ConfirmDialog | |
| isOpen={true} | |
| onClose={mockOnClose} | |
| onConfirm={mockOnConfirm} | |
| title="Delete Asset" | |
| description="Are you sure you want to delete this asset?" | |
| /> | |
| ); | |
| const results = await axe(container); | |
| expect(results).toHaveNoViolations(); | |
| }); | |
| it('should have proper dialog ARIA attributes', () => { | |
| const mockOnClose = jest.fn(); | |
| const mockOnConfirm = jest.fn(); | |
| render( | |
| <ConfirmDialog | |
| isOpen={true} | |
| onClose={mockOnClose} | |
| onConfirm={mockOnConfirm} | |
| title="Delete Asset" | |
| description="Are you sure you want to delete this asset?" | |
| /> | |
| ); | |
| 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( | |
| <ConfirmDialog | |
| isOpen={true} | |
| onClose={mockOnClose} | |
| onConfirm={mockOnConfirm} | |
| title="Delete Asset" | |
| description="Are you sure you want to delete this asset?" | |
| /> | |
| ); | |
| // 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( | |
| <ConfirmDialog | |
| isOpen={true} | |
| onClose={mockOnClose} | |
| onConfirm={mockOnConfirm} | |
| title="Delete Asset" | |
| description="Are you sure you want to delete this asset?" | |
| /> | |
| ); | |
| 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( | |
| <Pagination | |
| currentPage={1} | |
| totalPages={5} | |
| totalItems={50} | |
| itemsPerPage={10} | |
| onPageChange={mockOnPageChange} | |
| onItemsPerPageChange={mockOnItemsPerPageChange} | |
| /> | |
| ); | |
| const results = await axe(container); | |
| expect(results).toHaveNoViolations(); | |
| }); | |
| it('should have proper navigation structure', () => { | |
| const mockOnPageChange = jest.fn(); | |
| const mockOnItemsPerPageChange = jest.fn(); | |
| render( | |
| <Pagination | |
| currentPage={2} | |
| totalPages={5} | |
| totalItems={50} | |
| itemsPerPage={10} | |
| onPageChange={mockOnPageChange} | |
| onItemsPerPageChange={mockOnItemsPerPageChange} | |
| /> | |
| ); | |
| // 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( | |
| <Pagination | |
| currentPage={2} | |
| totalPages={5} | |
| totalItems={50} | |
| itemsPerPage={10} | |
| onPageChange={mockOnPageChange} | |
| onItemsPerPageChange={mockOnItemsPerPageChange} | |
| /> | |
| ); | |
| 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( | |
| <Pagination | |
| currentPage={1} | |
| totalPages={5} | |
| totalItems={50} | |
| itemsPerPage={10} | |
| onPageChange={mockOnPageChange} | |
| onItemsPerPageChange={mockOnItemsPerPageChange} | |
| /> | |
| ); | |
| // 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( | |
| <TestWrapper> | |
| <AssetDetail | |
| asset={{ ...mockAssets[0], systemDetails: mockSystemDetails, assignments: mockAssignments }} | |
| systemDetails={mockSystemDetails} | |
| assignments={mockAssignments} | |
| isLoading={false} | |
| /> | |
| </TestWrapper> | |
| ); | |
| const results = await axe(container); | |
| expect(results).toHaveNoViolations(); | |
| }); | |
| it('should have proper tab structure', () => { | |
| render( | |
| <TestWrapper> | |
| <AssetDetail | |
| asset={{ ...mockAssets[0], systemDetails: mockSystemDetails, assignments: mockAssignments }} | |
| systemDetails={mockSystemDetails} | |
| assignments={mockAssignments} | |
| isLoading={false} | |
| /> | |
| </TestWrapper> | |
| ); | |
| // 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( | |
| <TestWrapper> | |
| <AssetDetail | |
| asset={{ ...mockAssets[0], systemDetails: mockSystemDetails, assignments: mockAssignments }} | |
| systemDetails={mockSystemDetails} | |
| assignments={mockAssignments} | |
| isLoading={false} | |
| /> | |
| </TestWrapper> | |
| ); | |
| // 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( | |
| <TestWrapper> | |
| <AssetList | |
| assets={mockAssets} | |
| assignments={mockAssignments} | |
| isLoading={false} | |
| showActions={true} | |
| /> | |
| </TestWrapper> | |
| ); | |
| // 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( | |
| <TestWrapper> | |
| <AssetList | |
| assets={mockAssets} | |
| assignments={mockAssignments} | |
| isLoading={false} | |
| showActions={true} | |
| /> | |
| </TestWrapper> | |
| ); | |
| // 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( | |
| <TestWrapper> | |
| <AssetList | |
| assets={mockAssets} | |
| assignments={mockAssignments} | |
| isLoading={false} | |
| showActions={true} | |
| /> | |
| </TestWrapper> | |
| ); | |
| // 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( | |
| <TestWrapper> | |
| <AssetList | |
| assets={mockAssets} | |
| assignments={mockAssignments} | |
| isLoading={false} | |
| onView={mockOnView} | |
| showActions={true} | |
| /> | |
| </TestWrapper> | |
| ); | |
| // 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(); | |
| } | |
| }); | |
| }); | |
| }); |