test(web): increase frontend test coverage to ~70% page coverage

- Fix vitest config to include [locale] directory tests (was excluded)
- Fix register.spec.tsx: use getByRole('heading') to avoid duplicate text match
- Fix search.spec.tsx: add QueryClientProvider wrapper and mock saved searches hook
- Add 12 new page test files covering dashboard, admin, public, and OAuth pages:
  - dashboard (main, profile, payments, subscription, KYC)
  - admin (dashboard, users)
  - public (landing, pricing)
  - analytics
  - OAuth callbacks (Google, Zalo)
- 29 test files, 174 tests, 16/23 pages covered (69.6%)

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-10 23:14:16 +07:00
parent d62eb5f164
commit 68b65cb848
15 changed files with 1122 additions and 7 deletions

View File

@@ -0,0 +1,81 @@
/* eslint-disable import-x/order */
import { render, screen, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { useAuthStore } from '@/lib/auth-store';
const mockReplace = vi.fn();
let mockSearchParams = new URLSearchParams();
vi.mock('next/navigation', () => ({
useRouter: () => ({ replace: mockReplace, push: vi.fn() }),
useSearchParams: () => mockSearchParams,
}));
vi.mock('lucide-react', () => ({
Loader2: ({ className }: { className?: string }) => <div data-testid="loader" className={className} />,
}));
vi.mock('@/lib/auth-store', () => {
const store = {
handleOAuthCallback: vi.fn(),
};
return {
useAuthStore: vi.fn((selector) => {
if (typeof selector === 'function') return selector(store);
return store;
}),
};
});
import GoogleCallbackPage from '../google/page';
const mockedUseAuthStore = vi.mocked(useAuthStore);
describe('GoogleCallbackPage', () => {
let mockStore: { handleOAuthCallback: ReturnType<typeof vi.fn> };
beforeEach(() => {
vi.clearAllMocks();
mockStore = {
handleOAuthCallback: vi.fn().mockResolvedValue(undefined),
};
mockedUseAuthStore.mockImplementation((selector) => {
if (typeof selector === 'function') return (selector as (s: typeof mockStore) => unknown)(mockStore);
return mockStore as ReturnType<typeof useAuthStore>;
});
});
it('renders loading spinner and text', () => {
mockSearchParams = new URLSearchParams();
render(<GoogleCallbackPage />);
expect(screen.getByTestId('loader')).toBeInTheDocument();
expect(screen.getByText(/đang xử lý đăng nhập google/i)).toBeInTheDocument();
});
it('redirects to login on error param', async () => {
mockSearchParams = new URLSearchParams('error=access_denied');
render(<GoogleCallbackPage />);
await waitFor(() => {
expect(mockReplace).toHaveBeenCalledWith('/login?error=access_denied');
});
});
it('redirects to login when tokens are missing', async () => {
mockSearchParams = new URLSearchParams();
render(<GoogleCallbackPage />);
await waitFor(() => {
expect(mockReplace).toHaveBeenCalledWith('/login?error=oauth_failed');
});
});
it('calls handleOAuthCallback with tokens', async () => {
mockSearchParams = new URLSearchParams('accessToken=abc&refreshToken=def&expiresIn=3600');
render(<GoogleCallbackPage />);
await waitFor(() => {
expect(mockStore.handleOAuthCallback).toHaveBeenCalledWith('abc', 'def', 3600);
});
});
});

View File

@@ -0,0 +1,81 @@
/* eslint-disable import-x/order */
import { render, screen, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { useAuthStore } from '@/lib/auth-store';
const mockReplace = vi.fn();
let mockSearchParams = new URLSearchParams();
vi.mock('next/navigation', () => ({
useRouter: () => ({ replace: mockReplace, push: vi.fn() }),
useSearchParams: () => mockSearchParams,
}));
vi.mock('lucide-react', () => ({
Loader2: ({ className }: { className?: string }) => <div data-testid="loader" className={className} />,
}));
vi.mock('@/lib/auth-store', () => {
const store = {
handleOAuthCallback: vi.fn(),
};
return {
useAuthStore: vi.fn((selector) => {
if (typeof selector === 'function') return selector(store);
return store;
}),
};
});
import ZaloCallbackPage from '../zalo/page';
const mockedUseAuthStore = vi.mocked(useAuthStore);
describe('ZaloCallbackPage', () => {
let mockStore: { handleOAuthCallback: ReturnType<typeof vi.fn> };
beforeEach(() => {
vi.clearAllMocks();
mockStore = {
handleOAuthCallback: vi.fn().mockResolvedValue(undefined),
};
mockedUseAuthStore.mockImplementation((selector) => {
if (typeof selector === 'function') return (selector as (s: typeof mockStore) => unknown)(mockStore);
return mockStore as ReturnType<typeof useAuthStore>;
});
});
it('renders loading spinner and Zalo text', () => {
mockSearchParams = new URLSearchParams();
render(<ZaloCallbackPage />);
expect(screen.getByTestId('loader')).toBeInTheDocument();
expect(screen.getByText(/đang xử lý đăng nhập zalo/i)).toBeInTheDocument();
});
it('redirects to login on error param', async () => {
mockSearchParams = new URLSearchParams('error=access_denied');
render(<ZaloCallbackPage />);
await waitFor(() => {
expect(mockReplace).toHaveBeenCalledWith('/login?error=access_denied');
});
});
it('redirects to login when tokens are missing', async () => {
mockSearchParams = new URLSearchParams();
render(<ZaloCallbackPage />);
await waitFor(() => {
expect(mockReplace).toHaveBeenCalledWith('/login?error=oauth_failed');
});
});
it('calls handleOAuthCallback with tokens', async () => {
mockSearchParams = new URLSearchParams('accessToken=zalo123&refreshToken=zaloref&expiresIn=1800');
render(<ZaloCallbackPage />);
await waitFor(() => {
expect(mockStore.handleOAuthCallback).toHaveBeenCalledWith('zalo123', 'zaloref', 1800);
});
});
});