Tests refactor
This commit is contained in:
parent
245520fb50
commit
99134d8556
165 changed files with 911 additions and 531 deletions
250
packages/web/tests/app/(dashboard)/medicines/LibraryTab.test.tsx
Normal file
250
packages/web/tests/app/(dashboard)/medicines/LibraryTab.test.tsx
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, waitFor, fireEvent } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import type React from 'react';
|
||||
|
||||
const { mockListMedicines, mockCreateMedicine, mockDeleteMedicine } = vi.hoisted(() => ({
|
||||
mockListMedicines: vi.fn(),
|
||||
mockCreateMedicine: vi.fn(),
|
||||
mockDeleteMedicine: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/services/medicines', () => ({
|
||||
listMedicines: mockListMedicines,
|
||||
createMedicine: mockCreateMedicine,
|
||||
deleteMedicine: mockDeleteMedicine,
|
||||
}));
|
||||
|
||||
vi.mock('next/link', () => ({
|
||||
default: (props: { href?: string; children: React.ReactNode }) => (
|
||||
<a href={props.href}>{props.children}</a>
|
||||
),
|
||||
}));
|
||||
|
||||
import { LibraryTab } from '../../../../src/app/(dashboard)/medicines/LibraryTab';
|
||||
|
||||
const emptyResponse = { data: [], pagination: { cursor: null, hasMore: false } };
|
||||
|
||||
const medicine = {
|
||||
_id: 'med-1',
|
||||
name: 'Metformin',
|
||||
form: 'tablet',
|
||||
strength: 500,
|
||||
strengthUnit: 'mg',
|
||||
category: 'prescription',
|
||||
tags: [],
|
||||
};
|
||||
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
describe('LibraryTab', () => {
|
||||
it('shows loading skeleton initially', () => {
|
||||
mockListMedicines.mockReturnValue(new Promise(() => {}));
|
||||
render(<LibraryTab householdId="hh1" />);
|
||||
expect(screen.queryByText('Metformin')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders medicine list after load', async () => {
|
||||
mockListMedicines.mockResolvedValue({
|
||||
data: [medicine],
|
||||
pagination: { cursor: null, hasMore: false },
|
||||
});
|
||||
|
||||
render(<LibraryTab householdId="hh1" />);
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Metformin')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('shows empty state when no medicines', async () => {
|
||||
mockListMedicines.mockResolvedValue(emptyResponse);
|
||||
|
||||
render(<LibraryTab householdId="hh1" />);
|
||||
|
||||
await waitFor(() => expect(screen.getByText(/No medicines yet/)).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('shows error when list fails', async () => {
|
||||
mockListMedicines.mockRejectedValue(new Error('Failed to load'));
|
||||
|
||||
render(<LibraryTab householdId="hh1" />);
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Failed to load')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('dismisses error on Dismiss click', async () => {
|
||||
mockListMedicines.mockRejectedValue(new Error('Failed to load'));
|
||||
|
||||
render(<LibraryTab householdId="hh1" />);
|
||||
|
||||
await waitFor(() => screen.getByText('Failed to load'));
|
||||
await userEvent.click(screen.getByText('Dismiss'));
|
||||
expect(screen.queryByText('Failed to load')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('toggles Add Medicine form', async () => {
|
||||
mockListMedicines.mockResolvedValue(emptyResponse);
|
||||
|
||||
render(<LibraryTab householdId="hh1" />);
|
||||
|
||||
await waitFor(() => screen.getByText('Add Medicine'));
|
||||
await userEvent.click(screen.getByText('Add Medicine'));
|
||||
expect(screen.getByPlaceholderText('e.g. Metformin')).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(screen.getAllByText('Cancel')[0]);
|
||||
expect(screen.queryByPlaceholderText('e.g. Metformin')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('creates medicine and refreshes list', async () => {
|
||||
mockListMedicines.mockResolvedValue(emptyResponse);
|
||||
mockCreateMedicine.mockResolvedValue({ _id: 'med-2', name: 'Aspirin' });
|
||||
|
||||
render(<LibraryTab householdId="hh1" />);
|
||||
|
||||
await userEvent.click(screen.getByText('Add Medicine'));
|
||||
|
||||
await userEvent.type(screen.getByPlaceholderText('e.g. Metformin'), 'Aspirin');
|
||||
await userEvent.type(screen.getByPlaceholderText('500'), '100');
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Create Medicine' }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockCreateMedicine).toHaveBeenCalledWith(
|
||||
'hh1',
|
||||
expect.objectContaining({ name: 'Aspirin' }),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('deletes medicine after confirmation', async () => {
|
||||
mockListMedicines.mockResolvedValue({
|
||||
data: [medicine],
|
||||
pagination: { cursor: null, hasMore: false },
|
||||
});
|
||||
mockDeleteMedicine.mockResolvedValue({});
|
||||
vi.spyOn(window, 'confirm').mockReturnValue(true);
|
||||
|
||||
render(<LibraryTab householdId="hh1" />);
|
||||
|
||||
await waitFor(() => screen.getByText('Metformin'));
|
||||
await userEvent.click(screen.getByTitle('Delete'));
|
||||
|
||||
expect(mockDeleteMedicine).toHaveBeenCalledWith('hh1', 'med-1');
|
||||
});
|
||||
|
||||
it('changes form fields in add medicine form', async () => {
|
||||
mockListMedicines.mockResolvedValue(emptyResponse);
|
||||
|
||||
render(<LibraryTab householdId="hh1" />);
|
||||
|
||||
await waitFor(() => screen.getByText('Add Medicine'));
|
||||
await userEvent.click(screen.getByText('Add Medicine'));
|
||||
|
||||
await waitFor(() => screen.getByPlaceholderText('e.g. Metformin'));
|
||||
|
||||
// Change form type
|
||||
fireEvent.change(screen.getByDisplayValue('Tablet'), { target: { value: 'capsule' } });
|
||||
// Change strength unit
|
||||
fireEvent.change(screen.getByDisplayValue('mg'), { target: { value: 'mcg' } });
|
||||
// Change category
|
||||
fireEvent.change(screen.getByDisplayValue('OTC'), { target: { value: 'prescription' } });
|
||||
// Change notes (covers truthy branch)
|
||||
fireEvent.change(screen.getByPlaceholderText('Any additional notes'), {
|
||||
target: { value: 'test notes' },
|
||||
});
|
||||
// Clear notes (covers falsy branch → undefined)
|
||||
fireEvent.change(screen.getByPlaceholderText('Any additional notes'), {
|
||||
target: { value: '' },
|
||||
});
|
||||
|
||||
// Verify form is still visible
|
||||
expect(screen.getByPlaceholderText('e.g. Metformin')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('filters medicines by search, category, and form', async () => {
|
||||
mockListMedicines.mockResolvedValue({
|
||||
data: [medicine],
|
||||
pagination: { cursor: null, hasMore: false },
|
||||
});
|
||||
|
||||
render(<LibraryTab householdId="hh1" />);
|
||||
|
||||
await waitFor(() => screen.getByText('Metformin'));
|
||||
|
||||
// Search filter
|
||||
fireEvent.change(screen.getByPlaceholderText('Search medicines...'), {
|
||||
target: { value: 'met' },
|
||||
});
|
||||
|
||||
// Category filter
|
||||
fireEvent.change(screen.getByDisplayValue('All Categories'), {
|
||||
target: { value: 'prescription' },
|
||||
});
|
||||
|
||||
// Form filter
|
||||
fireEvent.change(screen.getByDisplayValue('All Forms'), { target: { value: 'tablet' } });
|
||||
|
||||
expect(screen.getByPlaceholderText('Search medicines...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows fallback error when non-Error is thrown on delete', async () => {
|
||||
mockListMedicines.mockResolvedValue({
|
||||
data: [medicine],
|
||||
pagination: { cursor: null, hasMore: false },
|
||||
});
|
||||
mockDeleteMedicine.mockRejectedValue('oops');
|
||||
vi.spyOn(window, 'confirm').mockReturnValue(true);
|
||||
|
||||
render(<LibraryTab householdId="hh1" />);
|
||||
|
||||
await waitFor(() => screen.getByText('Metformin'));
|
||||
await userEvent.click(screen.getByTitle('Delete'));
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Failed to delete')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('cancels CreateMedicineForm with internal Cancel button', async () => {
|
||||
mockListMedicines.mockResolvedValue(emptyResponse);
|
||||
|
||||
render(<LibraryTab householdId="hh1" />);
|
||||
|
||||
await waitFor(() => screen.getByText('Add Medicine'));
|
||||
await userEvent.click(screen.getByText('Add Medicine'));
|
||||
await waitFor(() => screen.getByPlaceholderText('e.g. Metformin'));
|
||||
|
||||
const cancelButtons = screen.getAllByText('Cancel');
|
||||
await userEvent.click(cancelButtons[cancelButtons.length - 1]!);
|
||||
|
||||
expect(screen.queryByPlaceholderText('e.g. Metformin')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows fallback error when non-Error is thrown on create medicine', async () => {
|
||||
mockListMedicines.mockResolvedValue(emptyResponse);
|
||||
mockCreateMedicine.mockRejectedValue('create failed');
|
||||
|
||||
render(<LibraryTab householdId="hh1" />);
|
||||
|
||||
await waitFor(() => screen.getByText('Add Medicine'));
|
||||
await userEvent.click(screen.getByText('Add Medicine'));
|
||||
|
||||
await waitFor(() => screen.getByPlaceholderText('e.g. Metformin'));
|
||||
await userEvent.type(screen.getByPlaceholderText('e.g. Metformin'), 'Aspirin');
|
||||
await userEvent.type(screen.getByPlaceholderText('500'), '100');
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Create Medicine' }));
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Failed to create medicine')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('does not delete medicine if confirmation cancelled', async () => {
|
||||
mockListMedicines.mockResolvedValue({
|
||||
data: [medicine],
|
||||
pagination: { cursor: null, hasMore: false },
|
||||
});
|
||||
vi.spyOn(window, 'confirm').mockReturnValue(false);
|
||||
|
||||
render(<LibraryTab householdId="hh1" />);
|
||||
|
||||
await waitFor(() => screen.getByText('Metformin'));
|
||||
await userEvent.click(screen.getByTitle('Delete'));
|
||||
|
||||
expect(mockDeleteMedicine).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue