- Implement Dashboard Page with summary cards (total beneficiaries, equipment count, active subscriptions) - Add BeneficiaryCard component with status badges for equipment, subscription, and role - Display beneficiary list with avatar, name, email, and address - Handle empty state with call-to-action for adding first beneficiary - Add loading and error states with retry functionality - Include comprehensive test suite (33 tests covering all scenarios) - Fix TypeScript types for jest-dom matchers in tsconfig 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
477 lines
14 KiB
TypeScript
477 lines
14 KiB
TypeScript
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
|
|
import { useRouter } from 'next/navigation';
|
|
import DashboardPage from '../page';
|
|
import api from '@/lib/api';
|
|
import { useAuthStore } from '@/stores/authStore';
|
|
import type { Beneficiary } from '@/types';
|
|
|
|
// Mock dependencies
|
|
jest.mock('next/navigation', () => ({
|
|
useRouter: jest.fn(),
|
|
}));
|
|
|
|
jest.mock('@/lib/api');
|
|
jest.mock('@/stores/authStore');
|
|
|
|
describe('DashboardPage', () => {
|
|
const mockRouter = {
|
|
push: jest.fn(),
|
|
};
|
|
|
|
const mockBeneficiaries: Beneficiary[] = [
|
|
{
|
|
id: 1,
|
|
name: 'Maria Garcia',
|
|
displayName: 'Maria Garcia',
|
|
email: 'maria@example.com',
|
|
status: 'offline',
|
|
hasDevices: true,
|
|
equipmentStatus: 'active',
|
|
subscription: {
|
|
status: 'active',
|
|
planType: 'monthly',
|
|
endDate: '2025-12-31',
|
|
cancelAtPeriodEnd: false,
|
|
},
|
|
role: 'custodian',
|
|
address: '123 Main St, City',
|
|
avatar: 'https://example.com/avatar.jpg',
|
|
},
|
|
{
|
|
id: 2,
|
|
name: 'John Smith',
|
|
displayName: 'John Smith',
|
|
email: 'john@example.com',
|
|
status: 'offline',
|
|
hasDevices: false,
|
|
equipmentStatus: 'none',
|
|
role: 'caretaker',
|
|
},
|
|
];
|
|
|
|
beforeEach(() => {
|
|
jest.clearAllMocks();
|
|
(useRouter as jest.Mock).mockReturnValue(mockRouter);
|
|
(useAuthStore as unknown as jest.Mock).mockReturnValue({
|
|
isAuthenticated: true,
|
|
user: { firstName: 'Test', user_id: 1 },
|
|
});
|
|
});
|
|
|
|
describe('Loading State', () => {
|
|
it('should show loading spinner while fetching data', () => {
|
|
(api.getAllBeneficiaries as jest.Mock).mockImplementation(
|
|
() => new Promise(() => {}) // Never resolves
|
|
);
|
|
|
|
render(<DashboardPage />);
|
|
|
|
expect(screen.getByRole('status')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
describe('Authentication', () => {
|
|
it('should redirect to login if not authenticated', () => {
|
|
(useAuthStore as unknown as jest.Mock).mockReturnValue({
|
|
isAuthenticated: false,
|
|
user: null,
|
|
});
|
|
|
|
render(<DashboardPage />);
|
|
|
|
expect(mockRouter.push).toHaveBeenCalledWith('/login');
|
|
});
|
|
|
|
it('should not redirect if authenticated', async () => {
|
|
(api.getAllBeneficiaries as jest.Mock).mockResolvedValue({
|
|
ok: true,
|
|
data: [],
|
|
});
|
|
|
|
render(<DashboardPage />);
|
|
|
|
await waitFor(() => {
|
|
expect(mockRouter.push).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Data Fetching', () => {
|
|
it('should fetch beneficiaries on mount', async () => {
|
|
(api.getAllBeneficiaries as jest.Mock).mockResolvedValue({
|
|
ok: true,
|
|
data: mockBeneficiaries,
|
|
});
|
|
|
|
render(<DashboardPage />);
|
|
|
|
await waitFor(() => {
|
|
expect(api.getAllBeneficiaries).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
it('should display beneficiaries after successful fetch', async () => {
|
|
(api.getAllBeneficiaries as jest.Mock).mockResolvedValue({
|
|
ok: true,
|
|
data: mockBeneficiaries,
|
|
});
|
|
|
|
render(<DashboardPage />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Maria Garcia')).toBeInTheDocument();
|
|
expect(screen.getByText('John Smith')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('should display error message on fetch failure', async () => {
|
|
const errorMessage = 'Failed to load beneficiaries';
|
|
(api.getAllBeneficiaries as jest.Mock).mockResolvedValue({
|
|
ok: false,
|
|
error: { message: errorMessage },
|
|
});
|
|
|
|
render(<DashboardPage />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(errorMessage)).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('should handle network errors gracefully', async () => {
|
|
(api.getAllBeneficiaries as jest.Mock).mockRejectedValue(
|
|
new Error('Network error')
|
|
);
|
|
|
|
render(<DashboardPage />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Network error')).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Header and Welcome Message', () => {
|
|
it('should display welcome message with user first name', async () => {
|
|
(api.getAllBeneficiaries as jest.Mock).mockResolvedValue({
|
|
ok: true,
|
|
data: [],
|
|
});
|
|
|
|
render(<DashboardPage />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/Welcome, Test/i)).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('should display welcome message without name if not available', async () => {
|
|
(useAuthStore as unknown as jest.Mock).mockReturnValue({
|
|
isAuthenticated: true,
|
|
user: { user_id: 1 },
|
|
});
|
|
|
|
(api.getAllBeneficiaries as jest.Mock).mockResolvedValue({
|
|
ok: true,
|
|
data: [],
|
|
});
|
|
|
|
render(<DashboardPage />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/Welcome$/i)).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Summary Cards', () => {
|
|
it('should display correct total beneficiaries count', async () => {
|
|
(api.getAllBeneficiaries as jest.Mock).mockResolvedValue({
|
|
ok: true,
|
|
data: mockBeneficiaries,
|
|
});
|
|
|
|
render(<DashboardPage />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Total Beneficiaries')).toBeInTheDocument();
|
|
const totalCard = screen.getByText('Total Beneficiaries').closest('div');
|
|
expect(totalCard).toHaveTextContent('2');
|
|
});
|
|
});
|
|
|
|
it('should display correct equipment count', async () => {
|
|
(api.getAllBeneficiaries as jest.Mock).mockResolvedValue({
|
|
ok: true,
|
|
data: mockBeneficiaries,
|
|
});
|
|
|
|
render(<DashboardPage />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('With Equipment')).toBeInTheDocument();
|
|
const equipmentCard = screen.getByText('With Equipment').closest('div');
|
|
expect(equipmentCard).toHaveTextContent('1'); // Only Maria has equipment
|
|
});
|
|
});
|
|
|
|
it('should display correct active subscriptions count', async () => {
|
|
(api.getAllBeneficiaries as jest.Mock).mockResolvedValue({
|
|
ok: true,
|
|
data: mockBeneficiaries,
|
|
});
|
|
|
|
render(<DashboardPage />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Active Subscriptions')).toBeInTheDocument();
|
|
const subscriptionCard = screen.getByText('Active Subscriptions').closest('div');
|
|
expect(subscriptionCard).toHaveTextContent('1'); // Only Maria has active subscription
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Empty State', () => {
|
|
it('should display empty state when no beneficiaries', async () => {
|
|
(api.getAllBeneficiaries as jest.Mock).mockResolvedValue({
|
|
ok: true,
|
|
data: [],
|
|
});
|
|
|
|
render(<DashboardPage />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('No beneficiaries yet')).toBeInTheDocument();
|
|
expect(
|
|
screen.getByText(/Add your first loved one to start monitoring/)
|
|
).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('should show add beneficiary button in empty state', async () => {
|
|
(api.getAllBeneficiaries as jest.Mock).mockResolvedValue({
|
|
ok: true,
|
|
data: [],
|
|
});
|
|
|
|
render(<DashboardPage />);
|
|
|
|
await waitFor(() => {
|
|
const addButton = screen.getByText('Add Your First Beneficiary');
|
|
expect(addButton).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Beneficiary Cards', () => {
|
|
it('should display beneficiary avatar if available', async () => {
|
|
(api.getAllBeneficiaries as jest.Mock).mockResolvedValue({
|
|
ok: true,
|
|
data: mockBeneficiaries,
|
|
});
|
|
|
|
render(<DashboardPage />);
|
|
|
|
await waitFor(() => {
|
|
const avatar = screen.getByRole('img', { name: 'Maria Garcia' });
|
|
expect(avatar).toBeInTheDocument();
|
|
expect(avatar).toHaveAttribute('src', 'https://example.com/avatar.jpg');
|
|
});
|
|
});
|
|
|
|
it('should display default avatar icon if no image', async () => {
|
|
(api.getAllBeneficiaries as jest.Mock).mockResolvedValue({
|
|
ok: true,
|
|
data: [mockBeneficiaries[1]], // John has no avatar
|
|
});
|
|
|
|
render(<DashboardPage />);
|
|
|
|
await waitFor(() => {
|
|
const card = screen.getByText('John Smith').closest('button');
|
|
expect(card).toHaveTextContent('👤');
|
|
});
|
|
});
|
|
|
|
it('should display equipment status badges correctly', async () => {
|
|
(api.getAllBeneficiaries as jest.Mock).mockResolvedValue({
|
|
ok: true,
|
|
data: mockBeneficiaries,
|
|
});
|
|
|
|
render(<DashboardPage />);
|
|
|
|
await waitFor(() => {
|
|
const activeBadges = screen.getAllByText('Active');
|
|
expect(activeBadges.length).toBeGreaterThan(0);
|
|
expect(screen.getByText('No Equipment')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('should display subscription status badges', async () => {
|
|
(api.getAllBeneficiaries as jest.Mock).mockResolvedValue({
|
|
ok: true,
|
|
data: mockBeneficiaries,
|
|
});
|
|
|
|
render(<DashboardPage />);
|
|
|
|
await waitFor(() => {
|
|
// Maria has active subscription - "Active" text appears twice (equipment + subscription)
|
|
const activeBadges = screen.getAllByText('Active');
|
|
expect(activeBadges.length).toBeGreaterThanOrEqual(1);
|
|
});
|
|
});
|
|
|
|
it('should display role badges', async () => {
|
|
(api.getAllBeneficiaries as jest.Mock).mockResolvedValue({
|
|
ok: true,
|
|
data: mockBeneficiaries,
|
|
});
|
|
|
|
render(<DashboardPage />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Custodian')).toBeInTheDocument();
|
|
expect(screen.getByText('Caretaker')).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it('should display address if available', async () => {
|
|
(api.getAllBeneficiaries as jest.Mock).mockResolvedValue({
|
|
ok: true,
|
|
data: mockBeneficiaries,
|
|
});
|
|
|
|
render(<DashboardPage />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/📍 123 Main St, City/)).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Navigation', () => {
|
|
it('should navigate to add beneficiary page when clicking add button', async () => {
|
|
(api.getAllBeneficiaries as jest.Mock).mockResolvedValue({
|
|
ok: true,
|
|
data: mockBeneficiaries,
|
|
});
|
|
|
|
render(<DashboardPage />);
|
|
|
|
await waitFor(() => {
|
|
const addButton = screen.getByText('+ Add Beneficiary');
|
|
fireEvent.click(addButton);
|
|
});
|
|
|
|
expect(mockRouter.push).toHaveBeenCalledWith('/add-loved-one');
|
|
});
|
|
|
|
it('should navigate to beneficiary detail when clicking card', async () => {
|
|
(api.getAllBeneficiaries as jest.Mock).mockResolvedValue({
|
|
ok: true,
|
|
data: mockBeneficiaries,
|
|
});
|
|
|
|
render(<DashboardPage />);
|
|
|
|
await waitFor(() => {
|
|
const mariaCard = screen.getByText('Maria Garcia').closest('button');
|
|
if (mariaCard) {
|
|
fireEvent.click(mariaCard);
|
|
}
|
|
});
|
|
|
|
expect(mockRouter.push).toHaveBeenCalledWith('/beneficiaries/1');
|
|
});
|
|
});
|
|
|
|
describe('Equipment Status Badge Colors', () => {
|
|
it.each([
|
|
['none', 'No Equipment', 'bg-slate-100'],
|
|
['ordered', 'Ordered', 'bg-blue-100'],
|
|
['shipped', 'Shipped', 'bg-yellow-100'],
|
|
['delivered', 'Delivered', 'bg-green-100'],
|
|
['active', 'Active', 'bg-green-100'],
|
|
['demo', 'Demo', 'bg-purple-100'],
|
|
])(
|
|
'should display %s status with correct badge',
|
|
async (status, text, colorClass) => {
|
|
const beneficiary: Beneficiary = {
|
|
...mockBeneficiaries[0],
|
|
equipmentStatus: status as any,
|
|
// Remove subscription for "active" status test to avoid duplicate "Active" text
|
|
subscription: status === 'active' ? undefined : mockBeneficiaries[0].subscription,
|
|
};
|
|
|
|
(api.getAllBeneficiaries as jest.Mock).mockResolvedValue({
|
|
ok: true,
|
|
data: [beneficiary],
|
|
});
|
|
|
|
const { container } = render(<DashboardPage />);
|
|
|
|
await waitFor(() => {
|
|
// Use getAllByText for "Active" which may appear in both equipment and subscription
|
|
const badges = screen.getAllByText(text);
|
|
expect(badges.length).toBeGreaterThan(0);
|
|
expect(badges[0].className).toContain(colorClass);
|
|
}, { timeout: 3000 });
|
|
}
|
|
);
|
|
});
|
|
|
|
describe('Subscription Status Badge Colors', () => {
|
|
it.each([
|
|
['active', 'Active', 'bg-green-100'],
|
|
['trial', 'Trial', 'bg-blue-100'],
|
|
['expired', 'Expired', 'bg-red-100'],
|
|
['cancelled', 'Cancelled', 'bg-slate-100'],
|
|
])(
|
|
'should display %s subscription with correct badge',
|
|
async (status, text, colorClass) => {
|
|
const beneficiary: Beneficiary = {
|
|
...mockBeneficiaries[0],
|
|
subscription: {
|
|
status: status as any,
|
|
planType: 'monthly',
|
|
endDate: '2025-12-31',
|
|
cancelAtPeriodEnd: false,
|
|
},
|
|
};
|
|
|
|
(api.getAllBeneficiaries as jest.Mock).mockResolvedValue({
|
|
ok: true,
|
|
data: [beneficiary],
|
|
});
|
|
|
|
const { container } = render(<DashboardPage />);
|
|
|
|
await waitFor(() => {
|
|
const badges = screen.getAllByText(text);
|
|
expect(badges.length).toBeGreaterThan(0);
|
|
expect(badges[0].className).toContain(colorClass);
|
|
}, { timeout: 3000 });
|
|
}
|
|
);
|
|
});
|
|
|
|
describe('Error Retry', () => {
|
|
it('should show retry button on error', async () => {
|
|
(api.getAllBeneficiaries as jest.Mock).mockResolvedValue({
|
|
ok: false,
|
|
error: { message: 'Test error' },
|
|
});
|
|
|
|
render(<DashboardPage />);
|
|
|
|
await waitFor(() => {
|
|
// ErrorMessage component uses "Retry" button text
|
|
const retryButton = screen.getByText('Retry');
|
|
expect(retryButton).toBeInTheDocument();
|
|
});
|
|
});
|
|
});
|
|
});
|