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:
@@ -0,0 +1,64 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { TransferItemTable } from '../transfer-item-table';
|
||||
|
||||
const baseItem = {
|
||||
id: 'i1',
|
||||
name: 'Tủ lạnh Toshiba',
|
||||
brand: 'Toshiba',
|
||||
modelName: 'GR-RT624WE-PMV',
|
||||
category: 'APPLIANCE' as const,
|
||||
condition: 'GOOD' as const,
|
||||
purchaseYear: 2022,
|
||||
originalPriceVND: '15000000',
|
||||
askingPriceVND: '8000000',
|
||||
aiEstimatePriceVND: '7500000',
|
||||
aiConfidence: 0.85,
|
||||
quantity: 1,
|
||||
};
|
||||
|
||||
describe('TransferItemTable', () => {
|
||||
it('renders empty state when no items', () => {
|
||||
render(<TransferItemTable items={[]} />);
|
||||
expect(
|
||||
screen.getByText('Chưa có danh sách vật phẩm.'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders all column headers', () => {
|
||||
render(<TransferItemTable items={[baseItem]} />);
|
||||
expect(screen.getByText('Tên')).toBeInTheDocument();
|
||||
expect(screen.getByText('Loại')).toBeInTheDocument();
|
||||
expect(screen.getByText('Tình trạng')).toBeInTheDocument();
|
||||
expect(screen.getByText('Thương hiệu')).toBeInTheDocument();
|
||||
expect(screen.getByText('SL')).toBeInTheDocument();
|
||||
expect(screen.getByText('Giá yêu cầu')).toBeInTheDocument();
|
||||
expect(screen.getByText('Giá AI')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders item row with localized currency formatting', () => {
|
||||
render(<TransferItemTable items={[baseItem]} />);
|
||||
expect(screen.getByText('Tủ lạnh Toshiba')).toBeInTheDocument();
|
||||
expect(screen.getByText('GR-RT624WE-PMV')).toBeInTheDocument();
|
||||
expect(screen.getByText(/8\.000\.000/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/7\.500\.000/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('falls back to em-dash for missing brand and AI estimate', () => {
|
||||
render(
|
||||
<TransferItemTable
|
||||
items={[
|
||||
{
|
||||
...baseItem,
|
||||
id: 'i2',
|
||||
brand: null,
|
||||
aiEstimatePriceVND: null,
|
||||
aiConfidence: null,
|
||||
},
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
const dashes = screen.getAllByText('—');
|
||||
expect(dashes.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import * as React from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { TransferListingCard } from '../transfer-listing-card';
|
||||
|
||||
vi.mock('@/i18n/navigation', () => ({
|
||||
Link: ({
|
||||
children,
|
||||
href,
|
||||
...rest
|
||||
}: React.PropsWithChildren<{ href: string } & Record<string, unknown>>) => (
|
||||
<a href={href} {...rest}>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}));
|
||||
|
||||
const baseListing = {
|
||||
id: 'tl1',
|
||||
sellerId: 's1',
|
||||
category: 'FURNITURE' as const,
|
||||
status: 'ACTIVE' as const,
|
||||
title: 'Bộ sofa gỗ còn mới 90%',
|
||||
address: '123 Lê Lợi',
|
||||
district: 'Quận 1',
|
||||
city: 'TP.HCM',
|
||||
latitude: 10.77,
|
||||
longitude: 106.7,
|
||||
askingPriceVND: '4500000',
|
||||
aiEstimatePriceVND: null,
|
||||
pricingSource: 'MANUAL' as const,
|
||||
isNegotiable: true,
|
||||
areaM2: 12,
|
||||
itemCount: 5,
|
||||
viewCount: 42,
|
||||
publishedAt: '2026-04-10T00:00:00.000Z',
|
||||
};
|
||||
|
||||
describe('TransferListingCard', () => {
|
||||
it('renders title, location and formatted price', () => {
|
||||
render(<TransferListingCard listing={baseListing} />);
|
||||
expect(screen.getByText('Bộ sofa gỗ còn mới 90%')).toBeInTheDocument();
|
||||
expect(screen.getByText(/Quận 1, TP\.HCM/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/4\.500\.000/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders ACTIVE status with green color and "Thương lượng" when negotiable', () => {
|
||||
render(<TransferListingCard listing={baseListing} />);
|
||||
expect(screen.getByText('Đang đăng')).toBeInTheDocument();
|
||||
expect(screen.getByText('Thương lượng')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders item and view counts and square-meter area', () => {
|
||||
render(<TransferListingCard listing={baseListing} />);
|
||||
expect(screen.getByText('5')).toBeInTheDocument();
|
||||
expect(screen.getByText('42')).toBeInTheDocument();
|
||||
expect(screen.getByText(/12 m/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('links to listing detail by id', () => {
|
||||
const { container } = render(
|
||||
<TransferListingCard listing={baseListing} />,
|
||||
);
|
||||
expect(container.querySelector('a')?.getAttribute('href')).toBe(
|
||||
'/chuyen-nhuong/tl1',
|
||||
);
|
||||
});
|
||||
|
||||
it('omits publish date footer when publishedAt is null', () => {
|
||||
render(
|
||||
<TransferListingCard
|
||||
listing={{ ...baseListing, publishedAt: null }}
|
||||
/>,
|
||||
);
|
||||
expect(screen.queryByText(/Đăng/)).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user