[WIP] Increase frontend test coverage to above 80% (#63)
* Initial plan * refactor: simplify useMedicationForm tests to avoid memory issues Co-authored-by: DanielVolz <3275994+DanielVolz@users.noreply.github.com> * Changes before error encountered Co-authored-by: DanielVolz <3275994+DanielVolz@users.noreply.github.com> * test: add comprehensive tests for SchedulePage, SettingsPage, MedicationsPage, and PlannerPage Co-authored-by: DanielVolz <3275994+DanielVolz@users.noreply.github.com> * test: add SharedSchedule theme persistence tests Co-authored-by: DanielVolz <3275994+DanielVolz@users.noreply.github.com> * test: add comprehensive MobileEditModal tests Co-authored-by: DanielVolz <3275994+DanielVolz@users.noreply.github.com> * test: add comprehensive MedDetailModal tests Co-authored-by: DanielVolz <3275994+DanielVolz@users.noreply.github.com> * fix: use fixed timestamps in tests for deterministic behavior Co-authored-by: DanielVolz <3275994+DanielVolz@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: DanielVolz <3275994+DanielVolz@users.noreply.github.com>
This commit is contained in:
@@ -3,46 +3,139 @@ import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { DashboardPage } from '../../pages/DashboardPage';
|
||||
|
||||
// Mock the context
|
||||
vi.mock('../../context', () => ({
|
||||
useAppContext: () => ({
|
||||
meds: [],
|
||||
settings: {
|
||||
lowStockThreshold: 30,
|
||||
criticalStockThreshold: 7,
|
||||
expiryWarningDays: 30,
|
||||
lowStockDays: 7,
|
||||
normalStockDays: 30,
|
||||
highStockDays: 90,
|
||||
emailEnabled: false,
|
||||
shoutrrrEnabled: false,
|
||||
reminderDaysBefore: 7
|
||||
},
|
||||
scheduleDays: 30,
|
||||
setScheduleDays: vi.fn(),
|
||||
showPastDays: false,
|
||||
setShowPastDays: vi.fn(),
|
||||
pastDays: [],
|
||||
futureDays: [],
|
||||
takenDoses: new Set(),
|
||||
markDoseTaken: vi.fn(),
|
||||
undoDoseTaken: vi.fn(),
|
||||
coverage: { all: [], low: [] },
|
||||
coverageByMed: {},
|
||||
depletionByMed: {},
|
||||
manuallyExpandedDays: new Set(),
|
||||
toggleDayCollapse: vi.fn(),
|
||||
openMedDetail: vi.fn(),
|
||||
openUserFilter: vi.fn(),
|
||||
openShare: vi.fn(),
|
||||
lowCoverage: [],
|
||||
criticalCoverage: [],
|
||||
// Mock data for tests with medications
|
||||
const mockMeds = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Aspirin',
|
||||
packCount: 1,
|
||||
blistersPerPack: 2,
|
||||
pillsPerBlister: 10,
|
||||
looseTablets: 5,
|
||||
takenBy: ['John'],
|
||||
blisters: [{ usage: 1, every: 1, start: '2024-01-01T09:00:00Z' }],
|
||||
intakeRemindersEnabled: true,
|
||||
notes: 'Take with food',
|
||||
expiryDate: '2025-12-31',
|
||||
imageUrl: null,
|
||||
updatedAt: null
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Vitamin D',
|
||||
packCount: 0,
|
||||
blistersPerPack: 1,
|
||||
pillsPerBlister: 30,
|
||||
looseTablets: 3,
|
||||
takenBy: [],
|
||||
blisters: [{ usage: 1, every: 1, start: '2024-01-01T08:00:00Z' }],
|
||||
intakeRemindersEnabled: false,
|
||||
notes: null,
|
||||
expiryDate: null,
|
||||
imageUrl: null,
|
||||
updatedAt: null
|
||||
}
|
||||
];
|
||||
|
||||
const mockCoverage = {
|
||||
all: [
|
||||
{ name: 'Aspirin', medsLeft: 25, daysLeft: 25, depletionDate: '2025-02-15', depletionTime: Date.now() + 25 * 86400000, nextDose: null },
|
||||
{ name: 'Vitamin D', medsLeft: 3, daysLeft: 3, depletionDate: '2025-01-25', depletionTime: Date.now() + 3 * 86400000, nextDose: null }
|
||||
],
|
||||
low: [
|
||||
{ name: 'Vitamin D', medsLeft: 3, daysLeft: 3, depletionDate: '2025-01-25', depletionTime: Date.now() + 3 * 86400000, nextDose: null }
|
||||
]
|
||||
};
|
||||
|
||||
const mockFutureDays = [
|
||||
{
|
||||
dateStr: 'Mon, Jan 22',
|
||||
date: new Date(),
|
||||
isPast: false,
|
||||
meds: [
|
||||
{
|
||||
medName: 'Aspirin',
|
||||
total: 1,
|
||||
doses: [
|
||||
{ id: '1-0-' + Date.now(), timeStr: '09:00', when: Date.now(), usage: 1, takenBy: ['John'] }
|
||||
],
|
||||
lastWhen: Date.now()
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const mockPastDays = [
|
||||
{
|
||||
dateStr: 'Sun, Jan 21',
|
||||
date: new Date(Date.now() - 86400000),
|
||||
isPast: true,
|
||||
meds: [
|
||||
{
|
||||
medName: 'Aspirin',
|
||||
total: 1,
|
||||
doses: [
|
||||
{ id: '1-0-' + (Date.now() - 86400000), timeStr: '09:00', when: Date.now() - 86400000, usage: 1, takenBy: ['John'] }
|
||||
],
|
||||
lastWhen: Date.now() - 86400000
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// Default mock factory
|
||||
const createMockAppContext = (overrides = {}) => ({
|
||||
meds: [],
|
||||
settings: {
|
||||
lowStockThreshold: 30,
|
||||
criticalStockThreshold: 7,
|
||||
expiryWarningDays: 30,
|
||||
lowStockDays: 7,
|
||||
normalStockDays: 30,
|
||||
highStockDays: 90,
|
||||
emailEnabled: false,
|
||||
shoutrrrEnabled: false,
|
||||
reminderDaysBefore: 7,
|
||||
notificationEmail: '',
|
||||
lastAutoEmailSent: null,
|
||||
lastNotificationType: null,
|
||||
lastNotificationChannel: null,
|
||||
medsError: null,
|
||||
openEditStockModal: vi.fn()
|
||||
})
|
||||
lastNotificationChannel: null
|
||||
},
|
||||
scheduleDays: 30,
|
||||
setScheduleDays: vi.fn(),
|
||||
showPastDays: false,
|
||||
setShowPastDays: vi.fn(),
|
||||
pastDays: [],
|
||||
futureDays: [],
|
||||
takenDoses: new Set(),
|
||||
dismissedDoses: new Set(),
|
||||
markDoseTaken: vi.fn(),
|
||||
undoDoseTaken: vi.fn(),
|
||||
coverage: { all: [], low: [] },
|
||||
coverageByMed: {},
|
||||
depletionByMed: {},
|
||||
manuallyExpandedDays: new Set(),
|
||||
manuallyCollapsedDays: new Set(),
|
||||
toggleDayCollapse: vi.fn(),
|
||||
openMedDetail: vi.fn(),
|
||||
openUserFilter: vi.fn(),
|
||||
openShareDialog: vi.fn(),
|
||||
openScheduleLightbox: vi.fn(),
|
||||
missedPastDoseIds: [],
|
||||
getDayStockStatus: vi.fn(() => 'success'),
|
||||
getDoseId: vi.fn((id, person) => person ? `${id}-${person}` : id),
|
||||
showClearMissedConfirm: false,
|
||||
setShowClearMissedConfirm: vi.fn(),
|
||||
clearingMissed: false,
|
||||
dismissMissedDoses: vi.fn(),
|
||||
...overrides
|
||||
});
|
||||
|
||||
let mockContextValue = createMockAppContext();
|
||||
|
||||
// Mock the context
|
||||
vi.mock('../../context', () => ({
|
||||
useAppContext: () => mockContextValue
|
||||
}));
|
||||
|
||||
vi.mock('../../components/Auth', () => ({
|
||||
@@ -55,6 +148,7 @@ describe('DashboardPage', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
localStorage.clear();
|
||||
mockContextValue = createMockAppContext();
|
||||
});
|
||||
|
||||
it('renders dashboard page', () => {
|
||||
@@ -211,6 +305,7 @@ describe('DashboardPage interactions', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
localStorage.clear();
|
||||
mockContextValue = createMockAppContext();
|
||||
});
|
||||
|
||||
it('has schedule days options', () => {
|
||||
@@ -246,6 +341,7 @@ describe('DashboardPage structure', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
localStorage.clear();
|
||||
mockContextValue = createMockAppContext();
|
||||
});
|
||||
|
||||
it('renders multiple section grids', () => {
|
||||
@@ -292,10 +388,380 @@ describe('DashboardPage with medications', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
localStorage.clear();
|
||||
mockContextValue = createMockAppContext({
|
||||
meds: mockMeds,
|
||||
coverage: mockCoverage,
|
||||
coverageByMed: {
|
||||
'Aspirin': mockCoverage.all[0],
|
||||
'Vitamin D': mockCoverage.all[1]
|
||||
},
|
||||
depletionByMed: {
|
||||
'Aspirin': Date.now() + 25 * 86400000,
|
||||
'Vitamin D': Date.now() + 3 * 86400000
|
||||
},
|
||||
futureDays: mockFutureDays
|
||||
});
|
||||
});
|
||||
|
||||
it('renders medication coverage cards', () => {
|
||||
// Test passes with default empty meds mock
|
||||
expect(true).toBe(true);
|
||||
it('renders medication rows in overview table', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<DashboardPage />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
// Should show medication names (may appear in multiple places)
|
||||
const aspirinElements = screen.getAllByText('Aspirin');
|
||||
const vitaminDElements = screen.getAllByText('Vitamin D');
|
||||
expect(aspirinElements.length).toBeGreaterThan(0);
|
||||
expect(vitaminDElements.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('renders low stock section with low stock medications', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<DashboardPage />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
// Should show the low stock medication name
|
||||
const vitaminDElements = screen.getAllByText('Vitamin D');
|
||||
expect(vitaminDElements.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('renders taken by badges', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<DashboardPage />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
// Should show taken by badge for Aspirin
|
||||
const johnBadges = screen.getAllByText('John');
|
||||
expect(johnBadges.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('renders medication icons for reminders and notes', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<DashboardPage />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
// Aspirin has intakeRemindersEnabled and notes
|
||||
const reminderIcons = document.querySelectorAll('.reminder-icon');
|
||||
expect(reminderIcons.length).toBeGreaterThan(0);
|
||||
|
||||
const notesIcons = document.querySelectorAll('.notes-icon');
|
||||
expect(notesIcons.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('renders schedule timeline with future doses', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<DashboardPage />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
// Should show day block
|
||||
const dayBlocks = document.querySelectorAll('.day-block');
|
||||
expect(dayBlocks.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('calls openMedDetail when clicking medication row', () => {
|
||||
const openMedDetail = vi.fn();
|
||||
mockContextValue = createMockAppContext({
|
||||
meds: mockMeds,
|
||||
coverage: mockCoverage,
|
||||
coverageByMed: { 'Aspirin': mockCoverage.all[0] },
|
||||
openMedDetail
|
||||
});
|
||||
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<DashboardPage />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
// Click on medication row
|
||||
const aspirinRow = screen.getAllByText('Aspirin')[0].closest('.table-row');
|
||||
if (aspirinRow) {
|
||||
fireEvent.click(aspirinRow);
|
||||
expect(openMedDetail).toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
|
||||
it('calls openUserFilter when clicking taken by badge', () => {
|
||||
const openUserFilter = vi.fn();
|
||||
mockContextValue = createMockAppContext({
|
||||
meds: mockMeds,
|
||||
coverage: mockCoverage,
|
||||
coverageByMed: { 'Aspirin': mockCoverage.all[0] },
|
||||
openUserFilter
|
||||
});
|
||||
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<DashboardPage />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
// Click on taken by badge
|
||||
const johnBadge = screen.getAllByText('John')[0];
|
||||
fireEvent.click(johnBadge);
|
||||
expect(openUserFilter).toHaveBeenCalledWith('John');
|
||||
});
|
||||
});
|
||||
|
||||
describe('DashboardPage with email notifications', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
localStorage.clear();
|
||||
mockContextValue = createMockAppContext({
|
||||
meds: mockMeds,
|
||||
coverage: mockCoverage,
|
||||
settings: {
|
||||
...createMockAppContext().settings,
|
||||
emailEnabled: true,
|
||||
notificationEmail: 'test@example.com'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('renders email status bar when email enabled', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<DashboardPage />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
// Should show email status bar
|
||||
const statusBar = document.querySelector('.email-status-bar');
|
||||
expect(statusBar).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows reminder email button when there are low stock meds', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<DashboardPage />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
// Should show send reminder button
|
||||
expect(screen.getByText(/dashboard\.reorder\.sendReminder/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('DashboardPage with shoutrrr notifications', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
localStorage.clear();
|
||||
mockContextValue = createMockAppContext({
|
||||
meds: mockMeds,
|
||||
coverage: mockCoverage,
|
||||
settings: {
|
||||
...createMockAppContext().settings,
|
||||
shoutrrrEnabled: true
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('renders notification status bar when shoutrrr enabled', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<DashboardPage />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
// Should show status bar
|
||||
const statusBar = document.querySelector('.email-status-bar');
|
||||
expect(statusBar).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('DashboardPage with past days', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
localStorage.clear();
|
||||
mockContextValue = createMockAppContext({
|
||||
meds: mockMeds,
|
||||
coverage: mockCoverage,
|
||||
pastDays: mockPastDays,
|
||||
futureDays: mockFutureDays,
|
||||
showPastDays: false,
|
||||
missedPastDoseIds: ['1-0-' + (Date.now() - 86400000) + '-John']
|
||||
});
|
||||
});
|
||||
|
||||
it('renders past days toggle when past days exist', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<DashboardPage />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
// Should show past days toggle
|
||||
const toggle = document.querySelector('.past-days-toggle');
|
||||
expect(toggle).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows missed dose warning count', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<DashboardPage />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
// Should show warning with missed count
|
||||
const warning = document.querySelector('.past-days-warning');
|
||||
expect(warning).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('toggles past days visibility', () => {
|
||||
const setShowPastDays = vi.fn();
|
||||
mockContextValue = createMockAppContext({
|
||||
pastDays: mockPastDays,
|
||||
showPastDays: false,
|
||||
setShowPastDays,
|
||||
missedPastDoseIds: []
|
||||
});
|
||||
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<DashboardPage />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
const toggle = document.querySelector('.past-days-toggle');
|
||||
if (toggle) {
|
||||
fireEvent.click(toggle);
|
||||
expect(setShowPastDays).toHaveBeenCalledWith(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('shows clear missed doses button when there are missed doses', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<DashboardPage />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
// Should show clear missed button
|
||||
const clearBtn = document.querySelector('.clear-missed-btn');
|
||||
expect(clearBtn).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('DashboardPage with expanded past days', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
localStorage.clear();
|
||||
mockContextValue = createMockAppContext({
|
||||
meds: mockMeds,
|
||||
coverage: mockCoverage,
|
||||
coverageByMed: { 'Aspirin': mockCoverage.all[0] },
|
||||
pastDays: mockPastDays,
|
||||
futureDays: mockFutureDays,
|
||||
showPastDays: true,
|
||||
manuallyExpandedDays: new Set(['Sun, Jan 21']),
|
||||
getDayStockStatus: vi.fn(() => 'success')
|
||||
});
|
||||
});
|
||||
|
||||
it('renders past day blocks when showPastDays is true', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<DashboardPage />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
// Should show past day block
|
||||
const pastDayBlocks = document.querySelectorAll('.day-block.past');
|
||||
expect(pastDayBlocks.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DashboardPage dose interactions', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('calls markDoseTaken when clicking take button', () => {
|
||||
const markDoseTaken = vi.fn();
|
||||
mockContextValue = createMockAppContext({
|
||||
meds: mockMeds,
|
||||
coverage: mockCoverage,
|
||||
coverageByMed: { 'Aspirin': mockCoverage.all[0] },
|
||||
depletionByMed: { 'Aspirin': Date.now() + 25 * 86400000 },
|
||||
futureDays: mockFutureDays,
|
||||
markDoseTaken
|
||||
});
|
||||
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<DashboardPage />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
// Find and click take button
|
||||
const takeBtn = document.querySelector('.dose-btn.take');
|
||||
if (takeBtn) {
|
||||
fireEvent.click(takeBtn);
|
||||
expect(markDoseTaken).toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
|
||||
it('calls undoDoseTaken when clicking undo button', () => {
|
||||
const undoDoseTaken = vi.fn();
|
||||
const doseId = '1-0-' + Date.now() + '-John';
|
||||
mockContextValue = createMockAppContext({
|
||||
meds: mockMeds,
|
||||
coverage: mockCoverage,
|
||||
coverageByMed: { 'Aspirin': mockCoverage.all[0] },
|
||||
depletionByMed: { 'Aspirin': Date.now() + 25 * 86400000 },
|
||||
futureDays: mockFutureDays,
|
||||
takenDoses: new Set([doseId]),
|
||||
undoDoseTaken,
|
||||
getDoseId: vi.fn(() => doseId)
|
||||
});
|
||||
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<DashboardPage />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
// Find and click undo button
|
||||
const undoBtn = document.querySelector('.dose-btn.undo');
|
||||
if (undoBtn) {
|
||||
fireEvent.click(undoBtn);
|
||||
expect(undoDoseTaken).toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('DashboardPage good stock state', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
localStorage.clear();
|
||||
mockContextValue = createMockAppContext({
|
||||
meds: mockMeds,
|
||||
coverage: {
|
||||
all: [{ name: 'Aspirin', medsLeft: 100, daysLeft: 100, depletionDate: '2025-05-01', depletionTime: Date.now() + 100 * 86400000, nextDose: null }],
|
||||
low: []
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('shows all good message when no low stock', () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<DashboardPage />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
// Should show all good message
|
||||
expect(screen.getByText(/dashboard\.reorder\.allGood/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user