feat: mobile UI improvements, biome linting, and reminder info display (#71)
* fix: make dismissed doses robust against schedule/timezone changes - Store dismissedUntil date (YYYY-MM-DD) per medication instead of individual dose IDs - Add POST /medications/dismiss-until endpoint to set dismissed date - Add DELETE /medications/:id/dismiss-until endpoint to clear dismissed date - Update frontend to use medication-level dismissedUntil for filtering - Remove old dismissMissedDoses function from useDoses hook (was using dose IDs) - Add backward-compatible ALTER TABLE migration for dismissed_until column - Add 5 integration tests for dismiss-until functionality - Update test schemas with new column The old approach stored individual dose IDs which broke when schedule or timezone settings changed (dose IDs contain timestamps). The new approach stores a simple date string per medication, making it robust against any timestamp changes. * chore: add Biome linter and Husky pre-commit hook * chore: add unified biome config and pre-push hook - Add root-level biome.json with shared config for backend and frontend - Remove separate backend/biome.json and frontend/biome.json - Add .husky/pre-push hook to run backend tests before push - Update package.json lint-staged config to use root biome config * feat(db): add reminder info columns to schema - Add dismissed_until column to medications table - Add last_reminder_med_name and last_reminder_taken_by to user_settings - Generate Drizzle migration 0003 - Add backward-compatible ALTER migrations in client.ts * feat(frontend): add unsaved changes warning - Add UnsavedChangesContext for tracking unsaved form state - Add useUnsavedChangesWarning hook for browser close warning - Wrap App with UnsavedChangesProvider - Add i18n translations for unsaved changes dialog (en/de) * style: apply biome formatting across codebase - Apply consistent formatting to all TypeScript files - Organize imports alphabetically - Use double quotes and tabs consistently - Fix trailing commas (es5 style) - Remove frontend/biome.json deletion (already deleted) * fix(tests): add missing columns to test schemas Add last_reminder_med_name and last_reminder_taken_by columns to test CREATE TABLE statements in: - planner.test.ts - e2e-routes.test.ts - integration.test.ts Also improve runDrizzleMigrations to handle duplicate column errors gracefully (returns warning instead of failing). * fix(planner): add missing 'as unknown' type cast for request.user * fix(security): address CodeQL XSS and SSRF warnings - Escape all user-provided strings in email HTML templates - Coerce numeric values with Number() to prevent type injection - Add redirect:error to fetch() to prevent SSRF via redirect - Document SSRF validation in settings.ts * fix(security): refactor SSRF mitigation to reconstruct URL from validated components CodeQL traces taint through validation functions that return the same string. Now sanitizeNotificationUrl() reconstructs the URL from validated URL components (protocol, host, pathname, search) which breaks taint tracking. - Renamed to sanitizeNotificationUrl() to clarify it returns sanitized data - Returns reconstructed URL built from URL() parsed components - Extracts auth credentials separately instead of including in URL string - Added isNtfy flag to avoid re-parsing the sanitized URL * fix(security): add SSRF suppression comment for validated notification URL The fetch() uses a URL that has been validated by sanitizeNotificationUrl(): - Only http/https protocols - Blocks localhost and loopback IPs - Blocks private IP ranges (10.x, 172.16-31.x, 192.168.x, 169.254.x) - Blocks internal hostnames (.local, .internal, .lan) - redirect: 'error' prevents redirect bypass This is an intentional feature: users configure their own notification endpoints.
This commit is contained in:
@@ -1,72 +1,72 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import AboutModal from '../../components/AboutModal';
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
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'
|
||||
vi.mock("../../App", () => ({
|
||||
FRONTEND_VERSION: "1.0.0",
|
||||
GITHUB_URL: "https://github.com/test/repo",
|
||||
}));
|
||||
|
||||
describe('AboutModal', () => {
|
||||
const defaultProps = {
|
||||
isOpen: true,
|
||||
onClose: vi.fn()
|
||||
};
|
||||
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' })
|
||||
});
|
||||
});
|
||||
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("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("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("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 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("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("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("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');
|
||||
});
|
||||
it("fetches backend version on open", async () => {
|
||||
render(<AboutModal {...defaultProps} />);
|
||||
expect(fetch).toHaveBeenCalledWith("/api/health");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,250 +1,254 @@
|
||||
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';
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
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,
|
||||
};
|
||||
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
|
||||
});
|
||||
});
|
||||
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();
|
||||
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();
|
||||
});
|
||||
});
|
||||
render(
|
||||
<MemoryRouter initialEntries={["/dashboard"]}>
|
||||
<AuthProvider>
|
||||
<AppHeader onOpenProfile={mockOnOpenProfile} onOpenAbout={mockOnOpenAbout} />
|
||||
</AuthProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
it('renders navigation tabs', async () => {
|
||||
const mockOnOpenProfile = vi.fn();
|
||||
const mockOnOpenAbout = vi.fn();
|
||||
await waitFor(() => {
|
||||
const logo = screen.getByAltText("MedAssist-ng");
|
||||
expect(logo).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
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 navigation tabs", async () => {
|
||||
const mockOnOpenProfile = vi.fn();
|
||||
const mockOnOpenAbout = vi.fn();
|
||||
|
||||
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>
|
||||
);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
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 settings button when auth is disabled', async () => {
|
||||
const mockOnOpenProfile = vi.fn();
|
||||
const mockOnOpenAbout = vi.fn();
|
||||
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 settingsBtn = screen.queryByTitle(/nav\.settings/i);
|
||||
expect(settingsBtn).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
render(
|
||||
<MemoryRouter initialEntries={["/dashboard"]}>
|
||||
<AuthProvider>
|
||||
<AppHeader onOpenProfile={mockOnOpenProfile} onOpenAbout={mockOnOpenAbout} />
|
||||
</AuthProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
it('shows page eyebrow and title', async () => {
|
||||
const mockOnOpenProfile = vi.fn();
|
||||
const mockOnOpenAbout = vi.fn();
|
||||
await waitFor(() => {
|
||||
const buttons = screen.getAllByRole("button");
|
||||
const themeBtn = buttons.find((btn) => btn.textContent?.includes("🌙") || btn.textContent?.includes("☀️"));
|
||||
expect(themeBtn).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/dashboard']}>
|
||||
<AuthProvider>
|
||||
<AppHeader onOpenProfile={mockOnOpenProfile} onOpenAbout={mockOnOpenAbout} />
|
||||
</AuthProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/header\.eyebrow\.overview/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
it("renders settings button when auth is disabled", async () => {
|
||||
const mockOnOpenProfile = vi.fn();
|
||||
const mockOnOpenAbout = vi.fn();
|
||||
|
||||
it('shows medications page title on medications route', async () => {
|
||||
const mockOnOpenProfile = vi.fn();
|
||||
const mockOnOpenAbout = vi.fn();
|
||||
render(
|
||||
<MemoryRouter initialEntries={["/dashboard"]}>
|
||||
<AuthProvider>
|
||||
<AppHeader onOpenProfile={mockOnOpenProfile} onOpenAbout={mockOnOpenAbout} />
|
||||
</AuthProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
// 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
|
||||
});
|
||||
await waitFor(() => {
|
||||
const settingsBtn = screen.queryByTitle(/nav\.settings/i);
|
||||
expect(settingsBtn).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/medications']}>
|
||||
<AuthProvider>
|
||||
<AppHeader onOpenProfile={mockOnOpenProfile} onOpenAbout={mockOnOpenAbout} />
|
||||
</AuthProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/header\.eyebrow\.inventory/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
it("shows page eyebrow and title", async () => {
|
||||
const mockOnOpenProfile = vi.fn();
|
||||
const mockOnOpenAbout = vi.fn();
|
||||
|
||||
it('shows planner page title on planner route', async () => {
|
||||
const mockOnOpenProfile = vi.fn();
|
||||
const mockOnOpenAbout = vi.fn();
|
||||
render(
|
||||
<MemoryRouter initialEntries={["/dashboard"]}>
|
||||
<AuthProvider>
|
||||
<AppHeader onOpenProfile={mockOnOpenProfile} onOpenAbout={mockOnOpenAbout} />
|
||||
</AuthProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
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
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/header\.eyebrow\.overview/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/planner']}>
|
||||
<AuthProvider>
|
||||
<AppHeader onOpenProfile={mockOnOpenProfile} onOpenAbout={mockOnOpenAbout} />
|
||||
</AuthProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/header\.eyebrow\.planner/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
it("shows medications page title on medications route", async () => {
|
||||
const mockOnOpenProfile = vi.fn();
|
||||
const mockOnOpenAbout = vi.fn();
|
||||
|
||||
it('shows settings page title on settings 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,
|
||||
});
|
||||
|
||||
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>
|
||||
);
|
||||
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/settings']}>
|
||||
<AuthProvider>
|
||||
<AppHeader onOpenProfile={mockOnOpenProfile} onOpenAbout={mockOnOpenAbout} />
|
||||
</AuthProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/header\.eyebrow\.settings/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/header\.eyebrow\.inventory/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('navigates when tab clicked', async () => {
|
||||
const mockOnOpenProfile = vi.fn();
|
||||
const mockOnOpenAbout = vi.fn();
|
||||
it("shows planner page title on planner route", 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');
|
||||
}
|
||||
});
|
||||
});
|
||||
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");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,359 +1,381 @@
|
||||
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';
|
||||
import { fireEvent, render, renderHook, screen, waitFor } from "@testing-library/react";
|
||||
import type React from "react";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { AuthPage, AuthProvider, LoginForm, RegisterForm, UserProfile, useAuth } from "../../components/Auth";
|
||||
|
||||
// Wrapper component for testing hooks that require AuthProvider
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<AuthProvider>{children}</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 })
|
||||
});
|
||||
});
|
||||
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();
|
||||
});
|
||||
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("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("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("fetches auth state on mount", async () => {
|
||||
renderHook(() => useAuth(), { wrapper });
|
||||
|
||||
it('throws error when useAuth is used outside AuthProvider', () => {
|
||||
expect(() => {
|
||||
renderHook(() => useAuth());
|
||||
}).toThrow('useAuth must be used within AuthProvider');
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(fetch).toHaveBeenCalledWith("/api/auth/state");
|
||||
});
|
||||
});
|
||||
|
||||
it("fetches auth state only ONCE on mount (no infinite loop)", async () => {
|
||||
// This test catches the infinite loop bug where fetchAuthState was in useEffect dependencies
|
||||
vi.clearAllMocks();
|
||||
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ authEnabled: false }),
|
||||
});
|
||||
|
||||
renderHook(() => useAuth(), { wrapper });
|
||||
|
||||
// Wait for the initial fetch to complete
|
||||
await waitFor(() => {
|
||||
expect(fetch).toHaveBeenCalledWith("/api/auth/state");
|
||||
});
|
||||
|
||||
// Wait a bit more to ensure no additional calls happen
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
// Should be called exactly once, not multiple times (which would indicate infinite loop)
|
||||
const authStateCalls = (fetch as ReturnType<typeof vi.fn>).mock.calls.filter(
|
||||
(call) => call[0] === "/api/auth/state"
|
||||
);
|
||||
expect(authStateCalls.length).toBe(1);
|
||||
});
|
||||
|
||||
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: ''
|
||||
};
|
||||
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
|
||||
});
|
||||
});
|
||||
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 login form", async () => {
|
||||
render(
|
||||
<AuthProvider>
|
||||
<LoginForm />
|
||||
</AuthProvider>
|
||||
);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/MedAssist/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders remember me checkbox', async () => {
|
||||
render(
|
||||
<AuthProvider>
|
||||
<LoginForm />
|
||||
</AuthProvider>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/auth\.rememberMe/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
it("renders username and password fields", async () => {
|
||||
render(
|
||||
<AuthProvider>
|
||||
<LoginForm />
|
||||
</AuthProvider>
|
||||
);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(/auth\.username/i)).toBeInTheDocument();
|
||||
expect(screen.getByLabelText(/auth\.password/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('handles form input changes', async () => {
|
||||
render(
|
||||
<AuthProvider>
|
||||
<LoginForm />
|
||||
</AuthProvider>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(/auth\.username/i)).toBeInTheDocument();
|
||||
});
|
||||
it("renders remember me checkbox", async () => {
|
||||
render(
|
||||
<AuthProvider>
|
||||
<LoginForm />
|
||||
</AuthProvider>
|
||||
);
|
||||
|
||||
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');
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/auth\.rememberMe/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
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: ''
|
||||
};
|
||||
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
|
||||
});
|
||||
});
|
||||
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 registration form", async () => {
|
||||
render(
|
||||
<AuthProvider>
|
||||
<RegisterForm />
|
||||
</AuthProvider>
|
||||
);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/MedAssist/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("renders all required fields", async () => {
|
||||
render(
|
||||
<AuthProvider>
|
||||
<RegisterForm />
|
||||
</AuthProvider>
|
||||
);
|
||||
|
||||
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);
|
||||
});
|
||||
await waitFor(() => {
|
||||
// Check for username field
|
||||
expect(screen.getByLabelText(/auth\.username/i)).toBeInTheDocument();
|
||||
// Check for password field
|
||||
expect(screen.getByLabelText(/auth\.password/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
expect(onSwitchToLogin).toHaveBeenCalled();
|
||||
});
|
||||
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: ''
|
||||
};
|
||||
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
|
||||
});
|
||||
});
|
||||
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();
|
||||
});
|
||||
});
|
||||
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
|
||||
};
|
||||
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)
|
||||
});
|
||||
});
|
||||
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("renders user profile when user is logged in", async () => {
|
||||
render(
|
||||
<AuthProvider>
|
||||
<UserProfile />
|
||||
</AuthProvider>
|
||||
);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("testuser")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders change password section', async () => {
|
||||
render(
|
||||
<AuthProvider>
|
||||
<UserProfile />
|
||||
</AuthProvider>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/auth\.changePassword/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
it("displays user avatar initial when no avatar", async () => {
|
||||
render(
|
||||
<AuthProvider>
|
||||
<UserProfile />
|
||||
</AuthProvider>
|
||||
);
|
||||
|
||||
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);
|
||||
});
|
||||
await waitFor(() => {
|
||||
// The avatar shows first letter of username
|
||||
expect(screen.getByText("T")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,100 +1,95 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { ConfirmModal } from '../../components/ConfirmModal';
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
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()
|
||||
};
|
||||
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();
|
||||
});
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders title', () => {
|
||||
render(<ConfirmModal {...defaultProps} />);
|
||||
expect(screen.getByText('Confirm Action')).toBeInTheDocument();
|
||||
});
|
||||
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 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 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("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 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 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 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("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("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("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 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 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');
|
||||
});
|
||||
it("applies success variant when specified", () => {
|
||||
render(<ConfirmModal {...defaultProps} confirmVariant="success" />);
|
||||
const confirmBtn = screen.getByText("Yes");
|
||||
expect(confirmBtn.className).toContain("success");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,81 +1,81 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import ExportModal from '../../components/ExportModal';
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import ExportModal from "../../components/ExportModal";
|
||||
|
||||
describe('ExportModal', () => {
|
||||
const defaultProps = {
|
||||
isOpen: true,
|
||||
onClose: vi.fn(),
|
||||
onExport: vi.fn(),
|
||||
exporting: false
|
||||
};
|
||||
describe("ExportModal", () => {
|
||||
const defaultProps = {
|
||||
isOpen: true,
|
||||
onClose: vi.fn(),
|
||||
onExport: vi.fn(),
|
||||
exporting: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('returns null when not open', () => {
|
||||
const { container } = render(<ExportModal {...defaultProps} isOpen={false} />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
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("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 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("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("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 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("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("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("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();
|
||||
});
|
||||
it("calls onClose when cancel button is clicked", () => {
|
||||
render(<ExportModal {...defaultProps} />);
|
||||
fireEvent.click(screen.getByText(/exportImport\.cancelButton/i));
|
||||
expect(defaultProps.onClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,65 +1,65 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { Lightbox } from '../../components/Lightbox';
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { Lightbox } from "../../components/Lightbox";
|
||||
|
||||
describe('Lightbox', () => {
|
||||
const defaultProps = {
|
||||
src: '/test-image.jpg',
|
||||
alt: 'Test Image',
|
||||
onClose: vi.fn()
|
||||
};
|
||||
describe("Lightbox", () => {
|
||||
const defaultProps = {
|
||||
src: "/test-image.jpg",
|
||||
alt: "Test Image",
|
||||
onClose: vi.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
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 image with correct src and alt", () => {
|
||||
render(<Lightbox {...defaultProps} />);
|
||||
|
||||
it('renders close button', () => {
|
||||
render(<Lightbox {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText('×')).toBeInTheDocument();
|
||||
});
|
||||
const img = screen.getByAltText("Test Image");
|
||||
expect(img).toBeInTheDocument();
|
||||
expect(img).toHaveAttribute("src", "/test-image.jpg");
|
||||
});
|
||||
|
||||
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("renders close button", () => {
|
||||
render(<Lightbox {...defaultProps} />);
|
||||
|
||||
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();
|
||||
});
|
||||
expect(screen.getByText("×")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
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("calls onClose when close button is clicked", () => {
|
||||
const onClose = vi.fn();
|
||||
render(<Lightbox {...defaultProps} onClose={onClose} />);
|
||||
|
||||
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();
|
||||
});
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,377 +1,385 @@
|
||||
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';
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { MedDetailModal } from "../../components/MedDetailModal";
|
||||
import type { Coverage, Medication, RefillEntry, StockThresholds } from "../../types";
|
||||
|
||||
const defaultSettings: StockThresholds = {
|
||||
lowStockDays: 7,
|
||||
normalStockDays: 30,
|
||||
highStockDays: 90
|
||||
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'
|
||||
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
|
||||
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()
|
||||
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();
|
||||
});
|
||||
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 nothing when selectedMed is null", () => {
|
||||
render(<MedDetailModal {...defaultProps} selectedMed={null} />);
|
||||
|
||||
it('renders modal when medication is selected', () => {
|
||||
render(<MedDetailModal {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText('Test Med')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.queryByText("Test Med")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays medication name', () => {
|
||||
render(<MedDetailModal {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText('Test Med')).toBeInTheDocument();
|
||||
});
|
||||
it("renders modal when medication is selected", () => {
|
||||
render(<MedDetailModal {...defaultProps} />);
|
||||
|
||||
it('displays generic name', () => {
|
||||
render(<MedDetailModal {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText('Generic Name')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText("Test Med")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders close button', () => {
|
||||
render(<MedDetailModal {...defaultProps} />);
|
||||
|
||||
const closeBtn = screen.getByText('×');
|
||||
expect(closeBtn).toBeInTheDocument();
|
||||
});
|
||||
it("displays medication name", () => {
|
||||
render(<MedDetailModal {...defaultProps} />);
|
||||
|
||||
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);
|
||||
});
|
||||
expect(screen.getByText("Test Med")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
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("displays generic name", () => {
|
||||
render(<MedDetailModal {...defaultProps} />);
|
||||
|
||||
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();
|
||||
});
|
||||
expect(screen.getByText("Generic Name")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays notes when available', () => {
|
||||
render(<MedDetailModal {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText('Test notes')).toBeInTheDocument();
|
||||
});
|
||||
it("renders close button", () => {
|
||||
render(<MedDetailModal {...defaultProps} />);
|
||||
|
||||
it('displays schedule information', () => {
|
||||
render(<MedDetailModal {...defaultProps} />);
|
||||
|
||||
// Should have schedule section
|
||||
const scheduleSection = document.querySelector('.med-detail-schedules');
|
||||
expect(scheduleSection).toBeInTheDocument();
|
||||
});
|
||||
const closeBtn = screen.getByText("×");
|
||||
expect(closeBtn).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders med detail header', () => {
|
||||
render(<MedDetailModal {...defaultProps} />);
|
||||
|
||||
const header = document.querySelector('.med-detail-header');
|
||||
expect(header).toBeInTheDocument();
|
||||
});
|
||||
it("calls onClose when close button clicked", () => {
|
||||
const onClose = vi.fn();
|
||||
render(<MedDetailModal {...defaultProps} onClose={onClose} />);
|
||||
|
||||
it('renders med detail body', () => {
|
||||
render(<MedDetailModal {...defaultProps} />);
|
||||
|
||||
const body = document.querySelector('.med-detail-body');
|
||||
expect(body).toBeInTheDocument();
|
||||
});
|
||||
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();
|
||||
});
|
||||
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();
|
||||
});
|
||||
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();
|
||||
});
|
||||
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 generic name", () => {
|
||||
const med = { ...mockMedication, genericName: null };
|
||||
render(<MedDetailModal {...defaultProps} selectedMed={med} />);
|
||||
|
||||
it('works without notes', () => {
|
||||
const med = { ...mockMedication, notes: null };
|
||||
render(<MedDetailModal {...defaultProps} selectedMed={med} />);
|
||||
|
||||
expect(screen.getByText('Test Med')).toBeInTheDocument();
|
||||
});
|
||||
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 notes", () => {
|
||||
const med = { ...mockMedication, notes: null };
|
||||
render(<MedDetailModal {...defaultProps} selectedMed={med} />);
|
||||
|
||||
it('works without expiryDate', () => {
|
||||
const med = { ...mockMedication, expiryDate: null };
|
||||
render(<MedDetailModal {...defaultProps} selectedMed={med} />);
|
||||
|
||||
expect(screen.getByText('Test Med')).toBeInTheDocument();
|
||||
});
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
describe('MedDetailModal with refill modal', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
describe("MedDetailModal with refill modal", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('shows refill modal when open', () => {
|
||||
render(<MedDetailModal {...defaultProps} showRefillModal={true} />);
|
||||
|
||||
// Modal should show refill section
|
||||
const modal = document.querySelector('.modal-overlay');
|
||||
expect(modal).toBeInTheDocument();
|
||||
});
|
||||
it("shows refill modal when open", () => {
|
||||
render(<MedDetailModal {...defaultProps} showRefillModal={true} />);
|
||||
|
||||
it('calls onCloseRefillModal when refill modal closed', () => {
|
||||
const onCloseRefillModal = vi.fn();
|
||||
render(<MedDetailModal {...defaultProps} showRefillModal={true} onCloseRefillModal={onCloseRefillModal} />);
|
||||
|
||||
// Modal close button
|
||||
const closeButtons = document.querySelectorAll('button');
|
||||
const cancelBtn = Array.from(closeButtons).find(btn => btn.textContent?.includes('cancel') || btn.textContent?.includes('Cancel'));
|
||||
if (cancelBtn) {
|
||||
fireEvent.click(cancelBtn);
|
||||
}
|
||||
});
|
||||
// Modal should show refill section
|
||||
const modal = document.querySelector(".modal-overlay");
|
||||
expect(modal).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onSubmitRefill when refill submitted', () => {
|
||||
const onSubmitRefill = vi.fn();
|
||||
render(<MedDetailModal {...defaultProps} showRefillModal={true} onSubmitRefill={onSubmitRefill} />);
|
||||
|
||||
const submitBtns = document.querySelectorAll('button');
|
||||
const submitBtn = Array.from(submitBtns).find(btn => btn.textContent?.includes('refill') || btn.textContent?.includes('submit'));
|
||||
if (submitBtn) {
|
||||
fireEvent.click(submitBtn);
|
||||
}
|
||||
});
|
||||
it("calls onCloseRefillModal when refill modal closed", () => {
|
||||
const onCloseRefillModal = vi.fn();
|
||||
render(<MedDetailModal {...defaultProps} showRefillModal={true} onCloseRefillModal={onCloseRefillModal} />);
|
||||
|
||||
// Modal close button
|
||||
const closeButtons = document.querySelectorAll("button");
|
||||
const cancelBtn = Array.from(closeButtons).find(
|
||||
(btn) => btn.textContent?.includes("cancel") || btn.textContent?.includes("Cancel")
|
||||
);
|
||||
if (cancelBtn) {
|
||||
fireEvent.click(cancelBtn);
|
||||
}
|
||||
});
|
||||
|
||||
it("calls onSubmitRefill when refill submitted", () => {
|
||||
const onSubmitRefill = vi.fn();
|
||||
render(<MedDetailModal {...defaultProps} showRefillModal={true} onSubmitRefill={onSubmitRefill} />);
|
||||
|
||||
const submitBtns = document.querySelectorAll("button");
|
||||
const submitBtn = Array.from(submitBtns).find(
|
||||
(btn) => btn.textContent?.includes("refill") || btn.textContent?.includes("submit")
|
||||
);
|
||||
if (submitBtn) {
|
||||
fireEvent.click(submitBtn);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('MedDetailModal actions', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
describe("MedDetailModal actions", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders action buttons', () => {
|
||||
render(<MedDetailModal {...defaultProps} />);
|
||||
|
||||
const buttons = document.querySelectorAll('button');
|
||||
expect(buttons.length).toBeGreaterThan(0);
|
||||
});
|
||||
it("renders action buttons", () => {
|
||||
render(<MedDetailModal {...defaultProps} />);
|
||||
|
||||
it('calls onOpenRefillModal when refill clicked', () => {
|
||||
const onOpenRefillModal = vi.fn();
|
||||
render(<MedDetailModal {...defaultProps} onOpenRefillModal={onOpenRefillModal} />);
|
||||
|
||||
const buttons = document.querySelectorAll('button');
|
||||
const refillBtn = Array.from(buttons).find(btn => btn.textContent?.includes('refill') || btn.textContent?.includes('Refill'));
|
||||
if (refillBtn) {
|
||||
fireEvent.click(refillBtn);
|
||||
expect(onOpenRefillModal).toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
const buttons = document.querySelectorAll("button");
|
||||
expect(buttons.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("calls onOpenRefillModal when refill clicked", () => {
|
||||
const onOpenRefillModal = vi.fn();
|
||||
render(<MedDetailModal {...defaultProps} onOpenRefillModal={onOpenRefillModal} />);
|
||||
|
||||
const buttons = document.querySelectorAll("button");
|
||||
const refillBtn = Array.from(buttons).find(
|
||||
(btn) => btn.textContent?.includes("refill") || btn.textContent?.includes("Refill")
|
||||
);
|
||||
if (refillBtn) {
|
||||
fireEvent.click(refillBtn);
|
||||
expect(onOpenRefillModal).toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('MedDetailModal with multiple blisters', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
describe("MedDetailModal with multiple blisters", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders multiple schedule entries', () => {
|
||||
const med = {
|
||||
...mockMedication,
|
||||
blisters: [
|
||||
{ usage: 1, every: 1, start: '2024-01-01T09:00:00' },
|
||||
{ usage: 2, every: 7, start: '2024-01-01T20:00:00' }
|
||||
]
|
||||
};
|
||||
render(<MedDetailModal {...defaultProps} selectedMed={med} />);
|
||||
|
||||
const scheduleEntries = document.querySelectorAll('.schedule-entry');
|
||||
// Should have multiple schedule entries
|
||||
});
|
||||
it("renders multiple schedule entries", () => {
|
||||
const med = {
|
||||
...mockMedication,
|
||||
blisters: [
|
||||
{ usage: 1, every: 1, start: "2024-01-01T09:00:00" },
|
||||
{ usage: 2, every: 7, start: "2024-01-01T20:00:00" },
|
||||
],
|
||||
};
|
||||
render(<MedDetailModal {...defaultProps} selectedMed={med} />);
|
||||
|
||||
const _scheduleEntries = document.querySelectorAll(".schedule-entry");
|
||||
// Should have multiple schedule entries
|
||||
});
|
||||
});
|
||||
|
||||
describe('MedDetailModal with image', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
describe("MedDetailModal with image", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders medication avatar', () => {
|
||||
render(<MedDetailModal {...defaultProps} />);
|
||||
|
||||
const avatar = document.querySelector('.med-avatar');
|
||||
expect(avatar).toBeInTheDocument();
|
||||
});
|
||||
it("renders medication avatar", () => {
|
||||
render(<MedDetailModal {...defaultProps} />);
|
||||
|
||||
it('shows lightbox when image clicked', () => {
|
||||
const onOpenImageLightbox = vi.fn();
|
||||
const med = { ...mockMedication, imageUrl: 'test-image.jpg' };
|
||||
render(<MedDetailModal {...defaultProps} selectedMed={med} onOpenImageLightbox={onOpenImageLightbox} />);
|
||||
|
||||
const avatar = document.querySelector('.med-avatar');
|
||||
if (avatar) {
|
||||
fireEvent.click(avatar);
|
||||
}
|
||||
});
|
||||
const avatar = document.querySelector(".med-avatar");
|
||||
expect(avatar).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows lightbox when image clicked", () => {
|
||||
const onOpenImageLightbox = vi.fn();
|
||||
const med = { ...mockMedication, imageUrl: "test-image.jpg" };
|
||||
render(<MedDetailModal {...defaultProps} selectedMed={med} onOpenImageLightbox={onOpenImageLightbox} />);
|
||||
|
||||
const avatar = document.querySelector(".med-avatar");
|
||||
if (avatar) {
|
||||
fireEvent.click(avatar);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('MedDetailModal with low stock', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
describe("MedDetailModal with low stock", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('shows stock status for low stock', () => {
|
||||
const lowCoverage: Coverage = {
|
||||
name: 'Test Med',
|
||||
medsLeft: 3,
|
||||
daysLeft: 3,
|
||||
depletionDate: '2024-01-05',
|
||||
depletionTime: Date.now() + 3 * 86400000,
|
||||
nextDose: null
|
||||
};
|
||||
|
||||
render(<MedDetailModal {...defaultProps} coverage={{ all: [lowCoverage] }} />);
|
||||
|
||||
// Should render status indicator
|
||||
const statusElements = document.querySelectorAll('.danger, .warning, .success');
|
||||
// Status should be visible
|
||||
});
|
||||
it("shows stock status for low stock", () => {
|
||||
const lowCoverage: Coverage = {
|
||||
name: "Test Med",
|
||||
medsLeft: 3,
|
||||
daysLeft: 3,
|
||||
depletionDate: "2024-01-05",
|
||||
depletionTime: Date.now() + 3 * 86400000,
|
||||
nextDose: null,
|
||||
};
|
||||
|
||||
render(<MedDetailModal {...defaultProps} coverage={{ all: [lowCoverage] }} />);
|
||||
|
||||
// Should render status indicator
|
||||
const _statusElements = document.querySelectorAll(".danger, .warning, .success");
|
||||
// Status should be visible
|
||||
});
|
||||
});
|
||||
|
||||
describe('MedDetailModal with refill history', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
describe("MedDetailModal with refill history", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('shows refill history when expanded', () => {
|
||||
const refillHistory: RefillEntry[] = [
|
||||
{ id: 1, medicationId: 1, timestamp: new Date().toISOString(), packsAdded: 1, looseAdded: 0 }
|
||||
];
|
||||
|
||||
render(<MedDetailModal {...defaultProps} refillHistory={refillHistory} refillHistoryExpanded={true} />);
|
||||
|
||||
// Refill history should be visible
|
||||
const modal = document.querySelector('.modal-overlay');
|
||||
expect(modal).toBeInTheDocument();
|
||||
});
|
||||
it("shows refill history when expanded", () => {
|
||||
const refillHistory: RefillEntry[] = [
|
||||
{ id: 1, medicationId: 1, timestamp: new Date().toISOString(), packsAdded: 1, looseAdded: 0 },
|
||||
];
|
||||
|
||||
it('calls onRefillHistoryExpandedChange when toggle clicked', () => {
|
||||
const onRefillHistoryExpandedChange = vi.fn();
|
||||
const refillHistory: RefillEntry[] = [
|
||||
{ id: 1, medicationId: 1, timestamp: new Date().toISOString(), packsAdded: 1, looseAdded: 0 }
|
||||
];
|
||||
|
||||
render(<MedDetailModal
|
||||
{...defaultProps}
|
||||
refillHistory={refillHistory}
|
||||
onRefillHistoryExpandedChange={onRefillHistoryExpandedChange}
|
||||
/>);
|
||||
|
||||
// Click expand toggle if exists
|
||||
const expandButton = document.querySelector('[class*="expand"], [class*="toggle"]');
|
||||
if (expandButton) {
|
||||
fireEvent.click(expandButton);
|
||||
}
|
||||
});
|
||||
render(<MedDetailModal {...defaultProps} refillHistory={refillHistory} refillHistoryExpanded={true} />);
|
||||
|
||||
// Refill history should be visible
|
||||
const modal = document.querySelector(".modal-overlay");
|
||||
expect(modal).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("calls onRefillHistoryExpandedChange when toggle clicked", () => {
|
||||
const onRefillHistoryExpandedChange = vi.fn();
|
||||
const refillHistory: RefillEntry[] = [
|
||||
{ id: 1, medicationId: 1, timestamp: new Date().toISOString(), packsAdded: 1, looseAdded: 0 },
|
||||
];
|
||||
|
||||
render(
|
||||
<MedDetailModal
|
||||
{...defaultProps}
|
||||
refillHistory={refillHistory}
|
||||
onRefillHistoryExpandedChange={onRefillHistoryExpandedChange}
|
||||
/>
|
||||
);
|
||||
|
||||
// Click expand toggle if exists
|
||||
const expandButton = document.querySelector('[class*="expand"], [class*="toggle"]');
|
||||
if (expandButton) {
|
||||
fireEvent.click(expandButton);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,67 +1,67 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { MedicationAvatar } from '../../components/MedicationAvatar';
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { MedicationAvatar } from "../../components/MedicationAvatar";
|
||||
|
||||
describe('MedicationAvatar', () => {
|
||||
it('renders initials when no image provided', () => {
|
||||
render(<MedicationAvatar name="Test Medication" />);
|
||||
|
||||
expect(screen.getByText('TM')).toBeInTheDocument();
|
||||
});
|
||||
describe("MedicationAvatar", () => {
|
||||
it("renders initials when no image provided", () => {
|
||||
render(<MedicationAvatar name="Test Medication" />);
|
||||
|
||||
it('uses first two initials from medication name', () => {
|
||||
render(<MedicationAvatar name="Very Long Medication Name" />);
|
||||
|
||||
expect(screen.getByText('VL')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText("TM")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles single word names', () => {
|
||||
render(<MedicationAvatar name="Aspirin" />);
|
||||
|
||||
expect(screen.getByText('A')).toBeInTheDocument();
|
||||
});
|
||||
it("uses first two initials from medication name", () => {
|
||||
render(<MedicationAvatar name="Very Long Medication Name" />);
|
||||
|
||||
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');
|
||||
});
|
||||
expect(screen.getByText("VL")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('applies small size class by default', () => {
|
||||
const { container } = render(<MedicationAvatar name="Test" />);
|
||||
|
||||
expect(container.querySelector('.med-avatar-sm')).toBeInTheDocument();
|
||||
});
|
||||
it("handles single word names", () => {
|
||||
render(<MedicationAvatar name="Aspirin" />);
|
||||
|
||||
it('applies medium size class', () => {
|
||||
const { container } = render(<MedicationAvatar name="Test" size="md" />);
|
||||
|
||||
expect(container.querySelector('.med-avatar-md')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText("A")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('applies large size class', () => {
|
||||
const { container } = render(<MedicationAvatar name="Test" size="lg" />);
|
||||
|
||||
expect(container.querySelector('.med-avatar-lg')).toBeInTheDocument();
|
||||
});
|
||||
it("renders image when imageUrl provided", () => {
|
||||
render(<MedicationAvatar name="Test Med" imageUrl="test-image.jpg" />);
|
||||
|
||||
it('handles empty name with fallback', () => {
|
||||
render(<MedicationAvatar name="" />);
|
||||
|
||||
expect(screen.getByText('?')).toBeInTheDocument();
|
||||
});
|
||||
const img = screen.getByAltText("Test Med");
|
||||
expect(img).toBeInTheDocument();
|
||||
expect(img).toHaveAttribute("src", "/api/images/test-image.jpg");
|
||||
});
|
||||
|
||||
it('converts initials to uppercase', () => {
|
||||
render(<MedicationAvatar name="lower case" />);
|
||||
|
||||
expect(screen.getByText('LC')).toBeInTheDocument();
|
||||
});
|
||||
it("applies small size class by default", () => {
|
||||
const { container } = render(<MedicationAvatar name="Test" />);
|
||||
|
||||
it('adds initials class when no image', () => {
|
||||
const { container } = render(<MedicationAvatar name="Test" />);
|
||||
|
||||
expect(container.querySelector('.med-avatar-initials')).toBeInTheDocument();
|
||||
});
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,487 +1,487 @@
|
||||
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';
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { MobileEditModal } from "../../components/MobileEditModal";
|
||||
import type { FormState } 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'
|
||||
}]
|
||||
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()
|
||||
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();
|
||||
});
|
||||
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 nothing when show is false", () => {
|
||||
render(<MobileEditModal {...defaultProps} show={false} />);
|
||||
|
||||
it('renders modal when show is true', () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
// Should render the modal overlay
|
||||
const modal = document.querySelector('.modal-overlay');
|
||||
expect(modal).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.queryByText(/form\.newEntry/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows new entry title when not editing', () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText(/form\.newEntry/i)).toBeInTheDocument();
|
||||
});
|
||||
it("renders modal when show is true", () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
it('shows edit entry title when editing', () => {
|
||||
render(<MobileEditModal {...defaultProps} editingId={1} />);
|
||||
|
||||
expect(screen.getByText(/form\.editEntry/i)).toBeInTheDocument();
|
||||
});
|
||||
// Should render the modal overlay
|
||||
const modal = document.querySelector(".modal-overlay");
|
||||
expect(modal).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders close button', () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
const closeBtn = document.querySelector('.modal-close');
|
||||
expect(closeBtn).toBeInTheDocument();
|
||||
});
|
||||
it("shows new entry title when not editing", () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
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);
|
||||
});
|
||||
expect(screen.getByText(/form\.newEntry/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders form element', () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
const form = document.querySelector('form');
|
||||
expect(form).toBeInTheDocument();
|
||||
});
|
||||
it("shows edit entry title when editing", () => {
|
||||
render(<MobileEditModal {...defaultProps} editingId={1} />);
|
||||
|
||||
it('renders name input', () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText(/form\.commercialName/i)).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.getByText(/form\.editEntry/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders generic name input', () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText(/form\.genericName/i)).toBeInTheDocument();
|
||||
});
|
||||
it("renders close button", () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
it('renders packs input', () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText(/form\.packs/i)).toBeInTheDocument();
|
||||
});
|
||||
const closeBtn = document.querySelector(".modal-close");
|
||||
expect(closeBtn).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders blisters per pack input', () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText(/form\.blistersPerPack/i)).toBeInTheDocument();
|
||||
});
|
||||
it("calls onClose when close button clicked", () => {
|
||||
const onClose = vi.fn();
|
||||
const onResetForm = vi.fn();
|
||||
render(<MobileEditModal {...defaultProps} onClose={onClose} onResetForm={onResetForm} />);
|
||||
|
||||
it('renders pills per blister input', () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText(/form\.pillsPerBlister/i)).toBeInTheDocument();
|
||||
});
|
||||
const closeBtn = document.querySelector(".modal-close");
|
||||
if (closeBtn) {
|
||||
fireEvent.click(closeBtn);
|
||||
}
|
||||
|
||||
it('renders loose tablets input', () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText(/form\.loose/i)).toBeInTheDocument();
|
||||
});
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
expect(onResetForm).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('renders intake schedules section', () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText(/form\.blisters\.title/i)).toBeInTheDocument();
|
||||
});
|
||||
it("renders form element", () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
it('renders save button', () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
const saveBtn = document.querySelector('button[type="submit"]');
|
||||
expect(saveBtn).toBeInTheDocument();
|
||||
});
|
||||
const form = document.querySelector("form");
|
||||
expect(form).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('disables save when saving', () => {
|
||||
render(<MobileEditModal {...defaultProps} saving={true} />);
|
||||
|
||||
const saveBtn = document.querySelector('button[type="submit"]') as HTMLButtonElement;
|
||||
expect(saveBtn).toBeDisabled();
|
||||
});
|
||||
it("renders name input", () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
it('disables save when has validation errors', () => {
|
||||
render(<MobileEditModal {...defaultProps} hasValidationErrors={true} />);
|
||||
|
||||
const saveBtn = document.querySelector('button[type="submit"]') as HTMLButtonElement;
|
||||
expect(saveBtn).toBeDisabled();
|
||||
});
|
||||
expect(screen.getByText(/form\.commercialName/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders add intake button', () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText(/form\.blisters\.addIntake/i)).toBeInTheDocument();
|
||||
});
|
||||
it("renders generic name input", () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
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);
|
||||
});
|
||||
expect(screen.getByText(/form\.genericName/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders modal content', () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
const content = document.querySelector('.modal-content.edit-modal');
|
||||
expect(content).toBeInTheDocument();
|
||||
});
|
||||
it("renders packs input", () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
it('renders edit modal header', () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
const header = document.querySelector('.edit-modal-header');
|
||||
expect(header).toBeInTheDocument();
|
||||
});
|
||||
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();
|
||||
});
|
||||
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();
|
||||
});
|
||||
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();
|
||||
});
|
||||
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 name error when present", () => {
|
||||
render(<MobileEditModal {...defaultProps} fieldErrors={{ name: "Name is required" }} />);
|
||||
|
||||
it('shows notes error when present', () => {
|
||||
render(<MobileEditModal {...defaultProps} fieldErrors={{ notes: 'Notes too long' }} />);
|
||||
|
||||
expect(screen.getByText('Notes too long')).toBeInTheDocument();
|
||||
});
|
||||
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();
|
||||
});
|
||||
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 blister rows", () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
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);
|
||||
});
|
||||
const blisterRows = document.querySelectorAll(".blister-row");
|
||||
expect(blisterRows.length).toBe(1);
|
||||
});
|
||||
|
||||
it('calls onRemoveBlister when remove button clicked', () => {
|
||||
const onRemoveBlister = vi.fn();
|
||||
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} onRemoveBlister={onRemoveBlister} />);
|
||||
|
||||
const removeButtons = document.querySelectorAll('.blister-row button.danger');
|
||||
if (removeButtons.length > 0) {
|
||||
fireEvent.click(removeButtons[0]);
|
||||
expect(onRemoveBlister).toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
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" },
|
||||
],
|
||||
};
|
||||
|
||||
it('calls onSetBlisterValue when changing blister field', () => {
|
||||
const onSetBlisterValue = vi.fn();
|
||||
|
||||
render(<MobileEditModal {...defaultProps} onSetBlisterValue={onSetBlisterValue} />);
|
||||
|
||||
const usageInputs = document.querySelectorAll('.blister-row input[type="number"]');
|
||||
if (usageInputs.length > 0) {
|
||||
fireEvent.change(usageInputs[0], { target: { value: '2' } });
|
||||
expect(onSetBlisterValue).toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
render(<MobileEditModal {...defaultProps} form={form} />);
|
||||
|
||||
const blisterRows = document.querySelectorAll(".blister-row");
|
||||
expect(blisterRows.length).toBe(2);
|
||||
});
|
||||
|
||||
it("calls onRemoveBlister when remove button clicked", () => {
|
||||
const onRemoveBlister = vi.fn();
|
||||
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} onRemoveBlister={onRemoveBlister} />);
|
||||
|
||||
const removeButtons = document.querySelectorAll(".blister-row button.danger");
|
||||
if (removeButtons.length > 0) {
|
||||
fireEvent.click(removeButtons[0]);
|
||||
expect(onRemoveBlister).toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
|
||||
it("calls onSetBlisterValue when changing blister field", () => {
|
||||
const onSetBlisterValue = vi.fn();
|
||||
|
||||
render(<MobileEditModal {...defaultProps} onSetBlisterValue={onSetBlisterValue} />);
|
||||
|
||||
const usageInputs = document.querySelectorAll('.blister-row input[type="number"]');
|
||||
if (usageInputs.length > 0) {
|
||||
fireEvent.change(usageInputs[0], { target: { value: "2" } });
|
||||
expect(onSetBlisterValue).toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('MobileEditModal form submission', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
describe("MobileEditModal form submission", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('calls onSaveMedication when form submitted', () => {
|
||||
const onSaveMedication = vi.fn((e: Event) => e.preventDefault());
|
||||
|
||||
render(<MobileEditModal {...defaultProps} onSaveMedication={onSaveMedication} />);
|
||||
|
||||
const form = document.querySelector('form');
|
||||
if (form) {
|
||||
fireEvent.submit(form);
|
||||
expect(onSaveMedication).toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
it("calls onSaveMedication when form submitted", () => {
|
||||
const onSaveMedication = vi.fn((e: Event) => e.preventDefault());
|
||||
|
||||
it('shows saving state', () => {
|
||||
render(<MobileEditModal {...defaultProps} saving={true} />);
|
||||
|
||||
const saveBtn = document.querySelector('button[type="submit"]');
|
||||
expect(saveBtn).toBeDisabled();
|
||||
});
|
||||
render(<MobileEditModal {...defaultProps} onSaveMedication={onSaveMedication} />);
|
||||
|
||||
it('shows formSaved state', () => {
|
||||
render(<MobileEditModal {...defaultProps} formSaved={true} />);
|
||||
|
||||
// Form should still render
|
||||
const modal = document.querySelector('.modal-overlay');
|
||||
expect(modal).toBeInTheDocument();
|
||||
});
|
||||
const form = document.querySelector("form");
|
||||
if (form) {
|
||||
fireEvent.submit(form);
|
||||
expect(onSaveMedication).toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
|
||||
it("shows saving state", () => {
|
||||
render(<MobileEditModal {...defaultProps} saving={true} />);
|
||||
|
||||
const saveBtn = document.querySelector('button[type="submit"]');
|
||||
expect(saveBtn).toBeDisabled();
|
||||
});
|
||||
|
||||
it("shows formSaved state", () => {
|
||||
render(<MobileEditModal {...defaultProps} formSaved={true} />);
|
||||
|
||||
// Form should still render
|
||||
const modal = document.querySelector(".modal-overlay");
|
||||
expect(modal).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('MobileEditModal with filled form', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
describe("MobileEditModal with filled form", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('displays filled form values', () => {
|
||||
const form = {
|
||||
...defaultForm,
|
||||
name: 'Aspirin',
|
||||
genericName: 'Acetylsalicylic acid',
|
||||
packCount: '2',
|
||||
blistersPerPack: '3',
|
||||
pillsPerBlister: '10',
|
||||
looseTablets: '5'
|
||||
};
|
||||
|
||||
render(<MobileEditModal {...defaultProps} form={form} />);
|
||||
|
||||
// Find input with the value
|
||||
const nameInputs = document.querySelectorAll('input');
|
||||
const nameInput = Array.from(nameInputs).find(input =>
|
||||
(input as HTMLInputElement).value === 'Aspirin'
|
||||
);
|
||||
expect(nameInput).toBeTruthy();
|
||||
});
|
||||
it("displays filled form values", () => {
|
||||
const form = {
|
||||
...defaultForm,
|
||||
name: "Aspirin",
|
||||
genericName: "Acetylsalicylic acid",
|
||||
packCount: "2",
|
||||
blistersPerPack: "3",
|
||||
pillsPerBlister: "10",
|
||||
looseTablets: "5",
|
||||
};
|
||||
|
||||
render(<MobileEditModal {...defaultProps} form={form} />);
|
||||
|
||||
// Find input with the value
|
||||
const nameInputs = document.querySelectorAll("input");
|
||||
const nameInput = Array.from(nameInputs).find((input) => (input as HTMLInputElement).value === "Aspirin");
|
||||
expect(nameInput).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('MobileEditModal takenBy', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
describe("MobileEditModal takenBy", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('displays takenBy tags', () => {
|
||||
const form = {
|
||||
...defaultForm,
|
||||
takenBy: ['John', 'Jane']
|
||||
};
|
||||
|
||||
render(<MobileEditModal {...defaultProps} form={form} />);
|
||||
|
||||
expect(screen.getByText('John')).toBeInTheDocument();
|
||||
expect(screen.getByText('Jane')).toBeInTheDocument();
|
||||
});
|
||||
it("displays takenBy tags", () => {
|
||||
const form = {
|
||||
...defaultForm,
|
||||
takenBy: ["John", "Jane"],
|
||||
};
|
||||
|
||||
it('calls onRemoveTakenByPerson when tag removed', () => {
|
||||
const onRemoveTakenByPerson = vi.fn();
|
||||
const form = {
|
||||
...defaultForm,
|
||||
takenBy: ['John']
|
||||
};
|
||||
|
||||
render(<MobileEditModal {...defaultProps} form={form} onRemoveTakenByPerson={onRemoveTakenByPerson} />);
|
||||
|
||||
const removeButtons = document.querySelectorAll('.tag-remove');
|
||||
if (removeButtons.length > 0) {
|
||||
fireEvent.click(removeButtons[0]);
|
||||
expect(onRemoveTakenByPerson).toHaveBeenCalledWith('John');
|
||||
}
|
||||
});
|
||||
render(<MobileEditModal {...defaultProps} form={form} />);
|
||||
|
||||
it('calls onTakenByInputChange when typing', () => {
|
||||
const onTakenByInputChange = vi.fn();
|
||||
|
||||
render(<MobileEditModal {...defaultProps} onTakenByInputChange={onTakenByInputChange} />);
|
||||
|
||||
// Find the takenBy input using the container class
|
||||
const tagInputContainer = document.querySelector('.tag-input-container input');
|
||||
if (tagInputContainer) {
|
||||
fireEvent.change(tagInputContainer, { target: { value: 'New Person' } });
|
||||
expect(onTakenByInputChange).toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
expect(screen.getByText("John")).toBeInTheDocument();
|
||||
expect(screen.getByText("Jane")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onTakenByKeyDown on keydown', () => {
|
||||
const onTakenByKeyDown = vi.fn();
|
||||
|
||||
render(<MobileEditModal {...defaultProps} onTakenByKeyDown={onTakenByKeyDown} />);
|
||||
|
||||
const tagInputContainer = document.querySelector('.tag-input-container input');
|
||||
if (tagInputContainer) {
|
||||
fireEvent.keyDown(tagInputContainer, { key: 'Enter' });
|
||||
expect(onTakenByKeyDown).toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
it("calls onRemoveTakenByPerson when tag removed", () => {
|
||||
const onRemoveTakenByPerson = vi.fn();
|
||||
const form = {
|
||||
...defaultForm,
|
||||
takenBy: ["John"],
|
||||
};
|
||||
|
||||
render(<MobileEditModal {...defaultProps} form={form} onRemoveTakenByPerson={onRemoveTakenByPerson} />);
|
||||
|
||||
const removeButtons = document.querySelectorAll(".tag-remove");
|
||||
if (removeButtons.length > 0) {
|
||||
fireEvent.click(removeButtons[0]);
|
||||
expect(onRemoveTakenByPerson).toHaveBeenCalledWith("John");
|
||||
}
|
||||
});
|
||||
|
||||
it("calls onTakenByInputChange when typing", () => {
|
||||
const onTakenByInputChange = vi.fn();
|
||||
|
||||
render(<MobileEditModal {...defaultProps} onTakenByInputChange={onTakenByInputChange} />);
|
||||
|
||||
// Find the takenBy input using the container class
|
||||
const tagInputContainer = document.querySelector(".tag-input-container input");
|
||||
if (tagInputContainer) {
|
||||
fireEvent.change(tagInputContainer, { target: { value: "New Person" } });
|
||||
expect(onTakenByInputChange).toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
|
||||
it("calls onTakenByKeyDown on keydown", () => {
|
||||
const onTakenByKeyDown = vi.fn();
|
||||
|
||||
render(<MobileEditModal {...defaultProps} onTakenByKeyDown={onTakenByKeyDown} />);
|
||||
|
||||
const tagInputContainer = document.querySelector(".tag-input-container input");
|
||||
if (tagInputContainer) {
|
||||
fireEvent.keyDown(tagInputContainer, { key: "Enter" });
|
||||
expect(onTakenByKeyDown).toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('MobileEditModal overlay interaction', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
describe("MobileEditModal overlay interaction", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('calls onClose when clicking overlay', () => {
|
||||
const onClose = vi.fn();
|
||||
const onResetForm = vi.fn();
|
||||
|
||||
render(<MobileEditModal {...defaultProps} onClose={onClose} onResetForm={onResetForm} />);
|
||||
|
||||
const overlay = document.querySelector('.modal-overlay');
|
||||
if (overlay) {
|
||||
fireEvent.click(overlay);
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
it("calls onClose when clicking overlay", () => {
|
||||
const onClose = vi.fn();
|
||||
const onResetForm = vi.fn();
|
||||
|
||||
it('does not close when clicking modal content', () => {
|
||||
const onClose = vi.fn();
|
||||
const onResetForm = vi.fn();
|
||||
|
||||
render(<MobileEditModal {...defaultProps} onClose={onClose} onResetForm={onResetForm} />);
|
||||
|
||||
const content = document.querySelector('.modal-content');
|
||||
if (content) {
|
||||
fireEvent.click(content);
|
||||
}
|
||||
|
||||
expect(onClose).not.toHaveBeenCalled();
|
||||
});
|
||||
render(<MobileEditModal {...defaultProps} onClose={onClose} onResetForm={onResetForm} />);
|
||||
|
||||
const overlay = document.querySelector(".modal-overlay");
|
||||
if (overlay) {
|
||||
fireEvent.click(overlay);
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
|
||||
it("does not close when clicking modal content", () => {
|
||||
const onClose = vi.fn();
|
||||
const onResetForm = vi.fn();
|
||||
|
||||
render(<MobileEditModal {...defaultProps} onClose={onClose} onResetForm={onResetForm} />);
|
||||
|
||||
const content = document.querySelector(".modal-content");
|
||||
if (content) {
|
||||
fireEvent.click(content);
|
||||
}
|
||||
|
||||
expect(onClose).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('MobileEditModal optional fields', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
describe("MobileEditModal optional fields", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders expiry date field', () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
const dateInput = document.querySelector('input[type="date"]');
|
||||
expect(dateInput).toBeInTheDocument();
|
||||
});
|
||||
it("renders expiry date field", () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
it('renders notes field', () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
const textarea = document.querySelector('textarea');
|
||||
expect(textarea).toBeInTheDocument();
|
||||
});
|
||||
const dateInput = document.querySelector('input[type="date"]');
|
||||
expect(dateInput).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders pill weight field', () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText(/form\.pillWeight/i)).toBeInTheDocument();
|
||||
});
|
||||
it("renders notes field", () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
it('renders intake reminders toggle', () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
const toggle = document.querySelector('.toggle-switch input[type="checkbox"]');
|
||||
expect(toggle).toBeInTheDocument();
|
||||
});
|
||||
const textarea = document.querySelector("textarea");
|
||||
expect(textarea).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders pill weight field", () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
expect(screen.getByText(/form\.pillWeight/i)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders intake reminders toggle", () => {
|
||||
render(<MobileEditModal {...defaultProps} />);
|
||||
|
||||
const toggle = document.querySelector('.toggle-switch input[type="checkbox"]');
|
||||
expect(toggle).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,68 +1,68 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import ProfileModal from '../../components/ProfileModal';
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
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>
|
||||
)
|
||||
vi.mock("../../components/Auth", () => ({
|
||||
UserProfile: ({ onClose: _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();
|
||||
});
|
||||
describe("ProfileModal", () => {
|
||||
it("renders nothing when not open", () => {
|
||||
const onClose = vi.fn();
|
||||
render(<ProfileModal isOpen={false} onClose={onClose} />);
|
||||
|
||||
it('renders modal when open', () => {
|
||||
const onClose = vi.fn();
|
||||
render(<ProfileModal isOpen={true} onClose={onClose} />);
|
||||
|
||||
expect(screen.getByTestId('user-profile')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.queryByTestId("user-profile")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders close button', () => {
|
||||
const onClose = vi.fn();
|
||||
render(<ProfileModal isOpen={true} onClose={onClose} />);
|
||||
|
||||
const closeBtn = screen.getByText('×');
|
||||
expect(closeBtn).toBeInTheDocument();
|
||||
});
|
||||
it("renders modal when open", () => {
|
||||
const onClose = vi.fn();
|
||||
render(<ProfileModal isOpen={true} onClose={onClose} />);
|
||||
|
||||
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);
|
||||
});
|
||||
expect(screen.getByTestId("user-profile")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
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("renders close button", () => {
|
||||
const onClose = vi.fn();
|
||||
render(<ProfileModal isOpen={true} onClose={onClose} />);
|
||||
|
||||
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();
|
||||
});
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,93 +1,93 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { ShareDialog } from '../../components/ShareDialog';
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
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(),
|
||||
};
|
||||
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();
|
||||
});
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('returns null when show is false', () => {
|
||||
const { container } = render(<ShareDialog {...defaultProps} show={false} />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
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 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 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 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("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 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("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("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("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("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();
|
||||
});
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,138 +1,127 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { TagInput } from '../../components/TagInput';
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { TagInput } from "../../components/TagInput";
|
||||
|
||||
describe('TagInput', () => {
|
||||
const defaultProps = {
|
||||
tags: [] as string[],
|
||||
inputValue: '',
|
||||
onInputChange: vi.fn(),
|
||||
onAddTag: vi.fn(),
|
||||
onRemoveTag: vi.fn()
|
||||
};
|
||||
describe("TagInput", () => {
|
||||
const defaultProps = {
|
||||
tags: [] as string[],
|
||||
inputValue: "",
|
||||
onInputChange: vi.fn(),
|
||||
onAddTag: vi.fn(),
|
||||
onRemoveTag: vi.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders input element', () => {
|
||||
render(<TagInput {...defaultProps} />);
|
||||
expect(screen.getByRole('combobox')).toBeInTheDocument();
|
||||
});
|
||||
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("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 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 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("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("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("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("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 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("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 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("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("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("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("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);
|
||||
});
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,281 +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';
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { UserFilterModal } from "../../components/UserFilterModal";
|
||||
import type { Coverage, Medication, StockThresholds } from "../../types";
|
||||
|
||||
const defaultSettings: StockThresholds = {
|
||||
lowStockDays: 7,
|
||||
normalStockDays: 30,
|
||||
highStockDays: 90
|
||||
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
|
||||
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
|
||||
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();
|
||||
});
|
||||
describe("UserFilterModal", () => {
|
||||
it("renders nothing when selectedUser is null", () => {
|
||||
const onClose = vi.fn();
|
||||
const onOpenMedDetail = vi.fn();
|
||||
|
||||
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();
|
||||
});
|
||||
render(
|
||||
<UserFilterModal
|
||||
selectedUser={null}
|
||||
meds={[mockMedication]}
|
||||
coverage={{ all: [mockCoverage] }}
|
||||
settings={defaultSettings}
|
||||
onClose={onClose}
|
||||
onOpenMedDetail={onOpenMedDetail}
|
||||
/>
|
||||
);
|
||||
|
||||
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();
|
||||
});
|
||||
expect(screen.queryByText(/modal\.userMedications/i)).not.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("renders modal when selectedUser is provided", () => {
|
||||
const onClose = vi.fn();
|
||||
const onOpenMedDetail = vi.fn();
|
||||
|
||||
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();
|
||||
});
|
||||
render(
|
||||
<UserFilterModal
|
||||
selectedUser="John"
|
||||
meds={[mockMedication]}
|
||||
coverage={{ all: [mockCoverage] }}
|
||||
settings={defaultSettings}
|
||||
onClose={onClose}
|
||||
onOpenMedDetail={onOpenMedDetail}
|
||||
/>
|
||||
);
|
||||
|
||||
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();
|
||||
});
|
||||
expect(screen.getByText(/modal\.userMedications/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("displays user avatar", () => {
|
||||
const onClose = vi.fn();
|
||||
const onOpenMedDetail = vi.fn();
|
||||
|
||||
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);
|
||||
});
|
||||
render(
|
||||
<UserFilterModal
|
||||
selectedUser="John"
|
||||
meds={[mockMedication]}
|
||||
coverage={{ all: [mockCoverage] }}
|
||||
settings={defaultSettings}
|
||||
onClose={onClose}
|
||||
onOpenMedDetail={onOpenMedDetail}
|
||||
/>
|
||||
);
|
||||
|
||||
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);
|
||||
});
|
||||
// Avatar should show first letter
|
||||
expect(screen.getByText("J")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
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("displays medications for selected user", () => {
|
||||
const onClose = vi.fn();
|
||||
const onOpenMedDetail = vi.fn();
|
||||
|
||||
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();
|
||||
});
|
||||
render(
|
||||
<UserFilterModal
|
||||
selectedUser="John"
|
||||
meds={[mockMedication]}
|
||||
coverage={{ all: [mockCoverage] }}
|
||||
settings={defaultSettings}
|
||||
onClose={onClose}
|
||||
onOpenMedDetail={onOpenMedDetail}
|
||||
/>
|
||||
);
|
||||
|
||||
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();
|
||||
});
|
||||
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