Feat/frontend tests (#62)

* test(frontend): add vitest test infrastructure

- Add vitest, testing-library, jsdom dependencies
- Configure vitest with jsdom environment
- Add test setup with mocks for fetch, localStorage, matchMedia
- Set 75% coverage threshold

* Add frontend tests (#61)

* Initial plan

* Add frontend tests - utilities, hooks, and components (21% coverage)

Co-authored-by: DanielVolz <3275994+DanielVolz@users.noreply.github.com>

* Add more component tests (Auth, AboutModal, ExportModal) and useRefill hook tests - 30% coverage

Co-authored-by: DanielVolz <3275994+DanielVolz@users.noreply.github.com>

* Add useMedicationForm utility function tests - 30% coverage

Co-authored-by: DanielVolz <3275994+DanielVolz@users.noreply.github.com>

* Add AppHeader tests and more schedule.ts tests - 32% coverage

Co-authored-by: DanielVolz <3275994+DanielVolz@users.noreply.github.com>

* Changes before error encountered

Co-authored-by: DanielVolz <3275994+DanielVolz@users.noreply.github.com>

* Fix page tests and add more tests - 326 tests passing, 34% coverage

Co-authored-by: DanielVolz <3275994+DanielVolz@users.noreply.github.com>

* Add tests for ProfileModal, UserFilterModal, MedDetailModal - 361 tests, 36% coverage

Co-authored-by: DanielVolz <3275994+DanielVolz@users.noreply.github.com>

* Add SharedSchedule tests - 366 tests, 39% coverage

Co-authored-by: DanielVolz <3275994+DanielVolz@users.noreply.github.com>

* Expand page tests - 383 tests, 39% coverage

Co-authored-by: DanielVolz <3275994+DanielVolz@users.noreply.github.com>

* Add MobileEditModal tests - 409 tests, 40% coverage

Co-authored-by: DanielVolz <3275994+DanielVolz@users.noreply.github.com>

* Expand Dashboard and Schedule page tests - 427 tests, 40% coverage

Co-authored-by: DanielVolz <3275994+DanielVolz@users.noreply.github.com>

* Fix code review issues - remove invalid remindEnabled property

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>

---------

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: DanielVolz <3275994+DanielVolz@users.noreply.github.com>
This commit is contained in:
Daniel Volz
2026-01-22 10:25:11 +01:00
committed by GitHub
parent 8718311876
commit fd055a3a2a
36 changed files with 7602 additions and 3 deletions
@@ -0,0 +1,250 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import { AppHeader } from '../../components/AppHeader';
import { AuthProvider } from '../../components/Auth';
// Mock useNavigate
const mockNavigate = vi.fn();
vi.mock('react-router-dom', async () => {
const actual = await vi.importActual('react-router-dom');
return {
...actual,
useNavigate: () => mockNavigate,
};
});
describe('AppHeader', () => {
beforeEach(() => {
vi.clearAllMocks();
mockNavigate.mockClear();
// Set up default auth mock - auth disabled
(global.fetch as ReturnType<typeof vi.fn>)
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({
authEnabled: false,
localAuthEnabled: true,
hasUsers: false,
needsSetup: false
})
})
.mockResolvedValueOnce({
status: 401,
ok: false
});
});
it('renders header with logo', async () => {
const mockOnOpenProfile = vi.fn();
const mockOnOpenAbout = vi.fn();
render(
<MemoryRouter initialEntries={['/dashboard']}>
<AuthProvider>
<AppHeader onOpenProfile={mockOnOpenProfile} onOpenAbout={mockOnOpenAbout} />
</AuthProvider>
</MemoryRouter>
);
await waitFor(() => {
const logo = screen.getByAltText('MedAssist-ng');
expect(logo).toBeInTheDocument();
});
});
it('renders navigation tabs', async () => {
const mockOnOpenProfile = vi.fn();
const mockOnOpenAbout = vi.fn();
render(
<MemoryRouter initialEntries={['/dashboard']}>
<AuthProvider>
<AppHeader onOpenProfile={mockOnOpenProfile} onOpenAbout={mockOnOpenAbout} />
</AuthProvider>
</MemoryRouter>
);
await waitFor(() => {
// Use getAllBy since there are multiple elements with same text
const dashboardElements = screen.getAllByText(/nav\.dashboard/i);
expect(dashboardElements.length).toBeGreaterThan(0);
});
});
it('renders theme toggle button', async () => {
const mockOnOpenProfile = vi.fn();
const mockOnOpenAbout = vi.fn();
render(
<MemoryRouter initialEntries={['/dashboard']}>
<AuthProvider>
<AppHeader onOpenProfile={mockOnOpenProfile} onOpenAbout={mockOnOpenAbout} />
</AuthProvider>
</MemoryRouter>
);
await waitFor(() => {
const buttons = screen.getAllByRole('button');
const themeBtn = buttons.find(btn => btn.textContent?.includes('🌙') || btn.textContent?.includes('☀️'));
expect(themeBtn).toBeInTheDocument();
});
});
it('renders settings button when auth is disabled', async () => {
const mockOnOpenProfile = vi.fn();
const mockOnOpenAbout = vi.fn();
render(
<MemoryRouter initialEntries={['/dashboard']}>
<AuthProvider>
<AppHeader onOpenProfile={mockOnOpenProfile} onOpenAbout={mockOnOpenAbout} />
</AuthProvider>
</MemoryRouter>
);
await waitFor(() => {
const settingsBtn = screen.queryByTitle(/nav\.settings/i);
expect(settingsBtn).toBeInTheDocument();
});
});
it('shows page eyebrow and title', async () => {
const mockOnOpenProfile = vi.fn();
const mockOnOpenAbout = vi.fn();
render(
<MemoryRouter initialEntries={['/dashboard']}>
<AuthProvider>
<AppHeader onOpenProfile={mockOnOpenProfile} onOpenAbout={mockOnOpenAbout} />
</AuthProvider>
</MemoryRouter>
);
await waitFor(() => {
expect(screen.getByText(/header\.eyebrow\.overview/i)).toBeInTheDocument();
});
});
it('shows medications page title on medications route', async () => {
const mockOnOpenProfile = vi.fn();
const mockOnOpenAbout = vi.fn();
// Reset mock for this test
vi.clearAllMocks();
(global.fetch as ReturnType<typeof vi.fn>)
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({
authEnabled: false,
localAuthEnabled: true,
hasUsers: false,
needsSetup: false
})
})
.mockResolvedValueOnce({
status: 401,
ok: false
});
render(
<MemoryRouter initialEntries={['/medications']}>
<AuthProvider>
<AppHeader onOpenProfile={mockOnOpenProfile} onOpenAbout={mockOnOpenAbout} />
</AuthProvider>
</MemoryRouter>
);
await waitFor(() => {
expect(screen.getByText(/header\.eyebrow\.inventory/i)).toBeInTheDocument();
});
});
it('shows planner page title on planner route', async () => {
const mockOnOpenProfile = vi.fn();
const mockOnOpenAbout = vi.fn();
vi.clearAllMocks();
(global.fetch as ReturnType<typeof vi.fn>)
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({
authEnabled: false,
localAuthEnabled: true,
hasUsers: false,
needsSetup: false
})
})
.mockResolvedValueOnce({
status: 401,
ok: false
});
render(
<MemoryRouter initialEntries={['/planner']}>
<AuthProvider>
<AppHeader onOpenProfile={mockOnOpenProfile} onOpenAbout={mockOnOpenAbout} />
</AuthProvider>
</MemoryRouter>
);
await waitFor(() => {
expect(screen.getByText(/header\.eyebrow\.planner/i)).toBeInTheDocument();
});
});
it('shows settings page title on settings route', async () => {
const mockOnOpenProfile = vi.fn();
const mockOnOpenAbout = vi.fn();
vi.clearAllMocks();
(global.fetch as ReturnType<typeof vi.fn>)
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({
authEnabled: false,
localAuthEnabled: true,
hasUsers: false,
needsSetup: false
})
})
.mockResolvedValueOnce({
status: 401,
ok: false
});
render(
<MemoryRouter initialEntries={['/settings']}>
<AuthProvider>
<AppHeader onOpenProfile={mockOnOpenProfile} onOpenAbout={mockOnOpenAbout} />
</AuthProvider>
</MemoryRouter>
);
await waitFor(() => {
expect(screen.getByText(/header\.eyebrow\.settings/i)).toBeInTheDocument();
});
});
it('navigates when tab clicked', async () => {
const mockOnOpenProfile = vi.fn();
const mockOnOpenAbout = vi.fn();
render(
<MemoryRouter initialEntries={['/dashboard']}>
<AuthProvider>
<AppHeader onOpenProfile={mockOnOpenProfile} onOpenAbout={mockOnOpenAbout} />
</AuthProvider>
</MemoryRouter>
);
await waitFor(() => {
const buttons = screen.getAllByRole('button');
const medsBtn = buttons.find(btn => btn.textContent?.includes('nav.medications'));
if (medsBtn) {
fireEvent.click(medsBtn);
expect(mockNavigate).toHaveBeenCalledWith('/medications');
}
});
});
});