Tests refactor

This commit is contained in:
Aerilyn Weber 2026-05-19 11:06:03 +09:00
parent 245520fb50
commit 99134d8556
165 changed files with 911 additions and 531 deletions

View 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();
});
});