test(web): add Vitest tests for search, auth, public, and admin layouts

- SearchLayout: verifies children pass-through (3 tests)
- AuthLayout: verifies role=main, #main-content, max-w-md centering (5 tests)
- PublicLayout: verifies navbar, ticker strip, footer, compare bar, #main-content (8 tests)
- AdminLayout: verifies sidebar nav, auth guard, loading state, logout, mobile toggle (10 tests)

All 156 web test files pass (1157 total web tests). Pre-existing API test
failures in unrelated modules (auth OTP handler, projects, search indexer,
admin settings encryption) are outside scope of this task.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-23 20:36:38 +07:00
parent 5a119df806
commit 2788b35108
4 changed files with 469 additions and 0 deletions

View File

@@ -0,0 +1,170 @@
/* eslint-disable import-x/order */
/**
* Kiểm thử PublicLayout: navbar, ticker strip, footer, main content.
*/
import { render, screen } from '@testing-library/react';
import * as React from 'react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
// Mock next-intl
const viMessages = await import('@/messages/vi.json');
vi.mock('next-intl', () => ({
useTranslations: (namespace?: string) => {
const messages = viMessages.default ?? viMessages;
const ns = namespace
? (messages[namespace as keyof typeof messages] as Record<string, unknown> | undefined)
: (messages as unknown as Record<string, unknown>);
return (key: string) => {
if (!ns) return key;
const parts = key.split('.');
let val: unknown = ns;
for (const p of parts) {
val = (val as Record<string, unknown>)?.[p];
}
return typeof val === 'string' ? val : key;
};
},
useLocale: () => 'vi',
NextIntlClientProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
}));
vi.mock('next/navigation', () => ({
useRouter: () => ({ push: vi.fn(), replace: vi.fn() }),
usePathname: () => '/',
}));
vi.mock('@/i18n/navigation', () => ({
Link: ({ children, href, ...props }: { children: React.ReactNode; href: string; [key: string]: unknown }) => (
<a href={href as string} {...props}>{children}</a>
),
useRouter: () => ({ push: vi.fn(), replace: vi.fn() }),
usePathname: () => '/',
}));
vi.mock('@/lib/auth-store', () => ({
useAuthStore: () => ({
user: null,
logout: vi.fn(),
}),
}));
vi.mock('@/components/providers/theme-provider', () => ({
useTheme: () => ({ theme: 'light', toggleTheme: vi.fn() }),
}));
vi.mock('@/components/notifications/notification-bell', () => ({
NotificationBell: () => <button aria-label="Thông báo">🔔</button>,
}));
vi.mock('@/components/comparison/compare-floating-bar', () => ({
CompareFloatingBar: () => <div data-testid="compare-bar" />,
}));
vi.mock('@/components/design-system/ticker-strip', () => ({
TickerStrip: ({ items }: { items: unknown[] }) => (
<div data-testid="ticker-strip" aria-label="ticker">{items.length} items</div>
),
}));
vi.mock('@/components/design-system/navbar', () => ({
Navbar: ({ brand, links }: { brand: string; links: { label: string }[] }) => (
<nav data-testid="navbar" aria-label="main-nav">
<span>{brand}</span>
{links.map((l) => <span key={l.label}>{l.label}</span>)}
</nav>
),
}));
vi.mock('@/components/design-system/footer', () => ({
Footer: ({ brand, copyright }: { brand: string; copyright: string }) => (
<footer data-testid="footer">
<span>{brand}</span>
<span>{copyright}</span>
</footer>
),
}));
vi.mock('@/components/ui/language-switcher', () => ({
LanguageSwitcher: () => <div data-testid="lang-switcher" />,
}));
import PublicLayout from '../layout';
describe('PublicLayout', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('renders main content wrapper with role="main"', () => {
render(
<PublicLayout>
<div data-testid="page">Trang chính</div>
</PublicLayout>,
);
expect(screen.getByRole('main')).toBeInTheDocument();
expect(screen.getByTestId('page')).toBeInTheDocument();
});
it('renders children inside main', () => {
render(
<PublicLayout>
<h1>Tiêu đ trang</h1>
</PublicLayout>,
);
expect(screen.getByText('Tiêu đề trang')).toBeInTheDocument();
});
it('renders the navbar', () => {
render(
<PublicLayout>
<div>Content</div>
</PublicLayout>,
);
expect(screen.getByTestId('navbar')).toBeInTheDocument();
});
it('renders the ticker strip', () => {
render(
<PublicLayout>
<div>Content</div>
</PublicLayout>,
);
expect(screen.getByTestId('ticker-strip')).toBeInTheDocument();
});
it('renders the footer', () => {
render(
<PublicLayout>
<div>Content</div>
</PublicLayout>,
);
expect(screen.getByTestId('footer')).toBeInTheDocument();
});
it('renders the compare floating bar', () => {
render(
<PublicLayout>
<div>Content</div>
</PublicLayout>,
);
expect(screen.getByTestId('compare-bar')).toBeInTheDocument();
});
it('ticker strip has 8 district items', () => {
render(
<PublicLayout>
<div>Content</div>
</PublicLayout>,
);
expect(screen.getByText('8 items')).toBeInTheDocument();
});
it('main content has id="main-content" for skip-nav', () => {
const { container } = render(
<PublicLayout>
<div>Content</div>
</PublicLayout>,
);
expect(container.querySelector('#main-content')).toBeInTheDocument();
});
});