import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { MemoryRouter, Route, Routes } from "react-router-dom";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { SharedSchedule } from "../../components/SharedSchedule";
// Mock fetch globally
const mockFetch = vi.fn();
// Store original setInterval
const originalSetInterval = global.setInterval;
const originalClearInterval = global.clearInterval;
// Helper to create mock medication data
function createMockData(overrides = {}) {
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
return {
takenBy: "TestPerson",
sharedBy: "TestOwner",
scheduleDays: 30,
medications: [
{
id: 1,
name: "TestMed",
genericName: "TestGeneric",
pillWeightMg: 100,
imageUrl: "test-image.jpg",
totalPills: 30,
packCount: 1,
blistersPerPack: 1,
looseTablets: 0,
pillsPerBlister: 30,
takenBy: ["TestPerson"],
blisters: [
{
usage: 1,
every: 1,
start: yesterday.toISOString(),
},
],
dismissedUntil: null,
},
],
stockThresholds: {
lowStockDays: 30,
},
...overrides,
};
}
// Helper to render SharedSchedule with router
function renderSharedSchedule(token = "test-token") {
return render(
} />
);
}
// Helper to setup fetch mock for standard success response
function setupSuccessMock(extraData = {}) {
mockFetch.mockImplementation((url: string) => {
if (url.includes("/doses")) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve({ doses: [] }),
});
}
return Promise.resolve({
ok: true,
json: () => Promise.resolve(createMockData(extraData)),
});
});
}
describe("SharedSchedule", () => {
beforeEach(() => {
vi.clearAllMocks();
localStorage.clear();
document.documentElement.removeAttribute("data-theme");
(global.fetch as ReturnType) = mockFetch;
// Mock setInterval to prevent polling from hanging tests
global.setInterval = vi.fn().mockReturnValue(999);
global.clearInterval = vi.fn();
setupSuccessMock();
});
afterEach(() => {
global.setInterval = originalSetInterval;
global.clearInterval = originalClearInterval;
});
it("shows loading state initially", () => {
renderSharedSchedule();
expect(screen.getByText(/common\.loading/i)).toBeInTheDocument();
});
it("renders app title during loading", () => {
renderSharedSchedule();
expect(screen.getByText(/MedAssist/i)).toBeInTheDocument();
});
it("renders shared schedule page container", () => {
renderSharedSchedule();
const container = document.querySelector(".shared-schedule-page");
expect(container).toBeInTheDocument();
});
it("has correct initial theme", () => {
renderSharedSchedule();
expect(document.documentElement.getAttribute("data-theme")).toBe("dark");
});
});
describe("SharedSchedule data loading", () => {
beforeEach(() => {
vi.clearAllMocks();
localStorage.clear();
document.documentElement.removeAttribute("data-theme");
(global.fetch as ReturnType) = mockFetch;
global.setInterval = vi.fn().mockReturnValue(999);
global.clearInterval = vi.fn();
});
afterEach(() => {
global.setInterval = originalSetInterval;
global.clearInterval = originalClearInterval;
});
it("displays schedule after successful data load", async () => {
setupSuccessMock();
renderSharedSchedule();
await waitFor(
() => {
expect(screen.getByText(/share\.scheduleFor/i)).toBeInTheDocument();
},
{ timeout: 3000 }
);
expect(screen.getByText("TestPerson")).toBeInTheDocument();
});
it("displays medication name after data load", async () => {
setupSuccessMock();
renderSharedSchedule();
await waitFor(
() => {
expect(screen.getByText("TestMed")).toBeInTheDocument();
},
{ timeout: 3000 }
);
});
it("shows error state for 404 response", async () => {
mockFetch.mockImplementation((url: string) => {
if (url.includes("/doses")) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve({ doses: [] }),
});
}
return Promise.resolve({
ok: false,
status: 404,
json: () => Promise.resolve({ error: "Not found" }),
});
});
renderSharedSchedule();
await waitFor(
() => {
expect(screen.getByText(/share\.notFound/i)).toBeInTheDocument();
},
{ timeout: 3000 }
);
});
it("shows expired state for 410 response", async () => {
mockFetch.mockImplementation((url: string) => {
if (url.includes("/doses")) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve({ doses: [] }),
});
}
return Promise.resolve({
ok: false,
status: 410,
json: () =>
Promise.resolve({
ownerUsername: "TestOwner",
takenBy: "TestPerson",
expiredAt: new Date().toISOString(),
}),
});
});
renderSharedSchedule();
await waitFor(
() => {
expect(screen.getByText(/share\.expired\.title/i)).toBeInTheDocument();
},
{ timeout: 3000 }
);
});
it("shows error state for network error", async () => {
mockFetch.mockImplementation((url: string) => {
if (url.includes("/doses")) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve({ doses: [] }),
});
}
return Promise.reject(new Error("Network error"));
});
renderSharedSchedule();
await waitFor(
() => {
expect(screen.getByText(/share\.error/i)).toBeInTheDocument();
},
{ timeout: 3000 }
);
});
it("shows no schedule message when no medications", async () => {
mockFetch.mockImplementation((url: string) => {
if (url.includes("/doses")) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve({ doses: [] }),
});
}
return Promise.resolve({
ok: true,
json: () => Promise.resolve(createMockData({ medications: [] })),
});
});
renderSharedSchedule();
await waitFor(
() => {
expect(screen.getByText(/share\.noSchedule/i)).toBeInTheDocument();
},
{ timeout: 3000 }
);
});
});
describe("SharedSchedule theme functionality", () => {
beforeEach(() => {
vi.clearAllMocks();
localStorage.clear();
document.documentElement.removeAttribute("data-theme");
(global.fetch as ReturnType) = mockFetch;
global.setInterval = vi.fn().mockReturnValue(999);
global.clearInterval = vi.fn();
setupSuccessMock();
});
afterEach(() => {
global.setInterval = originalSetInterval;
global.clearInterval = originalClearInterval;
});
it("uses saved theme from localStorage", () => {
localStorage.setItem("theme", "light");
renderSharedSchedule();
expect(document.documentElement.getAttribute("data-theme")).toBe("light");
});
it("defaults to dark theme when no saved theme", () => {
renderSharedSchedule();
expect(document.documentElement.getAttribute("data-theme")).toBe("dark");
});
it("toggles theme when theme button is clicked", async () => {
renderSharedSchedule();
await waitFor(
() => {
expect(screen.getByText(/share\.scheduleFor/i)).toBeInTheDocument();
},
{ timeout: 3000 }
);
const themeButton = screen.getByText("☀️");
await act(async () => {
fireEvent.click(themeButton);
});
expect(document.documentElement.getAttribute("data-theme")).toBe("light");
expect(localStorage.getItem("theme")).toBe("light");
});
it("shows moon icon in light mode", async () => {
localStorage.setItem("theme", "light");
renderSharedSchedule();
await waitFor(
() => {
expect(screen.getByText(/share\.scheduleFor/i)).toBeInTheDocument();
},
{ timeout: 3000 }
);
expect(screen.getByText("🌙")).toBeInTheDocument();
});
});
describe("SharedSchedule past days functionality", () => {
beforeEach(() => {
vi.clearAllMocks();
localStorage.clear();
document.documentElement.removeAttribute("data-theme");
(global.fetch as ReturnType) = mockFetch;
global.setInterval = vi.fn().mockReturnValue(999);
global.clearInterval = vi.fn();
});
afterEach(() => {
global.setInterval = originalSetInterval;
global.clearInterval = originalClearInterval;
});
it("shows past days toggle when there are past days", async () => {
const now = new Date();
const yesterday = new Date(now);
yesterday.setDate(yesterday.getDate() - 2);
mockFetch.mockImplementation((url: string) => {
if (url.includes("/doses")) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve({ doses: [] }),
});
}
return Promise.resolve({
ok: true,
json: () =>
Promise.resolve(
createMockData({
medications: [
{
id: 1,
name: "TestMed",
genericName: null,
pillWeightMg: null,
imageUrl: null,
totalPills: 30,
packCount: 1,
blistersPerPack: 1,
looseTablets: 0,
pillsPerBlister: 30,
takenBy: ["TestPerson"],
blisters: [{ usage: 1, every: 1, start: yesterday.toISOString() }],
dismissedUntil: null,
},
],
})
),
});
});
renderSharedSchedule();
await waitFor(
() => {
expect(screen.getByText(/dashboard\.schedules\.showPastDays/i)).toBeInTheDocument();
},
{ timeout: 3000 }
);
});
it("expands past days when toggle is clicked", async () => {
const now = new Date();
const yesterday = new Date(now);
yesterday.setDate(yesterday.getDate() - 2);
mockFetch.mockImplementation((url: string) => {
if (url.includes("/doses")) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve({ doses: [] }),
});
}
return Promise.resolve({
ok: true,
json: () =>
Promise.resolve(
createMockData({
medications: [
{
id: 1,
name: "TestMed",
genericName: null,
pillWeightMg: null,
imageUrl: null,
totalPills: 30,
packCount: 1,
blistersPerPack: 1,
looseTablets: 0,
pillsPerBlister: 30,
takenBy: ["TestPerson"],
blisters: [{ usage: 1, every: 1, start: yesterday.toISOString() }],
dismissedUntil: null,
},
],
})
),
});
});
renderSharedSchedule();
await waitFor(
() => {
expect(screen.getByText(/dashboard\.schedules\.showPastDays/i)).toBeInTheDocument();
},
{ timeout: 3000 }
);
const toggle = screen.getByText(/dashboard\.schedules\.showPastDays/i).closest(".past-days-toggle");
await act(async () => {
fireEvent.click(toggle!);
});
await waitFor(
() => {
expect(screen.getByText(/dashboard\.schedules\.hidePastDays/i)).toBeInTheDocument();
},
{ timeout: 3000 }
);
});
});
describe("SharedSchedule dose tracking", () => {
beforeEach(() => {
vi.clearAllMocks();
localStorage.clear();
document.documentElement.removeAttribute("data-theme");
(global.fetch as ReturnType) = mockFetch;
global.setInterval = vi.fn().mockReturnValue(999);
global.clearInterval = vi.fn();
});
afterEach(() => {
global.setInterval = originalSetInterval;
global.clearInterval = originalClearInterval;
});
it("marks dose as taken when take button is clicked", async () => {
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 10, 0);
mockFetch.mockImplementation((url: string, options?: RequestInit) => {
if (url.includes("/doses") && options?.method === "POST") {
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
}
if (url.includes("/doses")) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve({ doses: [] }),
});
}
return Promise.resolve({
ok: true,
json: () =>
Promise.resolve(
createMockData({
medications: [
{
id: 1,
name: "TestMed",
genericName: null,
pillWeightMg: null,
imageUrl: null,
totalPills: 30,
packCount: 1,
blistersPerPack: 1,
looseTablets: 0,
pillsPerBlister: 30,
takenBy: ["TestPerson"],
blisters: [{ usage: 1, every: 1, start: today.toISOString() }],
dismissedUntil: null,
},
],
})
),
});
});
renderSharedSchedule();
await waitFor(
() => {
expect(screen.getByText("TestMed")).toBeInTheDocument();
},
{ timeout: 3000 }
);
// Find and click a take button
const takeButtons = screen.getAllByTitle(/dose\.markAsTaken/i);
expect(takeButtons.length).toBeGreaterThan(0);
await act(async () => {
fireEvent.click(takeButtons[0]);
});
// Should have called POST to mark dose
expect(mockFetch).toHaveBeenCalledWith(
expect.stringContaining("/doses"),
expect.objectContaining({ method: "POST" })
);
});
});
describe("SharedSchedule schedule period display", () => {
beforeEach(() => {
vi.clearAllMocks();
localStorage.clear();
document.documentElement.removeAttribute("data-theme");
(global.fetch as ReturnType) = mockFetch;
global.setInterval = vi.fn().mockReturnValue(999);
global.clearInterval = vi.fn();
});
afterEach(() => {
global.setInterval = originalSetInterval;
global.clearInterval = originalClearInterval;
});
it("displays 1 month period", async () => {
mockFetch.mockImplementation((url: string) => {
if (url.includes("/doses")) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve({ doses: [] }),
});
}
return Promise.resolve({
ok: true,
json: () => Promise.resolve(createMockData({ scheduleDays: 30 })),
});
});
renderSharedSchedule();
await waitFor(
() => {
expect(screen.getByText(/dashboard\.schedules\.1month/i)).toBeInTheDocument();
},
{ timeout: 3000 }
);
});
it("displays 3 months period", async () => {
mockFetch.mockImplementation((url: string) => {
if (url.includes("/doses")) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve({ doses: [] }),
});
}
return Promise.resolve({
ok: true,
json: () => Promise.resolve(createMockData({ scheduleDays: 90 })),
});
});
renderSharedSchedule();
await waitFor(
() => {
expect(screen.getByText(/dashboard\.schedules\.3months/i)).toBeInTheDocument();
},
{ timeout: 3000 }
);
});
it("displays 6 months period", async () => {
mockFetch.mockImplementation((url: string) => {
if (url.includes("/doses")) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve({ doses: [] }),
});
}
return Promise.resolve({
ok: true,
json: () => Promise.resolve(createMockData({ scheduleDays: 180 })),
});
});
renderSharedSchedule();
await waitFor(
() => {
expect(screen.getByText(/dashboard\.schedules\.6months/i)).toBeInTheDocument();
},
{ timeout: 3000 }
);
});
});
describe("SharedSchedule undo dose", () => {
beforeEach(() => {
vi.clearAllMocks();
localStorage.clear();
document.documentElement.removeAttribute("data-theme");
(global.fetch as ReturnType) = mockFetch;
global.setInterval = vi.fn().mockReturnValue(999);
global.clearInterval = vi.fn();
});
afterEach(() => {
global.setInterval = originalSetInterval;
global.clearInterval = originalClearInterval;
});
it("undoes taken dose when undo button is clicked", async () => {
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 10, 0);
const doseId = `1-0-${today.getTime()}`;
mockFetch.mockImplementation((url: string, options?: RequestInit) => {
if (url.includes("/doses") && options?.method === "DELETE") {
return Promise.resolve({ ok: true });
}
if (url.includes("/doses")) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve({ doses: [{ doseId }] }),
});
}
return Promise.resolve({
ok: true,
json: () =>
Promise.resolve(
createMockData({
medications: [
{
id: 1,
name: "TestMed",
genericName: null,
pillWeightMg: null,
imageUrl: null,
totalPills: 30,
packCount: 1,
blistersPerPack: 1,
looseTablets: 0,
pillsPerBlister: 30,
takenBy: ["TestPerson"],
blisters: [{ usage: 1, every: 1, start: today.toISOString() }],
dismissedUntil: null,
},
],
})
),
});
});
renderSharedSchedule();
await waitFor(
() => {
expect(screen.getByText("TestMed")).toBeInTheDocument();
},
{ timeout: 3000 }
);
// Find undo button (for taken dose)
const undoButtons = screen.queryAllByTitle(/common\.undo/i);
if (undoButtons.length > 0) {
await act(async () => {
fireEvent.click(undoButtons[0]);
});
// Should have called DELETE
expect(mockFetch).toHaveBeenCalledWith(
expect.stringContaining("/doses/"),
expect.objectContaining({ method: "DELETE" })
);
}
});
it("handles undo error gracefully", async () => {
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 10, 0);
const doseId = `1-0-${today.getTime()}`;
mockFetch.mockImplementation((url: string, options?: RequestInit) => {
if (url.includes("/doses") && options?.method === "DELETE") {
return Promise.reject(new Error("Network error"));
}
if (url.includes("/doses")) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve({ doses: [{ doseId }] }),
});
}
return Promise.resolve({
ok: true,
json: () =>
Promise.resolve(
createMockData({
medications: [
{
id: 1,
name: "TestMed",
genericName: null,
pillWeightMg: null,
imageUrl: null,
totalPills: 30,
packCount: 1,
blistersPerPack: 1,
looseTablets: 0,
pillsPerBlister: 30,
takenBy: ["TestPerson"],
blisters: [{ usage: 1, every: 1, start: today.toISOString() }],
dismissedUntil: null,
},
],
})
),
});
});
renderSharedSchedule();
await waitFor(
() => {
expect(screen.getByText("TestMed")).toBeInTheDocument();
},
{ timeout: 3000 }
);
const undoButtons = screen.queryAllByTitle(/common\.undo/i);
if (undoButtons.length > 0) {
await act(async () => {
fireEvent.click(undoButtons[0]);
});
// Component should still be rendered
expect(screen.getByText("TestMed")).toBeInTheDocument();
}
});
});
describe("SharedSchedule footer and branding", () => {
beforeEach(() => {
vi.clearAllMocks();
localStorage.clear();
document.documentElement.removeAttribute("data-theme");
(global.fetch as ReturnType) = mockFetch;
global.setInterval = vi.fn().mockReturnValue(999);
global.clearInterval = vi.fn();
setupSuccessMock();
});
afterEach(() => {
global.setInterval = originalSetInterval;
global.clearInterval = originalClearInterval;
});
it("displays footer with MedAssist link", async () => {
renderSharedSchedule();
await waitFor(
() => {
expect(screen.getByText(/share\.scheduleFor/i)).toBeInTheDocument();
},
{ timeout: 3000 }
);
const footer = document.querySelector(".shared-schedule-footer");
expect(footer).toBeInTheDocument();
const link = footer?.querySelector('a[href="/"]');
expect(link).toBeInTheDocument();
expect(link?.textContent).toBe("MedAssist-ng");
});
it("displays sharedBy username in footer", async () => {
mockFetch.mockImplementation((url: string) => {
if (url.includes("/doses")) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve({ doses: [] }),
});
}
return Promise.resolve({
ok: true,
json: () => Promise.resolve(createMockData({ sharedBy: "TestOwner" })),
});
});
renderSharedSchedule();
await waitFor(
() => {
expect(screen.getByText("TestOwner")).toBeInTheDocument();
},
{ timeout: 3000 }
);
});
});
describe("SharedSchedule stock status display", () => {
beforeEach(() => {
vi.clearAllMocks();
localStorage.clear();
document.documentElement.removeAttribute("data-theme");
(global.fetch as ReturnType) = mockFetch;
global.setInterval = vi.fn().mockReturnValue(999);
global.clearInterval = vi.fn();
});
afterEach(() => {
global.setInterval = originalSetInterval;
global.clearInterval = originalClearInterval;
});
it("displays stock status for medications", async () => {
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 10, 0);
mockFetch.mockImplementation((url: string) => {
if (url.includes("/doses")) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve({ doses: [] }),
});
}
return Promise.resolve({
ok: true,
json: () =>
Promise.resolve(
createMockData({
medications: [
{
id: 1,
name: "TestMed",
genericName: null,
pillWeightMg: null,
imageUrl: null,
totalPills: 30,
packCount: 1,
blistersPerPack: 1,
looseTablets: 0,
pillsPerBlister: 30,
takenBy: ["TestPerson"],
blisters: [{ usage: 1, every: 1, start: today.toISOString() }],
dismissedUntil: null,
},
],
})
),
});
});
renderSharedSchedule();
await waitFor(
() => {
expect(screen.getByText("TestMed")).toBeInTheDocument();
},
{ timeout: 3000 }
);
// Should show stock status tag
const statusTags = document.querySelectorAll(".tag.success, .tag.warning, .tag.danger");
expect(statusTags.length).toBeGreaterThan(0);
});
it("shows pills total in schedule", async () => {
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 10, 0);
mockFetch.mockImplementation((url: string) => {
if (url.includes("/doses")) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve({ doses: [] }),
});
}
return Promise.resolve({
ok: true,
json: () =>
Promise.resolve(
createMockData({
medications: [
{
id: 1,
name: "TestMed",
genericName: null,
pillWeightMg: null,
imageUrl: null,
totalPills: 30,
packCount: 1,
blistersPerPack: 1,
looseTablets: 0,
pillsPerBlister: 30,
takenBy: ["TestPerson"],
blisters: [{ usage: 2, every: 1, start: today.toISOString() }],
dismissedUntil: null,
},
],
})
),
});
});
renderSharedSchedule();
await waitFor(
() => {
expect(screen.getByText("TestMed")).toBeInTheDocument();
},
{ timeout: 3000 }
);
// Should show pills total
expect(screen.getByText(/common\.pills/i)).toBeInTheDocument();
});
});
describe("SharedSchedule generic error state", () => {
beforeEach(() => {
vi.clearAllMocks();
localStorage.clear();
document.documentElement.removeAttribute("data-theme");
(global.fetch as ReturnType) = mockFetch;
global.setInterval = vi.fn().mockReturnValue(999);
global.clearInterval = vi.fn();
});
afterEach(() => {
global.setInterval = originalSetInterval;
global.clearInterval = originalClearInterval;
});
it("shows error for non-404/410 error responses", async () => {
mockFetch.mockImplementation((url: string) => {
if (url.includes("/doses")) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve({ doses: [] }),
});
}
return Promise.resolve({
ok: false,
status: 500,
json: () => Promise.resolve({ error: "Server error" }),
});
});
renderSharedSchedule();
await waitFor(
() => {
expect(screen.getByText(/share\.error/i)).toBeInTheDocument();
},
{ timeout: 3000 }
);
});
});
describe("SharedSchedule polling", () => {
beforeEach(() => {
vi.clearAllMocks();
localStorage.clear();
document.documentElement.removeAttribute("data-theme");
(global.fetch as ReturnType) = mockFetch;
// Don't mock setInterval for polling test
});
afterEach(() => {
vi.useRealTimers();
});
it("sets up polling interval on mount", async () => {
vi.useFakeTimers({ shouldAdvanceTime: true });
let doseFetchCount = 0;
mockFetch.mockImplementation((url: string) => {
if (url.includes("/doses")) {
doseFetchCount++;
return Promise.resolve({
ok: true,
json: () => Promise.resolve({ doses: [] }),
});
}
return Promise.resolve({
ok: true,
json: () => Promise.resolve(createMockData()),
});
});
renderSharedSchedule();
// Wait for initial fetch
await act(async () => {
await vi.advanceTimersByTimeAsync(100);
});
expect(doseFetchCount).toBeGreaterThanOrEqual(1);
const initialCount = doseFetchCount;
// Advance time by 5 seconds
await act(async () => {
await vi.advanceTimersByTimeAsync(5000);
});
// Should have fetched again due to polling
expect(doseFetchCount).toBeGreaterThan(initialCount);
});
});
describe("SharedSchedule keyboard handling", () => {
beforeEach(() => {
vi.clearAllMocks();
localStorage.clear();
document.documentElement.removeAttribute("data-theme");
(global.fetch as ReturnType) = mockFetch;
global.setInterval = vi.fn().mockReturnValue(999);
global.clearInterval = vi.fn();
setupSuccessMock();
});
afterEach(() => {
global.setInterval = originalSetInterval;
global.clearInterval = originalClearInterval;
});
it("handles Escape key without error", () => {
renderSharedSchedule();
fireEvent.keyDown(window, { key: "Escape" });
expect(document.querySelector(".shared-schedule-page")).toBeInTheDocument();
});
});
describe("SharedSchedule with different tokens", () => {
beforeEach(() => {
vi.clearAllMocks();
localStorage.clear();
document.documentElement.removeAttribute("data-theme");
(global.fetch as ReturnType) = mockFetch;
global.setInterval = vi.fn().mockReturnValue(999);
global.clearInterval = vi.fn();
setupSuccessMock();
});
afterEach(() => {
global.setInterval = originalSetInterval;
global.clearInterval = originalClearInterval;
});
it("renders with different token", () => {
renderSharedSchedule("another-token");
expect(screen.getByText(/common\.loading/i)).toBeInTheDocument();
});
it("renders with uuid token", () => {
renderSharedSchedule("550e8400-e29b-41d4-a716-446655440000");
expect(screen.getByText(/MedAssist/i)).toBeInTheDocument();
});
});
describe("SharedSchedule lightbox", () => {
beforeEach(() => {
vi.clearAllMocks();
localStorage.clear();
document.documentElement.removeAttribute("data-theme");
(global.fetch as ReturnType) = mockFetch;
global.setInterval = vi.fn().mockReturnValue(999);
global.clearInterval = vi.fn();
});
afterEach(() => {
global.setInterval = originalSetInterval;
global.clearInterval = originalClearInterval;
});
it("opens lightbox when clicking medication image", async () => {
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 10, 0);
mockFetch.mockImplementation((url: string) => {
if (url.includes("/doses")) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve({ doses: [] }),
});
}
return Promise.resolve({
ok: true,
json: () =>
Promise.resolve(
createMockData({
medications: [
{
id: 1,
name: "TestMed",
genericName: "TestGeneric",
pillWeightMg: 100,
imageUrl: "test-image.jpg",
totalPills: 30,
packCount: 1,
blistersPerPack: 1,
looseTablets: 0,
pillsPerBlister: 30,
takenBy: ["TestPerson"],
blisters: [{ usage: 1, every: 1, start: today.toISOString() }],
dismissedUntil: null,
},
],
})
),
});
});
renderSharedSchedule();
await waitFor(
() => {
expect(screen.getByText("TestMed")).toBeInTheDocument();
},
{ timeout: 3000 }
);
// Find clickable avatar
const clickableAvatars = document.querySelectorAll(".clickable .med-avatar");
if (clickableAvatars.length > 0) {
const parent = clickableAvatars[0].closest(".clickable");
if (parent) {
await act(async () => {
fireEvent.click(parent);
});
await waitFor(
() => {
expect(document.querySelector(".lightbox-overlay")).toBeInTheDocument();
},
{ timeout: 3000 }
);
}
}
});
it("closes lightbox on Escape key", async () => {
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 10, 0);
const mockHistoryBack = vi.spyOn(window.history, "back").mockImplementation(() => {});
mockFetch.mockImplementation((url: string) => {
if (url.includes("/doses")) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve({ doses: [] }),
});
}
return Promise.resolve({
ok: true,
json: () =>
Promise.resolve(
createMockData({
medications: [
{
id: 1,
name: "TestMed",
genericName: "TestGeneric",
pillWeightMg: 100,
imageUrl: "test-image.jpg",
totalPills: 30,
packCount: 1,
blistersPerPack: 1,
looseTablets: 0,
pillsPerBlister: 30,
takenBy: ["TestPerson"],
blisters: [{ usage: 1, every: 1, start: today.toISOString() }],
dismissedUntil: null,
},
],
})
),
});
});
renderSharedSchedule();
await waitFor(
() => {
expect(screen.getByText("TestMed")).toBeInTheDocument();
},
{ timeout: 3000 }
);
// Open lightbox
const clickableAvatars = document.querySelectorAll(".clickable .med-avatar");
if (clickableAvatars.length > 0) {
const parent = clickableAvatars[0].closest(".clickable");
if (parent) {
await act(async () => {
fireEvent.click(parent);
});
await waitFor(
() => {
expect(document.querySelector(".lightbox-overlay")).toBeInTheDocument();
},
{ timeout: 3000 }
);
// Press Escape
fireEvent.keyDown(window, { key: "Escape" });
expect(mockHistoryBack).toHaveBeenCalled();
}
}
mockHistoryBack.mockRestore();
});
});
describe("SharedSchedule day collapse", () => {
beforeEach(() => {
vi.clearAllMocks();
localStorage.clear();
document.documentElement.removeAttribute("data-theme");
(global.fetch as ReturnType) = mockFetch;
global.setInterval = vi.fn().mockReturnValue(999);
global.clearInterval = vi.fn();
});
afterEach(() => {
global.setInterval = originalSetInterval;
global.clearInterval = originalClearInterval;
});
it("saves collapsed state to localStorage", async () => {
const now = new Date();
const yesterday = new Date(now);
yesterday.setDate(yesterday.getDate() - 2);
mockFetch.mockImplementation((url: string) => {
if (url.includes("/doses")) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve({ doses: [] }),
});
}
return Promise.resolve({
ok: true,
json: () =>
Promise.resolve(
createMockData({
medications: [
{
id: 1,
name: "TestMed",
genericName: null,
pillWeightMg: null,
imageUrl: null,
totalPills: 30,
packCount: 1,
blistersPerPack: 1,
looseTablets: 0,
pillsPerBlister: 30,
takenBy: ["TestPerson"],
blisters: [{ usage: 1, every: 1, start: yesterday.toISOString() }],
dismissedUntil: null,
},
],
})
),
});
});
renderSharedSchedule();
await waitFor(
() => {
expect(screen.getByText(/dashboard\.schedules\.showPastDays/i)).toBeInTheDocument();
},
{ timeout: 3000 }
);
// Expand past days first
const toggle = screen.getByText(/dashboard\.schedules\.showPastDays/i).closest(".past-days-toggle");
await act(async () => {
fireEvent.click(toggle!);
});
await waitFor(
() => {
const dayDividers = document.querySelectorAll(".day-divider.clickable");
expect(dayDividers.length).toBeGreaterThan(0);
},
{ timeout: 3000 }
);
// Click a day divider to expand it
const dayDividers = document.querySelectorAll(".day-divider.clickable");
if (dayDividers.length > 0) {
await act(async () => {
fireEvent.click(dayDividers[0]);
});
// Check localStorage was updated
const expandedKey = "share_test-token_expandedDays";
const saved = localStorage.getItem(expandedKey);
expect(saved).toBeTruthy();
}
});
});