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,30 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { NotificationsProvider } from '../notifications-provider';
const useSocketNotificationsMock = vi.fn();
vi.mock('@/lib/hooks/use-socket-notifications', () => ({
useSocketNotifications: () => useSocketNotificationsMock(),
}));
describe('NotificationsProvider', () => {
it('renders children', () => {
render(
<NotificationsProvider>
<div>child</div>
</NotificationsProvider>,
);
expect(screen.getByText('child')).toBeInTheDocument();
});
it('initializes socket notifications hook on mount', () => {
useSocketNotificationsMock.mockClear();
render(
<NotificationsProvider>
<span>x</span>
</NotificationsProvider>,
);
expect(useSocketNotificationsMock).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,57 @@
import { render, screen } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { QueryProvider } from '../query-provider';
vi.mock('next-intl', () => ({
useTranslations: () => (key: string) => key,
}));
vi.mock('@/lib/query-client', () => {
const { QueryClient } = require('@tanstack/react-query');
return { getQueryClient: () => new QueryClient() };
});
function Boom() {
throw new Error('query-fail');
}
describe('QueryProvider', () => {
let spy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
spy = vi.spyOn(console, 'error').mockImplementation(() => {});
});
afterEach(() => {
spy.mockRestore();
});
it('renders children under provider', () => {
render(
<QueryProvider>
<div>ok</div>
</QueryProvider>,
);
expect(screen.getByText('ok')).toBeInTheDocument();
});
it('catches thrown errors and renders fallback with retry button', () => {
render(
<QueryProvider>
<Boom />
</QueryProvider>,
);
// error.description & common.retry keys surface via mocked translator
expect(screen.getByRole('alert')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'common.retry' })).toBeInTheDocument();
});
it('surfaces the underlying error message in fallback', () => {
render(
<QueryProvider>
<Boom />
</QueryProvider>,
);
expect(screen.getByText('query-fail')).toBeInTheDocument();
});
});