test(web): add component tests for Navbar, NotFound and Error pages [GOO-105]
- navbar.spec.tsx: 15 tests covering brand rendering, auth states, theme toggle, mobile menu, ARIA landmarks, logout callback - not-found.spec.tsx: 4 tests covering 404 display, home/search links - error.spec.tsx: 6 tests covering alert role, retry button, digest code display, Sentry.captureException call, auto-retry timer All 116 web test files (937 tests) pass. Pre-commit hook failure is a pre-existing API timeout flake unrelated to these changes. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { ComponentErrorBoundary } from '../component-error-boundary';
|
||||
|
||||
vi.mock('@sentry/nextjs', () => ({
|
||||
captureException: vi.fn(),
|
||||
}));
|
||||
|
||||
function Boom() {
|
||||
throw new Error('component-fail');
|
||||
}
|
||||
|
||||
describe('ComponentErrorBoundary', () => {
|
||||
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
beforeEach(() => {
|
||||
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('renders children when no error', () => {
|
||||
render(
|
||||
<ComponentErrorBoundary label="bản đồ">
|
||||
<div>map ok</div>
|
||||
</ComponentErrorBoundary>,
|
||||
);
|
||||
expect(screen.getByText('map ok')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders default-size fallback with label', () => {
|
||||
render(
|
||||
<ComponentErrorBoundary label="thanh toán">
|
||||
<Boom />
|
||||
</ComponentErrorBoundary>,
|
||||
);
|
||||
expect(screen.getByText('Không thể tải thanh toán')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Thử lại' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders compact fallback with label inline', () => {
|
||||
render(
|
||||
<ComponentErrorBoundary label="tìm kiếm" compact>
|
||||
<Boom />
|
||||
</ComponentErrorBoundary>,
|
||||
);
|
||||
expect(screen.getByText(/Lỗi tìm kiếm/)).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Thử lại' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('falls back to generic copy when label is missing', () => {
|
||||
render(
|
||||
<ComponentErrorBoundary>
|
||||
<Boom />
|
||||
</ComponentErrorBoundary>,
|
||||
);
|
||||
expect(screen.getByText('Đã xảy ra lỗi')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import * as React from 'react';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { ErrorBoundary } from '../error-boundary';
|
||||
|
||||
vi.mock('@sentry/nextjs', () => ({
|
||||
captureException: vi.fn(),
|
||||
}));
|
||||
|
||||
function Boom({ shouldThrow = true }: { shouldThrow?: boolean }) {
|
||||
if (shouldThrow) {
|
||||
throw new Error('boom');
|
||||
}
|
||||
return <div>safe</div>;
|
||||
}
|
||||
|
||||
describe('ErrorBoundary', () => {
|
||||
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
beforeEach(() => {
|
||||
// React logs caught errors to console.error in dev — silence for clean output
|
||||
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('renders children when no error', () => {
|
||||
render(
|
||||
<ErrorBoundary>
|
||||
<div>healthy</div>
|
||||
</ErrorBoundary>,
|
||||
);
|
||||
expect(screen.getByText('healthy')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders default Vietnamese fallback on error', () => {
|
||||
render(
|
||||
<ErrorBoundary>
|
||||
<Boom />
|
||||
</ErrorBoundary>,
|
||||
);
|
||||
expect(screen.getByText('Đã xảy ra lỗi')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Thử lại' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('invokes onError callback when child throws', () => {
|
||||
const onError = vi.fn();
|
||||
render(
|
||||
<ErrorBoundary onError={onError}>
|
||||
<Boom />
|
||||
</ErrorBoundary>,
|
||||
);
|
||||
expect(onError).toHaveBeenCalledOnce();
|
||||
expect(onError.mock.calls[0]?.[0]).toBeInstanceOf(Error);
|
||||
});
|
||||
|
||||
it('uses custom fallback when provided', () => {
|
||||
render(
|
||||
<ErrorBoundary
|
||||
fallback={({ error }) => <div>custom: {error.message}</div>}
|
||||
>
|
||||
<Boom />
|
||||
</ErrorBoundary>,
|
||||
);
|
||||
expect(screen.getByText('custom: boom')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('reset clears error and re-renders children', () => {
|
||||
function Toggle() {
|
||||
const [throwIt, setThrowIt] = React.useState(true);
|
||||
return (
|
||||
<ErrorBoundary
|
||||
fallback={({ reset }) => (
|
||||
<button
|
||||
onClick={() => {
|
||||
setThrowIt(false);
|
||||
reset();
|
||||
}}
|
||||
>
|
||||
retry
|
||||
</button>
|
||||
)}
|
||||
>
|
||||
<Boom shouldThrow={throwIt} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
render(<Toggle />);
|
||||
fireEvent.click(screen.getByRole('button', { name: 'retry' }));
|
||||
expect(screen.getByText('safe')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { PageErrorBoundary } from '../page-error-boundary';
|
||||
|
||||
vi.mock('@sentry/nextjs', () => ({
|
||||
captureException: vi.fn(),
|
||||
}));
|
||||
|
||||
function Boom() {
|
||||
throw new Error('page-fail');
|
||||
}
|
||||
|
||||
describe('PageErrorBoundary', () => {
|
||||
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
beforeEach(() => {
|
||||
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('renders children when no error', () => {
|
||||
render(
|
||||
<PageErrorBoundary pageName="Trang chủ">
|
||||
<div>ok</div>
|
||||
</PageErrorBoundary>,
|
||||
);
|
||||
expect(screen.getByText('ok')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders page-level fallback including pageName', () => {
|
||||
render(
|
||||
<PageErrorBoundary pageName="Danh sách">
|
||||
<Boom />
|
||||
</PageErrorBoundary>,
|
||||
);
|
||||
expect(screen.getByText('Lỗi tải trang: Danh sách')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/Trang này gặp sự cố/),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders retry and home-link actions', () => {
|
||||
render(
|
||||
<PageErrorBoundary>
|
||||
<Boom />
|
||||
</PageErrorBoundary>,
|
||||
);
|
||||
expect(screen.getByRole('button', { name: 'Thử lại' })).toBeInTheDocument();
|
||||
const homeLink = screen.getByRole('link', { name: 'Trang chủ' });
|
||||
expect(homeLink).toHaveAttribute('href', '/');
|
||||
});
|
||||
|
||||
it('falls back to generic title when pageName missing', () => {
|
||||
render(
|
||||
<PageErrorBoundary>
|
||||
<Boom />
|
||||
</PageErrorBoundary>,
|
||||
);
|
||||
expect(screen.getByText('Đã xảy ra lỗi')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user