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,197 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { renderHook, act, waitFor } from '@testing-library/react';
import { useMedications } from '../../hooks/useMedications';
describe('useMedications', () => {
beforeEach(() => {
vi.clearAllMocks();
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
ok: true,
json: () => Promise.resolve([])
});
});
afterEach(() => {
vi.clearAllMocks();
});
it('initializes with empty state', () => {
const { result } = renderHook(() => useMedications());
expect(result.current.meds).toEqual([]);
expect(result.current.loading).toBe(false);
expect(result.current.saving).toBe(false);
expect(result.current.uploadingImage).toBe(false);
});
it('loads medications from API', async () => {
const mockMeds = [
{ id: 1, name: 'TestMed', packCount: 1 }
];
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockMeds)
});
const { result } = renderHook(() => useMedications());
act(() => {
result.current.loadMeds();
});
await waitFor(() => {
expect(result.current.meds).toEqual(mockMeds);
});
expect(fetch).toHaveBeenCalledWith('/api/medications');
});
it('handles API error gracefully', async () => {
(global.fetch as ReturnType<typeof vi.fn>).mockRejectedValueOnce(new Error('Network error'));
const { result } = renderHook(() => useMedications());
act(() => {
result.current.loadMeds();
});
await waitFor(() => {
expect(result.current.loading).toBe(false);
});
expect(result.current.meds).toEqual([]);
});
it('handles non-array response', async () => {
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ not: 'array' })
});
const { result } = renderHook(() => useMedications());
act(() => {
result.current.loadMeds();
});
await waitFor(() => {
expect(result.current.loading).toBe(false);
});
expect(result.current.meds).toEqual([]);
});
it('deletes medication', async () => {
const mockMeds = [{ id: 1, name: 'TestMed' }];
(global.fetch as ReturnType<typeof vi.fn>)
.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve(mockMeds) })
.mockResolvedValueOnce({ ok: true })
.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve([]) });
const mockResetForm = vi.fn();
const { result } = renderHook(() => useMedications());
// First load meds
act(() => {
result.current.loadMeds();
});
await waitFor(() => {
expect(result.current.meds).toEqual(mockMeds);
});
// Then delete
await act(async () => {
await result.current.deleteMed(1, 1, mockResetForm);
});
expect(fetch).toHaveBeenCalledWith('/api/medications/1', { method: 'DELETE' });
expect(mockResetForm).toHaveBeenCalled();
});
it('does not call resetForm if editingId does not match', async () => {
(global.fetch as ReturnType<typeof vi.fn>)
.mockResolvedValueOnce({ ok: true })
.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve([]) });
const mockResetForm = vi.fn();
const { result } = renderHook(() => useMedications());
await act(async () => {
await result.current.deleteMed(1, 2, mockResetForm);
});
expect(mockResetForm).not.toHaveBeenCalled();
});
it('uploads medication image', async () => {
(global.fetch as ReturnType<typeof vi.fn>)
.mockResolvedValueOnce({ ok: true })
.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve([]) });
const { result } = renderHook(() => useMedications());
const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' });
await act(async () => {
await result.current.uploadMedImage(1, file);
});
expect(fetch).toHaveBeenCalledWith(
'/api/medications/1/image',
expect.objectContaining({
method: 'POST',
body: expect.any(FormData)
})
);
});
it('handles image upload error', async () => {
(global.fetch as ReturnType<typeof vi.fn>).mockRejectedValueOnce(new Error('Upload failed'));
const { result } = renderHook(() => useMedications());
const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' });
await act(async () => {
await result.current.uploadMedImage(1, file);
});
expect(result.current.uploadingImage).toBe(false);
});
it('deletes medication image', async () => {
(global.fetch as ReturnType<typeof vi.fn>)
.mockResolvedValueOnce({ ok: true })
.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve([]) });
const { result } = renderHook(() => useMedications());
await act(async () => {
await result.current.deleteMedImage(1);
});
expect(fetch).toHaveBeenCalledWith('/api/medications/1/image', { method: 'DELETE' });
});
it('allows setting meds directly', () => {
const { result } = renderHook(() => useMedications());
const newMeds = [{ id: 1, name: 'NewMed' }] as any;
act(() => {
result.current.setMeds(newMeds);
});
expect(result.current.meds).toEqual(newMeds);
});
it('allows setting saving state', () => {
const { result } = renderHook(() => useMedications());
act(() => {
result.current.setSaving(true);
});
expect(result.current.saving).toBe(true);
});
});