fix(a11y): resolve serious accessibility issues on search page (GOO-110)

- Add aria-hidden="true" to all decorative inline SVGs (bookmark, view-mode, funnel, checkmark)
- Convert save-search popover to proper dialog: role="dialog", aria-modal, focus trap, Escape key, focus return to trigger
- Add aria-pressed on list/map/split view-mode toggle buttons
- Add aria-expanded + aria-controls on mobile filter toggle button
- Add role="status" + aria-label="Đang tải..." on Suspense fallback

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-24 10:26:50 +07:00
parent 1d26393f16
commit f5118244b7
34 changed files with 2321 additions and 9 deletions

View File

@@ -0,0 +1,77 @@
import { fireEvent, render, screen } from '@testing-library/react';
import * as React from 'react';
import { describe, expect, it, vi } from 'vitest';
import { ReportCard } from '../report-card';
vi.mock('@/i18n/navigation', () => ({
Link: ({
children,
href,
...rest
}: React.PropsWithChildren<{ href: string } & Record<string, unknown>>) => (
<a href={href} {...rest}>
{children}
</a>
),
}));
const baseReport = {
id: 'r1',
type: 'RESIDENTIAL_MARKET' as const,
title: 'Báo cáo thị trường Q1',
params: {},
content: null,
pdfUrl: null,
status: 'READY' as const,
errorMsg: null,
createdAt: '2026-04-01T08:30:00.000Z',
updatedAt: '2026-04-01T08:30:00.000Z',
};
describe('ReportCard', () => {
it('renders title and type/status badges', () => {
render(<ReportCard report={baseReport} />);
expect(screen.getByText('Báo cáo thị trường Q1')).toBeInTheDocument();
expect(screen.getByText('Nhà ở')).toBeInTheDocument();
expect(screen.getByText('Hoàn thành')).toBeInTheDocument();
});
it('links to report detail for READY report (both detail icon link and bottom "Xem báo cáo" link)', () => {
const { container } = render(<ReportCard report={baseReport} />);
const links = container.querySelectorAll('a[href="/dashboard/reports/r1"]');
expect(links.length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('Xem báo cáo')).toBeInTheDocument();
});
it('does not render "Xem báo cáo" link for non-READY reports', () => {
render(
<ReportCard report={{ ...baseReport, status: 'GENERATING' }} />,
);
expect(screen.queryByText('Xem báo cáo')).toBeNull();
});
it('renders error message for FAILED report with errorMsg', () => {
render(
<ReportCard
report={{
...baseReport,
status: 'FAILED',
errorMsg: 'Thiếu dữ liệu',
}}
/>,
);
expect(screen.getByText('Thiếu dữ liệu')).toBeInTheDocument();
expect(screen.getByText('Lỗi')).toBeInTheDocument();
});
it('invokes onDelete with report id when delete button clicked', () => {
const onDelete = vi.fn();
render(<ReportCard report={baseReport} onDelete={onDelete} />);
const trashButton = screen
.getAllByRole('button')
.find((b) => b.className.includes('text-destructive'));
expect(trashButton).toBeDefined();
fireEvent.click(trashButton!);
expect(onDelete).toHaveBeenCalledWith('r1');
});
});