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
+359
View File
@@ -0,0 +1,359 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { renderHook, act } from '@testing-library/react';
import { AuthProvider, useAuth, LoginForm, RegisterForm, UserProfile, AuthPage } from '../../components/Auth';
import React from 'react';
// Wrapper component for testing hooks that require AuthProvider
const wrapper = ({ children }: { children: React.ReactNode }) => (
<AuthProvider>{children}</AuthProvider>
);
describe('AuthProvider', () => {
beforeEach(() => {
vi.clearAllMocks();
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
ok: true,
json: () => Promise.resolve({ authEnabled: true, localAuthEnabled: true })
});
});
afterEach(() => {
vi.clearAllMocks();
});
it('provides auth context to children', () => {
render(
<AuthProvider>
<div data-testid="child">Child content</div>
</AuthProvider>
);
expect(screen.getByTestId('child')).toBeInTheDocument();
});
it('initializes with loading state', () => {
const { result } = renderHook(() => useAuth(), { wrapper });
// Initially loading
expect(result.current.loading).toBe(true);
});
it('fetches auth state on mount', async () => {
renderHook(() => useAuth(), { wrapper });
await waitFor(() => {
expect(fetch).toHaveBeenCalledWith('/api/auth/state');
});
});
it('throws error when useAuth is used outside AuthProvider', () => {
expect(() => {
renderHook(() => useAuth());
}).toThrow('useAuth must be used within AuthProvider');
});
});
describe('LoginForm', () => {
const mockAuthState = {
authEnabled: true,
localAuthEnabled: true,
oidcEnabled: false,
registrationEnabled: true,
hasUsers: true,
needsSetup: false,
oidcProviderName: ''
};
beforeEach(() => {
vi.clearAllMocks();
(global.fetch as ReturnType<typeof vi.fn>)
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockAuthState)
})
.mockResolvedValueOnce({
status: 401,
ok: false
});
});
it('renders login form', async () => {
render(
<AuthProvider>
<LoginForm />
</AuthProvider>
);
await waitFor(() => {
expect(screen.getByText(/MedAssist/i)).toBeInTheDocument();
});
});
it('renders username and password fields', async () => {
render(
<AuthProvider>
<LoginForm />
</AuthProvider>
);
await waitFor(() => {
expect(screen.getByLabelText(/auth\.username/i)).toBeInTheDocument();
expect(screen.getByLabelText(/auth\.password/i)).toBeInTheDocument();
});
});
it('renders remember me checkbox', async () => {
render(
<AuthProvider>
<LoginForm />
</AuthProvider>
);
await waitFor(() => {
expect(screen.getByText(/auth\.rememberMe/i)).toBeInTheDocument();
});
});
it('renders create account link when registration enabled', async () => {
const onSwitchToRegister = vi.fn();
render(
<AuthProvider>
<LoginForm onSwitchToRegister={onSwitchToRegister} />
</AuthProvider>
);
await waitFor(() => {
const createAccountBtn = screen.getByText(/auth\.createAccount/i);
expect(createAccountBtn).toBeInTheDocument();
});
});
it('handles form input changes', async () => {
render(
<AuthProvider>
<LoginForm />
</AuthProvider>
);
await waitFor(() => {
expect(screen.getByLabelText(/auth\.username/i)).toBeInTheDocument();
});
fireEvent.change(screen.getByLabelText(/auth\.username/i), { target: { value: 'testuser' } });
fireEvent.change(screen.getByLabelText(/auth\.password/i), { target: { value: 'password123' } });
expect(screen.getByLabelText(/auth\.username/i)).toHaveValue('testuser');
expect(screen.getByLabelText(/auth\.password/i)).toHaveValue('password123');
});
it('renders submit button', async () => {
render(
<AuthProvider>
<LoginForm />
</AuthProvider>
);
await waitFor(() => {
const buttons = screen.getAllByRole('button');
const submitBtn = buttons.find(btn => btn.getAttribute('type') === 'submit');
expect(submitBtn).toBeInTheDocument();
});
});
});
describe('RegisterForm', () => {
const mockAuthState = {
authEnabled: true,
localAuthEnabled: true,
oidcEnabled: false,
registrationEnabled: true,
hasUsers: false,
needsSetup: true,
oidcProviderName: ''
};
beforeEach(() => {
vi.clearAllMocks();
(global.fetch as ReturnType<typeof vi.fn>)
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockAuthState)
})
.mockResolvedValueOnce({
status: 401,
ok: false
});
});
it('renders registration form', async () => {
render(
<AuthProvider>
<RegisterForm />
</AuthProvider>
);
await waitFor(() => {
expect(screen.getByText(/MedAssist/i)).toBeInTheDocument();
});
});
it('renders all required fields', async () => {
render(
<AuthProvider>
<RegisterForm />
</AuthProvider>
);
await waitFor(() => {
// Check for username field
expect(screen.getByLabelText(/auth\.username/i)).toBeInTheDocument();
// Check for password field
expect(screen.getByLabelText(/auth\.password/i)).toBeInTheDocument();
});
});
it('renders switch to login link', async () => {
const onSwitchToLogin = vi.fn();
render(
<AuthProvider>
<RegisterForm onSwitchToLogin={onSwitchToLogin} />
</AuthProvider>
);
await waitFor(() => {
const loginLink = screen.getByText(/auth\.alreadyHaveAccount/i);
expect(loginLink).toBeInTheDocument();
});
});
it('calls onSwitchToLogin when clicked', async () => {
const onSwitchToLogin = vi.fn();
render(
<AuthProvider>
<RegisterForm onSwitchToLogin={onSwitchToLogin} />
</AuthProvider>
);
await waitFor(() => {
const loginLink = screen.getByText(/auth\.alreadyHaveAccount/i);
fireEvent.click(loginLink);
});
expect(onSwitchToLogin).toHaveBeenCalled();
});
});
describe('AuthPage', () => {
const mockAuthState = {
authEnabled: true,
localAuthEnabled: true,
oidcEnabled: false,
registrationEnabled: true,
hasUsers: true,
needsSetup: false,
oidcProviderName: ''
};
beforeEach(() => {
vi.clearAllMocks();
(global.fetch as ReturnType<typeof vi.fn>)
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockAuthState)
})
.mockResolvedValueOnce({
status: 401,
ok: false
});
});
it('renders login form by default', async () => {
render(
<AuthProvider>
<AuthPage />
</AuthProvider>
);
await waitFor(() => {
// Should show login form with username field
expect(screen.getByLabelText(/auth\.username/i)).toBeInTheDocument();
});
});
});
describe('UserProfile', () => {
const mockUser = {
id: 1,
username: 'testuser',
avatarUrl: null
};
beforeEach(() => {
vi.clearAllMocks();
(global.fetch as ReturnType<typeof vi.fn>)
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ authEnabled: true, localAuthEnabled: true })
})
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockUser)
});
});
it('renders user profile when user is logged in', async () => {
render(
<AuthProvider>
<UserProfile />
</AuthProvider>
);
await waitFor(() => {
expect(screen.getByText('testuser')).toBeInTheDocument();
});
});
it('displays user avatar initial when no avatar', async () => {
render(
<AuthProvider>
<UserProfile />
</AuthProvider>
);
await waitFor(() => {
// The avatar shows first letter of username
expect(screen.getByText('T')).toBeInTheDocument();
});
});
it('renders change password section', async () => {
render(
<AuthProvider>
<UserProfile />
</AuthProvider>
);
await waitFor(() => {
expect(screen.getByText(/auth\.changePassword/i)).toBeInTheDocument();
});
});
it('renders cancel button that calls onClose', async () => {
const onClose = vi.fn();
render(
<AuthProvider>
<UserProfile onClose={onClose} />
</AuthProvider>
);
await waitFor(() => {
const cancelBtn = screen.getByText(/common\.cancel/i);
fireEvent.click(cancelBtn);
});
expect(onClose).toHaveBeenCalled();
});
});