pmtool / src /components /asset-management /__tests__ /accessibility.test.tsx
devarshia5's picture
Upload 487 files
d97b8f9 verified
Raw
History Blame Contribute Delete
20.5 kB
/**
* 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();
}
});
});
});