Tests refactor
This commit is contained in:
parent
245520fb50
commit
99134d8556
165 changed files with 911 additions and 531 deletions
397
packages/web/tests/app/(dashboard)/medicine-prices/page.test.tsx
Normal file
397
packages/web/tests/app/(dashboard)/medicine-prices/page.test.tsx
Normal file
|
|
@ -0,0 +1,397 @@
|
|||
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 { mockUseApi } = vi.hoisted(() => ({ mockUseApi: vi.fn() }));
|
||||
|
||||
const { mockRecordPrice, mockGetPriceHistory, mockCompareStores } = vi.hoisted(() => ({
|
||||
mockRecordPrice: vi.fn(),
|
||||
mockGetPriceHistory: vi.fn(),
|
||||
mockCompareStores: vi.fn(),
|
||||
}));
|
||||
|
||||
const { mockListMedicines, mockListMedicineProducts } = vi.hoisted(() => ({
|
||||
mockListMedicines: vi.fn(),
|
||||
mockListMedicineProducts: vi.fn(),
|
||||
}));
|
||||
|
||||
const { mockListStores } = vi.hoisted(() => ({ mockListStores: vi.fn() }));
|
||||
|
||||
vi.mock('@/lib/useApi', () => ({ useApi: mockUseApi }));
|
||||
vi.mock('@/services/medicine-prices', () => ({
|
||||
recordPrice: mockRecordPrice,
|
||||
getPriceHistory: mockGetPriceHistory,
|
||||
compareStores: mockCompareStores,
|
||||
}));
|
||||
vi.mock('@/services/medicines', () => ({
|
||||
listMedicines: mockListMedicines,
|
||||
listMedicineProducts: mockListMedicineProducts,
|
||||
}));
|
||||
vi.mock('@/services/stores', () => ({ listStores: mockListStores }));
|
||||
vi.mock('next/link', () => ({
|
||||
default: (props: { href?: string; children: React.ReactNode }) => props.children,
|
||||
}));
|
||||
|
||||
import MedicinePricesPage from '../../../../src/app/(dashboard)/medicine-prices/page';
|
||||
|
||||
const emptyResponse = { data: [], pagination: { cursor: null, hasMore: false } };
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockListMedicines.mockResolvedValue(emptyResponse);
|
||||
mockListStores.mockResolvedValue(emptyResponse);
|
||||
mockGetPriceHistory.mockResolvedValue(emptyResponse);
|
||||
mockCompareStores.mockResolvedValue({ data: [] });
|
||||
});
|
||||
|
||||
describe('MedicinePricesPage', () => {
|
||||
it('shows loading skeleton when session loading', () => {
|
||||
mockUseApi.mockReturnValue({ householdId: null, isLoading: true });
|
||||
render(<MedicinePricesPage />);
|
||||
expect(screen.getByText('Medicine Prices')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Record Price')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows household prompt when no householdId', () => {
|
||||
mockUseApi.mockReturnValue({ householdId: null, isLoading: false });
|
||||
render(<MedicinePricesPage />);
|
||||
expect(screen.getByText(/create or join a household/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders Medicine Prices heading when householdId exists', () => {
|
||||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
||||
render(<MedicinePricesPage />);
|
||||
expect(screen.getByText('Medicine Prices')).toBeInTheDocument();
|
||||
expect(screen.getByText('Record Price')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows empty prompt to select a medicine', async () => {
|
||||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
||||
render(<MedicinePricesPage />);
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText('Select a medicine to view price history.')).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
|
||||
it('loads price history when medicine selected', async () => {
|
||||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
||||
mockListMedicines.mockResolvedValue({
|
||||
data: [
|
||||
{ _id: 'med-1', name: 'Metformin', strength: 500, strengthUnit: 'mg', form: 'tablet' },
|
||||
],
|
||||
pagination: { cursor: null, hasMore: false },
|
||||
});
|
||||
mockGetPriceHistory.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
_id: 'pr-1',
|
||||
medicineId: 'med-1',
|
||||
medicineName: 'Metformin',
|
||||
storeId: 'st-1',
|
||||
storeName: 'Walgreens',
|
||||
price: 15.99,
|
||||
pricePerUnit: 0.18,
|
||||
currency: 'USD',
|
||||
quantity: 90,
|
||||
unit: 'tablet',
|
||||
date: '2026-01-01T00:00:00.000Z',
|
||||
createdAt: '2026-01-01T00:00:00.000Z',
|
||||
},
|
||||
],
|
||||
pagination: { cursor: null, hasMore: false },
|
||||
});
|
||||
|
||||
render(<MedicinePricesPage />);
|
||||
|
||||
await waitFor(() => screen.getByText('Select a medicine'));
|
||||
fireEvent.change(screen.getAllByRole('combobox')[0]!, { target: { value: 'med-1' } });
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockGetPriceHistory).toHaveBeenCalledWith('hh1', 'med-1', expect.any(Object)),
|
||||
);
|
||||
// Wait for price records to render
|
||||
await waitFor(() => expect(screen.getByText('Walgreens')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('shows validation error in form when required fields missing', async () => {
|
||||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
||||
|
||||
render(<MedicinePricesPage />);
|
||||
|
||||
await userEvent.click(screen.getByText('Record Price'));
|
||||
await waitFor(() => screen.getByRole('button', { name: 'Record Price', hidden: false }));
|
||||
|
||||
// The submit button inside the form also has text 'Record Price'
|
||||
const submitBtn = screen
|
||||
.getAllByRole('button', { name: 'Record Price' })
|
||||
.find((b) => b.getAttribute('type') === 'submit');
|
||||
if (submitBtn) {
|
||||
fireEvent.submit(submitBtn.closest('form')!);
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByText('Please select a medicine, a product, and a store.'),
|
||||
).toBeInTheDocument(),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('shows error when price history fails', async () => {
|
||||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
||||
mockListMedicines.mockResolvedValue({
|
||||
data: [
|
||||
{ _id: 'med-1', name: 'Metformin', strength: 500, strengthUnit: 'mg', form: 'tablet' },
|
||||
],
|
||||
pagination: { cursor: null, hasMore: false },
|
||||
});
|
||||
mockGetPriceHistory.mockRejectedValue(new Error('History load failed'));
|
||||
|
||||
render(<MedicinePricesPage />);
|
||||
|
||||
await waitFor(() => screen.getByText('Select a medicine'));
|
||||
fireEvent.change(screen.getAllByRole('combobox')[0]!, { target: { value: 'med-1' } });
|
||||
|
||||
await waitFor(() => expect(screen.getByText('History load failed')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('records a price successfully', async () => {
|
||||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
||||
mockListMedicines.mockResolvedValue({
|
||||
data: [
|
||||
{ _id: 'med-1', name: 'Metformin', strength: 500, strengthUnit: 'mg', form: 'tablet' },
|
||||
],
|
||||
pagination: { cursor: null, hasMore: false },
|
||||
});
|
||||
mockListStores.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
_id: 'st-1',
|
||||
name: 'Walgreens',
|
||||
tags: [],
|
||||
isActive: true,
|
||||
createdAt: '2026-01-01T00:00:00.000Z',
|
||||
},
|
||||
],
|
||||
pagination: { cursor: null, hasMore: false },
|
||||
});
|
||||
mockListMedicineProducts.mockResolvedValue({
|
||||
data: [{ _id: 'prod-1', brand: 'Generic', packageSize: 90, packageUnit: 'tablet' }],
|
||||
pagination: { cursor: null, hasMore: false },
|
||||
});
|
||||
mockRecordPrice.mockResolvedValue({});
|
||||
|
||||
render(<MedicinePricesPage />);
|
||||
|
||||
await waitFor(() => screen.getByText('Record Price'));
|
||||
await userEvent.click(screen.getByText('Record Price'));
|
||||
|
||||
await waitFor(() => screen.getByText('Record Price', { selector: 'h2' }));
|
||||
|
||||
// Select store
|
||||
fireEvent.change(screen.getByDisplayValue('Select store'), { target: { value: 'st-1' } });
|
||||
// Select medicine
|
||||
fireEvent.change(screen.getByDisplayValue('Select medicine'), { target: { value: 'med-1' } });
|
||||
|
||||
// Wait for products to load and select product
|
||||
await waitFor(() => screen.getByText(/Generic.*90/));
|
||||
fireEvent.change(screen.getByDisplayValue('Select product'), { target: { value: 'prod-1' } });
|
||||
|
||||
// Submit form
|
||||
fireEvent.submit(
|
||||
screen.getByRole('button', { name: 'Record Price', hidden: true }).closest('form')!,
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockRecordPrice).toHaveBeenCalledWith(
|
||||
'hh1',
|
||||
expect.objectContaining({ medicineId: 'med-1' }),
|
||||
),
|
||||
);
|
||||
// Form should close after success
|
||||
await waitFor(() =>
|
||||
expect(screen.queryByText('Record Price', { selector: 'h2' })).not.toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
|
||||
it('shows Load more button in price history', async () => {
|
||||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
||||
mockListMedicines.mockResolvedValue({
|
||||
data: [
|
||||
{ _id: 'med-1', name: 'Metformin', strength: 500, strengthUnit: 'mg', form: 'tablet' },
|
||||
],
|
||||
pagination: { cursor: null, hasMore: false },
|
||||
});
|
||||
mockGetPriceHistory.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
_id: 'pr-1',
|
||||
medicineId: 'med-1',
|
||||
medicineName: 'Metformin',
|
||||
storeId: 'st-1',
|
||||
storeName: 'Walgreens',
|
||||
price: 15.99,
|
||||
pricePerUnit: 0.18,
|
||||
currency: 'USD',
|
||||
quantity: 90,
|
||||
unit: 'tablet',
|
||||
date: '2026-01-01T00:00:00.000Z',
|
||||
createdAt: '2026-01-01T00:00:00.000Z',
|
||||
},
|
||||
],
|
||||
pagination: { cursor: 'cur1', hasMore: true },
|
||||
});
|
||||
|
||||
render(<MedicinePricesPage />);
|
||||
|
||||
await waitFor(() => screen.getByText('Select a medicine'));
|
||||
fireEvent.change(screen.getAllByRole('combobox')[0]!, { target: { value: 'med-1' } });
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Load more')).toBeInTheDocument());
|
||||
await userEvent.click(screen.getByText('Load more'));
|
||||
|
||||
expect(mockGetPriceHistory).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('shows store filter when medicine selected and changes it', async () => {
|
||||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
||||
mockListMedicines.mockResolvedValue({
|
||||
data: [
|
||||
{ _id: 'med-1', name: 'Metformin', strength: 500, strengthUnit: 'mg', form: 'tablet' },
|
||||
],
|
||||
pagination: { cursor: null, hasMore: false },
|
||||
});
|
||||
mockListStores.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
_id: 'st-1',
|
||||
name: 'Walgreens',
|
||||
tags: [],
|
||||
isActive: true,
|
||||
createdAt: '2026-01-01T00:00:00.000Z',
|
||||
},
|
||||
],
|
||||
pagination: { cursor: null, hasMore: false },
|
||||
});
|
||||
|
||||
render(<MedicinePricesPage />);
|
||||
|
||||
await waitFor(() => screen.getByText('Select a medicine'));
|
||||
fireEvent.change(screen.getAllByRole('combobox')[0]!, { target: { value: 'med-1' } });
|
||||
|
||||
await waitFor(() => screen.getByDisplayValue('All stores'));
|
||||
fireEvent.change(screen.getByDisplayValue('All stores'), { target: { value: 'st-1' } });
|
||||
|
||||
await waitFor(() => expect(mockGetPriceHistory).toHaveBeenCalledTimes(2));
|
||||
});
|
||||
|
||||
it('dismisses price history error', async () => {
|
||||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
||||
mockListMedicines.mockResolvedValue({
|
||||
data: [
|
||||
{ _id: 'med-1', name: 'Metformin', strength: 500, strengthUnit: 'mg', form: 'tablet' },
|
||||
],
|
||||
pagination: { cursor: null, hasMore: false },
|
||||
});
|
||||
mockGetPriceHistory.mockRejectedValue(new Error('History load failed'));
|
||||
|
||||
render(<MedicinePricesPage />);
|
||||
|
||||
await waitFor(() => screen.getByText('Select a medicine'));
|
||||
fireEvent.change(screen.getAllByRole('combobox')[0]!, { target: { value: 'med-1' } });
|
||||
|
||||
await waitFor(() => screen.getByText('History load failed'));
|
||||
await userEvent.click(screen.getByText('Dismiss'));
|
||||
expect(screen.queryByText('History load failed')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows store comparison table', async () => {
|
||||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
||||
mockListMedicines.mockResolvedValue({
|
||||
data: [
|
||||
{ _id: 'med-1', name: 'Metformin', strength: 500, strengthUnit: 'mg', form: 'tablet' },
|
||||
],
|
||||
pagination: { cursor: null, hasMore: false },
|
||||
});
|
||||
mockCompareStores.mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
storeId: 'st-1',
|
||||
storeName: 'Walgreens',
|
||||
latestPrice: 12.99,
|
||||
latestPricePerUnit: 0.14,
|
||||
currency: 'USD',
|
||||
date: '2026-01-01T00:00:00.000Z',
|
||||
isInsurancePrice: false,
|
||||
},
|
||||
{
|
||||
storeId: 'st-2',
|
||||
storeName: 'CVS',
|
||||
latestPrice: 14.99,
|
||||
latestPricePerUnit: 0.17,
|
||||
currency: 'USD',
|
||||
date: '2026-01-01T00:00:00.000Z',
|
||||
isInsurancePrice: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
render(<MedicinePricesPage />);
|
||||
|
||||
await waitFor(() => screen.getByText('Select a medicine'));
|
||||
fireEvent.change(screen.getAllByRole('combobox')[0]!, { target: { value: 'med-1' } });
|
||||
|
||||
await waitFor(() => expect(screen.getByText('Store comparison')).toBeInTheDocument());
|
||||
expect(screen.getByText('Walgreens')).toBeInTheDocument();
|
||||
// First row (cheapest) should be highlighted green
|
||||
expect(screen.getByText('cheapest')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('changes price, currency, quantity, and unit fields in form', async () => {
|
||||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
||||
|
||||
render(<MedicinePricesPage />);
|
||||
|
||||
await userEvent.click(screen.getByText('Record Price'));
|
||||
await waitFor(() => screen.getByPlaceholderText('9.99'));
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText('9.99'), { target: { value: '15.99' } });
|
||||
fireEvent.change(screen.getByPlaceholderText('USD'), { target: { value: 'EUR' } });
|
||||
fireEvent.change(screen.getByPlaceholderText('90'), { target: { value: '30' } });
|
||||
fireEvent.change(screen.getByDisplayValue('tablet'), { target: { value: 'capsule' } });
|
||||
|
||||
expect(screen.getByPlaceholderText('9.99')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('changes medicine search, notes and insurance price fields in form', async () => {
|
||||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
||||
|
||||
render(<MedicinePricesPage />);
|
||||
|
||||
await userEvent.click(screen.getByText('Record Price'));
|
||||
await waitFor(() => screen.getByPlaceholderText('Search medicines...'));
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText('Search medicines...'), {
|
||||
target: { value: 'Met' },
|
||||
});
|
||||
|
||||
// Notes field (no placeholder, but maxLength 1000)
|
||||
const notesInput = document.querySelector('input[maxLength="1000"]') as HTMLElement;
|
||||
if (notesInput) {
|
||||
fireEvent.change(notesInput, { target: { value: 'Some notes' } });
|
||||
}
|
||||
|
||||
// Insurance price checkbox
|
||||
fireEvent.click(screen.getByLabelText('Insurance price'));
|
||||
expect(screen.getByLabelText('Insurance price')).toBeChecked();
|
||||
});
|
||||
|
||||
it('toggles Record Price form', async () => {
|
||||
mockUseApi.mockReturnValue({ householdId: 'hh1', isLoading: false });
|
||||
render(<MedicinePricesPage />);
|
||||
await userEvent.click(screen.getByText('Record Price'));
|
||||
// Both the header toggle and the form show "Cancel" when open
|
||||
expect(screen.getAllByRole('button', { name: 'Cancel' }).length).toBeGreaterThan(0);
|
||||
await userEvent.click(screen.getAllByRole('button', { name: 'Cancel' })[0]);
|
||||
expect(screen.queryByRole('button', { name: 'Cancel' })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue