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:
@@ -0,0 +1,72 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import AboutModal from '../../components/AboutModal';
|
||||
|
||||
// Mock App module for constants
|
||||
vi.mock('../../App', () => ({
|
||||
FRONTEND_VERSION: '1.0.0',
|
||||
GITHUB_URL: 'https://github.com/test/repo'
|
||||
}));
|
||||
|
||||
describe('AboutModal', () => {
|
||||
const defaultProps = {
|
||||
isOpen: true,
|
||||
onClose: vi.fn()
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ version: '1.0.0' })
|
||||
});
|
||||
});
|
||||
|
||||
it('returns null when not open', () => {
|
||||
const { container } = render(<AboutModal {...defaultProps} isOpen={false} />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('renders when open', () => {
|
||||
render(<AboutModal {...defaultProps} />);
|
||||
expect(screen.getByText(/about\.appName/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays version number', () => {
|
||||
render(<AboutModal {...defaultProps} />);
|
||||
expect(screen.getByText(/1\.0\.0/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onClose when close button is clicked', () => {
|
||||
render(<AboutModal {...defaultProps} />);
|
||||
fireEvent.click(screen.getByText('×'));
|
||||
expect(defaultProps.onClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls onClose when overlay is clicked', () => {
|
||||
const { container } = render(<AboutModal {...defaultProps} />);
|
||||
const overlay = container.querySelector('.modal-overlay');
|
||||
fireEvent.click(overlay!);
|
||||
expect(defaultProps.onClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not call onClose when modal content is clicked', () => {
|
||||
const { container } = render(<AboutModal {...defaultProps} />);
|
||||
const content = container.querySelector('.about-modal');
|
||||
if (content) {
|
||||
fireEvent.click(content);
|
||||
expect(defaultProps.onClose).not.toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
|
||||
it('renders GitHub link', () => {
|
||||
render(<AboutModal {...defaultProps} />);
|
||||
const links = screen.getAllByRole('link');
|
||||
expect(links.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('fetches backend version on open', async () => {
|
||||
render(<AboutModal {...defaultProps} />);
|
||||
expect(fetch).toHaveBeenCalledWith('/api/health');
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,100 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { ConfirmModal } from '../../components/ConfirmModal';
|
||||
|
||||
describe('ConfirmModal', () => {
|
||||
const defaultProps = {
|
||||
title: 'Confirm Action',
|
||||
message: 'Are you sure you want to proceed?',
|
||||
confirmLabel: 'Yes',
|
||||
cancelLabel: 'No',
|
||||
onConfirm: vi.fn(),
|
||||
onCancel: vi.fn()
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders title', () => {
|
||||
render(<ConfirmModal {...defaultProps} />);
|
||||
expect(screen.getByText('Confirm Action')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders message as string', () => {
|
||||
render(<ConfirmModal {...defaultProps} />);
|
||||
expect(screen.getByText('Are you sure you want to proceed?')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders message as ReactNode', () => {
|
||||
render(
|
||||
<ConfirmModal
|
||||
{...defaultProps}
|
||||
message={<span data-testid="custom-message">Custom message</span>}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByTestId('custom-message')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders confirm and cancel buttons', () => {
|
||||
render(<ConfirmModal {...defaultProps} />);
|
||||
expect(screen.getByText('Yes')).toBeInTheDocument();
|
||||
expect(screen.getByText('No')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onConfirm when confirm button is clicked', () => {
|
||||
render(<ConfirmModal {...defaultProps} />);
|
||||
fireEvent.click(screen.getByText('Yes'));
|
||||
expect(defaultProps.onConfirm).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls onCancel when cancel button is clicked', () => {
|
||||
render(<ConfirmModal {...defaultProps} />);
|
||||
fireEvent.click(screen.getByText('No'));
|
||||
expect(defaultProps.onCancel).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls onCancel when close button is clicked', () => {
|
||||
render(<ConfirmModal {...defaultProps} />);
|
||||
fireEvent.click(screen.getByText('×'));
|
||||
expect(defaultProps.onCancel).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls onCancel when overlay is clicked', () => {
|
||||
const { container } = render(<ConfirmModal {...defaultProps} />);
|
||||
const overlay = container.querySelector('.modal-overlay');
|
||||
fireEvent.click(overlay!);
|
||||
expect(defaultProps.onCancel).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not call onCancel when modal content is clicked', () => {
|
||||
const { container } = render(<ConfirmModal {...defaultProps} />);
|
||||
const content = container.querySelector('.modal-content');
|
||||
fireEvent.click(content!);
|
||||
expect(defaultProps.onCancel).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('disables buttons when loading', () => {
|
||||
render(<ConfirmModal {...defaultProps} isLoading={true} />);
|
||||
expect(screen.getByText('Yes')).toBeDisabled();
|
||||
expect(screen.getByText('No')).toBeDisabled();
|
||||
});
|
||||
|
||||
it('applies primary variant by default', () => {
|
||||
render(<ConfirmModal {...defaultProps} />);
|
||||
const confirmBtn = screen.getByText('Yes');
|
||||
expect(confirmBtn.className).toContain('primary');
|
||||
});
|
||||
|
||||
it('applies danger variant when specified', () => {
|
||||
render(<ConfirmModal {...defaultProps} confirmVariant="danger" />);
|
||||
const confirmBtn = screen.getByText('Yes');
|
||||
expect(confirmBtn.className).toContain('danger');
|
||||
});
|
||||
|
||||
it('applies success variant when specified', () => {
|
||||
render(<ConfirmModal {...defaultProps} confirmVariant="success" />);
|
||||
const confirmBtn = screen.getByText('Yes');
|
||||
expect(confirmBtn.className).toContain('success');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import ExportModal from '../../components/ExportModal';
|
||||
|
||||
describe('ExportModal', () => {
|
||||
const defaultProps = {
|
||||
isOpen: true,
|
||||
onClose: vi.fn(),
|
||||
onExport: vi.fn(),
|
||||
exporting: false
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('returns null when not open', () => {
|
||||
const { container } = render(<ExportModal {...defaultProps} isOpen={false} />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('renders when open', () => {
|
||||
render(<ExportModal {...defaultProps} />);
|
||||
expect(screen.getByText(/exportImport\.exportOptions/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onClose when close button is clicked', () => {
|
||||
render(<ExportModal {...defaultProps} />);
|
||||
fireEvent.click(screen.getByText('×'));
|
||||
expect(defaultProps.onClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls onClose when overlay is clicked', () => {
|
||||
const { container } = render(<ExportModal {...defaultProps} />);
|
||||
const overlay = container.querySelector('.modal-overlay');
|
||||
fireEvent.click(overlay!);
|
||||
expect(defaultProps.onClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders export options', () => {
|
||||
const { container } = render(<ExportModal {...defaultProps} />);
|
||||
// Should have action card buttons
|
||||
const actionCards = container.querySelectorAll('.action-card');
|
||||
expect(actionCards.length).toBe(2);
|
||||
});
|
||||
|
||||
it('calls onExport with true when export with images button clicked', () => {
|
||||
const { container } = render(<ExportModal {...defaultProps} />);
|
||||
const actionCards = container.querySelectorAll('.action-card');
|
||||
fireEvent.click(actionCards[0]);
|
||||
expect(defaultProps.onClose).toHaveBeenCalled();
|
||||
expect(defaultProps.onExport).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('calls onExport with false when export data only button clicked', () => {
|
||||
const { container } = render(<ExportModal {...defaultProps} />);
|
||||
const actionCards = container.querySelectorAll('.action-card');
|
||||
fireEvent.click(actionCards[1]);
|
||||
expect(defaultProps.onClose).toHaveBeenCalled();
|
||||
expect(defaultProps.onExport).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('disables buttons when exporting', () => {
|
||||
const { container } = render(<ExportModal {...defaultProps} exporting={true} />);
|
||||
const actionCards = container.querySelectorAll('.action-card');
|
||||
actionCards.forEach(card => {
|
||||
expect(card).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders cancel button', () => {
|
||||
render(<ExportModal {...defaultProps} />);
|
||||
expect(screen.getByText(/exportImport\.cancelButton/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onClose when cancel button is clicked', () => {
|
||||
render(<ExportModal {...defaultProps} />);
|
||||
fireEvent.click(screen.getByText(/exportImport\.cancelButton/i));
|
||||
expect(defaultProps.onClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { Lightbox } from '../../components/Lightbox';
|
||||
|
||||
describe('Lightbox', () => {
|
||||
const defaultProps = {
|
||||
src: '/test-image.jpg',
|
||||
alt: 'Test Image',
|
||||
onClose: vi.fn()
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders image with correct src and alt', () => {
|
||||
render(<Lightbox {...defaultProps} />);
|
||||
|
||||
const img = screen.getByAltText('Test Image');
|
||||
expect(img).toBeInTheDocument();
|
||||
expect(img).toHaveAttribute('src', '/test-image.jpg');
|
||||
});
|
||||
|
||||
it('renders close button', () => {
|
||||
render(<Lightbox {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText('×')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onClose when close button is clicked', () => {
|
||||
const onClose = vi.fn();
|
||||
render(<Lightbox {...defaultProps} onClose={onClose} />);
|
||||
|
||||
fireEvent.click(screen.getByText('×'));
|
||||
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls onClose when overlay is clicked', () => {
|
||||
const onClose = vi.fn();
|
||||
const { container } = render(<Lightbox {...defaultProps} onClose={onClose} />);
|
||||
|
||||
const overlay = container.querySelector('.lightbox-overlay');
|
||||
fireEvent.click(overlay!);
|
||||
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not call onClose when image is clicked', () => {
|
||||
const onClose = vi.fn();
|
||||
render(<Lightbox {...defaultProps} onClose={onClose} />);
|
||||
|
||||
fireEvent.click(screen.getByAltText('Test Image'));
|
||||
|
||||
expect(onClose).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('applies correct CSS classes', () => {
|
||||
const { container } = render(<Lightbox {...defaultProps} />);
|
||||
|
||||
expect(container.querySelector('.lightbox-overlay')).toBeInTheDocument();
|
||||
expect(container.querySelector('.lightbox-close')).toBeInTheDocument();
|
||||
expect(container.querySelector('.lightbox-image')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,211 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { MedDetailModal } from '../../components/MedDetailModal';
|
||||
import type { Medication, Coverage, StockThresholds, RefillEntry } from '../../types';
|
||||
|
||||
const defaultSettings: StockThresholds = {
|
||||
lowStockDays: 7,
|
||||
normalStockDays: 30,
|
||||
highStockDays: 90
|
||||
};
|
||||
|
||||
const mockMedication: Medication = {
|
||||
id: 1,
|
||||
name: 'Test Med',
|
||||
genericName: 'Generic Name',
|
||||
packCount: 1,
|
||||
blistersPerPack: 1,
|
||||
pillsPerBlister: 30,
|
||||
looseTablets: 0,
|
||||
takenBy: ['John'],
|
||||
blisters: [{ usage: 1, every: 1, start: '2024-01-01T09:00:00' }],
|
||||
updatedAt: null,
|
||||
expiryDate: '2025-12-31',
|
||||
notes: 'Test notes'
|
||||
};
|
||||
|
||||
const mockCoverage: Coverage = {
|
||||
name: 'Test Med',
|
||||
medsLeft: 25,
|
||||
daysLeft: 25,
|
||||
depletionDate: '2024-04-01',
|
||||
depletionTime: Date.now() + 25 * 86400000,
|
||||
nextDose: null
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
selectedMed: mockMedication,
|
||||
coverage: { all: [mockCoverage] },
|
||||
settings: defaultSettings,
|
||||
showImageLightbox: false,
|
||||
showRefillModal: false,
|
||||
showEditStockModal: false,
|
||||
onClose: vi.fn(),
|
||||
onOpenImageLightbox: vi.fn(),
|
||||
onCloseImageLightbox: vi.fn(),
|
||||
onOpenRefillModal: vi.fn(),
|
||||
onCloseRefillModal: vi.fn(),
|
||||
onOpenEditStockModal: vi.fn(),
|
||||
onCloseEditStockModal: vi.fn(),
|
||||
refillPacks: 0,
|
||||
onRefillPacksChange: vi.fn(),
|
||||
refillLoose: 0,
|
||||
onRefillLooseChange: vi.fn(),
|
||||
refillSaving: false,
|
||||
refillHistory: [] as RefillEntry[],
|
||||
refillHistoryExpanded: false,
|
||||
onRefillHistoryExpandedChange: vi.fn(),
|
||||
onSubmitRefill: vi.fn(),
|
||||
editStockFullBlisters: 0,
|
||||
onEditStockFullBlistersChange: vi.fn(),
|
||||
editStockPartialBlisterPills: 0,
|
||||
onEditStockPartialBlisterPillsChange: vi.fn(),
|
||||
editStockSaving: false,
|
||||
onSubmitStockCorrection: vi.fn()
|
||||
};
|
||||
|
||||
describe('MedDetailModal', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders nothing when selectedMed is null', () => {
|
||||
render(<MedDetailModal {...defaultProps} selectedMed={null} />);
|
||||
|
||||
expect(screen.queryByText('Test Med')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders modal when medication is selected', () => {
|
||||
render(<MedDetailModal {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText('Test Med')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays medication name', () => {
|
||||
render(<MedDetailModal {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText('Test Med')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays generic name', () => {
|
||||
render(<MedDetailModal {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText('Generic Name')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders close button', () => {
|
||||
render(<MedDetailModal {...defaultProps} />);
|
||||
|
||||
const closeBtn = screen.getByText('×');
|
||||
expect(closeBtn).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onClose when close button clicked', () => {
|
||||
const onClose = vi.fn();
|
||||
render(<MedDetailModal {...defaultProps} onClose={onClose} />);
|
||||
|
||||
const closeBtn = screen.getByText('×');
|
||||
fireEvent.click(closeBtn);
|
||||
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('calls onClose when overlay clicked', () => {
|
||||
const onClose = vi.fn();
|
||||
render(<MedDetailModal {...defaultProps} onClose={onClose} />);
|
||||
|
||||
const overlay = document.querySelector('.modal-overlay');
|
||||
if (overlay) {
|
||||
fireEvent.click(overlay);
|
||||
}
|
||||
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not call onClose when modal content clicked', () => {
|
||||
const onClose = vi.fn();
|
||||
render(<MedDetailModal {...defaultProps} onClose={onClose} />);
|
||||
|
||||
const content = document.querySelector('.modal-content');
|
||||
if (content) {
|
||||
fireEvent.click(content);
|
||||
}
|
||||
|
||||
expect(onClose).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('displays notes when available', () => {
|
||||
render(<MedDetailModal {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText('Test notes')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays schedule information', () => {
|
||||
render(<MedDetailModal {...defaultProps} />);
|
||||
|
||||
// Should have schedule section
|
||||
const scheduleSection = document.querySelector('.med-detail-schedules');
|
||||
expect(scheduleSection).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders med detail header', () => {
|
||||
render(<MedDetailModal {...defaultProps} />);
|
||||
|
||||
const header = document.querySelector('.med-detail-header');
|
||||
expect(header).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders med detail body', () => {
|
||||
render(<MedDetailModal {...defaultProps} />);
|
||||
|
||||
const body = document.querySelector('.med-detail-body');
|
||||
expect(body).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('MedDetailModal without coverage', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('works without coverage data', () => {
|
||||
render(<MedDetailModal {...defaultProps} coverage={{ all: [] }} />);
|
||||
|
||||
// Should still render the medication name
|
||||
expect(screen.getByText('Test Med')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('MedDetailModal without optional fields', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('works without generic name', () => {
|
||||
const med = { ...mockMedication, genericName: null };
|
||||
render(<MedDetailModal {...defaultProps} selectedMed={med} />);
|
||||
|
||||
expect(screen.getByText('Test Med')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('works without notes', () => {
|
||||
const med = { ...mockMedication, notes: null };
|
||||
render(<MedDetailModal {...defaultProps} selectedMed={med} />);
|
||||
|
||||
expect(screen.getByText('Test Med')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('works without takenBy', () => {
|
||||
const med = { ...mockMedication, takenBy: [] };
|
||||
render(<MedDetailModal {...defaultProps} selectedMed={med} />);
|
||||
|
||||
expect(screen.getByText('Test Med')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('works without expiryDate', () => {
|
||||
const med = { ...mockMedication, expiryDate: null };
|
||||
render(<MedDetailModal {...defaultProps} selectedMed={med} />);
|
||||
|
||||
expect(screen.getByText('Test Med')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { MedicationAvatar } from '../../components/MedicationAvatar';
|
||||
|
||||
describe('MedicationAvatar', () => {
|
||||
it('renders initials when no image provided', () => {
|
||||
render(<MedicationAvatar name="Test Medication" />);
|
||||
|
||||
expect(screen.getByText('TM')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('uses first two initials from medication name', () => {
|
||||
render(<MedicationAvatar name="Very Long Medication Name" />);
|
||||
|
||||
expect(screen.getByText('VL')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles single word names', () => {
|
||||
render(<MedicationAvatar name="Aspirin" />);
|
||||
|
||||
expect(screen.getByText('A')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders image when imageUrl provided', () => {
|
||||
render(<MedicationAvatar name="Test Med" imageUrl="test-image.jpg" />);
|
||||
|
||||
const img = screen.getByAltText('Test Med');
|
||||
expect(img).toBeInTheDocument();
|
||||
expect(img).toHaveAttribute('src', '/api/images/test-image.jpg');
|
||||
});
|
||||
|
||||
it('applies small size class by default', () => {
|
||||
const { container } = render(<MedicationAvatar name="Test" />);
|
||||
|
||||
expect(container.querySelector('.med-avatar-sm')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('applies medium size class', () => {
|
||||
const { container } = render(<MedicationAvatar name="Test" size="md" />);
|
||||
|
||||
expect(container.querySelector('.med-avatar-md')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('applies large size class', () => {
|
||||
const { container } = render(<MedicationAvatar name="Test" size="lg" />);
|
||||
|
||||
expect(container.querySelector('.med-avatar-lg')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles empty name with fallback', () => {
|
||||
render(<MedicationAvatar name="" />);
|
||||
|
||||
expect(screen.getByText('?')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('converts initials to uppercase', () => {
|
||||
render(<MedicationAvatar name="lower case" />);
|
||||
|
||||
expect(screen.getByText('LC')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('adds initials class when no image', () => {
|
||||
const { container } = render(<MedicationAvatar name="Test" />);
|
||||
|
||||
expect(container.querySelector('.med-avatar-initials')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,271 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { MobileEditModal } from '../../components/MobileEditModal';
|
||||
import type { FormState, FormBlister } from '../../types';
|
||||
|
||||
const defaultForm: FormState = {
|
||||
name: '',
|
||||
genericName: '',
|
||||
takenBy: [],
|
||||
packCount: '1',
|
||||
blistersPerPack: '1',
|
||||
pillsPerBlister: '1',
|
||||
looseTablets: '0',
|
||||
pillWeightMg: '',
|
||||
expiryDate: '',
|
||||
notes: '',
|
||||
intakeRemindersEnabled: false,
|
||||
blisters: [{
|
||||
usage: '1',
|
||||
every: '1',
|
||||
startDate: '2024-01-01',
|
||||
startTime: '09:00'
|
||||
}]
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
show: true,
|
||||
editingId: null,
|
||||
form: defaultForm,
|
||||
onFormChange: vi.fn(),
|
||||
fieldErrors: {},
|
||||
saving: false,
|
||||
formSaved: false,
|
||||
formChanged: false,
|
||||
hasValidationErrors: false,
|
||||
takenByInput: '',
|
||||
onTakenByInputChange: vi.fn(),
|
||||
existingPeople: [],
|
||||
onAddTakenByPerson: vi.fn(),
|
||||
onRemoveTakenByPerson: vi.fn(),
|
||||
onTakenByKeyDown: vi.fn(),
|
||||
onSetBlisterValue: vi.fn(),
|
||||
onAddBlister: vi.fn(),
|
||||
onRemoveBlister: vi.fn(),
|
||||
onHandleValueChange: vi.fn(),
|
||||
refillPacks: 0,
|
||||
onRefillPacksChange: vi.fn(),
|
||||
refillLoose: 0,
|
||||
onRefillLooseChange: vi.fn(),
|
||||
refillSaving: false,
|
||||
onSubmitRefill: vi.fn(),
|
||||
meds: [],
|
||||
onUploadMedImage: vi.fn(),
|
||||
onDeleteMedImage: vi.fn(),
|
||||
onClose: vi.fn(),
|
||||
onResetForm: vi.fn(),
|
||||
onSaveMedication: vi.fn()
|
||||
};
|
||||
|
||||
describe('MobileEditModal', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders nothing when show is false', () => {
|
||||
render(<MobileEditModal {...defaultProps} show={false} />);
|
||||
|
||||
expect(screen.queryByText(/form\.newEntry/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders modal when show is true', () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
// Should render the modal overlay
|
||||
const modal = document.querySelector('.modal-overlay');
|
||||
expect(modal).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows new entry title when not editing', () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText(/form\.newEntry/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows edit entry title when editing', () => {
|
||||
render(<MobileEditModal {...defaultProps} editingId={1} />);
|
||||
|
||||
expect(screen.getByText(/form\.editEntry/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders close button', () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
const closeBtn = document.querySelector('.modal-close');
|
||||
expect(closeBtn).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onClose when close button clicked', () => {
|
||||
const onClose = vi.fn();
|
||||
const onResetForm = vi.fn();
|
||||
render(<MobileEditModal {...defaultProps} onClose={onClose} onResetForm={onResetForm} />);
|
||||
|
||||
const closeBtn = document.querySelector('.modal-close');
|
||||
if (closeBtn) {
|
||||
fireEvent.click(closeBtn);
|
||||
}
|
||||
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
expect(onResetForm).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('renders form element', () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
const form = document.querySelector('form');
|
||||
expect(form).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders name input', () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText(/form\.commercialName/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders generic name input', () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText(/form\.genericName/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders packs input', () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText(/form\.packs/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders blisters per pack input', () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText(/form\.blistersPerPack/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders pills per blister input', () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText(/form\.pillsPerBlister/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders loose tablets input', () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText(/form\.loose/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders intake schedules section', () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText(/form\.blisters\.title/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders save button', () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
const saveBtn = document.querySelector('button[type="submit"]');
|
||||
expect(saveBtn).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('disables save when saving', () => {
|
||||
render(<MobileEditModal {...defaultProps} saving={true} />);
|
||||
|
||||
const saveBtn = document.querySelector('button[type="submit"]') as HTMLButtonElement;
|
||||
expect(saveBtn).toBeDisabled();
|
||||
});
|
||||
|
||||
it('disables save when has validation errors', () => {
|
||||
render(<MobileEditModal {...defaultProps} hasValidationErrors={true} />);
|
||||
|
||||
const saveBtn = document.querySelector('button[type="submit"]') as HTMLButtonElement;
|
||||
expect(saveBtn).toBeDisabled();
|
||||
});
|
||||
|
||||
it('renders add intake button', () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText(/form\.blisters\.addIntake/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onAddBlister when add intake clicked', () => {
|
||||
const onAddBlister = vi.fn();
|
||||
render(<MobileEditModal {...defaultProps} onAddBlister={onAddBlister} />);
|
||||
|
||||
const addBtn = screen.getByText(/form\.blisters\.addIntake/i);
|
||||
fireEvent.click(addBtn);
|
||||
|
||||
expect(onAddBlister).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('renders modal content', () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
const content = document.querySelector('.modal-content.edit-modal');
|
||||
expect(content).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders edit modal header', () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
const header = document.querySelector('.edit-modal-header');
|
||||
expect(header).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('MobileEditModal with existing people', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders modal with existing people prop', () => {
|
||||
render(<MobileEditModal {...defaultProps} existingPeople={['John', 'Jane']} />);
|
||||
|
||||
// Should render the modal - suggestions shown on input focus
|
||||
const modal = document.querySelector('.modal-overlay');
|
||||
expect(modal).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('MobileEditModal with form errors', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('shows name error when present', () => {
|
||||
render(<MobileEditModal {...defaultProps} fieldErrors={{ name: 'Name is required' }} />);
|
||||
|
||||
expect(screen.getByText('Name is required')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows notes error when present', () => {
|
||||
render(<MobileEditModal {...defaultProps} fieldErrors={{ notes: 'Notes too long' }} />);
|
||||
|
||||
expect(screen.getByText('Notes too long')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('MobileEditModal blister management', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders blister rows', () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
const blisterRows = document.querySelectorAll('.blister-row');
|
||||
expect(blisterRows.length).toBe(1);
|
||||
});
|
||||
|
||||
it('renders remove button for each blister', () => {
|
||||
const form = {
|
||||
...defaultForm,
|
||||
blisters: [
|
||||
{ usage: '1', every: '1', startDate: '2024-01-01', startTime: '09:00' },
|
||||
{ usage: '2', every: '7', startDate: '2024-01-01', startTime: '10:00' }
|
||||
]
|
||||
};
|
||||
|
||||
render(<MobileEditModal {...defaultProps} form={form} />);
|
||||
|
||||
const blisterRows = document.querySelectorAll('.blister-row');
|
||||
expect(blisterRows.length).toBe(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import ProfileModal from '../../components/ProfileModal';
|
||||
|
||||
// Mock Auth UserProfile component
|
||||
vi.mock('../../components/Auth', () => ({
|
||||
UserProfile: ({ onClose }: { onClose: () => void }) => (
|
||||
<div data-testid="user-profile">User Profile Content</div>
|
||||
)
|
||||
}));
|
||||
|
||||
describe('ProfileModal', () => {
|
||||
it('renders nothing when not open', () => {
|
||||
const onClose = vi.fn();
|
||||
render(<ProfileModal isOpen={false} onClose={onClose} />);
|
||||
|
||||
expect(screen.queryByTestId('user-profile')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders modal when open', () => {
|
||||
const onClose = vi.fn();
|
||||
render(<ProfileModal isOpen={true} onClose={onClose} />);
|
||||
|
||||
expect(screen.getByTestId('user-profile')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders close button', () => {
|
||||
const onClose = vi.fn();
|
||||
render(<ProfileModal isOpen={true} onClose={onClose} />);
|
||||
|
||||
const closeBtn = screen.getByText('×');
|
||||
expect(closeBtn).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onClose when close button clicked', () => {
|
||||
const onClose = vi.fn();
|
||||
render(<ProfileModal isOpen={true} onClose={onClose} />);
|
||||
|
||||
const closeBtn = screen.getByText('×');
|
||||
fireEvent.click(closeBtn);
|
||||
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('calls onClose when overlay clicked', () => {
|
||||
const onClose = vi.fn();
|
||||
render(<ProfileModal isOpen={true} onClose={onClose} />);
|
||||
|
||||
const overlay = document.querySelector('.modal-overlay');
|
||||
if (overlay) {
|
||||
fireEvent.click(overlay);
|
||||
}
|
||||
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not call onClose when modal content clicked', () => {
|
||||
const onClose = vi.fn();
|
||||
render(<ProfileModal isOpen={true} onClose={onClose} />);
|
||||
|
||||
const content = document.querySelector('.modal-content');
|
||||
if (content) {
|
||||
fireEvent.click(content);
|
||||
}
|
||||
|
||||
expect(onClose).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { ShareDialog } from '../../components/ShareDialog';
|
||||
|
||||
describe('ShareDialog', () => {
|
||||
const defaultProps = {
|
||||
show: true,
|
||||
sharePeople: ['Alice', 'Bob'],
|
||||
shareSelectedPerson: 'Alice',
|
||||
onShareSelectedPersonChange: vi.fn(),
|
||||
shareSelectedDays: 30,
|
||||
onShareSelectedDaysChange: vi.fn(),
|
||||
shareGenerating: false,
|
||||
shareLink: null,
|
||||
onShareLinkChange: vi.fn(),
|
||||
shareCopied: false,
|
||||
onShareCopiedChange: vi.fn(),
|
||||
onClose: vi.fn(),
|
||||
onGenerateShareLink: vi.fn(),
|
||||
onCopyShareLink: vi.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('returns null when show is false', () => {
|
||||
const { container } = render(<ShareDialog {...defaultProps} show={false} />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('renders dialog when show is true', () => {
|
||||
render(<ShareDialog {...defaultProps} />);
|
||||
expect(screen.getByText(/share\.title/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders no people message when sharePeople is empty', () => {
|
||||
render(<ShareDialog {...defaultProps} sharePeople={[]} />);
|
||||
expect(screen.getByText(/share\.noPeople/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders person selection dropdown', () => {
|
||||
render(<ShareDialog {...defaultProps} />);
|
||||
expect(screen.getByRole('option', { name: 'Alice' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('option', { name: 'Bob' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders period selection dropdown', () => {
|
||||
render(<ShareDialog {...defaultProps} />);
|
||||
// The dropdown renders with 3 options for time periods
|
||||
const options = screen.getAllByRole('option');
|
||||
expect(options.length).toBeGreaterThanOrEqual(3);
|
||||
});
|
||||
|
||||
it('calls onClose when close button is clicked', () => {
|
||||
render(<ShareDialog {...defaultProps} />);
|
||||
fireEvent.click(screen.getByText('×'));
|
||||
expect(defaultProps.onClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls onClose when overlay is clicked', () => {
|
||||
const { container } = render(<ShareDialog {...defaultProps} />);
|
||||
const overlay = container.querySelector('.modal-overlay');
|
||||
fireEvent.click(overlay!);
|
||||
expect(defaultProps.onClose).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows generated link', () => {
|
||||
render(<ShareDialog {...defaultProps} shareLink="http://example.com/share/abc123" />);
|
||||
const input = screen.getByRole('textbox');
|
||||
expect(input).toHaveValue('http://example.com/share/abc123');
|
||||
});
|
||||
|
||||
it('calls onCopyShareLink when copy button is clicked', () => {
|
||||
render(<ShareDialog {...defaultProps} shareLink="http://example.com/share/abc123" />);
|
||||
fireEvent.click(screen.getByText('📋'));
|
||||
expect(defaultProps.onCopyShareLink).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows copied indicator after copy', () => {
|
||||
render(<ShareDialog {...defaultProps} shareLink="http://example.com/share/abc123" shareCopied={true} />);
|
||||
expect(screen.getByText('✓')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('selects link text when input is clicked', () => {
|
||||
render(<ShareDialog {...defaultProps} shareLink="http://example.com/share/abc123" />);
|
||||
const input = screen.getByRole('textbox') as HTMLInputElement;
|
||||
const selectMock = vi.fn();
|
||||
input.select = selectMock;
|
||||
fireEvent.click(input);
|
||||
expect(selectMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { MemoryRouter, Routes, Route } from 'react-router-dom';
|
||||
import { SharedSchedule } from '../../components/SharedSchedule';
|
||||
|
||||
describe('SharedSchedule', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it('shows loading state initially', () => {
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/share/test-token']}>
|
||||
<Routes>
|
||||
<Route path="/share/:token" element={<SharedSchedule />} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
// Should show loading state - actual translation key is common.loading
|
||||
expect(screen.getByText(/common\.loading/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders app title during loading', () => {
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/share/test-token']}>
|
||||
<Routes>
|
||||
<Route path="/share/:token" element={<SharedSchedule />} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
expect(screen.getByText(/MedAssist/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders shared schedule page container', () => {
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/share/test-token']}>
|
||||
<Routes>
|
||||
<Route path="/share/:token" element={<SharedSchedule />} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
const container = document.querySelector('.shared-schedule-page');
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders loading state container', () => {
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/share/test-token']}>
|
||||
<Routes>
|
||||
<Route path="/share/:token" element={<SharedSchedule />} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
const loading = document.querySelector('.shared-schedule-loading');
|
||||
expect(loading).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('has correct initial theme', () => {
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/share/test-token']}>
|
||||
<Routes>
|
||||
<Route path="/share/:token" element={<SharedSchedule />} />
|
||||
</Routes>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
// Default theme should be dark
|
||||
expect(document.documentElement.getAttribute('data-theme')).toBe('dark');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,138 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { TagInput } from '../../components/TagInput';
|
||||
|
||||
describe('TagInput', () => {
|
||||
const defaultProps = {
|
||||
tags: [] as string[],
|
||||
inputValue: '',
|
||||
onInputChange: vi.fn(),
|
||||
onAddTag: vi.fn(),
|
||||
onRemoveTag: vi.fn()
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders input element', () => {
|
||||
render(<TagInput {...defaultProps} />);
|
||||
expect(screen.getByRole('combobox')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders existing tags', () => {
|
||||
render(<TagInput {...defaultProps} tags={['Tag1', 'Tag2']} />);
|
||||
expect(screen.getByText('Tag1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Tag2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onInputChange when typing', () => {
|
||||
render(<TagInput {...defaultProps} />);
|
||||
const input = screen.getByRole('combobox');
|
||||
fireEvent.change(input, { target: { value: 'new tag' } });
|
||||
expect(defaultProps.onInputChange).toHaveBeenCalledWith('new tag');
|
||||
});
|
||||
|
||||
it('calls onAddTag when Enter is pressed with value', () => {
|
||||
render(<TagInput {...defaultProps} inputValue="new tag" />);
|
||||
const input = screen.getByRole('combobox');
|
||||
fireEvent.keyDown(input, { key: 'Enter' });
|
||||
expect(defaultProps.onAddTag).toHaveBeenCalledWith('new tag');
|
||||
});
|
||||
|
||||
it('calls onAddTag when comma is pressed with value', () => {
|
||||
render(<TagInput {...defaultProps} inputValue="new tag" />);
|
||||
const input = screen.getByRole('combobox');
|
||||
fireEvent.keyDown(input, { key: ',' });
|
||||
expect(defaultProps.onAddTag).toHaveBeenCalledWith('new tag');
|
||||
});
|
||||
|
||||
it('does not call onAddTag when Enter pressed with empty value', () => {
|
||||
render(<TagInput {...defaultProps} inputValue="" />);
|
||||
const input = screen.getByRole('combobox');
|
||||
fireEvent.keyDown(input, { key: 'Enter' });
|
||||
expect(defaultProps.onAddTag).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls onRemoveTag when Backspace is pressed with empty input', () => {
|
||||
render(<TagInput {...defaultProps} tags={['Tag1', 'Tag2']} inputValue="" />);
|
||||
const input = screen.getByRole('combobox');
|
||||
fireEvent.keyDown(input, { key: 'Backspace' });
|
||||
expect(defaultProps.onRemoveTag).toHaveBeenCalledWith('Tag2');
|
||||
});
|
||||
|
||||
it('does not call onRemoveTag when Backspace pressed with value', () => {
|
||||
render(<TagInput {...defaultProps} tags={['Tag1']} inputValue="text" />);
|
||||
const input = screen.getByRole('combobox');
|
||||
fireEvent.keyDown(input, { key: 'Backspace' });
|
||||
expect(defaultProps.onRemoveTag).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls onRemoveTag when tag remove button is clicked', () => {
|
||||
render(<TagInput {...defaultProps} tags={['Tag1', 'Tag2']} />);
|
||||
const removeButtons = screen.getAllByText('×');
|
||||
fireEvent.click(removeButtons[0]);
|
||||
expect(defaultProps.onRemoveTag).toHaveBeenCalledWith('Tag1');
|
||||
});
|
||||
|
||||
it('calls onAddTag on blur when there is a value', () => {
|
||||
render(<TagInput {...defaultProps} inputValue="pending tag" />);
|
||||
const input = screen.getByRole('combobox');
|
||||
fireEvent.blur(input);
|
||||
expect(defaultProps.onAddTag).toHaveBeenCalledWith('pending tag');
|
||||
});
|
||||
|
||||
it('shows placeholder when no tags', () => {
|
||||
render(<TagInput {...defaultProps} placeholder="Enter tags" />);
|
||||
expect(screen.getByPlaceholderText('Enter tags')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows addPlaceholder when tags exist', () => {
|
||||
render(
|
||||
<TagInput
|
||||
{...defaultProps}
|
||||
tags={['Tag1']}
|
||||
placeholder="Enter tags"
|
||||
addPlaceholder="Add more"
|
||||
/>
|
||||
);
|
||||
expect(screen.getByPlaceholderText('Add more')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('applies maxLength attribute', () => {
|
||||
render(<TagInput {...defaultProps} maxLength={50} />);
|
||||
const input = screen.getByRole('combobox');
|
||||
expect(input).toHaveAttribute('maxLength', '50');
|
||||
});
|
||||
|
||||
it('shows error message when provided', () => {
|
||||
render(<TagInput {...defaultProps} error="This field is required" />);
|
||||
expect(screen.getByText('This field is required')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders datalist for suggestions', () => {
|
||||
const { container } = render(
|
||||
<TagInput
|
||||
{...defaultProps}
|
||||
suggestions={['Option1', 'Option2']}
|
||||
datalistId="test-datalist"
|
||||
/>
|
||||
);
|
||||
const datalist = container.querySelector('#test-datalist');
|
||||
expect(datalist).toBeInTheDocument();
|
||||
expect(datalist?.querySelectorAll('option').length).toBe(2);
|
||||
});
|
||||
|
||||
it('excludes already selected tags from suggestions', () => {
|
||||
const { container } = render(
|
||||
<TagInput
|
||||
{...defaultProps}
|
||||
tags={['Option1']}
|
||||
suggestions={['Option1', 'Option2', 'Option3']}
|
||||
datalistId="test-datalist"
|
||||
/>
|
||||
);
|
||||
const datalist = container.querySelector('#test-datalist');
|
||||
expect(datalist?.querySelectorAll('option').length).toBe(2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,281 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { UserFilterModal } from '../../components/UserFilterModal';
|
||||
import type { Medication, Coverage, StockThresholds } from '../../types';
|
||||
|
||||
const defaultSettings: StockThresholds = {
|
||||
lowStockDays: 7,
|
||||
normalStockDays: 30,
|
||||
highStockDays: 90
|
||||
};
|
||||
|
||||
const mockMedication: Medication = {
|
||||
id: 1,
|
||||
name: 'Test Med',
|
||||
genericName: 'Generic Name',
|
||||
packCount: 1,
|
||||
blistersPerPack: 1,
|
||||
pillsPerBlister: 30,
|
||||
looseTablets: 0,
|
||||
takenBy: ['John'],
|
||||
blisters: [{ usage: 1, every: 1, start: '2024-01-01T09:00:00' }],
|
||||
updatedAt: null
|
||||
};
|
||||
|
||||
const mockCoverage: Coverage = {
|
||||
name: 'Test Med',
|
||||
medsLeft: 25,
|
||||
daysLeft: 25,
|
||||
depletionDate: null,
|
||||
depletionTime: null,
|
||||
nextDose: null
|
||||
};
|
||||
|
||||
describe('UserFilterModal', () => {
|
||||
it('renders nothing when selectedUser is null', () => {
|
||||
const onClose = vi.fn();
|
||||
const onOpenMedDetail = vi.fn();
|
||||
|
||||
render(
|
||||
<UserFilterModal
|
||||
selectedUser={null}
|
||||
meds={[mockMedication]}
|
||||
coverage={{ all: [mockCoverage] }}
|
||||
settings={defaultSettings}
|
||||
onClose={onClose}
|
||||
onOpenMedDetail={onOpenMedDetail}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByText(/modal\.userMedications/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders modal when selectedUser is provided', () => {
|
||||
const onClose = vi.fn();
|
||||
const onOpenMedDetail = vi.fn();
|
||||
|
||||
render(
|
||||
<UserFilterModal
|
||||
selectedUser="John"
|
||||
meds={[mockMedication]}
|
||||
coverage={{ all: [mockCoverage] }}
|
||||
settings={defaultSettings}
|
||||
onClose={onClose}
|
||||
onOpenMedDetail={onOpenMedDetail}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText(/modal\.userMedications/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays user avatar', () => {
|
||||
const onClose = vi.fn();
|
||||
const onOpenMedDetail = vi.fn();
|
||||
|
||||
render(
|
||||
<UserFilterModal
|
||||
selectedUser="John"
|
||||
meds={[mockMedication]}
|
||||
coverage={{ all: [mockCoverage] }}
|
||||
settings={defaultSettings}
|
||||
onClose={onClose}
|
||||
onOpenMedDetail={onOpenMedDetail}
|
||||
/>
|
||||
);
|
||||
|
||||
// Avatar should show first letter
|
||||
expect(screen.getByText('J')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays medications for selected user', () => {
|
||||
const onClose = vi.fn();
|
||||
const onOpenMedDetail = vi.fn();
|
||||
|
||||
render(
|
||||
<UserFilterModal
|
||||
selectedUser="John"
|
||||
meds={[mockMedication]}
|
||||
coverage={{ all: [mockCoverage] }}
|
||||
settings={defaultSettings}
|
||||
onClose={onClose}
|
||||
onOpenMedDetail={onOpenMedDetail}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Test Med')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays generic name when available', () => {
|
||||
const onClose = vi.fn();
|
||||
const onOpenMedDetail = vi.fn();
|
||||
|
||||
render(
|
||||
<UserFilterModal
|
||||
selectedUser="John"
|
||||
meds={[mockMedication]}
|
||||
coverage={{ all: [mockCoverage] }}
|
||||
settings={defaultSettings}
|
||||
onClose={onClose}
|
||||
onOpenMedDetail={onOpenMedDetail}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Generic Name')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows empty message when user has no medications', () => {
|
||||
const onClose = vi.fn();
|
||||
const onOpenMedDetail = vi.fn();
|
||||
|
||||
render(
|
||||
<UserFilterModal
|
||||
selectedUser="Jane"
|
||||
meds={[mockMedication]}
|
||||
coverage={{ all: [mockCoverage] }}
|
||||
settings={defaultSettings}
|
||||
onClose={onClose}
|
||||
onOpenMedDetail={onOpenMedDetail}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText(/modal\.noMedsForUser/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onClose when close button clicked', () => {
|
||||
const onClose = vi.fn();
|
||||
const onOpenMedDetail = vi.fn();
|
||||
|
||||
render(
|
||||
<UserFilterModal
|
||||
selectedUser="John"
|
||||
meds={[mockMedication]}
|
||||
coverage={{ all: [mockCoverage] }}
|
||||
settings={defaultSettings}
|
||||
onClose={onClose}
|
||||
onOpenMedDetail={onOpenMedDetail}
|
||||
/>
|
||||
);
|
||||
|
||||
const closeBtn = screen.getByText('×');
|
||||
fireEvent.click(closeBtn);
|
||||
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('calls onClose when overlay clicked', () => {
|
||||
const onClose = vi.fn();
|
||||
const onOpenMedDetail = vi.fn();
|
||||
|
||||
render(
|
||||
<UserFilterModal
|
||||
selectedUser="John"
|
||||
meds={[mockMedication]}
|
||||
coverage={{ all: [mockCoverage] }}
|
||||
settings={defaultSettings}
|
||||
onClose={onClose}
|
||||
onOpenMedDetail={onOpenMedDetail}
|
||||
/>
|
||||
);
|
||||
|
||||
const overlay = document.querySelector('.modal-overlay');
|
||||
if (overlay) {
|
||||
fireEvent.click(overlay);
|
||||
}
|
||||
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('calls onClose and onOpenMedDetail when medication clicked', () => {
|
||||
const onClose = vi.fn();
|
||||
const onOpenMedDetail = vi.fn();
|
||||
|
||||
render(
|
||||
<UserFilterModal
|
||||
selectedUser="John"
|
||||
meds={[mockMedication]}
|
||||
coverage={{ all: [mockCoverage] }}
|
||||
settings={defaultSettings}
|
||||
onClose={onClose}
|
||||
onOpenMedDetail={onOpenMedDetail}
|
||||
/>
|
||||
);
|
||||
|
||||
const medItem = document.querySelector('.user-med-item');
|
||||
if (medItem) {
|
||||
fireEvent.click(medItem);
|
||||
}
|
||||
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
expect(onOpenMedDetail).toHaveBeenCalledWith(mockMedication);
|
||||
});
|
||||
|
||||
it('calls onClose when footer close button clicked', () => {
|
||||
const onClose = vi.fn();
|
||||
const onOpenMedDetail = vi.fn();
|
||||
|
||||
render(
|
||||
<UserFilterModal
|
||||
selectedUser="John"
|
||||
meds={[mockMedication]}
|
||||
coverage={{ all: [mockCoverage] }}
|
||||
settings={defaultSettings}
|
||||
onClose={onClose}
|
||||
onOpenMedDetail={onOpenMedDetail}
|
||||
/>
|
||||
);
|
||||
|
||||
const footerCloseBtn = screen.getByText(/common\.close/i);
|
||||
fireEvent.click(footerCloseBtn);
|
||||
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('does not call onClose when modal content clicked', () => {
|
||||
const onClose = vi.fn();
|
||||
const onOpenMedDetail = vi.fn();
|
||||
|
||||
render(
|
||||
<UserFilterModal
|
||||
selectedUser="John"
|
||||
meds={[mockMedication]}
|
||||
coverage={{ all: [mockCoverage] }}
|
||||
settings={defaultSettings}
|
||||
onClose={onClose}
|
||||
onOpenMedDetail={onOpenMedDetail}
|
||||
/>
|
||||
);
|
||||
|
||||
const content = document.querySelector('.modal-content');
|
||||
if (content) {
|
||||
fireEvent.click(content);
|
||||
}
|
||||
|
||||
expect(onClose).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('filters medications by takenBy correctly', () => {
|
||||
const onClose = vi.fn();
|
||||
const onOpenMedDetail = vi.fn();
|
||||
|
||||
const meds: Medication[] = [
|
||||
{ ...mockMedication, id: 1, name: 'Med1', takenBy: ['John'] },
|
||||
{ ...mockMedication, id: 2, name: 'Med2', takenBy: ['Jane'] },
|
||||
{ ...mockMedication, id: 3, name: 'Med3', takenBy: ['John', 'Jane'] }
|
||||
];
|
||||
|
||||
render(
|
||||
<UserFilterModal
|
||||
selectedUser="John"
|
||||
meds={meds}
|
||||
coverage={{ all: [] }}
|
||||
settings={defaultSettings}
|
||||
onClose={onClose}
|
||||
onOpenMedDetail={onOpenMedDetail}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Med1')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Med2')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('Med3')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user