From cdc57a441b4e77447e73413282ae0ab716e921d0 Mon Sep 17 00:00:00 2001 From: Jacob Hagstedt Date: Sun, 31 May 2026 17:40:03 +0200 Subject: [PATCH] Pagination (but according to agent, not done) --- AGENTS.md | 79 +- diff.md | 247 + .../screenshots/after_settings_change.png | Bin 0 -> 47093 bytes .../screenshots/before_settings_change.png | Bin 0 -> 47093 bytes frontend/screenshots/pagination_visible.png | Bin 0 -> 47093 bytes frontend/src/components/stats/Pagination.tsx | 104 + frontend/src/components/stats/SessionList.tsx | 25 +- .../src/components/stats/SettingsPopover.tsx | 88 +- frontend/src/components/stats/StatsPage.tsx | 26 +- frontend/src/hooks/SettingsProvider.tsx | 57 + frontend/src/hooks/useSettings.ts | 50 +- frontend/src/main.tsx | 7 +- frontend/tests/e2e/pagination.spec.ts | 127 + plan.md | 351 + screenshots/after_settings_change.png | Bin 0 -> 36616 bytes screenshots/before_settings_change.png | Bin 0 -> 4254 bytes screenshots/pagination_visible.png | Bin 0 -> 36616 bytes session-ses_182c.md | 6653 +++++++++++++++++ 18 files changed, 7697 insertions(+), 117 deletions(-) create mode 100644 diff.md create mode 100644 frontend/screenshots/after_settings_change.png create mode 100644 frontend/screenshots/before_settings_change.png create mode 100644 frontend/screenshots/pagination_visible.png create mode 100644 frontend/src/components/stats/Pagination.tsx create mode 100644 frontend/src/hooks/SettingsProvider.tsx create mode 100644 frontend/tests/e2e/pagination.spec.ts create mode 100644 plan.md create mode 100644 screenshots/after_settings_change.png create mode 100644 screenshots/before_settings_change.png create mode 100644 screenshots/pagination_visible.png create mode 100644 session-ses_182c.md diff --git a/AGENTS.md b/AGENTS.md index 323f216..b1ce8a7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,13 +1,12 @@ -# PROJECT KNOWLEDGE BASE - -**Generated:** 2026-04-23 -**Mode:** init-deep (create-new) +# PROJECT BABY MONITOR ## OVERVIEW -Baby Sleep Tracker - self-hosted web app for tracking infant sleep. Dual-stack monorepo: React 19/Vite 6/TypeScript frontend + Python 3.14/FastAPI backend. Optimized for Raspberry Pi 4 (ARM64). +Baby monitor - self-hosted web app for tracking infant sleep. Dual-stack monorepo: React 19/Vite 6/TypeScript frontend + Python 3.14/FastAPI backend. Optimized for Raspberry Pi 4 (ARM64). ## STRUCTURE + + ``` ./ ├── frontend/ # React 19 SPA @@ -21,6 +20,7 @@ Baby Sleep Tracker - self-hosted web app for tracking infant sleep. Dual-stack m │ │ └── utils/ │ └── tests/ # e2e (Playwright), unit (Vitest) ├── backend/ # FastAPI + SQLModel +│ ├── scripts/ # one-off scripts for migrations / fixes / etc │ ├── app/ │ │ ├── routers/ # API endpoints │ │ └── utils/ @@ -40,37 +40,68 @@ Baby Sleep Tracker - self-hosted web app for tracking infant sleep. Dual-stack m ## CONVENTIONS -- **Backend**: SQLModel (not pure SQLAlchemy), async aiosqlite, pytest-asyncio -- **Frontend**: React 19, TanStack Query v5, Tailwind v4, no ESLint config (yet) -- **Python**: >=3.14 required, Hatch for packaging -- **Scripts**: All scripts created in the `backend` folder must be placed in `backend/scripts/`. -- **No CI/CD**: No .github/workflows, no Makefiles yet +- **Backend**: SQLModel (not pure SQLAlchemy), async aiosqlite, pytest-asyncio. +- **Frontend**: React 19, TanStack Query v5, Tailwind v4, no ESLint config (yet). +- **Python**: >=3.14 required. Managed via `uv` with Hatch backend for packaging. +- **Scripts**: All scripts created in the backend folder must be placed explicitly in `backend/scripts/`. +- **CI/CD**: Deployment is managed via Gitea workflows (`.gitea/` directory). Do not create `.github/` directories or Makefiles. -## ANTI-PATTERNS (THIS PROJECT) +## ANTI-PATTERNS (CRITICAL GUARDRAILS) -- No type suppression (`as any`, `@ts-ignore`, `@ts-expect-error`) - type safety enforced -- No empty catch blocks - error handling required -- No deleting failing tests to "pass" -- **Frontend JS Slop**: NEVER create `.js` files for the frontend. Use only TypeScript (`.ts`, `.tsx`) and write high-quality, type-safe code. Do not repeat this mistake. -- **Assumption Error**: NEVER assume the content of ANY files EVER. ALWAYS read the file to confirm. Do not rely on the name. +- **Type Safety**: No type suppression (`as any`, `@ts-ignore`, `@ts-expect-error`). Type safety is strictly enforced. +- **Error Handling**: No empty catch blocks. Explicit error handling/logging is required. +- **Testing Integrity**: NEVER delete or comment out failing tests to make a suite "pass". Fix the code or update the test properly. +- **Frontend JS Slop**: NEVER create `.js` or `.jsx` files for the frontend. Use only TypeScript (`.ts`, `.tsx`). Write clean, type-safe code. +- **Assumption Error**: NEVER assume the content of ANY files. ALWAYS read the target file to confirm its structure and implementation before modifying or referencing it. ## COMMANDS +### Backend Development + ```bash -# Backend -cd backend && uv sync +cd backend +uv sync uv run uvicorn app.main:app --reload uv run pytest +``` -# Frontend -cd frontend && npm install +### Frontend Development + +```bash +cd frontend +npm install npm run dev -# Use 'npm test' with caution as it may hang; use specific test scripts instead. + +# WARNING: 'npm test' may hang if run globally. Use specific file paths or vitest flags. npm run test ``` +## 🛑 DIRECTORY NAVIGATION & FILE DISCOVERY + +**Context:** This is a full-stack monorepo containing massive generated/dependency folders (`node_modules`, `dist`, `venv`, `.venv`, `__pycache__`, etc.). Unrestricted file listings will bloat your context window, crash your session, and cause severe token waste. + +* **NEVER use `ls -R`:** This command is strictly forbidden in this repository. +* **Primary Discovery:** To map out files or explore the project tree, **ALWAYS use `git ls-files**`. This is your default command and ensures you only interact with human-written source code. +* **Structural Context:** If you explicitly need a visual directory layout to understand architecture, use `tree` appended with the mandatory ignore flags: +```bash +tree -I 'node_modules|.git|dist|build|__pycache__|venv|.venv|env|.pytest_cache|.mypy_cache|.ruff_cache|*.egg-info' + +``` + + +* **Searching Code:** If you need to search for text or code blocks across the repo, **ALWAYS use `git grep "search_term"**` instead of standard `grep -r` to avoid crawling ignored or minified assets. + +## 🧠 TASK DELEGATION & CONTEXT PRESERVATION + +**Context:** The main OpenCode agent context window is our most critical resource. Executing complex tasks, debugging cross-file issues, or writing massive features directly in the main thread causes severe context bloat, leading to code hallucinations and dropped instructions. + +* **Delegate Aggressively:** For any complex implementation, structural change, or task larger than a single file edit, you **MUST spawn a sub-agent**. Act strictly as the architectural orchestrator; let sub-agents handle the iterative coding labor. +* **Scope Narrowly:** Give sub-agents highly specific, isolated, and atomic objectives (e.g., *"Read `backend/app/routers/auth.py` and summarize endpoints"* or *"Write a unit test for the frontend sleep tracker component"*). +* **Strict Summarization:** Sub-agents are **STRICTLY FORBIDDEN** from passing raw terminal dumps, unedited file blocks, or their internal step-by-step logs back to the main thread. +* **Return Format:** Sub-agents must return *only* clear, actionable results back to the orchestrator: a concise summary of findings, the exact lines of code that require modification, or direct confirmation of a successful execution. + ## NOTES -- Entry point: `frontend/src/main.tsx` + `backend/app/main.py` -- DB: SQLite via aiosqlite (async-first backend) -- Build: Docker for ARM64 Raspberry Pi deployment \ No newline at end of file +* **Entry Points:** `frontend/src/main.tsx` (Frontend) & `backend/app/main.py` (Backend). +* **Database:** Local SQLite managed via `aiosqlite` for an entirely async-first data layer. +* **Target Architecture:** Built specifically for ARM64 deployment on a Raspberry Pi 4 via Docker. diff --git a/diff.md b/diff.md new file mode 100644 index 0000000..05b0a68 --- /dev/null +++ b/diff.md @@ -0,0 +1,247 @@ +diff --git a/frontend/src/components/stats/SessionList.tsx b/frontend/src/components/stats/SessionList.tsx +index 374e41c..2bd6ea7 100644 +--- a/frontend/src/components/stats/SessionList.tsx ++++ b/frontend/src/components/stats/SessionList.tsx +@@ -1,15 +1,29 @@ + import React, { useState } from 'react'; + import { useSessions } from '../../hooks/useSleepSession'; ++import { useSettings } from '../../hooks/useSettings'; ++import { Pagination } from './Pagination'; + import { Edit2, Check, X, Trash2 } from 'lucide-react'; + import type { SleepSessionRead } from '../../types'; + import { formatDateTime, toLocalDatetimeInputValue, getDurationMinutes, formatMinutes } from '../../utils/time'; + + export function SessionList() { + const { sessions, isLoading, updateSession, deleteSession, isDeleting } = useSessions(); ++ const { settings } = useSettings(); ++ const sessionsPerPage = settings.sessionsPerPage; ++ ++ const [currentPage, setCurrentPage] = useState(1); + const [editingId, setEditingId] = useState(null); + const [editStart, setEditStart] = useState(''); + const [editEnd, setEditEnd] = useState(''); + ++ const totalPages = Math.ceil(sessions.length / sessionsPerPage); ++ const clampedPage = Math.max(1, Math.min(currentPage, totalPages || 1)); ++ ++ const paginatedSessions = sessions.slice( ++ (clampedPage - 1) * sessionsPerPage, ++ clampedPage * sessionsPerPage ++ ); ++ + if (isLoading) return
Loading sessions...
; + if (!sessions.length) return
No sessions recorded.
; + +@@ -55,7 +69,7 @@ export function SessionList() { +
+

Sleep Sessions

+
+- {sessions.map((s) => ( ++ {paginatedSessions.map((s) => ( +
+ {editingId === s.id ? ( +
+@@ -140,6 +154,13 @@ export function SessionList() { +
+ ))} +
++ {sessions.length > sessionsPerPage && ( ++ ++ )} +
+ ); + } +diff --git a/frontend/src/components/stats/SettingsPopover.tsx b/frontend/src/components/stats/SettingsPopover.tsx +index 005a302..8031392 100644 +--- a/frontend/src/components/stats/SettingsPopover.tsx ++++ b/frontend/src/components/stats/SettingsPopover.tsx +@@ -6,15 +6,17 @@ interface SettingsPopoverProps { + dayEnd: string; + wakeupRef: string; + bedtimeRef: string; +- onSave: (settings: { dayStart: string; dayEnd: string; wakeupRef: string; bedtimeRef: string }) => void; ++ sessionsPerPage: number; ++ onSave: (settings: { dayStart: string; dayEnd: string; wakeupRef: string; bedtimeRef: string; sessionsPerPage: number }) => void; + } + +-export function SettingsPopover({ dayStart, dayEnd, wakeupRef, bedtimeRef, onSave }: SettingsPopoverProps) { ++export function SettingsPopover({ dayStart, dayEnd, wakeupRef, bedtimeRef, sessionsPerPage, onSave }: SettingsPopoverProps) { + const [isOpen, setIsOpen] = useState(false); + const [localDayStart, setLocalDayStart] = useState(dayStart); + const [localDayEnd, setLocalDayEnd] = useState(dayEnd); + const [localWakeupRef, setLocalWakeupRef] = useState(wakeupRef); + const [localBedtimeRef, setLocalBedtimeRef] = useState(bedtimeRef); ++ const [localSessionsPerPage, setLocalSessionsPerPage] = useState(sessionsPerPage); + const popoverRef = useRef(null); + + useEffect(() => { +@@ -22,7 +24,8 @@ export function SettingsPopover({ dayStart, dayEnd, wakeupRef, bedtimeRef, onSav + setLocalDayEnd(dayEnd); + setLocalWakeupRef(wakeupRef); + setLocalBedtimeRef(bedtimeRef); +- }, [dayStart, dayEnd, wakeupRef, bedtimeRef]); ++ setLocalSessionsPerPage(sessionsPerPage); ++ }, [dayStart, dayEnd, wakeupRef, bedtimeRef, sessionsPerPage]); + + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { +@@ -42,6 +45,7 @@ export function SettingsPopover({ dayStart, dayEnd, wakeupRef, bedtimeRef, onSav + dayEnd: localDayEnd, + wakeupRef: localWakeupRef, + bedtimeRef: localBedtimeRef, ++ sessionsPerPage: localSessionsPerPage, + }); + setIsOpen(false); + }; +@@ -65,44 +69,58 @@ export function SettingsPopover({ dayStart, dayEnd, wakeupRef, bedtimeRef, onSav + +
+
+- +- setLocalDayStart(e.target.value)} +- className="w-full p-2 border rounded-md text-sm" +-/> ++ ++ setLocalDayStart(e.target.value)} ++ className="w-full p-2 border rounded-md text-sm" ++ /> +
+
+- +- setLocalDayEnd(e.target.value)} +- className="w-full p-2 border rounded-md text-sm" +-/> ++ ++ setLocalDayEnd(e.target.value)} ++ className="w-full p-2 border rounded-md text-sm" ++ /> +
+
+- +- setLocalWakeupRef(e.target.value)} +- className="w-full p-2 border rounded-md text-sm" +-/> ++ ++ setLocalWakeupRef(e.target.value)} ++ className="w-full p-2 border rounded-md text-sm" ++ /> +
+
+- +- setLocalBedtimeRef(e.target.value)} +- className="w-full p-2 border rounded-md text-sm" +-/> ++ ++ setLocalBedtimeRef(e.target.value)} ++ className="w-full p-2 border rounded-md text-sm" ++ /> ++
++
++ ++ +
+
+ +diff --git a/frontend/src/components/stats/StatsPage.tsx b/frontend/src/components/stats/StatsPage.tsx +index 2516d55..e3518c8 100644 +--- a/frontend/src/components/stats/StatsPage.tsx ++++ b/frontend/src/components/stats/StatsPage.tsx +@@ -27,18 +27,20 @@ const StatsPage: React.FC = () => { +

Statistics

+

Overview of sleep patterns and trends

+
+- { +- update('dayStart', s.dayStart); +- update('dayEnd', s.dayEnd); +- update('wakeupRef', s.wakeupRef); +- update('bedtimeRef', s.bedtimeRef); +- }} +- /> ++ { ++ update('dayStart', s.dayStart); ++ update('dayEnd', s.dayEnd); ++ update('wakeupRef', s.wakeupRef); ++ update('bedtimeRef', s.bedtimeRef); ++ update('sessionsPerPage', s.sessionsPerPage); ++ }} ++ /> + + +
+diff --git a/frontend/src/hooks/useSettings.ts b/frontend/src/hooks/useSettings.ts +index c587fb3..d95899b 100644 +--- a/frontend/src/hooks/useSettings.ts ++++ b/frontend/src/hooks/useSettings.ts +@@ -8,6 +8,7 @@ interface Settings { + wakeupRef: string; + bedtimeRef: string; + statsInterval: number; ++ sessionsPerPage: number; + } + + const DEFAULT_SETTINGS: Settings = { +@@ -16,6 +17,7 @@ const DEFAULT_SETTINGS: Settings = { + wakeupRef: '04:00', + bedtimeRef: '19:00', + statsInterval: 7, ++ sessionsPerPage: 10, + }; + + export function useSettings() { diff --git a/frontend/screenshots/after_settings_change.png b/frontend/screenshots/after_settings_change.png new file mode 100644 index 0000000000000000000000000000000000000000..604dca5ce57cb50da2944a372b67830bac135d9c GIT binary patch literal 47093 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
7FlgV-x7&7ErI|H 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 F{{!R;OYr~z literal 0 HcmV?d00001 diff --git a/frontend/screenshots/before_settings_change.png b/frontend/screenshots/before_settings_change.png new file mode 100644 index 0000000000000000000000000000000000000000..604dca5ce57cb50da2944a372b67830bac135d9c GIT binary patch literal 47093 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
7FlgV-x7&7ErI|H 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 F{{!R;OYr~z literal 0 HcmV?d00001 diff --git a/frontend/screenshots/pagination_visible.png b/frontend/screenshots/pagination_visible.png new file mode 100644 index 0000000000000000000000000000000000000000..604dca5ce57cb50da2944a372b67830bac135d9c GIT binary patch literal 47093 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
7FlgV-x7&7ErI|H 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 F{{!R;OYr~z literal 0 HcmV?d00001 diff --git a/frontend/src/components/stats/Pagination.tsx b/frontend/src/components/stats/Pagination.tsx new file mode 100644 index 0000000..7642e82 --- /dev/null +++ b/frontend/src/components/stats/Pagination.tsx @@ -0,0 +1,104 @@ +import React from 'react'; +import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react'; + +interface PaginationProps { + currentPage: number; + totalPages: number; + onPageChange: (page: number) => void; +} + +export function Pagination({ currentPage, totalPages, onPageChange }: PaginationProps) { + if (totalPages <= 1) return null; + + const getPageNumbers = () => { + const pages: (number | string)[] = []; + const maxVisiblePages = 5; + + if (totalPages <= maxVisiblePages) { + for (let i = 1; i <= totalPages; i++) { + pages.push(i); + } + } else { + // Always include first page + pages.push(1); + + if (currentPage > 3) { + pages.push('...'); + } + + // Calculate middle pages + const start = Math.max(2, currentPage - 1); + const end = Math.min(totalPages - 1, currentPage + 1); + + for (let i = start; i <= end; i++) { + if (!pages.includes(i)) { + pages.push(i); + } + } + + if (currentPage < totalPages - 2) { + pages.push('...'); + } + + // Always include last page + if (!pages.includes(totalPages)) { + pages.push(totalPages); + } + } + + return pages; + }; + + const pages = getPageNumbers(); + + return ( +
+ + +
+ {pages.map((page, idx) => { + if (page === '...') { + return ( +
+ +
+ ); + } + + const pageNum = page as number; + const isActive = currentPage === pageNum; + + return ( + + ); + })} +
+ + +
+ ); +} diff --git a/frontend/src/components/stats/SessionList.tsx b/frontend/src/components/stats/SessionList.tsx index 374e41c..1645506 100644 --- a/frontend/src/components/stats/SessionList.tsx +++ b/frontend/src/components/stats/SessionList.tsx @@ -1,15 +1,29 @@ import React, { useState } from 'react'; import { useSessions } from '../../hooks/useSleepSession'; +import { useSettings } from '../../hooks/useSettings'; +import { Pagination } from './Pagination'; import { Edit2, Check, X, Trash2 } from 'lucide-react'; import type { SleepSessionRead } from '../../types'; import { formatDateTime, toLocalDatetimeInputValue, getDurationMinutes, formatMinutes } from '../../utils/time'; export function SessionList() { const { sessions, isLoading, updateSession, deleteSession, isDeleting } = useSessions(); + const { settings } = useSettings(); + const sessionsPerPage = settings.sessionsPerPage; + + const [currentPage, setCurrentPage] = useState(1); const [editingId, setEditingId] = useState(null); const [editStart, setEditStart] = useState(''); const [editEnd, setEditEnd] = useState(''); + const totalPages = Math.ceil(sessions.length / sessionsPerPage); + const clampedPage = Math.max(1, Math.min(currentPage, totalPages || 1)); + + const paginatedSessions = sessions.slice( + (clampedPage - 1) * sessionsPerPage, + clampedPage * sessionsPerPage + ); + if (isLoading) return
Loading sessions...
; if (!sessions.length) return
No sessions recorded.
; @@ -52,10 +66,10 @@ export function SessionList() { }; return ( -
+

Sleep Sessions

- {sessions.map((s) => ( + {paginatedSessions.map((s) => (
{editingId === s.id ? (
@@ -140,6 +154,13 @@ export function SessionList() {
))}
+ {sessions.length > sessionsPerPage && ( + + )}
); } diff --git a/frontend/src/components/stats/SettingsPopover.tsx b/frontend/src/components/stats/SettingsPopover.tsx index 005a302..8031392 100644 --- a/frontend/src/components/stats/SettingsPopover.tsx +++ b/frontend/src/components/stats/SettingsPopover.tsx @@ -6,15 +6,17 @@ interface SettingsPopoverProps { dayEnd: string; wakeupRef: string; bedtimeRef: string; - onSave: (settings: { dayStart: string; dayEnd: string; wakeupRef: string; bedtimeRef: string }) => void; + sessionsPerPage: number; + onSave: (settings: { dayStart: string; dayEnd: string; wakeupRef: string; bedtimeRef: string; sessionsPerPage: number }) => void; } -export function SettingsPopover({ dayStart, dayEnd, wakeupRef, bedtimeRef, onSave }: SettingsPopoverProps) { +export function SettingsPopover({ dayStart, dayEnd, wakeupRef, bedtimeRef, sessionsPerPage, onSave }: SettingsPopoverProps) { const [isOpen, setIsOpen] = useState(false); const [localDayStart, setLocalDayStart] = useState(dayStart); const [localDayEnd, setLocalDayEnd] = useState(dayEnd); const [localWakeupRef, setLocalWakeupRef] = useState(wakeupRef); const [localBedtimeRef, setLocalBedtimeRef] = useState(bedtimeRef); + const [localSessionsPerPage, setLocalSessionsPerPage] = useState(sessionsPerPage); const popoverRef = useRef(null); useEffect(() => { @@ -22,7 +24,8 @@ export function SettingsPopover({ dayStart, dayEnd, wakeupRef, bedtimeRef, onSav setLocalDayEnd(dayEnd); setLocalWakeupRef(wakeupRef); setLocalBedtimeRef(bedtimeRef); - }, [dayStart, dayEnd, wakeupRef, bedtimeRef]); + setLocalSessionsPerPage(sessionsPerPage); + }, [dayStart, dayEnd, wakeupRef, bedtimeRef, sessionsPerPage]); useEffect(() => { const handleClickOutside = (e: MouseEvent) => { @@ -42,6 +45,7 @@ export function SettingsPopover({ dayStart, dayEnd, wakeupRef, bedtimeRef, onSav dayEnd: localDayEnd, wakeupRef: localWakeupRef, bedtimeRef: localBedtimeRef, + sessionsPerPage: localSessionsPerPage, }); setIsOpen(false); }; @@ -65,44 +69,58 @@ export function SettingsPopover({ dayStart, dayEnd, wakeupRef, bedtimeRef, onSav
- - setLocalDayStart(e.target.value)} - className="w-full p-2 border rounded-md text-sm" -/> + + setLocalDayStart(e.target.value)} + className="w-full p-2 border rounded-md text-sm" + />
- - setLocalDayEnd(e.target.value)} - className="w-full p-2 border rounded-md text-sm" -/> + + setLocalDayEnd(e.target.value)} + className="w-full p-2 border rounded-md text-sm" + />
- - setLocalWakeupRef(e.target.value)} - className="w-full p-2 border rounded-md text-sm" -/> + + setLocalWakeupRef(e.target.value)} + className="w-full p-2 border rounded-md text-sm" + />
- - setLocalBedtimeRef(e.target.value)} - className="w-full p-2 border rounded-md text-sm" -/> + + setLocalBedtimeRef(e.target.value)} + className="w-full p-2 border rounded-md text-sm" + /> +
+
+ +
diff --git a/frontend/src/components/stats/StatsPage.tsx b/frontend/src/components/stats/StatsPage.tsx index 2516d55..e3518c8 100644 --- a/frontend/src/components/stats/StatsPage.tsx +++ b/frontend/src/components/stats/StatsPage.tsx @@ -27,18 +27,20 @@ const StatsPage: React.FC = () => {

Statistics

Overview of sleep patterns and trends

- { - update('dayStart', s.dayStart); - update('dayEnd', s.dayEnd); - update('wakeupRef', s.wakeupRef); - update('bedtimeRef', s.bedtimeRef); - }} - /> + { + update('dayStart', s.dayStart); + update('dayEnd', s.dayEnd); + update('wakeupRef', s.wakeupRef); + update('bedtimeRef', s.bedtimeRef); + update('sessionsPerPage', s.sessionsPerPage); + }} + />
diff --git a/frontend/src/hooks/SettingsProvider.tsx b/frontend/src/hooks/SettingsProvider.tsx new file mode 100644 index 0000000..b4caf53 --- /dev/null +++ b/frontend/src/hooks/SettingsProvider.tsx @@ -0,0 +1,57 @@ +import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react'; + +const STORAGE_KEY = 'baby-sleep-settings'; + +export interface Settings { + dayStart: string; + dayEnd: string; + wakeupRef: string; + bedtimeRef: string; + statsInterval: number; + sessionsPerPage: number; +} + +interface SettingsContextType { + settings: Settings; + update: (key: keyof Settings, value: string | number) => void; +} + +export const SettingsContext = createContext(undefined); + +const DEFAULT_SETTINGS: Settings = { + dayStart: '07:00', + dayEnd: '20:00', + wakeupRef: '04:00', + bedtimeRef: '19:00', + statsInterval: 7, + sessionsPerPage: 10, +}; + +export function SettingsProvider({ children }: { children: ReactNode }) { + const [settings, setSettings] = useState(() => { + if (typeof window === 'undefined') return DEFAULT_SETTINGS; + const stored = localStorage.getItem(STORAGE_KEY); + if (stored) { + try { + return { ...DEFAULT_SETTINGS, ...JSON.parse(stored) }; + } catch { + return DEFAULT_SETTINGS; + } + } + return DEFAULT_SETTINGS; + }); + + useEffect(() => { + localStorage.setItem(STORAGE_KEY, JSON.stringify(settings)); + }, [settings]); + + const update = (key: keyof Settings, value: string | number) => { + setSettings((prev) => ({ ...prev, [key]: value })); + }; + + return ( + + {children} + + ); +} diff --git a/frontend/src/hooks/useSettings.ts b/frontend/src/hooks/useSettings.ts index c587fb3..2117d33 100644 --- a/frontend/src/hooks/useSettings.ts +++ b/frontend/src/hooks/useSettings.ts @@ -1,44 +1,10 @@ -import { useState, useEffect } from 'react'; - -const STORAGE_KEY = 'baby-sleep-settings'; - -interface Settings { - dayStart: string; - dayEnd: string; - wakeupRef: string; - bedtimeRef: string; - statsInterval: number; -} - -const DEFAULT_SETTINGS: Settings = { - dayStart: '07:00', - dayEnd: '20:00', - wakeupRef: '04:00', - bedtimeRef: '19:00', - statsInterval: 7, -}; +import { useContext } from 'react'; +import { SettingsContext, Settings } from './SettingsProvider'; export function useSettings() { - const [settings, setSettings] = useState(() => { - if (typeof window === 'undefined') return DEFAULT_SETTINGS; - const stored = localStorage.getItem(STORAGE_KEY); - if (stored) { - try { - return { ...DEFAULT_SETTINGS, ...JSON.parse(stored) }; - } catch { - return DEFAULT_SETTINGS; - } - } - return DEFAULT_SETTINGS; - }); - - useEffect(() => { - localStorage.setItem(STORAGE_KEY, JSON.stringify(settings)); - }, [settings]); - - const update = (key: keyof Settings, value: string | number) => { - setSettings((prev) => ({ ...prev, [key]: value })); - }; - - return { settings, update }; -} \ No newline at end of file + const context = useContext(SettingsContext); + if (context === undefined) { + throw new Error('useSettings must be used within a SettingsProvider'); + } + return context; +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 876304e..0890193 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -2,14 +2,17 @@ import React from 'react' import ReactDOM from 'react-dom/client' import './index.css' import App from './App' -import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { SettingsProvider } from './hooks/SettingsProvider'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; const queryClient = new QueryClient() ReactDOM.createRoot(document.getElementById('root')!).render( - + + + , ) diff --git a/frontend/tests/e2e/pagination.spec.ts b/frontend/tests/e2e/pagination.spec.ts new file mode 100644 index 0000000..017b4ce --- /dev/null +++ b/frontend/tests/e2e/pagination.spec.ts @@ -0,0 +1,127 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Pagination', () => { + test.beforeEach(async ({ page }) => { + // Monitor network requests to see if our mock is hit + page.on('request', request => console.log('>>', request.method(), request.url())); + page.on('response', response => console.log('<<', response.status(), response.url())); + + // Mock the sessions API + await page.route('**/api/sessions', async (route) => { + const mockSessions = Array.from({ length: 15 }, (_, i) => ({ + id: i + 1, + start_time: new Date(2024, 0, 1 + i).toISOString(), + end_time: new Date(2024, 0, 1 + i, 8, 0).toISOString(), + notes: null, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + })); + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ sessions: mockSessions, total: 15 }), + }); + }); + + // Mock other stats APIs to prevent crashes from accessing undefined properties + await page.route(/\/api\/stats\/.*/, async (route) => { + const url = route.request().url(); + let body = {}; + + if (url.includes('summary')) { + body = { + total_sleeps: 10, + avg_sleeps_per_day: 2.5, + total_sleeps_all_time: 100, + avg_sleeps_per_day_all_time: 2.0, + }; + } else if (url.includes('daily-summary')) { + body = { + days: [ + { date: '2024-01-01', total_minutes: 480, day_minutes: 240, night_minutes: 240 }, + ], + }; + } else if (url.includes('timetable')) { + body = { + slots: [], + }; + } else if (url.includes('wake-sleep-times')) { + body = { + days: [ + { date: '2024-01-01', wakeup_minutes: 480, bedtime_minutes: 1200 }, + ], + }; + } + + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(body), + }); + }); + + await page.goto('http://localhost:5173/stats'); + }); + + test('Pagination component is visible when sessions exceed page size', async ({ page }) => { + // Wait for one of the mocked dates to appear to ensure data is loaded + await expect(page.locator('body')).toContainText('2024-01-01', { timeout: 10000 }); + await page.screenshot({ path: 'screenshots/pagination_visible.png' }); + + const pagination = page.getByTestId('pagination'); + await expect(pagination).toBeVisible(); + + // Use a more specific selector for the first page button + await expect(page.getByTestId('pagination').getByRole('button', { name: '1' })).toBeVisible(); + }); + + test('Changing results per page in settings updates the list', async ({ page }) => { + // Wait for data to load first + await expect(page.locator('body')).toContainText('2024-01-01', { timeout: 10000 }); + await page.screenshot({ path: 'screenshots/before_settings_change.png' }); + + // Open Settings + await page.getByRole('button', { name: /Settings/i }).click(); + await expect(page.locator('text=Results per page')).toBeVisible(); + + // Change to 5 + await page.getByLabel(/Results per page/i).selectOption('5'); + await page.getByRole('button', { name: /Save/i }).click(); + + // Wait for the UI to update and settings to be saved + await page.waitForTimeout(2000); + + await page.screenshot({ path: 'screenshots/after_settings_change.png' }); + + // Verify sessions per page is 5 (only 5 should be visible) + const sessionRows = page.locator('.session-list .border-b'); + await expect(sessionRows).toHaveCount(5); + + // Verify settings is saved in localStorage + const localStorage = await page.evaluate(() => { + return window.localStorage.getItem('baby-sleep-settings'); + }); + expect(localStorage).toContain('"sessionsPerPage":5'); + }); + + test('Navigating through pages works', async ({ page }) => { + // Wait for data to load first + await expect(page.locator('body')).toContainText('2024-01-01', { timeout: 10000 }); + + // Set to 5 via settings first + await page.getByRole('button', { name: /Settings/i }).click(); + await page.getByLabel(/Results per page/i).selectOption('5'); + await page.getByRole('button', { name: /Save/i }).click(); + await page.waitForTimeout(2000); + + const nextButton = page.getByRole('button', { name: /Next/i }); + await expect(nextButton).toBeVisible(); + + await nextButton.click(); + await page.waitForTimeout(1000); + + // Should now show sessions from index 5 to 9 (if total is 15) + // Mocked start_time for i=5 is 2024-01-06 + await expect(page.getByText('2024-01-06')).toBeVisible(); + }); +}); \ No newline at end of file diff --git a/plan.md b/plan.md new file mode 100644 index 0000000..520bf59 --- /dev/null +++ b/plan.md @@ -0,0 +1,351 @@ +# Proposed Implementation Plan + +## Phase 1: Settings Update +* **`frontend/src/hooks/useSettings.ts`**: Add `sessionsPerPage` to the settings state and define a default value (e.g., 10). +* **`frontend/src/components/stats/SettingsPopover.tsx`**: + * Update the props and local state to include `sessionsPerPage`. + * Add a dropdown in the settings UI for selecting "Results per page" (e.g., 5, 10, 25, 50). +* **`frontend/src/components/stats/StatsPage.tsx`**: Update the `onSave` handler to persist the new `sessionsPerPage` setting. + +## Phase 2: Pagination UI and Logic +* **`frontend/src/components/stats/Pagination.tsx` (New Component)**: Implement a "Google-style" pagination component with `Previous`, page numbers, and `Next` buttons. +* **`frontend/src/components/stats/SessionList.tsx`**: + * Integrate `useSettings` to fetch the selected page size. + * Implement local pagination logic (managing `currentPage`). + * Slice the session data to show only the current page's items. + * Render the new `Pagination` component at the bottom of the list. + +## Phase 3: Verification +* Verify that changing "Results per page" in Settings correctly updates the list. +* Confirm pagination navigation (Prev/Next/Numbers) works as expected and handles edge cases (e.g., first/last page). + +I will use multiple sub-agents to execute these tasks in parallel. + +--- + +The user wants to implement client-side pagination for "Sleep Sessions". +Plan: +1. Update `useSettings` to include `sessionsPerPage`. +2. Update `SettingsPopover` to allow users to change `sessionsPerPage`. +3. Update `StatsPage`'s `onSave` in `SettingsPopover` to include the new setting. +4. Create a new `Pagination` component. +5. Update `SessionList` to use pagination logic and the new component. +6. Verify with Playwright and screenshots. + +I'll start by using `todowrite` to track progress. +Then I will launch sub-agents. + +Sub-agents: +1. `explore` (or `general`) for Phase 1 (Settings). +2. `general` for Phase 2 (Pagination component and SessionList logic). +3. `general` for Phase 3 (Playwright verification). + +Wait, the user said "use as many sub-agents as possible". I'll split it. + +Task 1: Update settings (useSettings, SettingsPopover, StatsPage). +Task 2: Implement Pagination component and integrate it into SessionList. +Task 3: Write Playwright tests to verify functionality and take screenshots. + +Let's start with `todowrite`. + +## TODOS + +```json +{ + "todos": [ + { + "content": "Update useSettings to include sessionsPerPage", + "priority": "high", + "status": "pending" + }, + { + "content": "Update SettingsPopover to allow changing sessionsPerPage", + "priority": "high", + "status": "pending" + }, + { + "content": "Update StatsPage to persist sessionsPerPage in onSave", + "priority": "high", + "status": "pending" + }, + { + "content": "Create Pagination component in frontend/src/components/stats/Pagination.tsx", + "priority": "high", + "status": "pending" + }, + { + "content": "Update SessionList to implement client-side pagination logic and use Pagination component", + "priority": "high", + "status": "pending" + }, + { + "content": "Write Playwright tests to verify pagination and take screenshots", + "priority": "medium", + "status": "pending" + } + ] +} +``` + +## Current progress (git diff) + +``` +diff --git a/frontend/src/components/stats/SessionList.tsx b/frontend/src/components/stats/SessionList.tsx +index 374e41c..2bd6ea7 100644 +--- a/frontend/src/components/stats/SessionList.tsx ++++ b/frontend/src/components/stats/SessionList.tsx +@@ -1,15 +1,29 @@ + import React, { useState } from 'react'; + import { useSessions } from '../../hooks/useSleepSession'; ++import { useSettings } from '../../hooks/useSettings'; ++import { Pagination } from './Pagination'; + import { Edit2, Check, X, Trash2 } from 'lucide-react'; + import type { SleepSessionRead } from '../../types'; + import { formatDateTime, toLocalDatetimeInputValue, getDurationMinutes, formatMinutes } from '../../utils/time'; + + export function SessionList() { + const { sessions, isLoading, updateSession, deleteSession, isDeleting } = useSessions(); ++ const { settings } = useSettings(); ++ const sessionsPerPage = settings.sessionsPerPage; ++ ++ const [currentPage, setCurrentPage] = useState(1); + const [editingId, setEditingId] = useState(null); + const [editStart, setEditStart] = useState(''); + const [editEnd, setEditEnd] = useState(''); + ++ const totalPages = Math.ceil(sessions.length / sessionsPerPage); ++ const clampedPage = Math.max(1, Math.min(currentPage, totalPages || 1)); ++ ++ const paginatedSessions = sessions.slice( ++ (clampedPage - 1) * sessionsPerPage, ++ clampedPage * sessionsPerPage ++ ); ++ + if (isLoading) return
Loading sessions...
; + if (!sessions.length) return
No sessions recorded.
; + +@@ -55,7 +69,7 @@ export function SessionList() { +
+

Sleep Sessions

+
+- {sessions.map((s) => ( ++ {paginatedSessions.map((s) => ( +
+ {editingId === s.id ? ( +
+@@ -140,6 +154,13 @@ export function SessionList() { +
+ ))} +
++ {sessions.length > sessionsPerPage && ( ++ ++ )} +
+ ); + } +diff --git a/frontend/src/components/stats/SettingsPopover.tsx b/frontend/src/components/stats/SettingsPopover.tsx +index 005a302..8031392 100644 +--- a/frontend/src/components/stats/SettingsPopover.tsx ++++ b/frontend/src/components/stats/SettingsPopover.tsx +@@ -6,15 +6,17 @@ interface SettingsPopoverProps { + dayEnd: string; + wakeupRef: string; + bedtimeRef: string; +- onSave: (settings: { dayStart: string; dayEnd: string; wakeupRef: string; bedtimeRef: string }) => void; ++ sessionsPerPage: number; ++ onSave: (settings: { dayStart: string; dayEnd: string; wakeupRef: string; bedtimeRef: string; sessionsPerPage: number }) => void; + } + +-export function SettingsPopover({ dayStart, dayEnd, wakeupRef, bedtimeRef, onSave }: SettingsPopoverProps) { ++export function SettingsPopover({ dayStart, dayEnd, wakeupRef, bedtimeRef, sessionsPerPage, onSave }: SettingsPopoverProps) { + const [isOpen, setIsOpen] = useState(false); + const [localDayStart, setLocalDayStart] = useState(dayStart); + const [localDayEnd, setLocalDayEnd] = useState(dayEnd); + const [localWakeupRef, setLocalWakeupRef] = useState(wakeupRef); + const [localBedtimeRef, setLocalBedtimeRef] = useState(bedtimeRef); ++ const [localSessionsPerPage, setLocalSessionsPerPage] = useState(sessionsPerPage); + const popoverRef = useRef(null); + + useEffect(() => { +@@ -22,7 +24,8 @@ export function SettingsPopover({ dayStart, dayEnd, wakeupRef, bedtimeRef, onSav + setLocalDayEnd(dayEnd); + setLocalWakeupRef(wakeupRef); + setLocalBedtimeRef(bedtimeRef); +- }, [dayStart, dayEnd, wakeupRef, bedtimeRef]); ++ setLocalSessionsPerPage(sessionsPerPage); ++ }, [dayStart, dayEnd, wakeupRef, bedtimeRef, sessionsPerPage]); + + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { +@@ -42,6 +45,7 @@ export function SettingsPopover({ dayStart, dayEnd, wakeupRef, bedtimeRef, onSav + dayEnd: localDayEnd, + wakeupRef: localWakeupRef, + bedtimeRef: localBedtimeRef, ++ sessionsPerPage: localSessionsPerPage, + }); + setIsOpen(false); + }; +@@ -65,44 +69,58 @@ export function SettingsPopover({ dayStart, dayEnd, wakeupRef, bedtimeRef, onSav + +
+
+- +- setLocalDayStart(e.target.value)} +- className="w-full p-2 border rounded-md text-sm" +-/> ++ ++ setLocalDayStart(e.target.value)} ++ className="w-full p-2 border rounded-md text-sm" ++ /> +
+
+- +- setLocalDayEnd(e.target.value)} +- className="w-full p-2 border rounded-md text-sm" +-/> ++ ++ setLocalDayEnd(e.target.value)} ++ className="w-full p-2 border rounded-md text-sm" ++ /> +
+
+- +- setLocalWakeupRef(e.target.value)} +- className="w-full p-2 border rounded-md text-sm" +-/> ++ ++ setLocalWakeupRef(e.target.value)} ++ className="w-full p-2 border rounded-md text-sm" ++ /> +
+
+- +- setLocalBedtimeRef(e.target.value)} +- className="w-full p-2 border rounded-md text-sm" +-/> ++ ++ setLocalBedtimeRef(e.target.value)} ++ className="w-full p-2 border rounded-md text-sm" ++ /> ++
++
++ ++ +
+
+ +diff --git a/frontend/src/components/stats/StatsPage.tsx b/frontend/src/components/stats/StatsPage.tsx +index 2516d55..e3518c8 100644 +--- a/frontend/src/components/stats/StatsPage.tsx ++++ b/frontend/src/components/stats/StatsPage.tsx +@@ -27,18 +27,20 @@ const StatsPage: React.FC = () => { +

Statistics

+

Overview of sleep patterns and trends

+
+- { +- update('dayStart', s.dayStart); +- update('dayEnd', s.dayEnd); +- update('wakeupRef', s.wakeupRef); +- update('bedtimeRef', s.bedtimeRef); +- }} +- /> ++ { ++ update('dayStart', s.dayStart); ++ update('dayEnd', s.dayEnd); ++ update('wakeupRef', s.wakeupRef); ++ update('bedtimeRef', s.bedtimeRef); ++ update('sessionsPerPage', s.sessionsPerPage); ++ }} ++ /> +
+ +
+diff --git a/frontend/src/hooks/useSettings.ts b/frontend/src/hooks/useSettings.ts +index c587fb3..d95899b 100644 +--- a/frontend/src/hooks/useSettings.ts ++++ b/frontend/src/hooks/useSettings.ts +@@ -8,6 +8,7 @@ interface Settings { + wakeupRef: string; + bedtimeRef: string; + statsInterval: number; ++ sessionsPerPage: number; + } + + const DEFAULT_SETTINGS: Settings = { +@@ -16,6 +17,7 @@ const DEFAULT_SETTINGS: Settings = { + wakeupRef: '04:00', + bedtimeRef: '19:00', + statsInterval: 7, ++ sessionsPerPage: 10, + }; + + export function useSettings() { + +``` + +## Last command + +The last command you ran was this: + +``` +npx playwright test frontend/tests/e2e/pagination.spec.ts --reporter=list +``` + +so please run this again and see the output, as this might help us know where to pick things up. diff --git a/screenshots/after_settings_change.png b/screenshots/after_settings_change.png new file mode 100644 index 0000000000000000000000000000000000000000..0f3ea4db8eb8b4a93f0fce2334f523c2508bcf95 GIT binary patch literal 36616 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 literal 0 HcmV?d00001 diff --git a/screenshots/pagination_visible.png b/screenshots/pagination_visible.png new file mode 100644 index 0000000000000000000000000000000000000000..0f3ea4db8eb8b4a93f0fce2334f523c2508bcf95 GIT binary patch literal 36616 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/Users/jhag/projects/babymonitor/frontend/src/components/stats/SessionList.tsx +file + +1: import React, { useState } from 'react'; +2: import { useSessions } from '../../hooks/useSleepSession'; +3: import { Edit2, Check, X, Trash2 } from 'lucide-react'; +4: import type { SleepSessionRead } from '../../types'; +5: import { formatDateTime, toLocalDatetimeInputValue, getDurationMinutes, formatMinutes } from '../../utils/time'; +6: +7: export function SessionList() { +8: const { sessions, isLoading, updateSession, deleteSession, isDeleting } = useSessions(); +9: const [editingId, setEditingId] = useState(null); +10: const [editStart, setEditStart] = useState(''); +11: const [editEnd, setEditEnd] = useState(''); +12: +13: if (isLoading) return
Loading sessions...
; +14: if (!sessions.length) return
No sessions recorded.
; +15: +16: const handleSave = async (id: number) => { +17: try { +18: await updateSession({ +19: sessionId: id, +20: payload: { +21: start_time: new Date(editStart).toISOString(), +22: end_time: editEnd ? new Date(editEnd).toISOString() : undefined, +23: } +24: }); +25: setEditingId(null); +26: } catch (err) { +27: console.error(err); +28: alert('Failed to update session'); +29: } +30: }; +31: +32: const handleDelete = async (e: React.MouseEvent, id: number) => { +33: e.stopPropagation(); +34: if (window.confirm('Are you sure you want to delete this sleep session?')) { +35: try { +36: await deleteSession(id); +37: } catch (err) { +38: console.error(err); +39: alert('Failed to delete session'); +40: } +41: } +42: }; +43: +44: const startEditing = (s: SleepSessionRead) => { +45: setEditingId(s.id); +46: setEditStart(toLocalDatetimeInputValue(new Date(s.start_time))); +47: setEditEnd(s.end_time ? toLocalDatetimeInputValue(new Date(s.end_time)) : ''); +48: }; +49: +50: const handleCancel = () => { +51: setEditingId(null); +52: }; +53: +54: return ( +55:
+56:

Sleep Sessions

+57:
+58: {sessions.map((s) => ( +59:
+60: {editingId === s.id ? ( +61:
+62:
+63:
+64: Start Time +65: setEditStart(e.target.value)} +70: /> +71:
+72:
+73: End Time +74: setEditEnd(e.target.value)} +79: /> +80:
+81:
+82:
+83: +91: +99:
+100:
+101: ) : ( +102:
startEditing(s)} +105: > +106:
+107:
+108: +109: {formatDateTime(s.start_time)} +110: +111: +112: {s.end_time && s.start_time ? formatMinutes(getDurationMinutes(s.start_time, s.end_time)) : '—'} +113: +114:
+115: +116: {s.end_time ? `${formatDateTime(s.start_time)} → ${formatDateTime(s.end_time)}` : 'In progress'} +117: +118:
+119: +120:
+121:
+122: {s.end_time && s.start_time ? formatMinutes(getDurationMinutes(s.start_time, s.end_time)) : '—'} +123:
+124:
+125: +133:
+134: +135:
+136:
+137:
+138:
+139: )} +140:
+141: ))} +142:
+143:
+144: ); +145: } + +(End of file - total 145 lines) +
+ + +Instructions from: /Users/jhag/projects/babymonitor/frontend/src/components/stats/AGENTS.md +# frontend/src/components/stats + +## OVERVIEW + +Stats visualization components: SessionList, StatsPage, DailySummary, BedtimePlot, WakeupPlot, SettingsPopover, IntervalPicker, SleepTimetable. + +## WHERE TO LOOK + +| Component | Purpose | +|----------|---------| +| `StatsPage.tsx` | Main stats tab container | +| `DailySummary.tsx` | Day vs. night sleep totals | +| `SleepTimetable.tsx` | Continuous time-slot grid | +| `BedtimePlot.tsx` | Bedtime trend chart | +| `WakeupPlot.tsx` | Wakeup time trend chart | + +## CONVENTIONS + +- Uses Recharts for visualization +- Tailwind v4 for styling (no custom CSS classes) +- Lucide icons for UI elements + +``` + +--- + +## Assistant (Plan · Gemma 4 26b A4B · 8.5s) + +_Thinking:_ + + +`SessionList.tsx` currently maps over `sessions` directly: +```tsx +58: {sessions.map((s) => ( +``` + +I also need to see `SettingsPopover.tsx` and `useSettings.ts`. + + +**Tool: read** + +**Input:** +```json +{ + "filePath": "/Users/jhag/projects/babymonitor/frontend/src/components/stats/SettingsPopover.tsx" +} +``` + +**Output:** +``` +/Users/jhag/projects/babymonitor/frontend/src/components/stats/SettingsPopover.tsx +file + +1: /* eslint-disable react-hooks/set-state-in-effect */ +2: import React, { useState, useRef, useEffect } from 'react'; +3: +4: interface SettingsPopoverProps { +5: dayStart: string; +6: dayEnd: string; +7: wakeupRef: string; +8: bedtimeRef: string; +9: onSave: (settings: { dayStart: string; dayEnd: string; wakeupRef: string; bedtimeRef: string }) => void; +10: } +11: +12: export function SettingsPopover({ dayStart, dayEnd, wakeupRef, bedtimeRef, onSave }: SettingsPopoverProps) { +13: const [isOpen, setIsOpen] = useState(false); +14: const [localDayStart, setLocalDayStart] = useState(dayStart); +15: const [localDayEnd, setLocalDayEnd] = useState(dayEnd); +16: const [localWakeupRef, setLocalWakeupRef] = useState(wakeupRef); +17: const [localBedtimeRef, setLocalBedtimeRef] = useState(bedtimeRef); +18: const popoverRef = useRef(null); +19: +20: useEffect(() => { +21: setLocalDayStart(dayStart); +22: setLocalDayEnd(dayEnd); +23: setLocalWakeupRef(wakeupRef); +24: setLocalBedtimeRef(bedtimeRef); +25: }, [dayStart, dayEnd, wakeupRef, bedtimeRef]); +26: +27: useEffect(() => { +28: const handleClickOutside = (e: MouseEvent) => { +29: if (popoverRef.current && !popoverRef.current.contains(e.target as Node)) { +30: setIsOpen(false); +31: } +32: }; +33: if (isOpen) { +34: document.addEventListener('mousedown', handleClickOutside); +35: } +36: return () => document.removeEventListener('mousedown', handleClickOutside); +37: }, [isOpen]); +38: +39: const handleSave = () => { +40: onSave({ +41: dayStart: localDayStart, +42: dayEnd: localDayEnd, +43: wakeupRef: localWakeupRef, +44: bedtimeRef: localBedtimeRef, +45: }); +46: setIsOpen(false); +47: }; +48: +49: return ( +50:
+51: +61: +62: {isOpen && ( +63:
+64:

Settings

+65: +66:
+67:
+68: +69: setLocalDayStart(e.target.value)} +74: className="w-full p-2 border rounded-md text-sm" +75: /> +76:
+77:
+78: +79: setLocalDayEnd(e.target.value)} +84: className="w-full p-2 border rounded-md text-sm" +85: /> +86:
+87:
+88: +89: setLocalWakeupRef(e.target.value)} +94: className="w-full p-2 border rounded-md text-sm" +95: /> +96:
+97:
+98: +99: setLocalBedtimeRef(e.target.value)} +104: className="w-full p-2 border rounded-md text-sm" +105: /> +106:
+107:
+108: +109: +115:
+116: )} +117:
+118: ); +119: } + +(End of file - total 119 lines) +
+``` + +--- + +## Assistant (Plan · Gemma 4 26b A4B · 7.0s) + +_Thinking:_ + + +And `useSettings.ts`: + + +**Tool: read** + +**Input:** +```json +{ + "filePath": "/Users/jhag/projects/babymonitor/frontend/src/hooks/useSettings.ts" +} +``` + +**Output:** +``` +/Users/jhag/projects/babymonitor/frontend/src/hooks/useSettings.ts +file + +1: import { useState, useEffect } from 'react'; +2: +3: const STORAGE_KEY = 'baby-sleep-settings'; +4: +5: interface Settings { +6: dayStart: string; +7: dayEnd: string; +8: wakeupRef: string; +9: bedtimeRef: string; +10: statsInterval: number; +11: } +12: +13: const DEFAULT_SETTINGS: Settings = { +14: dayStart: '07:00', +15: dayEnd: '20:00', +16: wakeupRef: '04:00', +17: bedtimeRef: '19:00', +18: statsInterval: 7, +19: }; +20: +21: export function useSettings() { +22: const [settings, setSettings] = useState(() => { +23: if (typeof window === 'undefined') return DEFAULT_SETTINGS; +24: const stored = localStorage.getItem(STORAGE_KEY); +25: if (stored) { +26: try { +27: return { ...DEFAULT_SETTINGS, ...JSON.parse(stored) }; +28: } catch { +29: return DEFAULT_SETTINGS; +30: } +31: } +32: return DEFAULT_SETTINGS; +33: }); +34: +35: useEffect(() => { +36: localStorage.setItem(STORAGE_KEY, JSON.stringify(settings)); +37: }, [settings]); +38: +39: const update = (key: keyof Settings, value: string | number) => { +40: setSettings((prev) => ({ ...prev, [key]: value })); +41: }; +42: +43: return { settings, update }; +44: } + +(End of file - total 44 lines) + + + +Instructions from: /Users/jhag/projects/babymonitor/frontend/src/hooks/AGENTS.md +# frontend/src/hooks + +## OVERVIEW + +Custom React hooks: useStats, useSettings, useSleepSession. + +## WHERE TO LOOK + +| Hook | Purpose | +|------|---------| +| `useSleepSession.ts` | Session start/stop/CRUD | +| `useStats.ts` | Stats query hooks | +| `useSettings.ts` | App settings state | + +## CONVENTIONS + +- TanStack Query v5 for caching +- React 19 hooks +- Export typed interfaces + +``` + +--- + +## Assistant (Plan · Gemma 4 26b A4B · 17.7s) + +_Thinking:_ + + +I have enough information to form a plan. + +**Plan for implementing client-side pagination for Sleep Sessions:** + +1. **Update Settings**: + * Modify `frontend/src/hooks/useSettings.ts` to include a new setting: `sessionsPerPage: number`. + * Update `DEFAULT_SETTINGS` with a default value (e.g., 10). + * Modify `frontend/src/components/stats/SettingsPopover.tsx` to include a dropdown or input for "Results per page". + * Ensure `onSave` in `SettingsPopover.tsx` (or how it's used) is updated to handle the new field if necessary, but actually `SettingsPopover` uses an `onSave` callback that passes the *local* state. I'll need to make sure the parent component calling `SettingsPopover` passes the correct update function or that I modify how settings are saved. + * Wait, `SettingsPopover` takes `onSave`. Let's check where it's used. It's likely in `StatsPage.tsx` or similar. + +2. **Implement Pagination Logic and UI**: + * Create a new component `frontend/src/components/stats/Pagination.tsx` to handle the Google-style pagination (Prev, Numbers, Next). + * Modify `frontend/src/components/stats/SessionList.tsx`: + * Use `useSettings` hook to get the `sessionsPerPage`. + * Manage `currentPage` state locally within `SessionList.tsx`. + * Calculate the slice of sessions to display based on `currentPage` and `sessionsPerPage`. + * Render the list of paginated sessions. + * Render the `Pagination` component below the list. + +3. **Detailed Steps for Sub-agents**: + * `explore` agent: Confirm where `SettingsPopover` is used and how it interacts with `useSettings`. + * `general` agent (Implementation): + * Task 1: Update `useSettings.ts` and `SettingsPopover.tsx`. + * Task 2: Implement `Pagination.tsx` component. + * Task 3: Update `SessionList.tsx` to use pagination and the new settings. + * Verification: Manual verification of UI/UX in a development environment (not possible for me directly, but I can write tests or suggest them). + +Let's find where `SettingsPopover` is used. + + +**Tool: grep** + +**Input:** +```json +{ + "pattern": "SettingsPopover" +} +``` + +**Output:** +``` +Found 11 matches +/Users/jhag/projects/babymonitor/frontend/src/components/stats/StatsPage.tsx: + Line 3: import { SettingsPopover } from './SettingsPopover'; + + Line 30: {} } }, + + +/Users/jhag/projects/babymonitor/frontend/src/components/stats/SettingsPopover.tsx: + Line 4: interface SettingsPopoverProps { + + Line 12: export function SettingsPopover({ dayStart, dayEnd, wakeupRef, bedtimeRef, onSave }: SettingsPopoverProps) { + + +/Users/jhag/projects/babymonitor/frontend/playground_plan.md: + Line 23: - `SettingsPopover` + + +/Users/jhag/projects/babymonitor/frontend_improvements.md: + Line 169: - `src/components/tracker/SettingsPopover.tsx:72,82,92,102` — four `onChange` handlers as inline arrow functions + + Line 209: | `SettingsPopover` | Click-outside dismiss, input changes persisted | + + Line 258: | 9 | Performance — memoisation | `SettingsPopover.tsx`, `SessionList.tsx` | Low | + + +/Users/jhag/projects/babymonitor/frontend/src/components/stats/AGENTS.md: + Line 5: Stats visualization components: SessionList, StatsPage, DailySummary, BedtimePlot, WakeupPlot, SettingsPopover, IntervalPicker, SleepTimetable. + +``` + +--- + +## Assistant (Plan · Gemma 4 26b A4B · 5.0s) + +_Thinking:_ + + +`frontend/src/components/stats/StatsPage.tsx` uses `SettingsPopover`. Let's check it. + + +**Tool: read** + +**Input:** +```json +{ + "filePath": "/Users/jhag/projects/babymonitor/frontend/src/components/stats/StatsPage.tsx" +} +``` + +**Output:** +``` +/Users/jhag/projects/babymonitor/frontend/src/components/stats/StatsPage.tsx +file + +1: import React from 'react'; +2: import { IntervalPicker } from './IntervalPicker'; +3: import { SettingsPopover } from './SettingsPopover'; +4: import SummaryCards from './SummaryCards'; +5: import DailySummary from './DailySummary'; +6: import Timetable from './SleepTimetable'; +7: import WakeupPlot from './WakeupPlot'; +8: import BedtimePlot from './BedtimePlot'; +9: import { SessionList } from './SessionList'; +10: import { ErrorBoundary } from '../../components/ErrorBoundary'; +11: import { useSettings } from '../../hooks/useSettings'; +12: import { useSummary } from '../../hooks/useStats'; +13: +14: const StatsPage: React.FC = () => { +15: const { settings, update } = useSettings(); +16: +17: const now = new Date(); +18: const to_date = now.toLocaleDateString('en-CA'); // YYYY-MM-DD in local time +19: const from_date = new Date(now.setDate(now.getDate() - settings.statsInterval)).toLocaleDateString('en-CA'); +20: +21: const { data: summary } = useSummary(from_date, to_date); +22: +23: return ( +24:
+25:
+26:
+27:

Statistics

+28:

Overview of sleep patterns and trends

+29:
+30: { +36: update('dayStart', s.dayStart); +37: update('dayEnd', s.dayEnd); +38: update('wakeupRef', s.wakeupRef); +39: update('bedtimeRef', s.bedtimeRef); +40: }} +41: /> +42:
+43: +44:
+45: update('statsInterval', days)} +48: /> +49:
+50: +51: +52: +53: +54: +60: +61: +62:
+63: +64: +70: +71: +72: +78: +79:
+80: +81: +82: +88: +89: +90: +91: +92: +93:
+94: ); +95: }; +96: +97: export default StatsPage; + +(End of file - total 97 lines) +
+``` + +--- + +## Assistant (Plan · Gemma 4 26b A4B · 37.2s) + +_Thinking:_ + + +Okay, `StatsPage.tsx` is where the settings are updated and `SessionList` is rendered. + +I see that `SettingsPopover` props only include the current values and an `onSave`. I will need to: +1. Update `SettingsPopoverProps` interface in `SettingsPopover.tsx`. +2. Add a new field to the `onSave` payload or update it. Looking at `SettingsPopover.tsx`: + ```tsx + interface SettingsPopoverProps { + dayStart: string; + dayEnd: string; + wakeupRef: string; + bedtimeRef: string; + onSave: (settings: { dayStart: string; dayEnd: string; wakeupRef: string; bedtimeRef: string }) => void; + } + ``` + I will add `sessionsPerPage: number` to it. + +3. Update `StatsPage.tsx`'s `onSave` handler to also call `update('sessionsPerPage', s.sessionsPerPage)`. + +Wait, I noticed something in `SettingsPopover.tsx` (lines 14-17): +```tsx +14: const [localDayStart, setLocalDayStart] = useState(dayStart); +15: const [localDayEnd, setLocalDayEnd] = useState(dayEnd); +16: const [localWakeupRef, setLocalWakeupRef] = useState(wakeupRef); +17: const [localBedtimeRef, setLocalBedtimeRef] = useState(bedtimeRef); +``` +I'll need to add `localSessionsPerPage` as well. + +**Final Plan for Implementation:** + +### Phase 1: Settings Update +- **File**: `frontend/src/hooks/useSettings.ts` + - Add `sessionsPerPage: number` to `Settings` interface and `DEFAULT_SETTINGS`. +- **File**: `frontend/src/components/stats/SettingsPopover.tsx` + - Update `SettingsPopoverProps` to include `sessionsPerPage`. + - Add local state for `localSessionsPerPage`. + - Update the UI to include a "Results per page" dropdown (e.g., 5, 10, 25, 50). + - Update `handleSave` to include `sessionsPerPage` in the payload. +- **File**: `frontend/src/components/stats/StatsPage.tsx` + - Update the `onSave` callback in `` to include `update('sessionsPerPage', s.sessionsPerPage)`. + +### Phase 2: Pagination UI and Logic +- **File**: `frontend/src/components/stats/Pagination.tsx` (New file) + - Create a component that takes `currentPage`, `totalPages`, `onPageChange`. + - Implementation of "Google style" pagination: `< Prev | 1 | 2 | 3 | Next >`. +- **File**: `frontend/src/components/stats/SessionList.tsx` + - Import `useSettings`. + - Introduce `currentPage` state. + - Calculate paginated data: `const paginatedSessions = sessions.slice((currentPage - 1) * sessionsPerPage, currentPage * sessionsPerPage);`. + - Render `paginatedSessions` instead of `sessions`. + - Render `` below the list. + - Ensure clicking a session to edit still works with the paginated data (it should, since we are just slicing). + +### Phase 3: Verification +- Ensure pagination works when `sessionsPerPage` is changed in Settings. +- Ensure page numbers and Next/Prev work correctly. + +I'll start by preparing the user for this plan. + + +I have analyzed the codebase and formulated a plan to implement client-side pagination for sleep sessions. + +### Proposed Implementation Plan + +#### Phase 1: Settings Update +* **`frontend/src/hooks/useSettings.ts`**: Add `sessionsPerPage` to the settings state and define a default value (e.g., 10). +* **`frontend/src/components/stats/SettingsPopover.tsx`**: + * Update the props and local state to include `sessionsPerPage`. + * Add a dropdown in the settings UI for selecting "Results per page" (e.g., 5, 10, 25, 50). +* **`frontend/src/components/stats/StatsPage.tsx`**: Update the `onSave` handler to persist the new `sessionsPerPage` setting. + +#### Phase 2: Pagination UI and Logic +* **`frontend/src/components/stats/Pagination.tsx` (New Component)**: Implement a "Google-style" pagination component with `Previous`, page numbers, and `Next` buttons. +* **`frontend/src/components/stats/SessionList.tsx`**: + * Integrate `useSettings` to fetch the selected page size. + * Implement local pagination logic (managing `currentPage`). + * Slice the session data to show only the current page's items. + * Render the new `Pagination` component at the bottom of the list. + +#### Phase 3: Verification +* Verify that changing "Results per page" in Settings correctly updates the list. +* Confirm pagination navigation (Prev/Next/Numbers) works as expected and handles edge cases (e.g., first/last page). + +I will use multiple sub-agents to execute these tasks in parallel. Shall I proceed? + +--- + +## User + +Great! lets go. Don't forget to verify using playwright and take screenshots so you can visually examine that it looks good. + +--- + +## Assistant (Build · Gemma 4 26b A4B · 31.3s) + +_Thinking:_ + + +The user wants to implement client-side pagination for "Sleep Sessions". +Plan: +1. Update `useSettings` to include `sessionsPerPage`. +2. Update `SettingsPopover` to allow users to change `sessionsPerPage`. +3. Update `StatsPage`'s `onSave` in `SettingsPopover` to include the new setting. +4. Create a new `Pagination` component. +5. Update `SessionList` to use pagination logic and the new component. +6. Verify with Playwright and screenshots. + +I'll start by using `todowrite` to track progress. +Then I will launch sub-agents. + +Sub-agents: +1. `explore` (or `general`) for Phase 1 (Settings). +2. `general` for Phase 2 (Pagination component and SessionList logic). +3. `general` for Phase 3 (Playwright verification). + +Wait, the user said "use as many sub-agents as possible". I'll split it. + +Task 1: Update settings (useSettings, SettingsPopover, StatsPage). +Task 2: Implement Pagination component and integrate it into SessionList. +Task 3: Write Playwright tests to verify functionality and take screenshots. + +Let's start with `todowrite`. + +**Tool: todowrite** + +**Input:** +```json +{ + "todos": [ + { + "content": "Update useSettings to include sessionsPerPage", + "priority": "high", + "status": "pending" + }, + { + "content": "Update SettingsPopover to allow changing sessionsPerPage", + "priority": "high", + "status": "pending" + }, + { + "content": "Update StatsPage to persist sessionsPerPage in onSave", + "priority": "high", + "status": "pending" + }, + { + "content": "Create Pagination component in frontend/src/components/stats/Pagination.tsx", + "priority": "high", + "status": "pending" + }, + { + "content": "Update SessionList to implement client-side pagination logic and use Pagination component", + "priority": "high", + "status": "pending" + }, + { + "content": "Write Playwright tests to verify pagination and take screenshots", + "priority": "medium", + "status": "pending" + } + ] +} +``` + +**Output:** +``` +[ + { + "content": "Update useSettings to include sessionsPerPage", + "status": "pending", + "priority": "high" + }, + { + "content": "Update SettingsPopover to allow changing sessionsPerPage", + "status": "pending", + "priority": "high" + }, + { + "content": "Update StatsPage to persist sessionsPerPage in onSave", + "status": "pending", + "priority": "high" + }, + { + "content": "Create Pagination component in frontend/src/components/stats/Pagination.tsx", + "status": "pending", + "priority": "high" + }, + { + "content": "Update SessionList to implement client-side pagination logic and use Pagination component", + "status": "pending", + "priority": "high" + }, + { + "content": "Write Playwright tests to verify pagination and take screenshots", + "status": "pending", + "priority": "medium" + } +] +``` + +--- + +## Assistant (Build · Gemma 4 26b A4B · 19.0s) + +_Thinking:_ + + +I'll launch the first two agents for Phase 1 and Phase 2. + +Agent 1 (Settings): +- Update `frontend/src/hooks/useSettings.ts` +- Update `frontend/src/components/stats/SettingsPopover.tsx` +- Update `frontend/src/components/stats/StatsPage.tsx` + +Agent 2 (Pagination): +- Create `frontend/src/components/stats/Pagination.tsx` +- Update `frontend/src/components/stats/SessionList.tsx` + +I'll wait for them to finish before doing Phase 3 (Playwright). + + +**Tool: task** + +**Input:** +```json +{ + "prompt": "Update settings to include `sessionsPerPage`. \n\n1. In `frontend/src/hooks/useSettings.ts`:\n - Add `sessionsPerPage: number` to the `Settings` interface.\n - Update `DEFAULT_SETTINGS` with `sessionsPerPage: 10`.\n\n2. In `frontend/src/components/stats/SettingsPopover.tsx`:\n - Update `SettingsPopoverProps` to include `sessionsPerPage: number`.\n - Add local state for `localSessionsPerPage`.\n - Update `useEffect` to sync with props.\n - Add a dropdown/select input in the JSX for \"Results per page\" with options like 5, 10, 25, 50.\n - Update `handleSave` to include `sessionsPerPage`.\n\n3. In `frontend/src/components/stats/StatsPage.tsx`:\n - Update the `onSave` handler for `` to include `sessionsPerPage`.\n\nVerify all changes by checking if the new setting is correctly passed and handled.", + "subagent_type": "general" +} +``` + +**Error:** +``` +The task tool was called with invalid arguments: SchemaError(Missing key + at ["description"]). +Please rewrite the input so it satisfies the expected schema. +``` + +**Tool: task** + +**Input:** +```json +{ + "prompt": "Implement client-side pagination for Sleep Sessions.\n\n1. Create a new component `frontend/src/components/stats/Pagination.tsx`:\n - A \"Google-style\" pagination component.\n - Props: `currentPage`, `totalPages`, `onPageChange`.\n - UI: \"Previous\" button, page numbers (with ellipsis if many), and \"Next\" button.\n - Use Tailwind CSS for styling and Lucide icons if needed.\n\n2. Update `frontend/src/components/stats/SessionList.tsx`:\n - Import `useSettings` to get `sessionsPerPage`.\n - Implement local state for `currentPage`.\n - Calculate `totalPages` based on `sessions.length` and `sessionsPerPage`.\n - Slice the `sessions` array to only show sessions for the current page.\n - Render the new `` component below the session list if `sessions.length > sessionsPerPage`.\n - Ensure pagination works correctly when changing pages or when the list of sessions changes (e.g., after deletion).\n - Make sure to reset `currentPage` to 1 if the total pages decrease (e.g., when items are deleted).", + "subagent_type": "general" +} +``` + +**Error:** +``` +The task tool was called with invalid arguments: SchemaError(Missing key + at ["description"]). +Please rewrite the input so it satisfies the expected schema. +``` + +--- + +## Assistant (Build · Gemma 4 26b A4B · 176.1s) + +_Thinking:_ + + +Ah, I missed the `description` parameter in the `task` tool call. My apologies. I will retry. + + +**Tool: task** + +**Input:** +```json +{ + "description": "Update settings to include `sessionsPerPage`. \n\n1. In `frontend/src/hooks/useSettings.ts`:\n - Add `sessionsPerPage: number` to the `Settings` interface.\n - Update `DEFAULT_SETTINGS` with `sessionsPerPage: 10`.\n\n2. In `frontend/src/components/stats/SettingsPopover.tsx`:\n - Update `SettingsPopoverProps` to include `sessionsPerPage: number`.\n - Add local state for `localSessionsPerPage`.\n - Update `useEffect` to sync with props.\n - Add a dropdown/select input in the JSX for \"Results per page\" with options like 5, 10, 25, 50.\n - Update `handleSave` to include `sessionsPerPage`.\n\n3. In `frontend/src/components/stats/StatsPage.tsx`:\n - Update the `onSave` handler for `` to include `sessionsPerPage`.\n\nVerify all changes by checking if the new setting is correctly passed and handled.", + "prompt": "Update settings to include `sessionsPerPage`. \n\n1. In `frontend/src/hooks/useSettings.ts`:\n - Add `sessionsPerPage: number` to the `Settings` interface.\n - Update `DEFAULT_SETTINGS` with `sessionsPerPage: 10`.\n\n2. In `frontend/src/components/stats/SettingsPopover.tsx`:\n - Update `SettingsPopoverProps` to include `sessionsPerPage: number`.\n - Add local state for `localSessionsPerPage`.\n - Update `useEffect` to sync with props.\n - Add a dropdown/select input in the JSX for \"Results per page\" with options like 5, 10, 25, 50.\n - Update `handleSave` to include `sessionsPerPage`.\n\n3. In `frontend/src/components/stats/StatsPage.tsx`:\n - Update the `onSave` handler for `` to include `sessionsPerPage`.\n\nVerify all changes by checking if the new setting is correctly passed and handled.", + "subagent_type": "general" +} +``` + +**Output:** +``` + + +The requested changes have been implemented: + +1. **`frontend/src/hooks/useSettings.ts`**: Added `sessionsPerPage: number` to the `Settings` interface and initialized it in `DEFAULT_SETTINGS` with a value of `10`. +2. **`frontend/src/components/stats/SettingsPopover.tsx`**: Updated `SettingsPopoverProps` to include `sessionsPerPage`. Added local state `localSessionsPerPage`, synced it via `useEffect`, added a dropdown for "Results per page" (5, 10, 25, 50), and updated `handleSave` to include the new value. +3. **`frontend/src/components/stats/StatsPage.tsx`**: Updated the `onSave` handler to pass `sessionsPerPage` back to the settings hook. + +Existing linting errors in other files were noted but not modified as they were unrelated to the current task. + + +``` + +--- + +## Assistant (Build · Gemma 4 26b A4B · 641.0s) + +_Thinking:_ + + +Agent 1 finished. Now for Agent 2 (Pagination implementation). I also need to add `description` this time. + + +**Tool: task** + +**Input:** +```json +{ + "description": "Implement client-side pagination for Sleep Sessions.\n\n1. Create a new component `frontend/src/components/stats/Pagination.tsx`:\n - A \"Google-style\" pagination component.\n - Props: `currentPage`, `totalPages`, `onPageChange`.\n - UI: \"Previous\" button, page numbers (with ellipsis if many), and \"Next\" button.\n - Use Tailwind CSS for styling and Lucide icons if needed.\n\n2. Update `frontend/src/components/stats/SessionList.tsx`:\n - Import `useSettings` to get `sessionsPerPage`.\n - Implement local state for `currentPage`.\n - Calculate `totalPages` based on `sessions.length` and `sessionsPerPage`.\n - Slice the `sessions` array to only show sessions for the current page.\n - Render the new `` component below the session list if `sessions.length > sessionsPerPage`.\n - Ensure pagination works correctly when changing pages or when the list of sessions changes (e.g., after deletion).\n - Make sure to reset `currentPage` to 1 if the total pages decrease (e.g., when items are deleted).", + "prompt": "Implement client-side pagination for Sleep Sessions.\n\n1. Create a new component `frontend/src/components/stats/Pagination.tsx`:\n - A \"Google-style\" pagination component.\n - Props: `currentPage`, `totalPages`, `onPageChange`.\n - UI: \"Previous\" button, page numbers (with ellipsis if many), and \"Next\" button.\n - Use Tailwind CSS for styling and Lucide icons if needed.\n\n2. Update `frontend/src/components/stats/SessionList.tsx`:\n - Import `useSettings` to get `sessionsPerPage`.\n - Implement local state for `currentPage`.\n - Calculate `totalPages` based on `sessions.length` and `sessionsPerPage`.\n - Slice the `sessions` array to only show sessions for the current page.\n - Render the new `` component below the session list if `sessions.length > sessionsPerPage`.\n - Ensure pagination works correctly when changing pages or when the list of sessions changes (e.g., after deletion).\n - Make sure to reset `currentPage` to 1 if the total pages decrease (e.g., when items are deleted).", + "subagent_type": "general" +} +``` + +**Output:** +``` + + +I've implemented client-side pagination for Sleep Sessions: + +1. Created `frontend/src/components/stats/Pagination.tsx`: A Google-style pagination component using Tailwind CSS and Lucide icons. +2. Updated `frontend/src/components/stats/SessionList.tsx`: + * Integrated `useSettings` to respect the `sessionsPerPage` setting. + * Implemented pagination logic with derived state (`clampedPage`) to handle page changes and session list updates (e.g., after deletion) safely without triggering linting warnings about `useEffect`. + * Added the `` component below the list when more than one page of sessions is available. + + +``` + +--- + +## Assistant (Build · Gemma 4 26b A4B · 8.6s) + +_Thinking:_ + + +Both Phase 1 and Phase 2 are complete. Now for Phase 3: Verification with Playwright and screenshots. + +I need to: +1. Ensure the app is running (or can be run). +2. Write a Playwright test that: + - Navigates to the Stats page. + - Verifies pagination exists if there are many sessions (I might need to seed some data or rely on existing data). + - Checks "Previous" and "Next" buttons. + - Opens Settings, changes results per page, and verifies the list updates. +3. Run the test with screenshots enabled. + +First, let's see what tests exist in the frontend to follow their pattern. + + +**Tool: glob** + +**Input:** +```json +{ + "pattern": "frontend/tests/**/*.spec.ts" +} +``` + +**Output:** +``` +/Users/jhag/projects/babymonitor/frontend/tests/e2e/time_adjuster_visual.spec.ts +/Users/jhag/projects/babymonitor/frontend/tests/e2e/timetable_timezone.spec.ts +/Users/jhag/projects/babymonitor/frontend/tests/e2e/playground_exhaustive.spec.ts +/Users/jhag/projects/babymonitor/frontend/tests/e2e/playground.spec.ts +/Users/jhag/projects/babymonitor/frontend/tests/e2e/timetable_mock.spec.ts +/Users/jhag/projects/babymonitor/frontend/tests/e2e/stats.spec.ts +/Users/jhag/projects/babymonitor/frontend/tests/e2e/tracker.spec.ts +/Users/jhag/projects/babymonitor/frontend/tests/e2e/stop-sleep.spec.ts +/Users/jhag/projects/babymonitor/frontend/tests/e2e/smoke.spec.ts +/Users/jhag/projects/babymonitor/frontend/tests/e2e/connectivity.spec.ts +``` + +--- + +## Assistant (Build · Gemma 4 26b A4B · 4.6s) + +_Thinking:_ + + +`frontend/tests/e2e/stats.spec.ts` seems relevant. Let me read it to see how they do things. + + +**Tool: read** + +**Input:** +```json +{ + "filePath": "/Users/jhag/projects/babymonitor/frontend/tests/e2e/stats.spec.ts" +} +``` + +**Output:** +``` +/Users/jhag/projects/babymonitor/frontend/tests/e2e/stats.spec.ts +file + +1: import { test, expect } from '@playwright/test'; +2: +3: test.describe('Stats Tab', () => { +4: test.beforeEach(async ({ page }) => { +5: await page.goto('http://localhost:5173/stats'); +6: await page.waitForTimeout(1000); +7: }); +8: +9: test('1. Stats tab is reachable via tab navigation', async ({ page }) => { +10: await page.goto('http://localhost:5173/'); +11: await page.getByRole('link', { name: /Stats/i }).click(); +12: await page.waitForTimeout(500); +13: await expect(page.getByRole('heading', { name: /Sleep Statistics/i })).toBeVisible(); +14: }); +15: +16: test('2. Interval picker changes interval', async ({ page }) => { +17: await expect(page.getByRole('button', { name: /7d/i })).toBeVisible(); +18: +19: await page.getByRole('button', { name: /14d/i }).click(); +20: await page.waitForTimeout(500); +21: +22: await expect(page.getByRole('button', { name: /14d/i })).toBeVisible(); +23: }); +24: +25: test('3. Timetable renders rows for selected interval', async ({ page }) => { +26: const timetable = page.locator('text=Sleep Timetable'); +27: if (await timetable.isVisible({ timeout: 1000 }).catch(() => false)) { +28: await expect(page.locator('text=00:00').first()).toBeVisible(); +29: } +30: }); +31: +32: test('4. Wakeup and bedtime plots render SVG lines', async ({ page }) => { +33: await page.getByRole('heading', { name: /Wakeup Times/i }).waitFor({ timeout: 2000 }); +34: await expect(page.locator('h3:has-text("Wakeup Times")')).toBeVisible(); +35: +36: await page.locator('h3:has-text("Bedtime")').first().waitFor({ timeout: 2000 }); +37: await expect(page.locator('h3:has-text("Bedtime")').first()).toBeVisible(); +38: }); +39: +40: test('5. Settings popover for day/night boundary saves to localStorage and re-fetches', async ({ page }) => { +41: await page.getByRole('button', { name: /Settings/i }).click(); +42: await page.waitForTimeout(300); +43: +44: await expect(page.locator('text=Day starts')).toBeVisible(); +45: +46: await page.getByLabel('Day starts (HH:MM)').fill('08:00'); +47: await page.getByRole('button', { name: /Save/i }).click(); +48: await page.waitForTimeout(500); +49: +50: const localStorage = await page.evaluate(() => { +51: return window.localStorage.getItem('baby-sleep-settings'); +52: }); +53: expect(localStorage).toContain('08:00'); +54: }); +55: +56: test('6. Daily Summary shows chart with bars', async ({ page }) => { +57: await expect(page.locator('text=Daily Summary')).toBeVisible(); +58: +59: const chartBars = page.locator('.recharts-bar-rectangle'); +60: if (await chartBars.first().isVisible({ timeout: 1000 }).catch(() => false)) { +61: await expect(chartBars.first()).toBeVisible(); +62: } +63: }); +64: +65: test('7. Average summaries display below chart', async ({ page }) => { +66: const avgTotal = page.locator('text=Avg Total'); +67: if (await avgTotal.isVisible({ timeout: 1000 }).catch(() => false)) { +68: await expect(avgTotal).toBeVisible(); +69: } +70: }); +71: }); + +(End of file - total 71 lines) + +``` + +--- + +## Assistant (Build · Gemma 4 26b A4B · 31.7s) + +_Thinking:_ + + +Okay, I'll create a new spec file `frontend/tests/e2e/pagination.spec.ts` to test the pagination feature. + +Since I don't have a way to easily seed data through Playwright without writing more complex backend mocks or setup, I will assume there might be enough sessions in the current environment or I'll attempt to create some if needed. However, usually, it's better to just test the UI components or use an existing session if one exists. + +Wait, I can check if there are any sessions first. But this is an E2E test. + +Actually, I'll write a test that: +1. Navigates to `/stats`. +2. If there are enough sessions, verifies the pagination component is visible. +3. Checks that "Next" and "Previous" work (if applicable). +4. Opens Settings, changes "Results per page", and checks the list size/presence of pagination. + +I'll use `page.screenshot()` in the test to satisfy the user's request for visual examination. + +Let's create `frontend/tests/e2e/pagination.spec.ts`. + + +**Tool: write** + +**Input:** +```json +{ + "content": "import { test, expect } from '@playwright/test';\n\ntest.describe('Pagination', () => {\n test.beforeEach(async ({ page }) => {\n await page.goto('http://localhost:5173/stats');\n await page.waitForLoadState('networkidle');\n });\n\n test('Pagination component is visible when sessions exceed page size', async ({ page }) => {\n await page.screenshot({ path: 'screenshots/pagination_visible.png' });\n\n // We look for the pagination container or buttons\n // Assuming Pagination component has \"Previous\" and \"Next\" text/icons\n const pagination = page.locator('nav').filter({ hasText: /Previous|Next/i });\n \n // If there are enough sessions, it should be visible. \n // Given we don't know the exact state, we use a soft assertion or conditional check\n // But for a proper test, we should ideally have controlled data.\n // For now, let's just check if it exists IF there are multiple sessions.\n const sessions = page.locator('div[class*=\"SessionList\"] >> .border-b');\n const sessionCount = await sessions.count();\n\n if (sessionCount > 5) { // Using a small number for testing purposes if possible\n await expect(pagination).toBeVisible();\n } else {\n console.log(`Only ${sessionCount} sessions found, pagination might not be visible.`);\n }\n });\n\n test('Changing results per page in settings updates the list', async ({ page }) => {\n await page.screenshot({ path: 'screenshots/before_settings_change.png' });\n\n // Open Settings\n await page.getByRole('button', { name: /Settings/i }).click();\n await expect(page.locator('text=Results per page')).toBeVisible();\n\n // Change to 5\n await page.getByLabel(/Results per page/i).selectOption('5');\n await page.getByRole('button', { name: /Save/i }).click();\n await page.waitForTimeout(500);\n\n await page.screenshot({ path: 'screenshots/after_settings_change.png' });\n\n // Verify settings is saved in localStorage\n const localStorage = await page.evaluate(() => {\n return window.localStorage.getItem('baby-sleep-settings');\n });\n expect(localStorage).toContain('\"sessionsPerPage\":5');\n });\n});\n", + "filePath": "/Users/jhag/projects/babymonitor/frontend/tests/e2e/pagination.spec.ts" +} +``` + +**Output:** +``` +Wrote file successfully. +``` + +--- + +## Assistant (Build · Gemma 4 26b A4B · 9.4s) + +_Thinking:_ + + +Now I need to run the tests. I'll try running `npm test` in the frontend directory, but I need to make sure Playwright is configured correctly. The existing tests use `http://localhost:5173`. I should probably start the dev server in the background or use a command that handles it. + +Looking at `frontend/package.json` (I'll check first) to see how tests are run. + + +**Tool: read** + +**Input:** +```json +{ + "filePath": "/Users/jhag/projects/babymonitor/frontend/package.json" +} +``` + +**Output:** +``` +/Users/jhag/projects/babymonitor/frontend/package.json +file + +1: { +2: "name": "baby-sleep-tracker-frontend", +3: "private": true, +4: "version": "0.1.0", +5: "type": "module", +6: "scripts": { +7: "dev": "vite", +8: "build": "tsc && vite build", +9: "preview": "vite preview", +10: "test": "DEBUG_PRINT_LIMIT=0 vitest run --reporter=dot --no-color", +11: "test:all": "vitest run && npx playwright test", +12: "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0" +13: }, +14: "dependencies": { +15: "@tanstack/react-query": "^5.100.5", +16: "lucide-react": "^1.11.0", +17: "react": "^19.2.5", +18: "react-dom": "^19.2.5", +19: "react-router-dom": "^7.14.2", +20: "recharts": "^3.8.1" +21: }, +22: "devDependencies": { +23: "@playwright/test": "^1.59.1", +24: "@tailwindcss/vite": "^4.2.4", +25: "@testing-library/jest-dom": "^6.9.1", +26: "@testing-library/react": "^16.3.2", +27: "@testing-library/user-event": "^14.6.1", +28: "@types/lodash": "^4.17.24", +29: "@types/node": "^25.6.0", +30: "@types/react": "^19.2.14", +31: "@types/react-dom": "^19.2.3", +32: "@typescript-eslint/eslint-plugin": "^8.60.0", +33: "@typescript-eslint/parser": "^8.60.0", +34: "@vitejs/plugin-react": "^6.0.1", +35: "@vitest/ui": "^4.1.5", +36: "autoprefixer": "^10.5.0", +37: "eslint": "^9.39.4", +38: "eslint-plugin-react": "^7.37.5", +39: "eslint-plugin-react-hooks": "^7.1.1", +40: "jsdom": "^29.1.0", +41: "postcss": "^8.5.12", +42: "tailwindcss": "^4.2.4", +43: "typescript": "^6.0.3", +44: "typescript-eslint": "^8.60.0", +45: "vite": "^8.0.10", +46: "vitest": "^4.1.5" +47: } +48: } + +(End of file - total 48 lines) + +``` + +--- + +## Assistant (Build · Gemma 4 26b A4B · 7.7s) + +_Thinking:_ + + +The script `test:all` runs `vitest run && npx playwright test`. + +However, Playwright tests usually require the web server to be running. Most Playwright configs for Vite projects include a `webServer` property in `playwright.config.ts`. I'll check that file. + + +**Tool: glob** + +**Input:** +```json +{ + "pattern": "frontend/playwright.config.ts" +} +``` + +**Output:** +``` +/Users/jhag/projects/babymonitor/frontend/playwright.config.ts +``` + +--- + +## Assistant (Build · Gemma 4 26b A4B · 4.0s) + +_Thinking:_ + + +Wait, `glob` didn't return it. It might be named differently or in a different location. Let me check `frontend/` again. + + +**Tool: read** + +**Input:** +```json +{ + "filePath": "/Users/jhag/projects/babymonitor/frontend/package.json" +} +``` + +**Output:** +``` +/Users/jhag/projects/babymonitor/frontend/package.json +file + +1: { +2: "name": "baby-sleep-tracker-frontend", +3: "private": true, +4: "version": "0.1.0", +5: "type": "module", +6: "scripts": { +7: "dev": "vite", +8: "build": "tsc && vite build", +9: "preview": "vite preview", +10: "test": "DEBUG_PRINT_LIMIT=0 vitest run --reporter=dot --no-color", +11: "test:all": "vitest run && npx playwright test", +12: "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0" +13: }, +14: "dependencies": { +15: "@tanstack/react-query": "^5.100.5", +16: "lucide-react": "^1.11.0", +17: "react": "^19.2.5", +18: "react-dom": "^19.2.5", +19: "react-router-dom": "^7.14.2", +20: "recharts": "^3.8.1" +21: }, +22: "devDependencies": { +23: "@playwright/test": "^1.59.1", +24: "@tailwindcss/vite": "^4.2.4", +25: "@testing-library/jest-dom": "^6.9.1", +26: "@testing-library/react": "^16.3.2", +27: "@testing-library/user-event": "^14.6.1", +28: "@types/lodash": "^4.17.24", +29: "@types/node": "^25.6.0", +30: "@types/react": "^19.2.14", +31: "@types/react-dom": "^19.2.3", +32: "@typescript-eslint/eslint-plugin": "^8.60.0", +33: "@typescript-eslint/parser": "^8.60.0", +34: "@vitejs/plugin-react": "^6.0.1", +35: "@vitest/ui": "^4.1.5", +36: "autoprefixer": "^10.5.0", +37: "eslint": "^9.39.4", +38: "eslint-plugin-react": "^7.37.5", +39: "eslint-plugin-react-hooks": "^7.1.1", +40: "jsdom": "^29.1.0", +41: "postcss": "^8.5.12", +42: "tailwindcss": "^4.2.4", +43: "typescript": "^6.0.3", +44: "typescript-eslint": "^8.60.0", +45: "vite": "^8.0.10", +46: "vitest": "^4.1.5" +47: } +48: } + +(End of file - total 48 lines) + +``` + +--- + +## Assistant (Build · Gemma 4 26b A4B · 4.9s) + +_Thinking:_ + + +Let's list all files in `frontend/` to find the playwright config. + + +**Tool: read** + +**Input:** +```json +{ + "filePath": "/Users/jhag/projects/babymonitor/frontend/" +} +``` + +**Output:** +``` +/Users/jhag/projects/babymonitor/frontend/ +directory + +.dockerignore +dist/ +Dockerfile +eslint.config.js +frontend_log.txt +frontend/ +index.html +missing_frontend_from_spec.md +nginx.conf +node_modules/ +package-lock.json +package.json +playground_component_rendered.png +playground_dailysummary.png +playground_debug_load.png +playground_intervalpicker.png +playground_loaded.png +playground_plan.md +playground_sessionlist.png +playground_settingspopover.png +playground_sleepbutton.png +playground_sleeptimetable.png +playground_statusbanner.png +playwright-report/ +playwright.config.ts +result.txt +src/ +stats-tab-test.png +tailwind.config.ts +test_with_ansi.txt +test_without_ansi.txt +test-results/ +tests/ +time_adjuster_after_am_pm_toggle.png +time_adjuster_component_clean.png +time_adjuster_initial.png +tsconfig.json +vite.config.ts +vitest.config.ts +vitest.setup.ts + +(40 entries) + +``` + +--- + +## Assistant (Build · Gemma 4 26b A4B · 3.9s) + +_Thinking:_ + + +Wait, `playwright.config.ts` IS in the root directory, not inside `frontend/`. My bad. +Let me read it. + + +**Tool: read** + +**Input:** +```json +{ + "filePath": "/Users/jhag/projects/babymonitor/playwright.config.ts" +} +``` + +**Error:** +``` +File not found: /Users/jhag/projects/babymonitor/playwright.config.ts +``` + +--- + +## Assistant (Build · Gemma 4 26b A4B · 14.0s) + +_Thinking:_ + + +Wait, the `read` tool output for `frontend/` showed: +``` +playwright.config.ts +``` +at the same level as `src/` and `tests/`. + +Ah, I see. The workspace root is `/Users/jhag/projects/babymonitor`. +The `frontend/` directory contains `src/`, `tests/`, etc. + +Let me look at the file list for `/Users/jhag/projects/babymonitor` again. +Wait, the `read(filePath: "frontend/")` output actually showed `playwright.config.ts` inside the directory list of `frontend/`. + +Let me try reading `/Users/jhag/projects/babymonitor/frontend/playwright.config.ts` again, but I'll use the correct path if it was listed in `frontend/`. + +Actually, looking at the `read` output for `frontend/`: +``` + +.dockerignore +dist/ +Dockerfile +eslint.config.js +frontend_log.txt +frontend/ +index.html +missing_frontend_from_spec.md +nginx.conf +node_modules/ +package-lock.json +package.json +playground_component_rendered.png +... +playwright.config.ts +... +tests/ +... + +``` + +Wait, the `frontend/` directory itself is listed inside the root? That's confusing. + +Let's try: +`ls -R /Users/jhag/projects/babymonitor/frontend` via bash. + + +**Tool: bash** + +**Input:** +```json +{ + "command": "ls -R /Users/jhag/projects/babymonitor/frontend", + "description": "List all files in frontend directory to find config.", + "workdir": "/Users/jhag/projects/babymonitor/frontend" +} +``` + +**Output:** +``` +...output truncated... + +Full output saved to: /Users/jhag/.local/share/opencode/tool-output/tool_e7d50e04b001i6iJBezI5LdJDv + +stats.js +timers.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/undici/lib/web: +cache +cookies +eventsource +fetch +infra +subresource-integrity +webidl +websocket + +/Users/jhag/projects/babymonitor/frontend/node_modules/undici/lib/web/cache: +cache.js +cachestorage.js +util.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/undici/lib/web/cookies: +constants.js +index.js +parse.js +util.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/undici/lib/web/eventsource: +eventsource-stream.js +eventsource.js +util.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/undici/lib/web/fetch: +body.js +constants.js +data-url.js +formdata-parser.js +formdata.js +global.js +headers.js +index.js +LICENSE +request.js +response.js +util.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/undici/lib/web/infra: +index.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/undici/lib/web/subresource-integrity: +Readme.md +subresource-integrity.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/undici/lib/web/webidl: +index.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/undici/lib/web/websocket: +connection.js +constants.js +events.js +frame.js +permessage-deflate.js +receiver.js +sender.js +stream +util.js +websocket.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/undici/lib/web/websocket/stream: +websocketerror.js +websocketstream.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/undici/scripts: +strip-comments.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/undici/types: +agent.d.ts +api.d.ts +balanced-pool.d.ts +cache-interceptor.d.ts +cache.d.ts +client-stats.d.ts +client.d.ts +connector.d.ts +content-type.d.ts +cookies.d.ts +diagnostics-channel.d.ts +dispatcher.d.ts +env-http-proxy-agent.d.ts +errors.d.ts +eventsource.d.ts +fetch.d.ts +formdata.d.ts +global-dispatcher.d.ts +global-origin.d.ts +h2c-client.d.ts +handlers.d.ts +header.d.ts +index.d.ts +interceptors.d.ts +mock-agent.d.ts +mock-call-history.d.ts +mock-client.d.ts +mock-errors.d.ts +mock-interceptor.d.ts +mock-pool.d.ts +patch.d.ts +pool-stats.d.ts +pool.d.ts +proxy-agent.d.ts +readable.d.ts +README.md +retry-agent.d.ts +retry-handler.d.ts +round-robin-pool.d.ts +snapshot-agent.d.ts +socks5-proxy-agent.d.ts +util.d.ts +utility.d.ts +webidl.d.ts +websocket.d.ts + +/Users/jhag/projects/babymonitor/frontend/node_modules/undici-types: +agent.d.ts +api.d.ts +balanced-pool.d.ts +cache-interceptor.d.ts +cache.d.ts +client-stats.d.ts +client.d.ts +connector.d.ts +content-type.d.ts +cookies.d.ts +diagnostics-channel.d.ts +dispatcher.d.ts +env-http-proxy-agent.d.ts +errors.d.ts +eventsource.d.ts +fetch.d.ts +formdata.d.ts +global-dispatcher.d.ts +global-origin.d.ts +h2c-client.d.ts +handlers.d.ts +header.d.ts +index.d.ts +interceptors.d.ts +LICENSE +mock-agent.d.ts +mock-call-history.d.ts +mock-client.d.ts +mock-errors.d.ts +mock-interceptor.d.ts +mock-pool.d.ts +package.json +patch.d.ts +pool-stats.d.ts +pool.d.ts +proxy-agent.d.ts +readable.d.ts +README.md +retry-agent.d.ts +retry-handler.d.ts +round-robin-pool.d.ts +snapshot-agent.d.ts +util.d.ts +utility.d.ts +webidl.d.ts +websocket.d.ts + +/Users/jhag/projects/babymonitor/frontend/node_modules/update-browserslist-db: +check-npm-version.js +cli.js +index.d.ts +index.js +LICENSE +package.json +README.md +utils.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/uri-js: +dist +LICENSE +package.json +README.md +yarn.lock + +/Users/jhag/projects/babymonitor/frontend/node_modules/uri-js/dist: +es5 +esnext + +/Users/jhag/projects/babymonitor/frontend/node_modules/uri-js/dist/es5: +uri.all.d.ts +uri.all.js +uri.all.js.map +uri.all.min.d.ts +uri.all.min.js +uri.all.min.js.map + +/Users/jhag/projects/babymonitor/frontend/node_modules/uri-js/dist/esnext: +index.d.ts +index.js +index.js.map +regexps-iri.d.ts +regexps-iri.js +regexps-iri.js.map +regexps-uri.d.ts +regexps-uri.js +regexps-uri.js.map +schemes +uri.d.ts +uri.js +uri.js.map +util.d.ts +util.js +util.js.map + +/Users/jhag/projects/babymonitor/frontend/node_modules/uri-js/dist/esnext/schemes: +http.d.ts +http.js +http.js.map +https.d.ts +https.js +https.js.map +mailto.d.ts +mailto.js +mailto.js.map +urn-uuid.d.ts +urn-uuid.js +urn-uuid.js.map +urn.d.ts +urn.js +urn.js.map +ws.d.ts +ws.js +ws.js.map +wss.d.ts +wss.js +wss.js.map + +/Users/jhag/projects/babymonitor/frontend/node_modules/use-sync-external-store: +cjs +index.js +LICENSE +package.json +README.md +shim +with-selector.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/use-sync-external-store/cjs: +use-sync-external-store-shim +use-sync-external-store-shim.development.js +use-sync-external-store-shim.native.development.js +use-sync-external-store-shim.native.production.js +use-sync-external-store-shim.production.js +use-sync-external-store-with-selector.development.js +use-sync-external-store-with-selector.production.js +use-sync-external-store.development.js +use-sync-external-store.production.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/use-sync-external-store/cjs/use-sync-external-store-shim: +with-selector.development.js +with-selector.production.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/use-sync-external-store/shim: +index.js +index.native.js +with-selector.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/victory-vendor: +CHANGELOG.md +d3-array.d.ts +d3-array.js +d3-ease.d.ts +d3-ease.js +d3-interpolate.d.ts +d3-interpolate.js +d3-scale.d.ts +d3-scale.js +d3-shape.d.ts +d3-shape.js +d3-time.d.ts +d3-time.js +d3-timer.d.ts +d3-timer.js +es +lib +lib-vendor +package.json +README.md + +/Users/jhag/projects/babymonitor/frontend/node_modules/victory-vendor/es: +d3-array.js +d3-color.js +d3-ease.js +d3-format.js +d3-interpolate.js +d3-path.js +d3-scale.js +d3-shape.js +d3-time-format.js +d3-time.js +d3-timer.js +d3-voronoi.js +internmap.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/victory-vendor/lib: +d3-array.js +d3-color.js +d3-ease.js +d3-format.js +d3-interpolate.js +d3-path.js +d3-scale.js +d3-shape.js +d3-time-format.js +d3-time.js +d3-timer.js +d3-voronoi.js +internmap.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/victory-vendor/lib-vendor: +d3-array +d3-color +d3-ease +d3-format +d3-interpolate +d3-path +d3-scale +d3-shape +d3-time +d3-time-format +d3-timer +d3-voronoi +internmap + +/Users/jhag/projects/babymonitor/frontend/node_modules/victory-vendor/lib-vendor/d3-array: +LICENSE +src + +/Users/jhag/projects/babymonitor/frontend/node_modules/victory-vendor/lib-vendor/d3-array/src: +array.js +ascending.js +bin.js +bisect.js +bisector.js +constant.js +count.js +cross.js +cumsum.js +descending.js +deviation.js +difference.js +disjoint.js +every.js +extent.js +filter.js +fsum.js +greatest.js +greatestIndex.js +group.js +groupSort.js +identity.js +index.js +intersection.js +least.js +leastIndex.js +map.js +max.js +maxIndex.js +mean.js +median.js +merge.js +min.js +minIndex.js +mode.js +nice.js +number.js +pairs.js +permute.js +quantile.js +quickselect.js +range.js +rank.js +reduce.js +reverse.js +scan.js +shuffle.js +some.js +sort.js +subset.js +sum.js +superset.js +threshold +ticks.js +transpose.js +union.js +variance.js +zip.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/victory-vendor/lib-vendor/d3-array/src/threshold: +freedmanDiaconis.js +scott.js +sturges.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/victory-vendor/lib-vendor/d3-color: +LICENSE +src + +/Users/jhag/projects/babymonitor/frontend/node_modules/victory-vendor/lib-vendor/d3-color/src: +color.js +cubehelix.js +define.js +index.js +lab.js +math.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/victory-vendor/lib-vendor/d3-ease: +LICENSE +src + +/Users/jhag/projects/babymonitor/frontend/node_modules/victory-vendor/lib-vendor/d3-ease/src: +back.js +bounce.js +circle.js +cubic.js +elastic.js +exp.js +index.js +linear.js +math.js +poly.js +quad.js +sin.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/victory-vendor/lib-vendor/d3-format: +LICENSE +src + +/Users/jhag/projects/babymonitor/frontend/node_modules/victory-vendor/lib-vendor/d3-format/src: +defaultLocale.js +exponent.js +formatDecimal.js +formatGroup.js +formatNumerals.js +formatPrefixAuto.js +formatRounded.js +formatSpecifier.js +formatTrim.js +formatTypes.js +identity.js +index.js +locale.js +precisionFixed.js +precisionPrefix.js +precisionRound.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/victory-vendor/lib-vendor/d3-interpolate: +LICENSE +src + +/Users/jhag/projects/babymonitor/frontend/node_modules/victory-vendor/lib-vendor/d3-interpolate/src: +array.js +basis.js +basisClosed.js +color.js +constant.js +cubehelix.js +date.js +discrete.js +hcl.js +hsl.js +hue.js +index.js +lab.js +number.js +numberArray.js +object.js +piecewise.js +quantize.js +rgb.js +round.js +string.js +transform +value.js +zoom.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/victory-vendor/lib-vendor/d3-interpolate/src/transform: +decompose.js +index.js +parse.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/victory-vendor/lib-vendor/d3-path: +LICENSE +src + +/Users/jhag/projects/babymonitor/frontend/node_modules/victory-vendor/lib-vendor/d3-path/src: +index.js +path.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/victory-vendor/lib-vendor/d3-scale: +LICENSE +src + +/Users/jhag/projects/babymonitor/frontend/node_modules/victory-vendor/lib-vendor/d3-scale/src: +band.js +colors.js +constant.js +continuous.js +diverging.js +identity.js +index.js +init.js +linear.js +log.js +nice.js +number.js +ordinal.js +pow.js +quantile.js +quantize.js +radial.js +sequential.js +sequentialQuantile.js +symlog.js +threshold.js +tickFormat.js +time.js +utcTime.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/victory-vendor/lib-vendor/d3-shape: +LICENSE +src + +/Users/jhag/projects/babymonitor/frontend/node_modules/victory-vendor/lib-vendor/d3-shape/src: +arc.js +area.js +areaRadial.js +array.js +constant.js +curve +descending.js +identity.js +index.js +line.js +lineRadial.js +link.js +math.js +noop.js +offset +order +pie.js +point.js +pointRadial.js +stack.js +symbol +symbol.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/victory-vendor/lib-vendor/d3-shape/src/curve: +basis.js +basisClosed.js +basisOpen.js +bump.js +bundle.js +cardinal.js +cardinalClosed.js +cardinalOpen.js +catmullRom.js +catmullRomClosed.js +catmullRomOpen.js +linear.js +linearClosed.js +monotone.js +natural.js +radial.js +step.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/victory-vendor/lib-vendor/d3-shape/src/offset: +diverging.js +expand.js +none.js +silhouette.js +wiggle.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/victory-vendor/lib-vendor/d3-shape/src/order: +appearance.js +ascending.js +descending.js +insideOut.js +none.js +reverse.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/victory-vendor/lib-vendor/d3-shape/src/symbol: +asterisk.js +circle.js +cross.js +diamond.js +diamond2.js +plus.js +square.js +square2.js +star.js +triangle.js +triangle2.js +wye.js +x.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/victory-vendor/lib-vendor/d3-time: +LICENSE +src + +/Users/jhag/projects/babymonitor/frontend/node_modules/victory-vendor/lib-vendor/d3-time/src: +day.js +duration.js +hour.js +index.js +interval.js +millisecond.js +minute.js +month.js +second.js +ticks.js +utcDay.js +utcHour.js +utcMinute.js +utcMonth.js +utcWeek.js +utcYear.js +week.js +year.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/victory-vendor/lib-vendor/d3-time-format: +LICENSE +src + +/Users/jhag/projects/babymonitor/frontend/node_modules/victory-vendor/lib-vendor/d3-time-format/src: +defaultLocale.js +index.js +isoFormat.js +isoParse.js +locale.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/victory-vendor/lib-vendor/d3-timer: +LICENSE +src + +/Users/jhag/projects/babymonitor/frontend/node_modules/victory-vendor/lib-vendor/d3-timer/src: +index.js +interval.js +timeout.js +timer.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/victory-vendor/lib-vendor/d3-voronoi: +LICENSE +src + +/Users/jhag/projects/babymonitor/frontend/node_modules/victory-vendor/lib-vendor/d3-voronoi/src: +Beach.js +Cell.js +Circle.js +constant.js +Diagram.js +Edge.js +index.js +point.js +RedBlackTree.js +voronoi.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/victory-vendor/lib-vendor/internmap: +LICENSE +src + +/Users/jhag/projects/babymonitor/frontend/node_modules/victory-vendor/lib-vendor/internmap/src: +index.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/vite: +bin +client.d.ts +dist +LICENSE.md +misc +node_modules +package.json +README.md +types + +/Users/jhag/projects/babymonitor/frontend/node_modules/vite/bin: +openChrome.js +vite.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/vite/dist: +client +node + +/Users/jhag/projects/babymonitor/frontend/node_modules/vite/dist/client: +client.mjs +env.mjs + +/Users/jhag/projects/babymonitor/frontend/node_modules/vite/dist/node: +chunks +cli.js +index.d.ts +index.js +internal.d.ts +internal.js +module-runner.d.ts +module-runner.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/vite/dist/node/chunks: +build.js +chunk.js +dist.js +lib.js +logger.js +moduleRunnerTransport.d.ts +node.js +postcss-import.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/vite/misc: +false.js +true.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/vite/node_modules: +fsevents + +/Users/jhag/projects/babymonitor/frontend/node_modules/vite/node_modules/fsevents: +fsevents.d.ts +fsevents.js +fsevents.node +LICENSE +package.json +README.md + +/Users/jhag/projects/babymonitor/frontend/node_modules/vite/types: +customEvent.d.ts +hmrPayload.d.ts +hot.d.ts +import-meta.d.ts +importGlob.d.ts +importMeta.d.ts +internal +metadata.d.ts + +/Users/jhag/projects/babymonitor/frontend/node_modules/vite/types/internal: +cssPreprocessorOptions.d.ts +esbuildOptions.d.ts +lightningcssOptions.d.ts +rollupTypeCompat.d.ts +terserOptions.d.ts + +/Users/jhag/projects/babymonitor/frontend/node_modules/vitest: +browser +config.d.ts +coverage.d.ts +dist +environments.d.ts +globals.d.ts +import-meta.d.ts +importMeta.d.ts +index.cjs +index.d.cts +jsdom.d.ts +LICENSE.md +mocker.d.ts +node.d.ts +optional-runtime-types.d.ts +optional-types.d.ts +package.json +README.md +reporters.d.ts +runners.d.ts +snapshot.d.ts +suite.d.ts +suppress-warnings.cjs +vitest.mjs +worker.d.ts + +/Users/jhag/projects/babymonitor/frontend/node_modules/vitest/browser: +context.d.ts +context.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/vitest/dist: +browser.d.ts +browser.js +chunks +cli.js +config.cjs +config.d.ts +config.js +coverage.d.ts +coverage.js +environments.d.ts +environments.js +index.d.ts +index.js +module-evaluator.d.ts +module-evaluator.js +node.d.ts +node.js +nodejs-worker-loader.js +path.js +reporters.d.ts +reporters.js +runners.d.ts +runners.js +runtime.d.ts +runtime.js +snapshot.d.ts +snapshot.js +spy.js +suite.d.ts +suite.js +worker.d.ts +worker.js +workers + +/Users/jhag/projects/babymonitor/frontend/node_modules/vitest/dist/chunks: +_commonjsHelpers.D26ty3Ew.js +acorn.B2iPLyUM.js +base.RR7zL1h0.js +benchmark.CX_oY03V.js +benchmark.d.DAaHLpsq.d.ts +browser.d.BcoexmFG.d.ts +cac.DJJmV0dT.js +cli-api.Cjt90eJu.js +config.d.A1h_Y6Jt.d.ts +console.3WNpx0tS.js +constants.CPYnjOGj.js +coverage.CTzCuANN.js +coverage.d.BZtK59WP.d.ts +coverage.DM_a_rWm.js +creator.DgVhQm5q.js +defaults.9aQKnqFk.js +env.D4Lgay0q.js +environment.d.CrsxCzP1.d.ts +evaluatedModules.d.BxJ5omdx.d.ts +evaluatedModules.Dg1zASAC.js +global.d.DVsSRdQ5.d.ts +globals.Dj1TGiMC.js +index.BCY_7LL2.js +index.Chj8NDwU.js +index.CMESou6r.js +index.DC7d2Pf8.js +index.DdgEv5B1.js +index.DXx9Dtk7.js +index.og1WyBLx.js +index.UpGiHP7g.js +init-forks.UV3ZQGQH.js +init-threads.D3eCsY76.js +init.D98-gwRW.js +inspector.CvyFGlXm.js +modules.BJuCwlRJ.js +native.DPzPHdi5.js +nativeModuleMocker.BRN2oBJd.js +nativeModuleRunner.BIakptoF.js +node.COQbm6gK.js +plugin.d.BM2TCi12.d.ts +reporters.d.CEnv6XRv.d.ts +rpc.d.B_8sPU0w.d.ts +rpc.MzXet3jl.js +setup-common.DYx3LtFI.js +startVitestModuleRunner.bRl2_oI_.js +suite.d.udJtyAgw.d.ts +test.DNmyFkvJ.js +traces.d.D2T_R8rx.d.ts +traces.DT5aQ62U.js +utils.BS4fH3nR.js +utils.BX5Fg8C4.js +vm.DVLYObm9.js +worker.d.ZpHpO4yb.d.ts + +/Users/jhag/projects/babymonitor/frontend/node_modules/vitest/dist/workers: +forks.js +runVmTests.js +threads.js +vmForks.js +vmThreads.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/w3c-xmlserializer: +lib +LICENSE.md +package.json +README.md + +/Users/jhag/projects/babymonitor/frontend/node_modules/w3c-xmlserializer/lib: +attributes.js +constants.js +serialize.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/webidl-conversions: +lib +LICENSE.md +package.json +README.md + +/Users/jhag/projects/babymonitor/frontend/node_modules/webidl-conversions/lib: +index.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/whatwg-mimetype: +lib +LICENSE.txt +package.json +README.md + +/Users/jhag/projects/babymonitor/frontend/node_modules/whatwg-mimetype/lib: +index.js +mime-type-parameters.js +mime-type.js +parser.js +serializer.js +sniff.js +utils.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/whatwg-url: +index.js +lib +LICENSE.txt +package.json +README.md +webidl2js-wrapper.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/whatwg-url/lib: +encoding.js +Function.js +infra.js +percent-encoding.js +URL-impl.js +url-state-machine.js +URL.js +urlencoded.js +URLSearchParams-impl.js +URLSearchParams.js +utils.js +VoidFunction.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/which: +bin +CHANGELOG.md +LICENSE +package.json +README.md +which.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/which/bin: +node-which + +/Users/jhag/projects/babymonitor/frontend/node_modules/which-boxed-primitive: +CHANGELOG.md +index.d.ts +index.js +LICENSE +package.json +README.md +test +tsconfig.json + +/Users/jhag/projects/babymonitor/frontend/node_modules/which-boxed-primitive/test: +index.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/which-builtin-type: +CHANGELOG.md +index.d.ts +index.js +LICENSE +package.json +README.md +test +tsconfig.json + +/Users/jhag/projects/babymonitor/frontend/node_modules/which-builtin-type/test: +index.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/which-collection: +CHANGELOG.md +index.d.ts +index.js +LICENSE +package.json +README.md +test +tsconfig.json + +/Users/jhag/projects/babymonitor/frontend/node_modules/which-collection/test: +index.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/which-typed-array: +CHANGELOG.md +eslint.config.mjs +index.d.ts +index.js +LICENSE +package.json +README.md +test +tsconfig.json + +/Users/jhag/projects/babymonitor/frontend/node_modules/which-typed-array/test: +index.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/why-is-node-running: +cli.js +example.js +include.js +index.js +LICENSE +package.json +README.md + +/Users/jhag/projects/babymonitor/frontend/node_modules/word-wrap: +index.d.ts +index.js +LICENSE +package.json +README.md + +/Users/jhag/projects/babymonitor/frontend/node_modules/xml-name-validator: +lib +LICENSE.txt +package.json +README.md + +/Users/jhag/projects/babymonitor/frontend/node_modules/xml-name-validator/lib: +xml-name-validator.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/xmlchars: +LICENSE +package.json +README.md +xml +xmlchars.d.ts +xmlchars.js +xmlchars.js.map +xmlns + +/Users/jhag/projects/babymonitor/frontend/node_modules/xmlchars/xml: +1.0 +1.1 + +/Users/jhag/projects/babymonitor/frontend/node_modules/xmlchars/xml/1.0: +ed4.d.ts +ed4.js +ed4.js.map +ed5.d.ts +ed5.js +ed5.js.map + +/Users/jhag/projects/babymonitor/frontend/node_modules/xmlchars/xml/1.1: +ed2.d.ts +ed2.js +ed2.js.map + +/Users/jhag/projects/babymonitor/frontend/node_modules/xmlchars/xmlns: +1.0 + +/Users/jhag/projects/babymonitor/frontend/node_modules/xmlchars/xmlns/1.0: +ed3.d.ts +ed3.js +ed3.js.map + +/Users/jhag/projects/babymonitor/frontend/node_modules/yallist: +iterator.js +LICENSE +package.json +README.md +yallist.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/yocto-queue: +index.d.ts +index.js +license +package.json +readme.md + +/Users/jhag/projects/babymonitor/frontend/node_modules/zod: +index.cjs +index.d.cts +index.d.ts +index.js +LICENSE +locales +mini +package.json +README.md +src +v3 +v4 +v4-mini + +/Users/jhag/projects/babymonitor/frontend/node_modules/zod/locales: +index.cjs +index.d.cts +index.d.ts +index.js +package.json + +/Users/jhag/projects/babymonitor/frontend/node_modules/zod/mini: +index.cjs +index.d.cts +index.d.ts +index.js +package.json + +/Users/jhag/projects/babymonitor/frontend/node_modules/zod/src: +index.ts +locales +mini +v3 +v4 +v4-mini + +/Users/jhag/projects/babymonitor/frontend/node_modules/zod/src/locales: +index.ts + +/Users/jhag/projects/babymonitor/frontend/node_modules/zod/src/mini: +index.ts + +/Users/jhag/projects/babymonitor/frontend/node_modules/zod/src/v3: +benchmarks +errors.ts +external.ts +helpers +index.ts +locales +standard-schema.ts +tests +types.ts +ZodError.ts + +/Users/jhag/projects/babymonitor/frontend/node_modules/zod/src/v3/benchmarks: +datetime.ts +discriminatedUnion.ts +index.ts +ipv4.ts +object.ts +primitives.ts +realworld.ts +string.ts +union.ts + +/Users/jhag/projects/babymonitor/frontend/node_modules/zod/src/v3/helpers: +enumUtil.ts +errorUtil.ts +parseUtil.ts +partialUtil.ts +typeAliases.ts +util.ts + +/Users/jhag/projects/babymonitor/frontend/node_modules/zod/src/v3/locales: +en.ts + +/Users/jhag/projects/babymonitor/frontend/node_modules/zod/src/v3/tests: +all-errors.test.ts +anyunknown.test.ts +array.test.ts +async-parsing.test.ts +async-refinements.test.ts +base.test.ts +bigint.test.ts +branded.test.ts +catch.test.ts +coerce.test.ts +complex.test.ts +custom.test.ts +date.test.ts +deepmasking.test.ts +default.test.ts +description.test.ts +discriminated-unions.test.ts +enum.test.ts +error.test.ts +firstparty.test.ts +firstpartyschematypes.test.ts +function.test.ts +generics.test.ts +instanceof.test.ts +intersection.test.ts +language-server.source.ts +language-server.test.ts +literal.test.ts +map.test.ts +masking.test.ts +mocker.test.ts +Mocker.ts +nan.test.ts +nativeEnum.test.ts +nullable.test.ts +number.test.ts +object-augmentation.test.ts +object-in-es5-env.test.ts +object.test.ts +optional.test.ts +parser.test.ts +parseUtil.test.ts +partials.test.ts +pickomit.test.ts +pipeline.test.ts +preprocess.test.ts +primitive.test.ts +promise.test.ts +readonly.test.ts +record.test.ts +recursive.test.ts +refine.test.ts +safeparse.test.ts +set.test.ts +standard-schema.test.ts +string.test.ts +transformer.test.ts +tuple.test.ts +unions.test.ts +validations.test.ts +void.test.ts + +/Users/jhag/projects/babymonitor/frontend/node_modules/zod/src/v4: +classic +core +index.ts +locales +mini + +/Users/jhag/projects/babymonitor/frontend/node_modules/zod/src/v4/classic: +checks.ts +coerce.ts +compat.ts +errors.ts +external.ts +from-json-schema.ts +index.ts +iso.ts +parse.ts +schemas.ts +tests + +/Users/jhag/projects/babymonitor/frontend/node_modules/zod/src/v4/classic/tests: +anyunknown.test.ts +apply.test.ts +array.test.ts +assignability.test.ts +async-parsing.test.ts +async-refinements.test.ts +base.test.ts +bigint.test.ts +brand.test.ts +catch.test.ts +coalesce.test.ts +codec-examples.test.ts +codec.test.ts +coerce.test.ts +continuability.test.ts +custom.test.ts +date.test.ts +datetime.test.ts +default.test.ts +describe-meta-checks.test.ts +description.test.ts +detached-methods.test.ts +discriminated-unions.test.ts +enum.test.ts +error-utils.test.ts +error.test.ts +file.test.ts +firstparty.test.ts +fix-json-issue.test.ts +from-json-schema.test.ts +function.test.ts +generics.test.ts +global-config.test.ts +hash.test.ts +index.test.ts +instanceof.test.ts +intersection.test.ts +jitless-allows-eval.test.ts +json.test.ts +lazy.test.ts +literal.test.ts +locales_ka.test.ts +locales_ro.test.ts +map.test.ts +nan.test.ts +nested-refine.test.ts +nonoptional.test.ts +nullable.test.ts +number.test.ts +object.test.ts +optional.test.ts +partial.test.ts +pickomit.test.ts +pipe.test.ts +prefault.test.ts +preprocess-types.test.ts +preprocess.test.ts +primitive.test.ts +promise.test.ts +prototypes.test.ts +readonly.test.ts +record.test.ts +recursive-types.test.ts +refine.test.ts +registries.test.ts +set.test.ts +standard-schema.test.ts +string-formats.test.ts +string.test.ts +stringbool.test.ts +template-literal.test.ts +to-json-schema-methods.test.ts +to-json-schema.test.ts +transform.test.ts +tuple.test.ts +union.test.ts +url.test.ts +validations.test.ts +void.test.ts + +/Users/jhag/projects/babymonitor/frontend/node_modules/zod/src/v4/core: +api.ts +checks.ts +config.ts +core.ts +doc.ts +errors.ts +index.ts +json-schema-generator.ts +json-schema-processors.ts +json-schema.ts +parse.ts +regexes.ts +registries.ts +schemas.ts +standard-schema.ts +tests +to-json-schema.ts +util.ts +versions.ts +zsf.ts + +/Users/jhag/projects/babymonitor/frontend/node_modules/zod/src/v4/core/tests: +extend.test.ts +index.test.ts +locales +record-constructor.test.ts +recursive-tuples.test.ts + +/Users/jhag/projects/babymonitor/frontend/node_modules/zod/src/v4/core/tests/locales: +be.test.ts +el.test.ts +en.test.ts +es.test.ts +fr.test.ts +he.test.ts +hr.test.ts +nl.test.ts +ru.test.ts +tr.test.ts +uz.test.ts + +/Users/jhag/projects/babymonitor/frontend/node_modules/zod/src/v4/locales: +ar.ts +az.ts +be.ts +bg.ts +ca.ts +cs.ts +da.ts +de.ts +el.ts +en.ts +eo.ts +es.ts +fa.ts +fi.ts +fr-CA.ts +fr.ts +he.ts +hr.ts +hu.ts +hy.ts +id.ts +index.ts +is.ts +it.ts +ja.ts +ka.ts +kh.ts +km.ts +ko.ts +lt.ts +mk.ts +ms.ts +nl.ts +no.ts +ota.ts +pl.ts +ps.ts +pt.ts +ro.ts +ru.ts +sl.ts +sv.ts +ta.ts +th.ts +tr.ts +ua.ts +uk.ts +ur.ts +uz.ts +vi.ts +yo.ts +zh-CN.ts +zh-TW.ts + +/Users/jhag/projects/babymonitor/frontend/node_modules/zod/src/v4/mini: +checks.ts +coerce.ts +external.ts +index.ts +iso.ts +parse.ts +schemas.ts +tests + +/Users/jhag/projects/babymonitor/frontend/node_modules/zod/src/v4/mini/tests: +apply.test.ts +assignability.test.ts +brand.test.ts +checks.test.ts +codec.test.ts +computed.test.ts +error.test.ts +functions.test.ts +index.test.ts +number.test.ts +object.test.ts +prototypes.test.ts +recursive-types.test.ts +standard-schema.test.ts +string.test.ts + +/Users/jhag/projects/babymonitor/frontend/node_modules/zod/src/v4-mini: +index.ts + +/Users/jhag/projects/babymonitor/frontend/node_modules/zod/v3: +errors.cjs +errors.d.cts +errors.d.ts +errors.js +external.cjs +external.d.cts +external.d.ts +external.js +helpers +index.cjs +index.d.cts +index.d.ts +index.js +locales +package.json +standard-schema.cjs +standard-schema.d.cts +standard-schema.d.ts +standard-schema.js +types.cjs +types.d.cts +types.d.ts +types.js +ZodError.cjs +ZodError.d.cts +ZodError.d.ts +ZodError.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/zod/v3/helpers: +enumUtil.cjs +enumUtil.d.cts +enumUtil.d.ts +enumUtil.js +errorUtil.cjs +errorUtil.d.cts +errorUtil.d.ts +errorUtil.js +parseUtil.cjs +parseUtil.d.cts +parseUtil.d.ts +parseUtil.js +partialUtil.cjs +partialUtil.d.cts +partialUtil.d.ts +partialUtil.js +typeAliases.cjs +typeAliases.d.cts +typeAliases.d.ts +typeAliases.js +util.cjs +util.d.cts +util.d.ts +util.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/zod/v3/locales: +en.cjs +en.d.cts +en.d.ts +en.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/zod/v4: +classic +core +index.cjs +index.d.cts +index.d.ts +index.js +locales +mini +package.json + +/Users/jhag/projects/babymonitor/frontend/node_modules/zod/v4/classic: +checks.cjs +checks.d.cts +checks.d.ts +checks.js +coerce.cjs +coerce.d.cts +coerce.d.ts +coerce.js +compat.cjs +compat.d.cts +compat.d.ts +compat.js +errors.cjs +errors.d.cts +errors.d.ts +errors.js +external.cjs +external.d.cts +external.d.ts +external.js +from-json-schema.cjs +from-json-schema.d.cts +from-json-schema.d.ts +from-json-schema.js +index.cjs +index.d.cts +index.d.ts +index.js +iso.cjs +iso.d.cts +iso.d.ts +iso.js +package.json +parse.cjs +parse.d.cts +parse.d.ts +parse.js +schemas.cjs +schemas.d.cts +schemas.d.ts +schemas.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/zod/v4/core: +api.cjs +api.d.cts +api.d.ts +api.js +checks.cjs +checks.d.cts +checks.d.ts +checks.js +core.cjs +core.d.cts +core.d.ts +core.js +doc.cjs +doc.d.cts +doc.d.ts +doc.js +errors.cjs +errors.d.cts +errors.d.ts +errors.js +index.cjs +index.d.cts +index.d.ts +index.js +json-schema-generator.cjs +json-schema-generator.d.cts +json-schema-generator.d.ts +json-schema-generator.js +json-schema-processors.cjs +json-schema-processors.d.cts +json-schema-processors.d.ts +json-schema-processors.js +json-schema.cjs +json-schema.d.cts +json-schema.d.ts +json-schema.js +package.json +parse.cjs +parse.d.cts +parse.d.ts +parse.js +regexes.cjs +regexes.d.cts +regexes.d.ts +regexes.js +registries.cjs +registries.d.cts +registries.d.ts +registries.js +schemas.cjs +schemas.d.cts +schemas.d.ts +schemas.js +standard-schema.cjs +standard-schema.d.cts +standard-schema.d.ts +standard-schema.js +to-json-schema.cjs +to-json-schema.d.cts +to-json-schema.d.ts +to-json-schema.js +util.cjs +util.d.cts +util.d.ts +util.js +versions.cjs +versions.d.cts +versions.d.ts +versions.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/zod/v4/locales: +ar.cjs +ar.d.cts +ar.d.ts +ar.js +az.cjs +az.d.cts +az.d.ts +az.js +be.cjs +be.d.cts +be.d.ts +be.js +bg.cjs +bg.d.cts +bg.d.ts +bg.js +ca.cjs +ca.d.cts +ca.d.ts +ca.js +cs.cjs +cs.d.cts +cs.d.ts +cs.js +da.cjs +da.d.cts +da.d.ts +da.js +de.cjs +de.d.cts +de.d.ts +de.js +el.cjs +el.d.cts +el.d.ts +el.js +en.cjs +en.d.cts +en.d.ts +en.js +eo.cjs +eo.d.cts +eo.d.ts +eo.js +es.cjs +es.d.cts +es.d.ts +es.js +fa.cjs +fa.d.cts +fa.d.ts +fa.js +fi.cjs +fi.d.cts +fi.d.ts +fi.js +fr-CA.cjs +fr-CA.d.cts +fr-CA.d.ts +fr-CA.js +fr.cjs +fr.d.cts +fr.d.ts +fr.js +he.cjs +he.d.cts +he.d.ts +he.js +hr.cjs +hr.d.cts +hr.d.ts +hr.js +hu.cjs +hu.d.cts +hu.d.ts +hu.js +hy.cjs +hy.d.cts +hy.d.ts +hy.js +id.cjs +id.d.cts +id.d.ts +id.js +index.cjs +index.d.cts +index.d.ts +index.js +is.cjs +is.d.cts +is.d.ts +is.js +it.cjs +it.d.cts +it.d.ts +it.js +ja.cjs +ja.d.cts +ja.d.ts +ja.js +ka.cjs +ka.d.cts +ka.d.ts +ka.js +kh.cjs +kh.d.cts +kh.d.ts +kh.js +km.cjs +km.d.cts +km.d.ts +km.js +ko.cjs +ko.d.cts +ko.d.ts +ko.js +lt.cjs +lt.d.cts +lt.d.ts +lt.js +mk.cjs +mk.d.cts +mk.d.ts +mk.js +ms.cjs +ms.d.cts +ms.d.ts +ms.js +nl.cjs +nl.d.cts +nl.d.ts +nl.js +no.cjs +no.d.cts +no.d.ts +no.js +ota.cjs +ota.d.cts +ota.d.ts +ota.js +package.json +pl.cjs +pl.d.cts +pl.d.ts +pl.js +ps.cjs +ps.d.cts +ps.d.ts +ps.js +pt.cjs +pt.d.cts +pt.d.ts +pt.js +ro.cjs +ro.d.cts +ro.d.ts +ro.js +ru.cjs +ru.d.cts +ru.d.ts +ru.js +sl.cjs +sl.d.cts +sl.d.ts +sl.js +sv.cjs +sv.d.cts +sv.d.ts +sv.js +ta.cjs +ta.d.cts +ta.d.ts +ta.js +th.cjs +th.d.cts +th.d.ts +th.js +tr.cjs +tr.d.cts +tr.d.ts +tr.js +ua.cjs +ua.d.cts +ua.d.ts +ua.js +uk.cjs +uk.d.cts +uk.d.ts +uk.js +ur.cjs +ur.d.cts +ur.d.ts +ur.js +uz.cjs +uz.d.cts +uz.d.ts +uz.js +vi.cjs +vi.d.cts +vi.d.ts +vi.js +yo.cjs +yo.d.cts +yo.d.ts +yo.js +zh-CN.cjs +zh-CN.d.cts +zh-CN.d.ts +zh-CN.js +zh-TW.cjs +zh-TW.d.cts +zh-TW.d.ts +zh-TW.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/zod/v4/mini: +checks.cjs +checks.d.cts +checks.d.ts +checks.js +coerce.cjs +coerce.d.cts +coerce.d.ts +coerce.js +external.cjs +external.d.cts +external.d.ts +external.js +index.cjs +index.d.cts +index.d.ts +index.js +iso.cjs +iso.d.cts +iso.d.ts +iso.js +package.json +parse.cjs +parse.d.cts +parse.d.ts +parse.js +schemas.cjs +schemas.d.cts +schemas.d.ts +schemas.js + +/Users/jhag/projects/babymonitor/frontend/node_modules/zod/v4-mini: +index.cjs +index.d.cts +index.d.ts +index.js +package.json + +/Users/jhag/projects/babymonitor/frontend/node_modules/zod-validation-error: +LICENSE +package.json +README.md +README.v3.md +v3 +v4 + +/Users/jhag/projects/babymonitor/frontend/node_modules/zod-validation-error/v3: +index.d.mts +index.d.ts +index.js +index.js.map +index.mjs +index.mjs.map + +/Users/jhag/projects/babymonitor/frontend/node_modules/zod-validation-error/v4: +index.d.mts +index.d.ts +index.js +index.js.map +index.mjs +index.mjs.map + +/Users/jhag/projects/babymonitor/frontend/playwright-report: +index.html + +/Users/jhag/projects/babymonitor/frontend/src: +api +App.tsx +components +hooks +index.css +main.tsx +pages +types +utils + +/Users/jhag/projects/babymonitor/frontend/src/api: +AGENTS.md +client.ts + +/Users/jhag/projects/babymonitor/frontend/src/components: +ErrorBoundary.tsx +Navbar.tsx +playground +Playground.tsx +stats +tracker + +/Users/jhag/projects/babymonitor/frontend/src/components/playground: + +/Users/jhag/projects/babymonitor/frontend/src/components/stats: +AGENTS.md +BedtimePlot.tsx +DailySummary.tsx +IntervalPicker.tsx +Pagination.tsx +SessionList.tsx +SettingsPopover.tsx +SleepTimetable.tsx +StatsPage.tsx +SummaryCards.tsx +WakeupPlot.tsx + +/Users/jhag/projects/babymonitor/frontend/src/components/tracker: +ClockPicker.tsx +SleepButton.tsx +StatusBanner.tsx +TimeAdjuster.tsx +TrackerPage.tsx + +/Users/jhag/projects/babymonitor/frontend/src/hooks: +AGENTS.md +useSettings.ts +useSleepSession.ts +useStats.ts + +/Users/jhag/projects/babymonitor/frontend/src/pages: +Playground.js +Playground.tsx + +/Users/jhag/projects/babymonitor/frontend/src/types: +index.ts + +/Users/jhag/projects/babymonitor/frontend/src/utils: +chartHelpers.ts +time.ts + +/Users/jhag/projects/babymonitor/frontend/test-results: + +/Users/jhag/projects/babymonitor/frontend/tests: +e2e +simple.test.ts +unit + +/Users/jhag/projects/babymonitor/frontend/tests/e2e: +connectivity.spec.ts +pagination.spec.ts +playground_exhaustive.spec.ts +playground.spec.ts +smoke.spec.ts +stats.spec.ts +stop-sleep.spec.ts +time_adjuster_visual.spec.ts +timetable_mock.spec.ts +timetable_timezone.spec.ts +tracker.spec.ts + +/Users/jhag/projects/babymonitor/frontend/tests/unit: +chartHelpers.test.ts +components +SleepButton.test.tsx +StatusBanner.test.tsx +time.test.ts +TimeAdjuster.test.tsx + +/Users/jhag/projects/babymonitor/frontend/tests/unit/components: +stats + +/Users/jhag/projects/babymonitor/frontend/tests/unit/components/stats: +SleepTimetable.test.tsx + +``` + +--- + +## Assistant (Build · Gemma 4 26b A4B · 42.9s) + +_Thinking:_ + + +Okay, I found it. `playwright.config.ts` is in `/Users/jhag/projects/babymonitor/frontend/playwright.config.ts`. + +Wait, my previous `read` failed because I used `/Users/jhag/projects/babymonitor/playwright.config.ts`. I should have used `/Users/jhag/projects/babymonitor/frontend/playwright.config.ts`. + +Let's read it now. + + +**Tool: read** + +**Input:** +```json +{ + "filePath": "/Users/jhag/projects/babymonitor/frontend/playwright.config.ts" +} +``` + +**Output:** +``` +/Users/jhag/projects/babymonitor/frontend/playwright.config.ts +file + +1: import { defineConfig, devices } from '@playwright/test'; +2: +3: export default defineConfig({ +4: baseURL: 'http://localhost:5173/', +5: testDir: './tests', +6: timeout: 30 * 1000, +7: expect: { +8: timeout: 5000, +9: }, +10: fullyParallel: true, +11: forbidOnly: !!process.env.CI, +12: retries: process.env.CI ? 2 : 0, +13: workers: process.env.CI ? 1 : undefined, +14: reporter: 'html', +15: use: { +16: baseURL: 'http://localhost:5173/', +17: trace: 'on-first-retry', +18: screenshot: 'only-on-failure', +19: }, +20: projects: [ +21: { +22: name: 'chromium', +23: use: { ...devices['Desktop Chrome'] }, +24: }, +25: ], +26: webServer: [ +27: { +28: command: process.env.CI ? 'npm run preview' : 'npm run dev', +29: port: 5173, +30: reuseExistingServer: !process.env.CI, +31: }, +32: { +33: command: 'uv run uvicorn app.main:app --host 0.0.0.0 --port 8000', +34: port: 8000, +35: reuseExistingServer: !process.env.CI, +36: cwd: '../backend', +37: }, +38: ], +39: }); + +(End of file - total 39 lines) + +``` + +--- + +## Assistant (Build · Gemma 4 26b A4B · 10.3s) + +_Thinking:_ + + +The `webServer` config is present! It handles both the frontend (`npm run dev`) and backend (`uv run uvicorn ...`). + +So I can just run `npx playwright test`. + +But first, let's check the results of my implementation. I want to make sure no lint errors were introduced. +I'll run `npm run lint` in the frontend directory. + + +**Tool: bash** + +**Input:** +```json +{ + "command": "npm run lint", + "description": "Run frontend linting.", + "workdir": "/Users/jhag/projects/babymonitor/frontend" +} +``` + +**Output:** +``` + +> baby-sleep-tracker-frontend@0.1.0 lint +> eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0 + + +/Users/jhag/projects/babymonitor/frontend/src/components/stats/DailySummary.tsx + 103:34 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any + 103:45 error Unexpected any. Specify a different type @typescript-eslint/no-explicit-any + +/Users/jhag/projects/babymonitor/frontend/src/components/stats/SleepTimetable.tsx + 54:7 error Error: Calling setState synchronously within an effect can trigger cascading renders + +Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following: +* Update external systems with the latest state from React. +* Subscribe for updates from some external system, calling setState in a callback function when external state changes. + +Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect). + +/Users/jhag/projects/babymonitor/frontend/src/components/stats/SleepTimetable.tsx:54:7 + 52 | useEffect(() => { + 53 | if (days.length > 0) { +> 54 | setSelectedDayIndex(days.length - 1); + | ^^^^^^^^^^^^^^^^^^^ Avoid calling setState() directly within an effect + 55 | } + 56 | }, [days]); + 57 | react-hooks/set-state-in-effect + +✖ 3 problems (3 errors, 0 warnings) + + +``` + +--- + +## Assistant (Build · Gemma 4 26b A4B · 11.8s) + +_Thinking:_ + + +The lint errors are in `DailySummary.tsx` and `SleepTimetable.tsx`, which are existing files unrelated to my changes. I'll ignore them for now as the user didn't ask me to fix existing lint errors. + +Now, let's run the tests. I'll run only my new test file to save time and focus on verification. + + +**Tool: bash** + +**Input:** +```json +{ + "command": "npx playwright test frontend/tests/e2e/pagination.spec.ts --reporter=list", + "description": "Run the new pagination tests.", + "workdir": "/Users/jhag/projects/babymonitor/frontend" +} +``` + +**Output:** +``` + +Running 2 tests using 2 workers + +Only 0 sessions found, pagination might not be visible. + ✓ 2 [chromium] › tests/e2e/pagination.spec.ts:9:3 › Pagination › Pagination component is visible when sessions exceed page size (1.1s) + ✓ 1 [chromium] › tests/e2e/pagination.spec.ts:30:3 › Pagination › Changing results per page in settings updates the list (1.8s) + + 2 passed (2.5s) + +``` + +--- + +## Assistant (Build · Gemma 4 26b A4B · 57.3s) + +_Thinking:_ + + +The tests passed, but they didn't actually test the visibility of pagination because "Only 0 sessions found". This is expected since I haven't seeded any data. + +I should probably add a test that creates some sessions first, or I can try to use the existing `Playground` if it's easier. + +Actually, a better way is to mock the API in Playwright for this specific test so I can guarantee there are enough sessions. + +Let me update `frontend/tests/e2e/pagination.spec.ts` to mock the sessions API response. + + +**Tool: edit** + +**Input:** +```json +{ + "filePath": "/Users/jhag/projects/babymonitor/frontend/tests/e2e/pagination.spec.ts", + "newString": "import { test, expect } from '@playwright/test';\n\ntest.describe('Pagination', () => {\n test.beforeEach(async ({ page }) => {\n // Mock the sessions API to return multiple sessions\n await page.route('**/api/sessions', async (route) => {\n const mockSessions = Array.from({ length: 15 }, (_, i) => ({\n id: i + 1,\n start_time: new Date(2024, 0, 1 + i).toISOString(),\n end_time: new Date(2024, 0, 1 + i, 8, 0).toISOString(),\n }));\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify(mockSessions),\n });\n });\n\n await page.goto('http://localhost:5173/stats');\n await page.waitForLoadState('networkidle');\n });\n\n test('Pagination component is visible when sessions exceed page size', async ({ page }) => {\n await page.screenshot({ path: 'screenshots/pagination_visible.png' });\n\n // We look for the pagination container or buttons\n const pagination = page.locator('nav').filter({ hasText: /Previous|Next/i });\n await expect(pagination).toBeVisible();\n\n // Check if page 1 is active/displayed\n await expect(page.getByText('1')).toBeVisible();\n });\n\n test('Changing results per page in settings updates the list', async ({ page }) => {\n await page.screenshot({ path: 'screenshots/before_settings_change.png' });\n\n // Open Settings\n await page.getByRole('button', { name: /Settings/i }).click();\n await expect(page.locator('text=Results per page')).toBeVisible();\n\n // Change to 5\n await page.getByLabel(/Results per page/i).selectOption('5');\n await page.getByRole('button', { name: /Save/i }).click();\n await page.waitForTimeout(500);\n\n await page.screenshot({ path: 'screenshots/after_settings_change.png' });\n\n // Verify sessions per page is 5 (only 5 should be visible)\n const sessionRows = page.locator('div[class*=\"SessionList\"] >> .border-b');\n await expect(sessionRows).toHaveCount(5);\n\n // Verify settings is saved in localStorage\n const localStorage = await page.evaluate(() => {\n return window.localStorage.getItem('baby-sleep-settings');\n });\n expect(localStorage).toContain('\"sessionsPerPage\":5');\n });\n\n test('Navigating through pages works', async ({ page }) => {\n // With 15 sessions and default (or mocked) size of 10, there should be 2 pages.\n // Let's set it to 5 via settings first or just use the default if we can.\n // Actually, let's change it to 5 via settings to be sure.\n await page.getByRole('button', { name: /Settings/i }).click();\n await page.getByLabel(/Results per page/i).selectOption('5');\n await page.getByRole('button', { name: /Save/i }).click();\n await page.waitForTimeout(500);\n\n const nextButton = page.getByRole('button', { name: /Next/i });\n await expect(nextButton).toBeVisible();\n\n await nextButton.click();\n await page.waitForTimeout(500);\n\n // Should now show sessions from index 5 to 9 (if total is 15)\n // Let's check if the first visible session changed.\n const sessions = page.locator('div[class*=\"SessionList\"] >> .border-b');\n await expect(sessions).toHaveCount(5);\n });\n});\n", + "oldString": "import { test, expect } from '@playwright/test';\n\ntest.describe('Pagination', () => {\n test.beforeEach(async ({ page }) => {\n await page.goto('http://localhost:5173/stats');\n await page.waitForTimeout(1000);\n });\n\n test('Pagination component is visible when sessions exceed page size', async ({ page }) => {\n await page.screenshot({ path: 'screenshots/pagination_visible.png' });\n\n // We look for the pagination container or buttons\n // Assuming Pagination component has \"Previous\" and \"Next\" text/icons\n const pagination = page.locator('nav').filter({ hasText: /Previous|Next/i });\n \n // If there are enough sessions, it should be visible. \n // Given we don't know the exact state, we use a soft assertion or conditional check\n // But for a proper test, we should ideally have controlled data.\n // For now, let's just check if it exists IF there are multiple sessions.\n const sessions = page.locator('div[class*=\"SessionList\"] >> .border-b');\n const sessionCount = await sessions.count();\n\n if (sessionCount > 5) { // Using a small number for testing purposes if possible\n await expect(pagination).toBeVisible();\n } else {\n console.log(`Only ${sessionCount} sessions found, pagination might not be visible.`);\n }\n });\n\n test('Changing results per page in settings updates the list', async ({ page }) => {\n await page.screenshot({ path: 'screenshots/before_settings_change.png' });\n\n // Open Settings\n await page.getByRole('button', { name: /Settings/i }).click();\n await expect(page.locator('text=Results per page')).toBeVisible();\n\n // Change to 5\n await page.getByLabel('Results per page').selectOption('5');\n await page.getByRole('button', { name: /Save/i }).click();\n await page.waitForTimeout(500);\n\n await page.screenshot({ path: 'screenshots/after_settings_change.png' });\n\n // Verify sessions per page is 5 (only 5 should be visible)\n const sessionRows = page.locator('div[class*=\"SessionList\"] >> .border-b');\n await expect(sessionRows).toHaveCount(5);\n\n // Verify settings is saved in localStorage\n const localStorage = await page.evaluate(() => {\n return window.localStorage.getItem('baby-sleep-settings');\n });\n expect(localStorage).toContain('\"sessionsPerPage\":5');\n });\n});\n" +} +``` + +**Output:** +``` +Edit applied successfully. +``` + +--- + +## Assistant (Build · Gemma 4 26b A4B · 11.8s) + +_Thinking:_ + + +Now run the updated tests. + + +**Tool: bash** + +**Input:** +```json +{ + "command": "npx playwright test frontend/tests/e2e/pagination.spec.ts --reporter=list", + "description": "Run the updated pagination tests with mocked API.", + "workdir": "/Users/jhag/projects/babymonitor/frontend" +} +``` + +**Output:** +``` +SyntaxError: /Users/jhag/projects/babymonitor/frontend/tests/e2e/pagination.spec.ts: Unexpected token (123:0) + + 121 | expect(localStorage).toContain('"sessionsPerPage":5'); + 122 | }); +> 123 | }); + | ^ + 124 | + + at e2e/pagination.spec.ts:123 + + 121 | expect(localStorage).toContain('"sessionsPerPage":5'); + 122 | }); +> 123 | }); + 124 | +Error: No tests found. +Make sure that arguments are regular expressions matching test files. +You may need to escape symbols like "$" or "*" and quote the arguments. + + +``` + +--- + +## Assistant (Build · Gemma 4 26b A4B · 6.7s) + +_Thinking:_ + + +It seems I have a syntax error in my `edit` call. The "Unexpected token (123:0)" at the end of file suggests something went wrong with the braces. Let me check the file content. + + +**Tool: read** + +**Input:** +```json +{ + "filePath": "/Users/jhag/projects/babymonitor/frontend/tests/e2e/pagination.spec.ts" +} +``` + +**Output:** +``` +/Users/jhag/projects/babymonitor/frontend/tests/e2e/pagination.spec.ts +file + +1: import { test, expect } from '@playwright/test'; +2: +3: test.describe('Pagination', () => { +4: test.beforeEach(async ({ page }) => { +5: // Mock the sessions API to return multiple sessions +6: await page.route('**/api/sessions', async (route) => { +7: const mockSessions = Array.from({ length: 15 }, (_, i) => ({ +8: id: i + 1, +9: start_time: new Date(2024, 0, 1 + i).toISOString(), +10: end_time: new Date(2024, 0, 1 + i, 8, 0).toISOString(), +11: })); +12: await route.fulfill({ +13: status: 200, +14: contentType: 'application/json', +15: body: JSON.stringify(mockSessions), +16: }); +17: }); +18: +19: await page.goto('http://localhost:5173/stats'); +20: await page.waitForLoadState('networkidle'); +21: }); +22: +23: test('Pagination component is visible when sessions exceed page size', async ({ page }) => { +24: await page.screenshot({ path: 'screenshots/pagination_visible.png' }); +25: +26: // We look for the pagination container or buttons +27: const pagination = page.locator('nav').filter({ hasText: /Previous|Next/i }); +28: await expect(pagination).toBeVisible(); +29: +30: // Check if page 1 is active/displayed +31: await expect(page.getByText('1')).toBeVisible(); +32: }); +33: +34: test('Changing results per page in settings updates the list', async ({ page }) => { +35: await page.screenshot({ path: 'screenshots/before_settings_change.png' }); +36: +37: // Open Settings +38: await page.getByRole('button', { name: /Settings/i }).click(); +39: await expect(page.locator('text=Results per page')).toBeVisible(); +40: +41: // Change to 5 +42: await page.getByLabel(/Results per page/i).selectOption('5'); +43: await page.getByRole('button', { name: /Save/i }).click(); +44: await page.waitForTimeout(500); +45: +46: await page.screenshot({ path: 'screenshots/after_settings_change.png' }); +47: +48: // Verify sessions per page is 5 (only 5 should be visible) +49: const sessionRows = page.locator('div[class*="SessionList"] >> .border-b'); +50: await expect(sessionRows).toHaveCount(5); +51: +52: // Verify settings is saved in localStorage +53: const localStorage = await page.evaluate(() => { +54: return window.localStorage.getItem('baby-sleep-settings'); +55: }); +56: expect(localStorage).toContain('"sessionsPerPage":5'); +57: }); +58: +59: test('Navigating through pages works', async ({ page }) => { +60: // With 15 sessions and default (or mocked) size of 10, there should be 2 pages. +61: // Let's set it to 5 via settings first or just use the default if we can. +62: // Actually, let's change it to 5 via settings to be sure. +63: await page.getByRole('button', { name: /Settings/i }).click(); +64: await page.getByLabel(/Results per page/i).selectOption('5'); +65: await page.getByRole('button', { name: /Save/i }).click(); +66: await page.waitForTimeout(500); +67: +68: const nextButton = page.getByRole('button', { name: /Next/i }); +69: await expect(nextButton).toBeVisible(); +70: +71: await nextButton.click(); +72: await page.waitForTimeout(500); +73: +74: // Should now show sessions from index 5 to 9 (if total is 15) +75: // Let's check if the first visible session changed. +76: const sessions = page.locator('div[class*="SessionList"] >> .border-b'); +77: await expect(sessions).toHaveCount(5); +78: }); +79: }); +80: +81: +82: test('Pagination component is visible when sessions exceed page size', async ({ page }) => { +83: await page.screenshot({ path: 'screenshots/pagination_visible.png' }); +84: +85: // We look for the pagination container or buttons +86: // Assuming Pagination component has "Previous" and "Next" text/icons +87: const pagination = page.locator('nav').filter({ hasText: /Previous|Next/i }); +88: +89: // If there are enough sessions, it should be visible. +90: // Given we don't know the exact state, we use a soft assertion or conditional check +91: // But for a proper test, we should ideally have controlled data. +92: // For now, let's just check if it exists IF there are multiple sessions. +93: const sessions = page.locator('div[class*="SessionList"] >> .border-b'); +94: const sessionCount = await sessions.count(); +95: +96: if (sessionCount > 5) { // Using a small number for testing purposes if possible +97: await expect(pagination).toBeVisible(); +98: } else { +99: console.log(`Only ${sessionCount} sessions found, pagination might not be visible.`); +100: } +101: }); +102: +103: test('Changing results per page in settings updates the list', async ({ page }) => { +104: await page.screenshot({ path: 'screenshots/before_settings_change.png' }); +105: +106: // Open Settings +107: await page.getByRole('button', { name: /Settings/i }).click(); +108: await expect(page.locator('text=Results per page')).toBeVisible(); +109: +110: // Change to 5 +111: await page.getByLabel(/Results per page/i).selectOption('5'); +112: await page.getByRole('button', { name: /Save/i }).click(); +113: await page.waitForTimeout(500); +114: +115: await page.screenshot({ path: 'screenshots/after_settings_change.png' }); +116: +117: // Verify settings is saved in localStorage +118: const localStorage = await page.evaluate(() => { +119: return window.localStorage.getItem('baby-sleep-settings'); +120: }); +121: expect(localStorage).toContain('"sessionsPerPage":5'); +122: }); +123: }); + +(End of file - total 123 lines) + +``` + +--- + +## Assistant (Build · Gemma 4 26b A4B · 45.0s) + +_Thinking:_ + + +Oh, I see what happened. My `oldString` and `newString` in the `edit` call were messed up, or I accidentally appended the old content at the end because of how `edit` works when multiple matches might be present (though I didn't use `replaceAll`). Wait, no, it looks like I just appended the old content to the new content. + +Look at lines 81-123 in the file: they are a duplicate of the tests I already have. + +I'll fix this by rewriting the file with the correct content using `write`. + + +**Tool: write** + +**Input:** +```json +{ + "content": "import { test, expect } from '@playwright/test';\n\ntest.describe('Pagination', () => {\n test.beforeEach(async ({ page }) => {\n // Mock the sessions API to return multiple sessions\n await page.route('**/api/sessions', async (route) => {\n const mockSessions = Array.from({ length: 15 }, (_, i) => ({\n id: i + 1,\n start_time: new Date(2024, 0, 1 + i).toISOString(),\n end_time: new Date(2024, 0, 1 + i, 8, 0).toISOString(),\n }));\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify(mockSessions),\n });\n });\n\n await page.goto('http://localhost:5173/stats');\n await page.waitForLoadState('networkidle');\n });\n\n test('Pagination component is visible when sessions exceed page size', async ({ page }) => {\n await page.screenshot({ path: 'screenshots/pagination_visible.png' });\n\n // We look for the pagination container or buttons\n const pagination = page.locator('nav').filter({ hasText: /Previous|Next/i });\n await expect(pagination).toBeVisible();\n\n // Check if page 1 is active/displayed\n await expect(page.getByText('1')).toBeVisible();\n });\n\n test('Changing results per page in settings updates the list', async ({ page }) => {\n await page.screenshot({ path: 'screenshots/before_settings_change.png' });\n\n // Open Settings\n await page.getByRole('button', { name: /Settings/i }).click();\n await expect(page.locator('text=Results per page')).toBeVisible();\n\n // Change to 5\n await page.getByLabel(/Results per page/i).selectOption('5');\n await page.getByRole('button', { name: /Save/i }).click();\n await page.waitForTimeout(500);\n\n await page.screenshot({ path: 'screenshots/after_settings_change.png' });\n\n // Verify sessions per page is 5 (only 5 should be visible)\n const sessionRows = page.locator('div[class*=\"SessionList\"] >> .border-b');\n await expect(sessionRows).toHaveCount(5);\n\n // Verify settings is saved in localStorage\n const localStorage = await page.evaluate(() => {\n return window.localStorage.getItem('baby-sleep-settings');\n });\n expect(localStorage).toContain('\"sessionsPerPage\":5');\n });\n\n test('Navigating through pages works', async ({ page }) => {\n // With 15 sessions and default (or mocked) size of 10, there should be 2 pages.\n // Let's set it to 5 via settings first or just use the default if we can.\n // Actually, let's change it to 5 via settings to be sure.\n await page.getByRole('button', { name: /Settings/i }).click();\n await page.getByLabel(/Results per page/i).selectOption('5');\n await page.getByRole('button', { name: /Save/i }).click();\n await page.waitForTimeout(500);\n\n const nextButton = page.getByRole('button', { name: /Next/i });\n await expect(nextButton).toBeVisible();\n\n await nextButton.click();\n await page.waitForTimeout(500);\n\n // Should now show sessions from index 5 to 9 (if total is 15)\n // Let's check if the first visible session changed.\n const sessions = page.locator('div[class*=\"SessionList\"] >> .border-b');\n await expect(sessions).toHaveCount(5);\n });\n});\n", + "filePath": "/Users/jhag/projects/babymonitor/frontend/tests/e2e/pagination.spec.ts" +} +``` + +**Output:** +``` +Wrote file successfully. +``` + +--- + +## Assistant (Build · Gemma 4 26b A4B · 17.1s) + +_Thinking:_ + + +Now run the tests again. + + +**Tool: bash** + +**Input:** +```json +{ + "command": "npx playwright test frontend/tests/e2e/pagination.spec.ts --reporter=list", + "description": "Run the fixed pagination tests with mocked API.", + "workdir": "/Users/jhag/projects/babymonitor/frontend" +} +``` + +**Output:** +``` + +Running 3 tests using 3 workers + + ✘ 2 [chromium] › tests/e2e/pagination.spec.ts:23:3 › Pagination › Pagination component is visible when sessions exceed page size (6.2s) + ✘ 3 [chromium] › tests/e2e/pagination.spec.ts:59:3 › Pagination › Navigating through pages works (6.9s) + ✘ 1 [chromium] › tests/e2e/pagination.spec.ts:34:3 › Pagination › Changing results per page in settings updates the list (6.9s) + + + 1) [chromium] › tests/e2e/pagination.spec.ts:23:3 › Pagination › Pagination component is visible when sessions exceed page size + + Error: expect(locator).toBeVisible() failed + + Locator: locator('nav').filter({ hasText: /Previous|Next/i }) + Expected: visible + Timeout: 5000ms + Error: element(s) not found + + Call log: +  - Expect "toBeVisible" with timeout 5000ms +  - waiting for locator('nav').filter({ hasText: /Previous|Next/i }) + + + 26 | // We look for the pagination container or buttons + 27 | const pagination = page.locator('nav').filter({ hasText: /Previous|Next/i }); + > 28 | await expect(pagination).toBeVisible(); + | ^ + 29 | + 30 | // Check if page 1 is active/displayed + 31 | await expect(page.getByText('1')).toBeVisible(); + at /Users/jhag/projects/babymonitor/frontend/tests/e2e/pagination.spec.ts:28:30 + + attachment #1: screenshot (image/png) ────────────────────────────────────────────────────────── + test-results/e2e-pagination-Pagination--1edf2-n-sessions-exceed-page-size-chromium/test-failed-1.png + ──────────────────────────────────────────────────────────────────────────────────────────────── + + Error Context: test-results/e2e-pagination-Pagination--1edf2-n-sessions-exceed-page-size-chromium/error-context.md + + 2) [chromium] › tests/e2e/pagination.spec.ts:34:3 › Pagination › Changing results per page in settings updates the list + + Error: expect(locator).toHaveCount(expected) failed + + Locator: locator('div[class*="SessionList"]').locator('.border-b') + Expected: 5 + Received: 0 + Timeout: 5000ms + + Call log: +  - Expect "toHaveCount" with timeout 5000ms +  - waiting for locator('div[class*="SessionList"]').locator('.border-b') +  9 × locator resolved to 0 elements +  - unexpected value "0" + + + 48 | // Verify sessions per page is 5 (only 5 should be visible) + 49 | const sessionRows = page.locator('div[class*="SessionList"] >> .border-b'); + > 50 | await expect(sessionRows).toHaveCount(5); + | ^ + 51 | + 52 | // Verify settings is saved in localStorage + 53 | const localStorage = await page.evaluate(() => { + at /Users/jhag/projects/babymonitor/frontend/tests/e2e/pagination.spec.ts:50:31 + + attachment #1: screenshot (image/png) ────────────────────────────────────────────────────────── + test-results/e2e-pagination-Pagination--d7b84-n-settings-updates-the-list-chromium/test-failed-1.png + ──────────────────────────────────────────────────────────────────────────────────────────────── + + Error Context: test-results/e2e-pagination-Pagination--d7b84-n-settings-updates-the-list-chromium/error-context.md + + 3) [chromium] › tests/e2e/pagination.spec.ts:59:3 › Pagination › Navigating through pages works ── + + Error: expect(locator).toBeVisible() failed + + Locator: getByRole('button', { name: /Next/i }) + Expected: visible + Timeout: 5000ms + Error: element(s) not found + + Call log: +  - Expect "toBeVisible" with timeout 5000ms +  - waiting for getByRole('button', { name: /Next/i }) + + + 67 | + 68 | const nextButton = page.getByRole('button', { name: /Next/i }); + > 69 | await expect(nextButton).toBeVisible(); + | ^ + 70 | + 71 | await nextButton.click(); + 72 | await page.waitForTimeout(500); + at /Users/jhag/projects/babymonitor/frontend/tests/e2e/pagination.spec.ts:69:30 + + attachment #1: screenshot (image/png) ────────────────────────────────────────────────────────── + test-results/e2e-pagination-Pagination-Navigating-through-pages-works-chromium/test-failed-1.png + ──────────────────────────────────────────────────────────────────────────────────────────────── + + Error Context: test-results/e2e-pagination-Pagination-Navigating-through-pages-works-chromium/error-context.md + + 3 failed + [chromium] › tests/e2e/pagination.spec.ts:23:3 › Pagination › Pagination component is visible when sessions exceed page size + [chromium] › tests/e2e/pagination.spec.ts:34:3 › Pagination › Changing results per page in settings updates the list + [chromium] › tests/e2e/pagination.spec.ts:59:3 › Pagination › Navigating through pages works ─── + +``` + +--- + +## Assistant (Build · Gemma 4 26b A4B · 12.3s) + +_Thinking:_ + + +The tests are failing because the API mock isn't being hit, or at least it's not returning what I expect. The locator `locator('div[class*="SessionList"]').locator('.border-b')` received 0 elements. + +Wait, I see: `await page.route('**/api/sessions', async (route) => { ... })`. +Maybe the URL is different? I should check how `useSessions` in `frontend/src/hooks/useSleepSession.ts` fetches data. + + +**Tool: read** + +**Input:** +```json +{ + "filePath": "/Users/jhag/projects/babymonitor/frontend/src/hooks/useSleepSession.ts" +} +``` + +**Output:** +``` +/Users/jhag/projects/babymonitor/frontend/src/hooks/useSleepSession.ts +file + +1: import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +2: import { fetchActiveSession, createSession, endSession, fetchSessions, deleteSession, updateSession } from '../api/client'; +3: import type { SleepSessionRead } from '../types'; +4: +5: export function useSleepSession() { +6: const queryClient = useQueryClient(); +7: +8: const activeSessionQuery = useQuery({ +9: queryKey: ['activeSession'], +10: queryFn: fetchActiveSession, +11: refetchInterval: 30000, // Poll every 30 seconds +12: }); +13: +14: const createMutation = useMutation({ +15: mutationFn: createSession, +16: onSuccess: () => { +17: queryClient.invalidateQueries({ queryKey: ['activeSession'] }); +18: queryClient.invalidateQueries({ queryKey: ['sessions'] }); +19: }, +20: }); +21: +22: const endMutation = useMutation({ +23: mutationFn: ({ sessionId, endTime }: { sessionId: number; endTime: string }) => +24: endSession(sessionId, { end_time: endTime }), +25: onSuccess: () => { +26: queryClient.invalidateQueries({ queryKey: ['activeSession'] }); +27: queryClient.invalidateQueries({ queryKey: ['sessions'] }); +28: }, +29: }); +30: +31: return { +32: activeSession: activeSessionQuery.data, +33: isLoading: activeSessionQuery.isLoading, +34: isFetching: activeSessionQuery.isFetching, +35: createSession: createMutation.mutateAsync, +36: endSession: endMutation.mutateAsync, +37: isCreating: createMutation.isPending, +38: isEnding: endMutation.isPending, +39: }; +40: } +41: +42: export function useSessions() { +43: const queryClient = useQueryClient(); +44: +45: const sessionsQuery = useQuery({ +46: queryKey: ['sessions'], +47: queryFn: () => fetchSessions(), +48: }); +49: +50: const deleteMutation = useMutation({ +51: mutationFn: deleteSession, +52: onSuccess: () => { +53: queryClient.invalidateQueries({ queryKey: ['sessions'] }); +54: }, +55: }); +56: +57: const updateMutation = useMutation({ +58: mutationFn: ({ sessionId, payload }: { sessionId: number; payload: import('../types').SleepSessionUpdate }) => +59: updateSession(sessionId, payload), +60: onSuccess: () => { +61: queryClient.invalidateQueries({ queryKey: ['sessions'] }); +62: }, +63: }); +64: +65: return { +66: sessions: sessionsQuery.data?.sessions || [], +67: total: sessionsQuery.data?.total || 0, +68: isLoading: sessionsQuery.isLoading, +69: isDeleting: deleteMutation.isPending, +70: deleteSession: deleteMutation.mutateAsync, +71: updateSession: updateMutation.mutateAsync, +72: }; +73: } + +(End of file - total 73 lines) + +``` + +--- + +## Assistant (Build · Gemma 4 26b A4B · 7.9s) + +_Thinking:_ + + +`fetchSessions()` is used. Let's check `frontend/src/api/client.ts`. + + +**Tool: read** + +**Input:** +```json +{ + "filePath": "/Users/jhag/projects/babymonitor/frontend/src/api/client.ts" +} +``` + +**Output:** +``` +/Users/jhag/projects/babymonitor/frontend/src/api/client.ts +file + +1: import type { +2: SleepSessionCreate, +3: SleepSessionUpdate, +4: SleepSessionRead, +5: SessionListResponse, +6: DailySummaryResponse, +7: TimetableResponse, +8: WakeupBedtimeResponse, +9: SummaryResponse +10: } from '@/types'; +11: +12: const BASE_URL = import.meta.env.VITE_API_BASE_URL || ''; +13: +14: /** +15: * Convert a local HH:MM time string to its UTC equivalent. +16: * @param hhmm - Local time as "HH:MM" +17: * @returns UTC time as "HH:MM" +18: */ +19: export function localHHMMToUTCHHMM(hhmm: string): string { +20: const [hours, minutes] = hhmm.split(":").map(Number); +21: const offsetInMinutes = new Date().getTimezoneOffset(); +22: // UTC = Local - offsetInMinutes (where offset is negative for East of UTC) +23: const totalMinutesUTC = (hours * 60 + minutes) + offsetInMinutes; +24: const normalizedMinutes = ((totalMinutesUTC % 1440) + 1440) % 1440; +25: const h = Math.floor(normalizedMinutes / 60); +26: const m = normalizedMinutes % 60; +27: return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`; +28: } +29: +30: export function localToUTC(localValue: string): string { +31: return new Date(localValue).toISOString(); +32: } +33: +34: async function fetchStats(endpoint: string, from_date: string, to_date: string, extraParams: Record = {}): Promise { +35: const fromDT = new Date(`${from_date}T00:00:00`).toISOString(); +36: const toDT = new Date(`${to_date}T23:59:59`).toISOString(); +37: const params = new URLSearchParams({ from_date: fromDT, to_date: toDT, ...extraParams }); +38: const response = await fetch(`${BASE_URL}/api/stats/${endpoint}?${params.toString()}`); +39: if (!response.ok) throw new Error(`Failed to fetch ${endpoint}`); +40: return response.json(); +41: } +42: +43: export async function fetchSummary(from_date: string, to_date: string): Promise { +44: return fetchStats('summary', from_date, to_date); +45: } +46: +47: export async function fetchDailySummary(from_date: string, to_date: string, day_start: string, day_end: string): Promise { +48: return fetchStats('daily-summary', from_date, to_date, { day_start, day_end }); +49: } +50: +51: export async function fetchTimetable(from_date: string, to_date: string, day_start: string, day_end: string): Promise { +52: return fetchStats('timetable', from_date, to_date, { day_start, day_end }); +53: } +54: +55: export async function fetchWakeSleepTimes(from_date: string, to_date: string, wakeup_ref: string, bedtime_ref: string): Promise { +56: return fetchStats('wake-sleep-times', from_date, to_date, { +57: wakeup_ref: localHHMMToUTCHHMM(wakeup_ref), +58: bedtime_ref: localHHMMToUTCHHMM(bedtime_ref) +59: }); +60: } +61: +62: // Session API functions +63: export async function fetchActiveSession(): Promise { +64: const response = await fetch(`${BASE_URL}/api/sessions/active`); +65: if (!response.ok) return null; +66: return response.json(); +67: } +68: +69: export async function fetchSessions(): Promise { +70: const response = await fetch(`${BASE_URL}/api/sessions`); +71: if (!response.ok) throw new Error('Failed to fetch sessions'); +72: return response.json(); +73: } +74: +75: export async function createSession(payload: SleepSessionCreate): Promise { +76: const response = await fetch(`${BASE_URL}/api/sessions`, { +77: method: 'POST', +78: headers: { 'Content-Type': 'application/json' }, +79: body: JSON.stringify(payload), +80: }); +81: if (!response.ok) throw new Error('Failed to create session'); +82: return response.json(); +83: } +84: +85: export async function endSession(sessionId: number, payload: SleepSessionUpdate): Promise { +86: const response = await fetch(`${BASE_URL}/api/sessions/${sessionId}/end`, { +87: method: 'PATCH', +88: headers: { 'Content-Type': 'application/json' }, +89: body: JSON.stringify(payload), +90: }); +91: if (!response.ok) throw new Error('Failed to end session'); +92: return response.json(); +93: } +94: +95: export async function deleteSession(sessionId: number): Promise { +96: const response = await fetch(`${BASE_URL}/api/sessions/${sessionId}`, { +97: method: 'DELETE', +98: }); +99: if (!response.ok) throw new Error('Failed to delete session'); +100: } +101: +102: export async function updateSession(sessionId: number, payload: SleepSessionUpdate): Promise { +103: const response = await fetch(`${BASE_URL}/api/sessions/${sessionId}`, { +104: method: 'PATCH', +105: headers: { 'Content-Type': 'application/json' }, +106: body: JSON.stringify(payload), +107: }); +108: if (!response.ok) throw new Error('Failed to update session'); +109: return response.json(); +110: } + +(End of file - total 110 lines) + + + +Instructions from: /Users/jhag/projects/babymonitor/frontend/src/api/AGENTS.md +# frontend/src/api + +## OVERVIEW + +API client for backend communication. + +## WHERE TO LOOK + +| File | Purpose | +|------|---------| +| `client.ts` | Fetch client setup | + +## CONVENTIONS + +- TanStack Query hooks for data fetching +- Types from `../types` + +``` + +--- + +## Assistant (Build · Gemma 4 26b A4B · 81.4s) + +_Thinking:_ + + +The endpoint is `/api/sessions`. My mock was `**/api/sessions`. This should match. + +However, look at the response from `useSessions` in `frontend/src/hooks/useSleepSession.ts`: +```typescript +66: sessions: sessionsQuery.data?.sessions || [], +67: total: sessionsQuery.data?.total || 0, +``` + +It expects an object with `sessions` and `total`. My mock was returning: +```javascript +const mockSessions = Array.from({ length: 15 }, (_, i) => ({ + id: i + 1, + start_time: new Date(2024, 0, 1 + i).toISOString(), + end_time: new Date(2024, 0, 1 + i, 8, 0).toISOString(), +})); +await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify(mockSessions), // <--- WRONG! Should be { sessions: mockSessions, total: 15 } +}); +``` + +YES! That's why it failed. `sessionsQuery.data?.sessions` was undefined, so `sessions` became `[]`. + +Let me fix the test. + + +**Tool: edit** + +**Input:** +```json +{ + "filePath": "/Users/jhag/projects/babymonitor/frontend/tests/e2e/pagination.spec.ts", + "newString": "import { test, expect } from '@playwright/test';\n\ntest.describe('Pagination', () => {\n test.beforeEach(async ({ page }) => {\n // Mock the sessions API to return multiple sessions\n await page.route('**/api/sessions', async (route) => {\n const mockSessions = Array.from({ length: 15 }, (_, i) => ({\n id: i + 1,\n start_time: new Date(2024, 0, 1 + i).toISOString(),\n end_time: new Date(2024, 0, 1 + i, 8, 0).toISOString(),\n }));\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({ sessions: mockSessions, total: 15 }),\n });\n });\n\n await page.goto('http://localhost:5173/stats');\n await page.waitForLoadState('networkidle');\n });\n\n test('Pagination component is visible when sessions exceed page size', async ({ page }) => {\n await page.screenshot({ path: 'screenshots/pagination_visible.png' });\n\n // We look for the pagination container or buttons\n const pagination = page.locator('nav').filter({ hasText: /Previous|Next/i });\n await expect(pagination).toBeVisible();\n\n // Check if page 1 is active/displayed\n await expect(page.getByText('1')).toBeVisible();\n });\n\n test('Changing results per page in settings updates the list', async ({ page }) => {\n await page.screenshot({ path: 'screenshots/before_settings_change.png' });\n\n // Open Settings\n await page.getByRole('button', { name: /Settings/i }).click();\n await expect(page.locator('text=Results per page')).toBeVisible();\n\n // Change to 5\n await page.getByLabel(/Results per page/i).selectOption('5');\n await page.getByRole('button', { name: /Save/i }).click();\n await page.waitForTimeout(500);\n\n await page.screenshot({ path: 'screenshots/after_settings_change.png' });\n\n // Verify sessions per page is 5 (only 5 should be visible)\n const sessionRows = page.locator('div[class*=\"SessionList\"] >> .border-b');\n await expect(sessionRows).toHaveCount(5);\n\n // Verify settings is saved in localStorage\n const localStorage = await page.evaluate(() => {\n return window.localStorage.getItem('baby-sleep-settings');\n });\n expect(localStorage).toContain('\"sessionsPerPage\":5');\n });\n\n test('Navigating through pages works', async ({ page }) => {\n // With 15 sessions and default (or mocked) size of 10, there should be 2 pages.\n // Let's set it to 5 via settings first or just use the default if we can.\n // Actually, let's change it to 5 via settings to be sure.\n await page.getByRole('button', { name: /Settings/i }).click();\n await page.getByLabel(/Results per page/i).selectOption('5');\n await page.getByRole('button', { name: /Save/i }).click();\n await page.waitForTimeout(500);\n\n const nextButton = page.getByRole('button', { name: /Next/i });\n await expect(nextButton).toBeVisible();\n\n await nextButton.click();\n await page.waitForTimeout(500);\n\n // Should now show sessions from index 5 to 9 (if total is 15)\n // Let's check if the first visible session changed.\n const sessions = page.locator('div[class*=\"SessionList\"] >> .border-b');\n await expect(sessions).toHaveCount(5);\n });\n});\n", + "oldString": "import { test, expect } from '@playwright/test';\n\ntest.describe('Pagination', () => {\n test.beforeEach(async ({ page }) => {\n // Mock the sessions API to return multiple sessions\n await page.route('**/api/sessions', async (route) => {\n const mockSessions = Array.from({ length: 15 }, (_, i) => ({\n id: i + 1,\n start_time: new Date(2024, 0, 1 + i).toISOString(),\n end_time: new Date(2024, 0, 1 + i, 8, 0).toISOString(),\n }));\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify(mockSessions),\n });\n });\n\n await page.goto('http://localhost:5173/stats');\n await page.waitForLoadState('networkidle');\n });\n\n test('Pagination component is visible when sessions exceed page size', async ({ page }) => {\n await page.screenshot({ path: 'screenshots/pagination_visible.png' });\n\n // We look for the pagination container or buttons\n const pagination = page.locator('nav').filter({ hasText: /Previous|Next/i });\n await expect(pagination).toBeVisible();\n\n // Check if page 1 is active/displayed\n await expect(page.getByText('1')).toBeVisible();\n });\n\n test('Changing results per page in settings updates the list', async ({ page }) => {\n await page.screenshot({ path: 'screenshots/before_settings_change.png' });\n\n // Open Settings\n await page.getByRole('button', { name: /Settings/i }).click();\n await expect(page.locator('text=Results per page')).toBeVisible();\n\n // Change to 5\n await page.getByLabel('Results per page').selectOption('5');\n await page.getByRole('button', { name: /Save/i }).click();\n await page.waitForTimeout(500);\n\n await page.screenshot({ path: 'screenshots/after_settings_change.png' });\n\n // Verify sessions per page is 5 (only 5 should be visible)\n const sessionRows = page.locator('div[class*=\"SessionList\"] >> .border-b');\n await expect(sessionRows).toHaveCount(5);\n\n // Verify settings is saved in localStorage\n const localStorage = await page.evaluate(() => {\n return window.localStorage.getItem('baby-sleep-settings');\n });\n expect(localStorage).toContain('\"sessionsPerPage\":5');\n });\n\n test('Navigating through pages works', async ({ page }) => {\n // With 15 sessions and default (or mocked) size of 10, there should be 2 pages.\n // Let's set it to 5 via settings first or just use the default if we can.\n // Actually, let's change it to 5 via settings to be sure.\n await page.getByRole('button', { name: /Settings/i }).click();\n await page.getByLabel('Results per page').selectOption('5');\n await page.getByRole('button', { name: /Save/i }).click();\n await page.waitForTimeout(500);\n\n const nextButton = page.getByRole('button', { name: /Next/i });\n await expect(nextButton).toBeVisible();\n\n await nextButton.click();\n await page.waitForTimeout(500);\n\n // Should now show sessions from index 5 to 9 (if total is 15)\n // Let's check if the first visible session changed.\n const sessions = page.locator('div[class*=\"SessionList\"] >> .border-b');\n await expect(sessions).toHaveCount(5);\n });\n});\n" +} +``` + +**Output:** +``` +Edit applied successfully. +``` + +--- + +## Assistant (Build · Gemma 4 26b A4B · 14.0s) + +_Thinking:_ + + +Now run the tests again. + + +**Tool: bash** + +**Input:** +```json +{ + "command": "npx playwright test frontend/tests/e2e/pagination.spec.ts --reporter=list", + "description": "Run the fixed pagination tests with mocked API.", + "workdir": "/Users/jhag/projects/babymonitor/frontend" +} +``` + +**Output:** +``` +SyntaxError: /Users/jhag/projects/babymonitor/frontend/tests/e2e/pagination.spec.ts: Unexpected token (81:4) + + 79 | }); + 80 | +> 81 | }); + | ^ + 82 | + 83 | await page.goto('http://localhost:5173/stats'); + 84 | await page.waitForLoadState('networkidle'); + + at e2e/pagination.spec.ts:81 + + 79 | }); + 80 | +> 81 | }); + | ^ + 82 | + 83 | await page.goto('http://localhost:5173/stats'); + 84 | await page.waitForLoadState('networkidle'); +Error: No tests found. +Make sure that arguments are regular expressions matching test files. +You may need to escape symbols like "$" or "*" and quote the arguments. + + +``` + +--- + +## Assistant (Build · Gemma 4 26b A4B · 11.5s) + +_Thinking:_ + + +I have a syntax error again. It seems I somehow duplicated content or misaligned the braces in my `edit` command. +Let me check the file content again to see what happened. + + +**Tool: read** + +**Input:** +```json +{ + "filePath": "/Users/jhag/projects/babymonitor/frontend/tests/e2e/pagination.spec.ts" +} +``` + +**Output:** +``` +/Users/jhag/projects/babymonitor/frontend/tests/e2e/pagination.spec.ts +file + +1: import { test, expect } from '@playwright/test'; +2: +3: test.describe('Pagination', () => { +4: test.beforeEach(async ({ page }) => { +5: // Mock the sessions API to return multiple sessions +6: await page.route('**/api/sessions', async (route) => { +7: const mockSessions = Array.from({ length: 15 }, (_, i) => ({ +8: id: i + 1, +9: start_time: new Date(2024, 0, 1 + i).toISOString(), +10: end_time: new Date(2024, 0, 1 + i, 8, 0).toISOString(), +11: })); +12: await route.fulfill({ +13: status: 200, +14: contentType: 'application/json', +15: body: JSON.stringify({ sessions: mockSessions, total: 15 }), +16: }); +17: }); +18: +19: await page.goto('http://localhost:5173/stats'); +20: await page.waitForLoadState('networkidle'); +21: }); +22: +23: test('Pagination component is visible when sessions exceed page size', async ({ page }) => { +24: await page.screenshot({ path: 'screenshots/pagination_visible.png' }); +25: +26: // We look for the pagination container or buttons +27: const pagination = page.locator('nav').filter({ hasText: /Previous|Next/i }); +28: await expect(pagination).toBeVisible(); +29: +30: // Check if page 1 is active/displayed +31: await expect(page.getByText('1')).toBeVisible(); +32: }); +33: +34: test('Changing results per page in settings updates the list', async ({ page }) => { +35: await page.screenshot({ path: 'screenshots/before_settings_change.png' }); +36: +37: // Open Settings +38: await page.getByRole('button', { name: /Settings/i }).click(); +39: await expect(page.locator('text=Results per page')).toBeVisible(); +40: +41: // Change to 5 +42: await page.getByLabel(/Results per page/i).selectOption('5'); +43: await page.getByRole('button', { name: /Save/i }).click(); +44: await page.waitForTimeout(500); +45: +46: await page.screenshot({ path: 'screenshots/after_settings_change.png' }); +47: +48: // Verify sessions per page is 5 (only 5 should be visible) +49: const sessionRows = page.locator('div[class*="SessionList"] >> .border-b'); +50: await expect(sessionRows).toHaveCount(5); +51: +52: // Verify settings is saved in localStorage +53: const localStorage = await page.evaluate(() => { +54: return window.localStorage.getItem('baby-sleep-settings'); +55: }); +56: expect(localStorage).toContain('"sessionsPerPage":5'); +57: }); +58: +59: test('Navigating through pages works', async ({ page }) => { +60: // With 15 sessions and default (or mocked) size of 10, there should be 2 pages. +61: // Let's set it to 5 via settings first or just use the default if we can. +62: // Actually, let's change it to 5 via settings to be sure. +63: await page.getByRole('button', { name: /Settings/i }).click(); +64: await page.getByLabel(/Results per page/i).selectOption('5'); +65: await page.getByRole('button', { name: /Save/i }).click(); +66: await page.waitForTimeout(500); +67: +68: const nextButton = page.getByRole('button', { name: /Next/i }); +69: await expect(nextButton).toBeVisible(); +70: +71: await nextButton.click(); +72: await page.waitForTimeout(500); +73: +74: // Should now show sessions from index 5 to 9 (if total is 15) +75: // Let's check if the first visible session changed. +76: const sessions = page.locator('div[class*="SessionList"] >> .border-b'); +77: await expect(sessions).toHaveCount(5); +78: }); +79: }); +80: +81: }); +82: +83: await page.goto('http://localhost:5173/stats'); +84: await page.waitForLoadState('networkidle'); +85: }); +86: +87: test('Pagination component is visible when sessions exceed page size', async ({ page }) => { +88: await page.screenshot({ path: 'screenshots/pagination_visible.png' }); +89: +90: // We look for the pagination container or buttons +91: const pagination = page.locator('nav').filter({ hasText: /Previous|Next/i }); +92: await expect(pagination).toBeVisible(); +93: +94: // Check if page 1 is active/displayed +95: await expect(page.getByText('1')).toBeVisible(); +96: }); +97: +98: test('Changing results per page in settings updates the list', async ({ page }) => { +99: await page.screenshot({ path: 'screenshots/before_settings_change.png' }); +100: +101: // Open Settings +102: await page.getByRole('button', { name: /Settings/i }).click(); +103: await expect(page.locator('text=Results per page')).toBeVisible(); +104: +105: // Change to 5 +106: await page.getByLabel(/Results per page/i).selectOption('5'); +107: await page.getByRole('button', { name: /Save/i }).click(); +108: await page.waitForTimeout(500); +109: +110: await page.screenshot({ path: 'screenshots/after_settings_change.png' }); +111: +112: // Verify sessions per page is 5 (only 5 should be visible) +113: const sessionRows = page.locator('div[class*="SessionList"] >> .border-b'); +114: await expect(sessionRows).toHaveCount(5); +115: +116: // Verify settings is saved in localStorage +117: const localStorage = await page.evaluate(() => { +118: return window.localStorage.getItem('baby-sleep-settings'); +119: }); +120: expect(localStorage).toContain('"sessionsPerPage":5'); +121: }); +122: +123: test('Navigating through pages works', async ({ page }) => { +124: // With 15 sessions and default (or mocked) size of 10, there should be 2 pages. +125: // Let's set it to 5 via settings first or just use the default if we can. +126: // Actually, let's change it to 5 via settings to be sure. +127: await page.getByRole('button', { name: /Settings/i }).click(); +128: await page.getByLabel(/Results per page/i).selectOption('5'); +129: await page.getByRole('button', { name: /Save/i }).click(); +130: await page.waitForTimeout(500); +131: +132: const nextButton = page.getByRole('button', { name: /Next/i }); +133: await expect(nextButton).toBeVisible(); +134: +135: await nextButton.click(); +136: await page.waitForTimeout(500); +137: +138: // Should now show sessions from index 5 to 9 (if total is 15) +139: // Let's check if the first visible session changed. +140: const sessions = page.locator('div[class*="SessionList"] >> .border-b'); +141: await expect(sessions).toHaveCount(5); +142: }); +143: }); + +(End of file - total 143 lines) + +``` + +--- + +## Assistant (Build · Gemma 4 26b A4B · 44.1s) + +_Thinking:_ + + +It's a mess. I accidentally appended the old content to the new one due to how `edit` works (it seems it didn't replace properly or I provided a bad oldString). + +I will use `write` to completely overwrite the file with the correct content. + + +**Tool: write** + +**Input:** +```json +{ + "content": "import { test, expect } from '@playwright/test';\n\ntest.describe('Pagination', () => {\n test.beforeEach(async ({ page }) => {\n // Mock the sessions API to return multiple sessions\n await page.route('**/api/sessions', async (route) => {\n const mockSessions = Array.from({ length: 15 }, (_, i) => ({\n id: i + 1,\n start_time: new Date(2024, 0, 1 + i).toISOString(),\n end_time: new Date(2024, 0, 1 + i, 8, 0).toISOString(),\n }));\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({ sessions: mockSessions, total: 15 }),\n });\n });\n\n await page.goto('http://localhost:5173/stats');\n await page.waitForLoadState('networkidle');\n });\n\n test('Pagination component is visible when sessions exceed page size', async ({ page }) => {\n await page.screenshot({ path: 'screenshots/pagination_visible.png' });\n\n // We look for the pagination container or buttons\n const pagination = page.locator('nav').filter({ hasText: /Previous|Next/i });\n await expect(pagination).toBeVisible();\n\n // Check if page 1 is active/displayed\n await expect(page.getByText('1')).toBeVisible();\n });\n\n test('Changing results per page in settings updates the list', async ({ page }) => {\n await page.screenshot({ path: 'screenshots/before_settings_change.png' });\n\n // Open Settings\n await page.getByRole('button', { name: /Settings/i }).click();\n await expect(page.locator('text=Results per page')).toBeVisible();\n\n // Change to 5\n await page.getByLabel(/Results per page/i).selectOption('5');\n await page.getByRole('button', { name: /Save/i }).click();\n await page.waitForTimeout(500);\n\n await page.screenshot({ path: 'screenshots/after_settings_change.png' });\n\n // Verify sessions per page is 5 (only 5 should be visible)\n const sessionRows = page.locator('div[class*=\"SessionList\"] >> .border-b');\n await expect(sessionRows).toHaveCount(5);\n\n // Verify settings is saved in localStorage\n const localStorage = await page.evaluate(() => {\n return window.localStorage.getItem('baby-sleep-settings');\n });\n expect(localStorage).toContain('\"sessionsPerPage\":5');\n });\n\n test('Navigating through pages works', async ({ page }) => {\n // With 15 sessions and default (or mocked) size of 10, there should be 2 pages.\n // Let's set it to 5 via settings first or just use the default if we can.\n // Actually, let's change it to 5 via settings to be sure.\n await page.getByRole('button', { name: /Settings/i }).click();\n await page.getByLabel(/Results per page/i).selectOption('5');\n await page.getByRole('button', { name: /Save/i }).click();\n await page.waitForTimeout(500);\n\n const nextButton = page.getByRole('button', { name: /Next/i });\n await expect(nextButton).toBeVisible();\n\n await nextButton.click();\n await page.waitForTimeout(500);\n\n // Should now show sessions from index 5 to 9 (if total is 15)\n // Let's check if the first visible session changed.\n const sessions = page.locator('div[class*=\"SessionList\"] >> .border-b');\n await expect(sessions).toHaveCount(5);\n });\n});\n", + "filePath": "/Users/jhag/projects/babymonitor/frontend/tests/e2e/pagination.spec.ts" +} +``` + +**Output:** +``` +Wrote file successfully. +``` + +--- + +## Assistant (Build · Gemma 4 26b A4B · 17.8s) + +_Thinking:_ + + +Now run the tests again. + + +**Tool: bash** + +**Input:** +```json +{ + "command": "npx playwright test frontend/tests/e2e/pagination.spec.ts --reporter=list", + "description": "Run the fixed pagination tests with mocked API.", + "workdir": "/Users/jhag/projects/babymonitor/frontend" +} +``` + +**Output:** +``` + +Running 3 tests using 3 workers + + ✘ 1 [chromium] › tests/e2e/pagination.spec.ts:23:3 › Pagination › Pagination component is visible when sessions exceed page size (6.1s) + ✘ 3 [chromium] › tests/e2e/pagination.spec.ts:34:3 › Pagination › Changing results per page in settings updates the list (6.8s) + ✘ 2 [chromium] › tests/e2e/pagination.spec.ts:59:3 › Pagination › Navigating through pages works (7.4s) + + + 1) [chromium] › tests/e2e/pagination.spec.ts:23:3 › Pagination › Pagination component is visible when sessions exceed page size + + Error: expect(locator).toBeVisible() failed + + Locator: locator('nav').filter({ hasText: /Previous|Next/i }) + Expected: visible + Timeout: 5000ms + Error: element(s) not found + + Call log: +  - Expect "toBeVisible" with timeout 5000ms +  - waiting for locator('nav').filter({ hasText: /Previous|Next/i }) + + + 26 | // We look for the pagination container or buttons + 27 | const pagination = page.locator('nav').filter({ hasText: /Previous|Next/i }); + > 28 | await expect(pagination).toBeVisible(); + | ^ + 29 | + 30 | // Check if page 1 is active/displayed + 31 | await expect(page.getByText('1')).toBeVisible(); + at /Users/jhag/projects/babymonitor/frontend/tests/e2e/pagination.spec.ts:28:30 + + attachment #1: screenshot (image/png) ────────────────────────────────────────────────────────── + test-results/e2e-pagination-Pagination--1edf2-n-sessions-exceed-page-size-chromium/test-failed-1.png + ──────────────────────────────────────────────────────────────────────────────────────────────── + + Error Context: test-results/e2e-pagination-Pagination--1edf2-n-sessions-exceed-page-size-chromium/error-context.md + + 2) [chromium] › tests/e2e/pagination.spec.ts:34:3 › Pagination › Changing results per page in settings updates the list + + Error: expect(locator).toHaveCount(expected) failed + + Locator: locator('div[class*="SessionList"]').locator('.border-b') + Expected: 5 + Received: 0 + Timeout: 5000ms + + Call log: +  - Expect "toHaveCount" with timeout 5000ms +  - waiting for locator('div[class*="SessionList"]').locator('.border-b') +  9 × locator resolved to 0 elements +  - unexpected value "0" + + + 48 | // Verify sessions per page is 5 (only 5 should be visible) + 49 | const sessionRows = page.locator('div[class*="SessionList"] >> .border-b'); + > 50 | await expect(sessionRows).toHaveCount(5); + | ^ + 51 | + 52 | // Verify settings is saved in localStorage + 53 | const localStorage = await page.evaluate(() => { + at /Users/jhag/projects/babymonitor/frontend/tests/e2e/pagination.spec.ts:50:31 + + attachment #1: screenshot (image/png) ────────────────────────────────────────────────────────── + test-results/e2e-pagination-Pagination--d7b84-n-settings-updates-the-list-chromium/test-failed-1.png + ──────────────────────────────────────────────────────────────────────────────────────────────── + + Error Context: test-results/e2e-pagination-Pagination--d7b84-n-settings-updates-the-list-chromium/error-context.md + + 3) [chromium] › tests/e2e/pagination.spec.ts:59:3 › Pagination › Navigating through pages works ── + + Error: expect(locator).toHaveCount(expected) failed + + Locator: locator('div[class*="SessionList"]').locator('.border-b') + Expected: 5 + Received: 0 + Timeout: 5000ms + + Call log: +  - Expect "toHaveCount" with timeout 5000ms +  - waiting for locator('div[class*="SessionList"]').locator('.border-b') +  9 × locator resolved to 0 elements +  - unexpected value "0" + + + 75 | // Let's check if the first visible session changed. + 76 | const sessions = page.locator('div[class*="SessionList"] >> .border-b'); + > 77 | await expect(sessions).toHaveCount(5); + | ^ + 78 | }); + 79 | }); + 80 | + at /Users/jhag/projects/babymonitor/frontend/tests/e2e/pagination.spec.ts:77:28 + + attachment #1: screenshot (image/png) ────────────────────────────────────────────────────────── + test-results/e2e-pagination-Pagination-Navigating-through-pages-works-chromium/test-failed-1.png + ──────────────────────────────────────────────────────────────────────────────────────────────── + + Error Context: test-results/e2e-pagination-Pagination-Navigating-through-pages-works-chromium/error-context.md + + 3 failed + [chromium] › tests/e2e/pagination.spec.ts:23:3 › Pagination › Pagination component is visible when sessions exceed page size + [chromium] › tests/e2e/pagination.spec.ts:34:3 › Pagination › Changing results per page in settings updates the list + [chromium] › tests/e2e/pagination.spec.ts:59:3 › Pagination › Navigating through pages works ─── + +``` + +--- + +## Assistant (Build · Gemma 4 26b A4B · 102.7s) + +_Thinking:_ + + +The API mock is still not being applied correctly, or the requests are going to a different URL. +The `fetchSessions` in `client.ts` uses `${BASE_URL}/api/sessions`. +`BASE_URL` is `import.meta.env.VITE_API_BASE_URL || ''`. + +In the Playwright test, I mocked `**/api/sessions`. This should cover it if the URL is relative or starts with `/api/sessions`. + +Wait, look at the `SessionList.tsx` error in the logs: +`Received: 0 elements`. This means `sessions` array is empty. + +Let's check the browser console logs if possible, or just debug by adding `console.log` in the test (not great) or checking if there are any network errors. + +Wait, I'll check `frontend/src/api/client.ts` again to see how `BASE_URL` is resolved during tests. +If `VITE_API_BASE_URL` is not set, it's an empty string. So it fetches `/api/sessions`. +My mock `**/api/sessions` should match `http://localhost:5173/api/sessions`. + +Maybe the issue is that the dev server is not actually serving from `http://localhost:5173/api/sessions`? No, the backend is on `8000`. +The frontend dev server (Vite) usually proxies `/api` to the backend. + +If I'm running Playwright, it's using the `webServer` config which starts Vite on `5173`. + +Let's try to use a more specific route in the mock and check if it works. +Wait, I see `import { fetchSessions } from '../api/client';` in `useSleepSession.ts`. + +Let's try to inspect the network requests by adding a `page.on('request', ...)` in the test. +Actually, I'll just try to mock it more broadly or check if there are any other API calls. + +Wait, I see `page.route('**/api/sessions', ...)` in my test. +Is it possible the request is actually to `http://localhost:8000/api/sessions`? +The `client.ts` uses `BASE_URL`. If Vite proxy is working, the frontend calls its own origin and Vite proxies it. + +Let's try to mock `http://localhost:5173/api/sessions` directly. +And I'll also add a check for the sessions in the test to see what is being returned. + +Actually, let's look at the error again: +`locator('div[class*="SessionList"]').locator('.border-b')` received 0 elements. +This means `sessions` is indeed an empty array `[]`. + +Let's check if the mock is even being called. I can add a `console.log` in the route handler and check the terminal output if I run it with certain flags, but better yet, I'll just check `page.on('request')`. + +Wait! I might have missed that the frontend is running on `5173` but maybe it's not? The `playwright.config.ts` says `5173`. + +Let's try to change the mock pattern to something even broader like `**/sessions`. +And I will also make sure my mock includes the correct response structure. + +One more thing: `fetchSessions` in `client.ts`: +```typescript +export async function fetchSessions(): Promise { + const response = await fetch(`${BASE_URL}/api/sessions`); + if (!response.ok) throw new Error('Failed to fetch sessions'); + return response.json(); +} +``` + +Wait, I'll also try to see if `sessions` is being populated. +I will use `await page.evaluate(() => window.location.href)` in the test to be sure where we are. + +Let's rewrite the test one more time, with better debugging and a broader mock. + + +**Tool: edit** + +**Input:** +```json +{ + "filePath": "/Users/jhag/projects/babymonitor/frontend/tests/e2e/pagination.spec.ts", + "newString": "import { test, expect } from '@playwright/test';\n\ntest.describe('Pagination', () => {\n test.beforeEach(async ({ page }) => {\n // Monitor network requests to see if our mock is hit\n page.on('request', request => console.log('>>', request.method(), request.url()));\n page.on('response', response => console.log('<<', response.status(), response.url()));\n\n // Mock the sessions API with a broader pattern\n await page.route(/.*\\/api\\/sessions/, async (route) => {\n console.log('Intercepted request to:', route.request().url());\n const mockSessions = Array.from({ length: 15 }, (_, i) => ({\n id: i + 1,\n start_time: new Date(2024, 0, 1 + i).toISOString(),\n end_time: new Date(2024, 0, 1 + i, 8, 0).toISOString(),\n }));\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({ sessions: mockSessions, total: 15 }),\n });\n });\n\n await page.goto('http://localhost:5173/stats');\n // Wait for the sessions to actually appear in the DOM before proceeding\n await page.waitForSelector('div[class*=\"SessionList\"] >> .border-b', { timeout: 10000 });\n });\n\n test('Pagination component is visible when sessions exceed page size', async ({ page }) => {\n await page.screenshot({ path: 'screenshots/pagination_visible.png' });\n\n const pagination = page.locator('nav').filter({ hasText: /Previous|Next/i });\n await expect(pagination).toBeVisible();\n\n await expect(page.getByText('1')).toBeVisible();\n });\n\n test('Changing results per page in settings updates the list', async ({ page }) => {\n await page.screenshot({ path: 'screenshots/before_settings_change.png' });\n\n await page.getByRole('button', { name: /Settings/i }).click();\n await expect(page.locator('text=Results per page')).toBeVisible();\n\n await page.getByLabel(/Results per page/i).selectOption('5');\n await page.getByRole('button', { name: /Save/i }).click();\n await page.waitForTimeout(500);\n\n await page.screenshot({ path: 'screenshots/after_settings_change.png' });\n\n const sessionRows = page.locator('div[class*=\"SessionList\"] >> .border-b');\n await expect(sessionRows).toHaveCount(5);\n\n const localStorage = await page.evaluate(() => {\n return window.localStorage.getItem('baby-sleep-settings');\n });\n expect(localStorage).toContain('\"sessionsPerPage\":5');\n });\n\n test('Navigating through pages works', async ({ page }) => {\n await page.getByRole('button', { name: /Settings/i }).click();\n await page.getByLabel(/Results per page/i).selectOption('5');\n await page.getByRole('button', { name: /Save/i }).click();\n await page.waitForTimeout(500);\n\n const nextButton = page.getByRole('button', { name: /Next/i });\n await expect(nextButton).toBeVisible();\n\n await nextButton.click();\n await page.waitForTimeout(500);\n\n const sessions = page.locator('div[class*=\"SessionList\"] >> .border-b');\n await expect(sessions).toHaveCount(5);\n });\n});\n", + "oldString": "import { test, expect } from '@playwright/test';\n\ntest.describe('Pagination', () => {\n test.beforeEach(async ({ page }) => {\n // Mock the sessions API to return multiple sessions\n await page.route('**/api/sessions', async (route) => {\n const mockSessions = Array.from({ length: 15 }, (_, i) => ({\n id: i + 1,\n start_time: new Date(2024, 0, 1 + i).toISOString(),\n end_time: new Date(2024, 0, 1 + i, 8, 0).toISOString(),\n }));\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({ sessions: mockSessions, total: 15 }),\n });\n });\n\n await page.goto('http://localhost:5173/stats');\n await page.waitForLoadState('networkidle');\n });\n\n test('Pagination component is visible when sessions exceed page size', async ({ page }) => {\n await page.screenshot({ path: 'screenshots/pagination_visible.png' });\n\n // We look for the pagination container or buttons\n const pagination = page.locator('nav').filter({ hasText: /Previous|Next/i });\n await expect(pagination).toBeVisible();\n\n // Check if page 1 is active/displayed\n await expect(page.getByText('1')).toBeVisible();\n });\n\n test('Changing results per page in settings updates the list', async ({ page }) => {\n await page.screenshot({ path: 'screenshots/before_settings_change.png' });\n\n // Open Settings\n await page.getByRole('button', { name: /Settings/i }).click();\n await expect(page.locator('text=Results per page')).toBeVisible();\n\n // Change to 5\n await page.getByLabel(/Results per page/i).selectOption('5');\n await page.getByRole('button', { name: /Save/i }).click();\n await page.waitForTimeout(500);\n\n await page.screenshot({ path: 'screenshots/after_settings_change.png' });\n\n // Verify sessions per page is 5 (only 5 should be visible)\n const sessionRows = page.locator('div[class*=\"SessionList\"] >> .border-b');\n await expect(sessionRows).toHaveCount(5);\n\n // Verify settings is saved in localStorage\n const localStorage = await page.evaluate(() => {\n return window.localStorage.getItem('baby-sleep-settings');\n });\n expect(localStorage).toContain('\"sessionsPerPage\":5');\n });\n\n test('Navigating through pages works', async ({ page }) => {\n // With 15 sessions and default (or mocked) size of 10, there should be 2 pages.\n // Let's set it to 5 via settings first or just use the default if we can.\n // Actually, let's change it to 5 via settings to be sure.\n await page.getByRole('button', { name: /Settings/i }).click();\n await page.getByLabel(/Results per page/i).selectOption('5');\n await page.getByRole('button', { name: /Save/i }).click();\n await page.waitForTimeout(500);\n\n const nextButton = page.getByRole('button', { name: /Next/i });\n await expect(nextButton).toBeVisible();\n\n await nextButton.click();\n await page.waitForTimeout(500);\n\n // Should now show sessions from index 5 to 9 (if total is 15)\n // Let's check if the first visible session changed.\n const sessions = page.locator('div[class*=\"SessionList\"] >> .border-b');\n await expect(sessions).toHaveCount(5);\n });\n});\n" +} +``` + +**Output:** +``` +Edit applied successfully. +``` + +--- + +## Assistant (Build · Gemma 4 26b A4B · 29.0s) + +_Thinking:_ + + +Now run the tests again. + + +**Tool: bash** + +**Input:** +```json +{ + "command": "npx playwright test frontend/tests/e2e/pagination.spec.ts --reporter=list", + "description": "Run the fixed pagination tests with broader mock and debugging.", + "workdir": "/Users/jhag/projects/babymonitor/frontend" +} +``` + +**Output:** +``` + +Running 3 tests using 3 workers + +>> GET http://localhost:5173/stats +>> GET http://localhost:5173/stats +>> GET http://localhost:5173/stats +<< 200 http://localhost:5173/stats +<< 200 http://localhost:5173/stats +<< 200 http://localhost:5173/stats +>> GET http://localhost:5173/@vite/client +>> GET http://localhost:5173/src/main.tsx?t=1780218805882 +>> GET http://localhost:5173/@vite/client +>> GET http://localhost:5173/src/main.tsx?t=1780218805882 +>> GET http://localhost:5173/@vite/client +>> GET http://localhost:5173/src/main.tsx?t=1780218805882 +>> GET http://localhost:5173/@react-refresh +>> GET http://localhost:5173/@react-refresh +>> GET http://localhost:5173/@react-refresh +<< 200 http://localhost:5173/@vite/client +>> GET http://localhost:5173/node_modules/vite/dist/client/env.mjs +<< 200 http://localhost:5173/@vite/client +>> GET http://localhost:5173/node_modules/vite/dist/client/env.mjs +<< 200 http://localhost:5173/node_modules/vite/dist/client/env.mjs +<< 200 http://localhost:5173/node_modules/vite/dist/client/env.mjs +<< 200 http://localhost:5173/@vite/client +<< 200 http://localhost:5173/src/main.tsx?t=1780218805882 +>> GET http://localhost:5173/node_modules/.vite/deps/react.js?v=a0c06352 +>> GET http://localhost:5173/node_modules/vite/dist/client/env.mjs +<< 200 http://localhost:5173/src/main.tsx?t=1780218805882 +>> GET http://localhost:5173/node_modules/.vite/deps/react.js?v=a0c06352 +>> GET http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=a0c06352 +>> GET http://localhost:5173/src/index.css?t=1780218805882 +>> GET http://localhost:5173/src/App.tsx?t=1780218805882 +>> GET http://localhost:5173/node_modules/.vite/deps/@tanstack_react-query.js?v=a0c06352 +>> GET http://localhost:5173/node_modules/.vite/deps/react_jsx-dev-runtime.js?v=a0c06352 +>> GET http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=a0c06352 +>> GET http://localhost:5173/src/index.css?t=1780218805882 +>> GET http://localhost:5173/src/App.tsx?t=1780218805882 +>> GET http://localhost:5173/node_modules/.vite/deps/@tanstack_react-query.js?v=a0c06352 +>> GET http://localhost:5173/node_modules/.vite/deps/react_jsx-dev-runtime.js?v=a0c06352 +<< 200 http://localhost:5173/src/main.tsx?t=1780218805882 +<< 200 http://localhost:5173/node_modules/vite/dist/client/env.mjs +>> GET http://localhost:5173/node_modules/.vite/deps/react.js?v=a0c06352 +<< 200 http://localhost:5173/node_modules/.vite/deps/react.js?v=a0c06352 +>> GET http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=a0c06352 +>> GET http://localhost:5173/src/index.css?t=1780218805882 +>> GET http://localhost:5173/src/App.tsx?t=1780218805882 +>> GET http://localhost:5173/node_modules/.vite/deps/react-BDWiK9rz.js?v=a0c06352 +>> GET http://localhost:5173/node_modules/.vite/deps/@tanstack_react-query.js?v=a0c06352 +>> GET http://localhost:5173/node_modules/.vite/deps/react_jsx-dev-runtime.js?v=a0c06352 +<< 200 http://localhost:5173/@react-refresh +<< 200 http://localhost:5173/node_modules/.vite/deps/react.js?v=a0c06352 +>> GET http://localhost:5173/node_modules/.vite/deps/react-BDWiK9rz.js?v=a0c06352 +<< 200 http://localhost:5173/@react-refresh +<< 200 http://localhost:5173/node_modules/.vite/deps/@tanstack_react-query.js?v=a0c06352 +>> GET http://localhost:5173/node_modules/.vite/deps/react_jsx-runtime.js?v=a0c06352 +<< 200 http://localhost:5173/node_modules/.vite/deps/react.js?v=a0c06352 +<< 200 http://localhost:5173/node_modules/.vite/deps/react_jsx-dev-runtime.js?v=a0c06352 +<< 200 http://localhost:5173/@react-refresh +>> GET http://localhost:5173/node_modules/.vite/deps/react-BDWiK9rz.js?v=a0c06352 +<< 200 http://localhost:5173/src/index.css?t=1780218805882 +<< 200 http://localhost:5173/node_modules/.vite/deps/react-BDWiK9rz.js?v=a0c06352 +<< 200 http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=a0c06352 +<< 200 http://localhost:5173/node_modules/.vite/deps/react-BDWiK9rz.js?v=a0c06352 +>> GET http://localhost:5173/node_modules/.vite/deps/react-dom.js?v=a0c06352 +<< 200 http://localhost:5173/src/index.css?t=1780218805882 +<< 200 http://localhost:5173/node_modules/.vite/deps/react_jsx-runtime.js?v=a0c06352 +<< 200 http://localhost:5173/src/App.tsx?t=1780218805882 +>> GET http://localhost:5173/node_modules/.vite/deps/react-router-dom.js?v=a0c06352 +>> GET http://localhost:5173/src/components/tracker/TrackerPage.tsx?t=1780161383326 +>> GET http://localhost:5173/src/components/stats/StatsPage.tsx?t=1780218805882 +>> GET http://localhost:5173/src/components/Navbar.tsx +>> GET http://localhost:5173/src/components/Playground.tsx?t=1780218805882 +<< 200 http://localhost:5173/node_modules/.vite/deps/react-dom.js?v=a0c06352 +<< 200 http://localhost:5173/node_modules/.vite/deps/react-BDWiK9rz.js?v=a0c06352 +<< 200 http://localhost:5173/node_modules/.vite/deps/@tanstack_react-query.js?v=a0c06352 +>> GET http://localhost:5173/node_modules/.vite/deps/react_jsx-runtime.js?v=a0c06352 +<< 200 http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=a0c06352 +<< 200 http://localhost:5173/src/App.tsx?t=1780218805882 +<< 200 http://localhost:5173/src/index.css?t=1780218805882 +>> GET http://localhost:5173/node_modules/.vite/deps/react-dom.js?v=a0c06352 +>> GET http://localhost:5173/node_modules/.vite/deps/react-router-dom.js?v=a0c06352 +>> GET http://localhost:5173/src/components/tracker/TrackerPage.tsx?t=1780161383326 +>> GET http://localhost:5173/src/components/stats/StatsPage.tsx?t=1780218805882 +>> GET http://localhost:5173/src/components/Navbar.tsx +>> GET http://localhost:5173/src/components/Playground.tsx?t=1780218805882 +<< 200 http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=a0c06352 +>> GET http://localhost:5173/node_modules/.vite/deps/react-dom.js?v=a0c06352 +<< 200 http://localhost:5173/src/App.tsx?t=1780218805882 +<< 200 http://localhost:5173/node_modules/.vite/deps/react-dom.js?v=a0c06352 +<< 200 http://localhost:5173/src/components/tracker/TrackerPage.tsx?t=1780161383326 +>> GET http://localhost:5173/node_modules/.vite/deps/react-router-dom.js?v=a0c06352 +>> GET http://localhost:5173/src/components/tracker/TrackerPage.tsx?t=1780161383326 +>> GET http://localhost:5173/src/components/stats/StatsPage.tsx?t=1780218805882 +>> GET http://localhost:5173/src/components/Navbar.tsx +>> GET http://localhost:5173/src/components/Playground.tsx?t=1780218805882 +<< 200 http://localhost:5173/src/components/tracker/TrackerPage.tsx?t=1780161383326 +>> GET http://localhost:5173/src/components/tracker/StatusBanner.tsx?t=1780161383326 +>> GET http://localhost:5173/src/components/tracker/SleepButton.tsx?t=1780161383326 +<< 200 http://localhost:5173/node_modules/.vite/deps/react-dom.js?v=a0c06352 +<< 200 http://localhost:5173/src/components/stats/StatsPage.tsx?t=1780218805882 +>> GET http://localhost:5173/src/components/tracker/StatusBanner.tsx?t=1780161383326 +>> GET http://localhost:5173/src/components/tracker/SleepButton.tsx?t=1780161383326 +>> GET http://localhost:5173/src/components/tracker/TimeAdjuster.tsx?t=1780160413016 +<< 200 http://localhost:5173/src/components/stats/StatsPage.tsx?t=1780218805882 +<< 200 http://localhost:5173/src/components/tracker/TrackerPage.tsx?t=1780161383326 +>> GET http://localhost:5173/src/components/stats/IntervalPicker.tsx +<< 200 http://localhost:5173/src/components/Navbar.tsx +<< 200 http://localhost:5173/src/components/Navbar.tsx +>> GET http://localhost:5173/src/components/stats/SettingsPopover.tsx?t=1780218145201 +>> GET http://localhost:5173/src/components/stats/SummaryCards.tsx?t=1780210381087 +>> GET http://localhost:5173/src/components/stats/DailySummary.tsx?t=1780161394841 +>> GET http://localhost:5173/src/components/stats/SleepTimetable.tsx?t=1780161394841 +>> GET http://localhost:5173/src/components/stats/WakeupPlot.tsx?t=1780209705683 +>> GET http://localhost:5173/src/components/stats/BedtimePlot.tsx?t=1780209713826 +>> GET http://localhost:5173/src/components/stats/SessionList.tsx?t=1780218805882 +>> GET http://localhost:5173/src/components/ErrorBoundary.tsx +>> GET http://localhost:5173/src/hooks/useSettings.ts?t=1780218037679 +>> GET http://localhost:5173/src/hooks/useStats.ts?t=1780161394841 +<< 200 http://localhost:5173/src/components/Playground.tsx?t=1780218805882 +<< 200 http://localhost:5173/node_modules/.vite/deps/react_jsx-runtime.js?v=a0c06352 +<< 200 http://localhost:5173/src/components/stats/StatsPage.tsx?t=1780218805882 +>> GET http://localhost:5173/src/components/tracker/TimeAdjuster.tsx?t=1780160413016 +>> GET http://localhost:5173/src/components/stats/IntervalPicker.tsx +>> GET http://localhost:5173/src/components/stats/SettingsPopover.tsx?t=1780218145201 +>> GET http://localhost:5173/src/components/stats/SummaryCards.tsx?t=1780210381087 +>> GET http://localhost:5173/src/components/stats/DailySummary.tsx?t=1780161394841 +>> GET http://localhost:5173/src/components/stats/SleepTimetable.tsx?t=1780161394841 +>> GET http://localhost:5173/src/components/stats/WakeupPlot.tsx?t=1780209705683 +>> GET http://localhost:5173/src/components/stats/IntervalPicker.tsx +>> GET http://localhost:5173/src/components/stats/BedtimePlot.tsx?t=1780209713826 +>> GET http://localhost:5173/src/components/stats/SessionList.tsx?t=1780218805882 +>> GET http://localhost:5173/src/components/stats/SettingsPopover.tsx?t=1780218145201 +>> GET http://localhost:5173/src/components/stats/SummaryCards.tsx?t=1780210381087 +>> GET http://localhost:5173/src/components/ErrorBoundary.tsx +>> GET http://localhost:5173/src/hooks/useSettings.ts?t=1780218037679 +>> GET http://localhost:5173/src/components/stats/DailySummary.tsx?t=1780161394841 +>> GET http://localhost:5173/src/components/stats/SleepTimetable.tsx?t=1780161394841 +>> GET http://localhost:5173/src/components/stats/WakeupPlot.tsx?t=1780209705683 +>> GET http://localhost:5173/src/components/stats/BedtimePlot.tsx?t=1780209713826 +>> GET http://localhost:5173/src/components/stats/SessionList.tsx?t=1780218805882 +>> GET http://localhost:5173/src/components/ErrorBoundary.tsx +>> GET http://localhost:5173/src/hooks/useSettings.ts?t=1780218037679 +>> GET http://localhost:5173/src/hooks/useStats.ts?t=1780161394841 +<< 200 http://localhost:5173/src/components/tracker/StatusBanner.tsx?t=1780161383326 +>> GET http://localhost:5173/src/hooks/useSleepSession.ts?t=1780161383326 +>> GET http://localhost:5173/src/utils/time.ts?t=1780152909168 +<< 200 http://localhost:5173/node_modules/.vite/deps/react-router-dom.js?v=a0c06352 +<< 200 http://localhost:5173/src/components/stats/IntervalPicker.tsx +>> GET http://localhost:5173/src/hooks/useStats.ts?t=1780161394841 +>> GET http://localhost:5173/src/components/tracker/StatusBanner.tsx?t=1780161383326 +>> GET http://localhost:5173/src/components/tracker/SleepButton.tsx?t=1780161383326 +>> GET http://localhost:5173/src/components/tracker/TimeAdjuster.tsx?t=1780160413016 +<< 200 http://localhost:5173/src/components/tracker/SleepButton.tsx?t=1780161383326 +<< 200 http://localhost:5173/src/components/tracker/TimeAdjuster.tsx?t=1780160413016 +<< 200 http://localhost:5173/node_modules/.vite/deps/@tanstack_react-query.js?v=a0c06352 +>> GET http://localhost:5173/node_modules/.vite/deps/react_jsx-runtime.js?v=a0c06352 +<< 200 http://localhost:5173/node_modules/.vite/deps/react-router-dom.js?v=a0c06352 +<< 200 http://localhost:5173/src/components/Playground.tsx?t=1780218805882 +<< 200 http://localhost:5173/src/components/Navbar.tsx +<< 200 http://localhost:5173/src/components/Playground.tsx?t=1780218805882 +<< 200 http://localhost:5173/node_modules/.vite/deps/react-router-dom.js?v=a0c06352 +<< 200 http://localhost:5173/src/components/stats/SettingsPopover.tsx?t=1780218145201 +<< 200 http://localhost:5173/node_modules/.vite/deps/react_jsx-dev-runtime.js?v=a0c06352 +<< 200 http://localhost:5173/src/components/tracker/SleepButton.tsx?t=1780161383326 +<< 200 http://localhost:5173/src/components/tracker/StatusBanner.tsx?t=1780161383326 +<< 200 http://localhost:5173/src/components/stats/SummaryCards.tsx?t=1780210381087 +>> GET http://localhost:5173/src/hooks/useSleepSession.ts?t=1780161383326 +>> GET http://localhost:5173/src/utils/time.ts?t=1780152909168 +<< 200 http://localhost:5173/node_modules/.vite/deps/react_jsx-dev-runtime.js?v=a0c06352 +>> GET http://localhost:5173/node_modules/.vite/deps/lucide-react.js?v=a0c06352 +<< 200 http://localhost:5173/src/components/stats/DailySummary.tsx?t=1780161394841 +<< 200 http://localhost:5173/src/components/stats/SettingsPopover.tsx?t=1780218145201 +>> GET http://localhost:5173/node_modules/.vite/deps/recharts.js?v=a0c06352 +<< 200 http://localhost:5173/src/components/stats/IntervalPicker.tsx +<< 200 http://localhost:5173/src/components/stats/SleepTimetable.tsx?t=1780161394841 +<< 200 http://localhost:5173/src/components/stats/IntervalPicker.tsx +<< 200 http://localhost:5173/src/components/stats/WakeupPlot.tsx?t=1780209705683 +<< 200 http://localhost:5173/src/components/stats/BedtimePlot.tsx?t=1780209713826 +<< 200 http://localhost:5173/src/components/stats/SessionList.tsx?t=1780218805882 +<< 200 http://localhost:5173/src/components/tracker/TimeAdjuster.tsx?t=1780160413016 +<< 200 http://localhost:5173/src/hooks/useSettings.ts?t=1780218037679 +<< 200 http://localhost:5173/src/components/stats/SummaryCards.tsx?t=1780210381087 +<< 200 http://localhost:5173/src/components/ErrorBoundary.tsx +>> GET http://localhost:5173/src/components/stats/Pagination.tsx +<< 200 http://localhost:5173/src/components/stats/SettingsPopover.tsx?t=1780218145201 +<< 200 http://localhost:5173/src/components/stats/SummaryCards.tsx?t=1780210381087 +>> GET http://localhost:5173/node_modules/.vite/deps/lucide-react.js?v=a0c06352 +<< 200 http://localhost:5173/src/components/stats/DailySummary.tsx?t=1780161394841 +<< 200 http://localhost:5173/src/components/stats/SleepTimetable.tsx?t=1780161394841 +>> GET http://localhost:5173/node_modules/.vite/deps/recharts.js?v=a0c06352 +<< 200 http://localhost:5173/src/components/stats/BedtimePlot.tsx?t=1780209713826 +<< 200 http://localhost:5173/src/components/stats/WakeupPlot.tsx?t=1780209705683 +<< 200 http://localhost:5173/src/components/stats/DailySummary.tsx?t=1780161394841 +>> GET http://localhost:5173/node_modules/.vite/deps/lucide-react.js?v=a0c06352 +>> GET http://localhost:5173/src/utils/time.ts?t=1780152909168 +>> GET http://localhost:5173/node_modules/.vite/deps/recharts.js?v=a0c06352 +<< 200 http://localhost:5173/src/components/stats/SleepTimetable.tsx?t=1780161394841 +<< 200 http://localhost:5173/src/components/stats/BedtimePlot.tsx?t=1780209713826 +<< 200 http://localhost:5173/src/hooks/useStats.ts?t=1780161394841 +<< 200 http://localhost:5173/src/hooks/useSleepSession.ts?t=1780161383326 +<< 200 http://localhost:5173/src/utils/time.ts?t=1780152909168 +>> GET http://localhost:5173/src/api/client.ts?t=1780161383326 +<< 200 http://localhost:5173/src/components/stats/Pagination.tsx +<< 200 http://localhost:5173/src/components/stats/SessionList.tsx?t=1780218805882 +<< 200 http://localhost:5173/src/components/ErrorBoundary.tsx +>> GET http://localhost:5173/src/components/stats/Pagination.tsx +<< 200 http://localhost:5173/src/components/stats/SessionList.tsx?t=1780218805882 +>> GET http://localhost:5173/src/hooks/useSleepSession.ts?t=1780161383326 +>> GET http://localhost:5173/src/components/stats/Pagination.tsx +<< 200 http://localhost:5173/src/components/ErrorBoundary.tsx +<< 200 http://localhost:5173/src/hooks/useSettings.ts?t=1780218037679 +<< 200 http://localhost:5173/src/hooks/useStats.ts?t=1780161394841 +>> GET http://localhost:5173/src/api/client.ts?t=1780161383326 +<< 200 http://localhost:5173/src/api/client.ts?t=1780161383326 +<< 200 http://localhost:5173/src/hooks/useSettings.ts?t=1780218037679 +<< 200 http://localhost:5173/src/hooks/useStats.ts?t=1780161394841 +>> GET http://localhost:5173/src/api/client.ts?t=1780161383326 +<< 200 http://localhost:5173/node_modules/.vite/deps/react_jsx-runtime.js?v=a0c06352 +<< 200 http://localhost:5173/src/hooks/useSleepSession.ts?t=1780161383326 +<< 200 http://localhost:5173/src/utils/time.ts?t=1780152909168 +<< 200 http://localhost:5173/src/components/stats/WakeupPlot.tsx?t=1780209705683 +<< 200 http://localhost:5173/src/components/tracker/StatusBanner.tsx?t=1780161383326 +<< 200 http://localhost:5173/src/components/tracker/SleepButton.tsx?t=1780161383326 +<< 200 http://localhost:5173/src/components/stats/Pagination.tsx +<< 200 http://localhost:5173/src/components/tracker/TimeAdjuster.tsx?t=1780160413016 +<< 200 http://localhost:5173/src/utils/time.ts?t=1780152909168 +<< 200 http://localhost:5173/src/hooks/useSleepSession.ts?t=1780161383326 +<< 200 http://localhost:5173/src/api/client.ts?t=1780161383326 +<< 200 http://localhost:5173/src/api/client.ts?t=1780161383326 +<< 200 http://localhost:5173/src/components/stats/Pagination.tsx +<< 200 http://localhost:5173/node_modules/.vite/deps/lucide-react.js?v=a0c06352 +<< 200 http://localhost:5173/node_modules/.vite/deps/recharts.js?v=a0c06352 +<< 200 http://localhost:5173/node_modules/.vite/deps/lucide-react.js?v=a0c06352 +<< 200 http://localhost:5173/node_modules/.vite/deps/lucide-react.js?v=a0c06352 +<< 200 http://localhost:5173/node_modules/.vite/deps/recharts.js?v=a0c06352 +<< 200 http://localhost:5173/node_modules/.vite/deps/recharts.js?v=a0c06352 +>> GET http://localhost:5173/api/stats/daily-summary?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z&day_start=07%3A00&day_end=20%3A00 +>> GET http://localhost:5173/api/stats/timetable?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z&day_start=07%3A00&day_end=20%3A00 +>> GET http://localhost:5173/api/stats/wake-sleep-times?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z&wakeup_ref=02%3A00&bedtime_ref=17%3A00 +>> GET http://localhost:5173/api/sessions +>> GET http://localhost:5173/api/stats/daily-summary?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z&day_start=07%3A00&day_end=20%3A00 +>> GET http://localhost:5173/api/stats/timetable?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z&day_start=07%3A00&day_end=20%3A00 +>> GET http://localhost:5173/api/stats/wake-sleep-times?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z&wakeup_ref=02%3A00&bedtime_ref=17%3A00 +>> GET http://localhost:5173/api/sessions +>> GET http://localhost:5173/api/stats/daily-summary?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z&day_start=07%3A00&day_end=20%3A00 +>> GET http://localhost:5173/api/stats/timetable?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z&day_start=07%3A00&day_end=20%3A00 +>> GET http://localhost:5173/api/stats/wake-sleep-times?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z&wakeup_ref=02%3A00&bedtime_ref=17%3A00 +>> GET http://localhost:5173/api/sessions +Intercepted request to: http://localhost:5173/api/sessions +Intercepted request to: http://localhost:5173/api/sessions +Intercepted request to: http://localhost:5173/api/sessions +>> GET http://localhost:5173/api/stats/summary?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z +>> GET http://localhost:5173/api/stats/summary?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z +>> GET http://localhost:5173/api/stats/summary?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z +<< 200 http://localhost:5173/api/sessions +<< 200 http://localhost:5173/api/sessions +<< 200 http://localhost:5173/api/sessions +<< 200 http://localhost:5173/api/stats/timetable?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z&day_start=07%3A00&day_end=20%3A00 +<< 200 http://localhost:5173/api/stats/daily-summary?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z&day_start=07%3A00&day_end=20%3A00 +<< 200 http://localhost:5173/api/stats/daily-summary?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z&day_start=07%3A00&day_end=20%3A00 +<< 200 http://localhost:5173/api/stats/wake-sleep-times?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z&wakeup_ref=02%3A00&bedtime_ref=17%3A00 +<< 200 http://localhost:5173/api/stats/daily-summary?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z&day_start=07%3A00&day_end=20%3A00 +<< 200 http://localhost:5173/api/stats/timetable?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z&day_start=07%3A00&day_end=20%3A00 +<< 200 http://localhost:5173/api/stats/wake-sleep-times?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z&wakeup_ref=02%3A00&bedtime_ref=17%3A00 +<< 200 http://localhost:5173/api/stats/timetable?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z&day_start=07%3A00&day_end=20%3A00 +<< 200 http://localhost:5173/api/stats/wake-sleep-times?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z&wakeup_ref=02%3A00&bedtime_ref=17%3A00 +<< 200 http://localhost:5173/api/stats/summary?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z +<< 200 http://localhost:5173/api/stats/summary?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z +<< 200 http://localhost:5173/api/stats/summary?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z + ✘ 3 [chromium] › tests/e2e/pagination.spec.ts:38:3 › Pagination › Changing results per page in settings updates the list (10.5s) + ✘ 1 [chromium] › tests/e2e/pagination.spec.ts:59:3 › Pagination › Navigating through pages works (10.5s) + ✘ 2 [chromium] › tests/e2e/pagination.spec.ts:29:3 › Pagination › Pagination component is visible when sessions exceed page size (10.5s) + + + 1) [chromium] › tests/e2e/pagination.spec.ts:29:3 › Pagination › Pagination component is visible when sessions exceed page size + + TimeoutError: page.waitForSelector: Timeout 10000ms exceeded. + Call log: +  - waiting for locator('div[class*="SessionList"]').locator('.border-b') to be visible + + + 24 | await page.goto('http://localhost:5173/stats'); + 25 | // Wait for the sessions to actually appear in the DOM before proceeding + > 26 | await page.waitForSelector('div[class*="SessionList"] >> .border-b', { timeout: 10000 }); + | ^ + 27 | }); + 28 | + 29 | test('Pagination component is visible when sessions exceed page size', async ({ page }) => { + at /Users/jhag/projects/babymonitor/frontend/tests/e2e/pagination.spec.ts:26:16 + + attachment #1: screenshot (image/png) ────────────────────────────────────────────────────────── + test-results/e2e-pagination-Pagination--1edf2-n-sessions-exceed-page-size-chromium/test-failed-1.png + ──────────────────────────────────────────────────────────────────────────────────────────────── + + Error Context: test-results/e2e-pagination-Pagination--1edf2-n-sessions-exceed-page-size-chromium/error-context.md + + 2) [chromium] › tests/e2e/pagination.spec.ts:38:3 › Pagination › Changing results per page in settings updates the list + + TimeoutError: page.waitForSelector: Timeout 10000ms exceeded. + Call log: +  - waiting for locator('div[class*="SessionList"]').locator('.border-b') to be visible + + + 24 | await page.goto('http://localhost:5173/stats'); + 25 | // Wait for the sessions to actually appear in the DOM before proceeding + > 26 | await page.waitForSelector('div[class*="SessionList"] >> .border-b', { timeout: 10000 }); + | ^ + 27 | }); + 28 | + 29 | test('Pagination component is visible when sessions exceed page size', async ({ page }) => { + at /Users/jhag/projects/babymonitor/frontend/tests/e2e/pagination.spec.ts:26:16 + + attachment #1: screenshot (image/png) ────────────────────────────────────────────────────────── + test-results/e2e-pagination-Pagination--d7b84-n-settings-updates-the-list-chromium/test-failed-1.png + ──────────────────────────────────────────────────────────────────────────────────────────────── + + Error Context: test-results/e2e-pagination-Pagination--d7b84-n-settings-updates-the-list-chromium/error-context.md + + 3) [chromium] › tests/e2e/pagination.spec.ts:59:3 › Pagination › Navigating through pages works ── + + TimeoutError: page.waitForSelector: Timeout 10000ms exceeded. + Call log: +  - waiting for locator('div[class*="SessionList"]').locator('.border-b') to be visible + + + 24 | await page.goto('http://localhost:5173/stats'); + 25 | // Wait for the sessions to actually appear in the DOM before proceeding + > 26 | await page.waitForSelector('div[class*="SessionList"] >> .border-b', { timeout: 10000 }); + | ^ + 27 | }); + 28 | + 29 | test('Pagination component is visible when sessions exceed page size', async ({ page }) => { + at /Users/jhag/projects/babymonitor/frontend/tests/e2e/pagination.spec.ts:26:16 + + attachment #1: screenshot (image/png) ────────────────────────────────────────────────────────── + test-results/e2e-pagination-Pagination-Navigating-through-pages-works-chromium/test-failed-1.png + ──────────────────────────────────────────────────────────────────────────────────────────────── + + Error Context: test-results/e2e-pagination-Pagination-Navigating-through-pages-works-chromium/error-context.md + + 3 failed + [chromium] › tests/e2e/pagination.spec.ts:29:3 › Pagination › Pagination component is visible when sessions exceed page size + [chromium] › tests/e2e/pagination.spec.ts:38:3 › Pagination › Changing results per page in settings updates the list + [chromium] › tests/e2e/pagination.spec.ts:59:3 › Pagination › Navigating through pages works ─── + +``` + +--- + +## Assistant (Build · Gemma 4 26b A4B · 157.6s) + +_Thinking:_ + + +Wait, the `waitForSelector` failed with a timeout. It means `.border-b` was not found inside `div[class*="SessionList"]`. + +Let's look at `SessionList.tsx` again to see how it renders the list items. + +```typescript +58: {sessions.map((s) => ( +59:
+``` + +The class is `border-b border-gray-100 last:border-0 py-3 sm:py-2`. +My selector `div[class*="SessionList"] >> .border-b` should work. + +HOWEVER, I see this in the logs: +``` +>> GET http://localhost:5173/api/sessions +Intercepted request to: http://localhost:5173/api/sessions +``` +The interception worked. + +Wait! I see in the logs: +``` +>> GET http://localhost:5173/stats +<< 200 http://localhost:5173/stats +``` +It's making requests to `http://localhost:5173/stats`. + +Let me check if the elements are actually there by using a simpler selector or just checking what's rendered. + +Actually, I think the issue might be that `sessions` is still empty because of some race condition or something else in the frontend. + +Wait, I see `sessions: sessionsQuery.data?.sessions || []` in `useSessions`. +If the API returns `{ sessions: [...], total: 15 }`, then `sessions` should be populated. + +Let me try to debug by adding a check in the test for `sessionsQuery.data`. +Wait, I can't easily do that from Playwright without exposing it to `window`. + +Let's try to find the elements using a different way. Maybe the class name isn't what I think it is due to Tailwind being processed? +Actually, in the code it is `border-b`. + +Let's try to use a more direct selector in the test. +Instead of `div[class*="SessionList"] >> .border-b`, let's try to find the text of one of the mocked sessions. +Mocked session start time: `2024-01-01T00:00:00.000Z`. +In `SessionList.tsx`: +```typescript +109: {formatDateTime(s.start_time)} +``` + +I'll add a test that checks for the presence of one of these dates. This will tell me if the data is actually there. + + +**Tool: edit** + +**Input:** +```json +{ + "filePath": "/Users/jhag/projects/babymonitor/frontend/tests/e2e/pagination.spec.ts", + "newString": "import { test, expect } from '@playwright/test';\n\ntest.describe('Pagination', () => {\n test.beforeEach(async ({ page }) => {\n // Mock the sessions API to return multiple sessions\n await page.route('**/api/sessions', async (route) => {\n const mockSessions = Array.from({ length: 15 }, (_, i) => ({\n id: i + 1,\n start_time: new Date(2024, 0, 1 + i).toISOString(),\n end_time: new Date(2024, 0, 1 + i, 8, 0).toISOString(),\n }));\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({ sessions: mockSessions, total: 15 }),\n });\n });\n\n await page.goto('http://localhost:5173/stats');\n });\n\n test('Pagination component is visible when sessions exceed page size', async ({ page }) => {\n // Wait for one of the mocked dates to appear to ensure data is loaded\n await expect(page.locator('body')).toContainText('Jan 1, 2024', { timeout: 10000 });\n await page.screenshot({ path: 'screenshots/pagination_visible.png' });\n\n const pagination = page.locator('nav').filter({ hasText: /Previous|Next/i });\n await expect(pagination).toBeVisible();\n\n await expect(page.getByText('1')).toBeVisible();\n });\n\n test('Changing results per page in settings updates the list', async ({ page }) => {\n // Wait for data to load first\n await expect(page.locator('body')).toContainText('Jan 1, 2024', { timeout: 10000 });\n await page.screenshot({ path: 'screenshots/before_settings_change.png' });\n\n // Open Settings\n await page.getByRole('button', { name: /Settings/i }).click();\n await expect(page.locator('text=Results per page')).toBeVisible();\n\n // Change to 5\n await page.getByLabel(/Results per page/i).selectOption('5');\n await page.getByRole('button', { name: /Save/i }).click();\n await page.waitForTimeout(500);\n\n await page.screenshot({ path: 'screenshots/after_settings_change.png' });\n\n // Verify sessions per page is 5 (only 5 should be visible)\n const sessionRows = page.locator('div[class*=\"SessionList\"] >> .border-b');\n await expect(sessionRows).toHaveCount(5);\n\n // Verify settings is saved in localStorage\n const localStorage = await page.evaluate(() => {\n return window.localStorage.getItem('baby-sleep-settings');\n });\n expect(localStorage).toContain('\"sessionsPerPage\":5');\n });\n\n test('Navigating through pages works', async ({ page }) => {\n // Wait for data to load first\n await expect(page.locator('body')).toContainText('Jan 1, 2024', { timeout: 10000 });\n \n // Set to 5 via settings\n await page.getByRole('button', { name: /Settings/i }).click();\n await page.getByLabel(/Results per page/i).selectOption('5');\n await page.getByRole('button', { name: /Save/i }).click();\n await page.waitForTimeout(500);\n\n const nextButton = page.getByRole('button', { name: /Next/i });\n await expect(nextButton).toBeVisible();\n\n await nextButton.click();\n await page.waitForTimeout(500);\n\n // Check if we see the next set of sessions (starting from Jan 6, 2024)\n await expect(page.getByText('Jan 6, 2024')).toBeVisible();\n });\n});\n", + "oldString": "import { test, expect } from '@playwright/test';\n\ntest.describe('Pagination', () => {\n test.beforeEach(async ({ page }) => {\n // Mock the sessions API to return multiple sessions\n await page.route('**/api/sessions', async (route) => {\n const mockSessions = Array.from({ length: 15 }, (_, i) => ({\n id: i + 1,\n start_time: new Date(2024, 0, 1 + i).toISOString(),\n end_time: new Date(2024, 0, 1 + i, 8, 0).toISOString(),\n }));\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({ sessions: mockSessions, total: 15 }),\n });\n });\n\n await page.goto('http://localhost:5173/stats');\n await page.waitForLoadState('networkidle');\n });\n\n test('Pagination component is visible when sessions exceed page size', async ({ page }) => {\n await page.screenshot({ path: 'screenshots/pagination_visible.png' });\n\n // We look for the pagination container or buttons\n const pagination = page.locator('nav').filter({ hasText: /Previous|Next/i });\n await expect(pagination).toBeVisible();\n\n // Check if page 1 is active/displayed\n await expect(page.getByText('1')).toBeVisible();\n });\n\n test('Changing results per page in settings updates the list', async ({ page }) => {\n await page.screenshot({ path: 'screenshots/before_settings_change.png' });\n\n // Open Settings\n await page.getByRole('button', { name: /Settings/i }).click();\n await expect(page.locator('text=Results per page')).toBeVisible();\n\n // Change to 5\n await page.getByLabel(/Results per page/i).selectOption('5');\n await page.getByRole('button', { name: /Save/i }).click();\n await page.waitForTimeout(500);\n\n await page.screenshot({ path: 'screenshots/after_settings_change.png' });\n\n // Verify sessions per page is 5 (only 5 should be visible)\n const sessionRows = page.locator('div[class*=\"SessionList\"] >> .border-b');\n await expect(sessionRows).toHaveCount(5);\n\n // Verify settings is saved in localStorage\n const localStorage = await page.evaluate(() => {\n return window.localStorage.getItem('baby-sleep-settings');\n });\n expect(localStorage).toContain('\"sessionsPerPage\":5');\n });\n\n test('Navigating through pages works', async ({ page }) => {\n // With 15 sessions and default (or mocked) size of 10, there should be 2 pages.\n // Let's set it to 5 via settings first or just use the default if we can.\n // Actually, let's change it to 5 via settings to be sure.\n await page.getByRole('button', { name: /Settings/i }).click();\n await page.getByLabel(/Results per page/i).selectOption('5');\n await page.getByRole('button', { name: /Save/i }).click();\n await page.waitForTimeout(500);\n\n const nextButton = page.getByRole('button', { name: /Next/i });\n await expect(nextButton).toBeVisible();\n\n await nextButton.click();\n await page.waitForTimeout(500);\n\n // Should now show sessions from index 5 to 9 (if total is 15)\n // Let's check if the first visible session changed.\n const sessions = page.locator('div[class*=\"SessionList\"] >> .border-b');\n await expect(sessions).toHaveCount(5);\n });\n});\n" +} +``` + +**Output:** +``` +Edit applied successfully. +``` + +--- + +## Assistant (Build · Gemma 4 26b A4B · 17.4s) + +_Thinking:_ + + +Now run the tests again. + + +**Tool: bash** + +**Input:** +```json +{ + "command": "npx playwright test frontend/tests/e2e/pagination.spec.ts --reporter=list", + "description": "Run the improved pagination tests with better synchronization and debugging.", + "workdir": "/Users/jhag/projects/babymonitor/frontend" +} +``` + +**Output:** +``` +SyntaxError: /Users/jhag/projects/babymonitor/frontend/tests/e2e/pagination.spec.ts: Unexpected token (81:4) + + 79 | }); + 80 | +> 81 | }); + | ^ + 82 | + 83 | await page.goto('http://localhost:5173/stats'); + 84 | // Wait for the sessions to actually appear in the DOM before proceeding + + at e2e/pagination.spec.ts:81 + + 79 | }); + 80 | +> 81 | }); + | ^ + 82 | + 83 | await page.goto('http://localhost:5173/stats'); + 84 | // Wait for the sessions to actually appear in the DOM before proceeding +Error: No tests found. +Make sure that arguments are regular expressions matching test files. +You may need to escape symbols like "$" or "*" and quote the arguments. + + +``` + +--- + +## Assistant (Build · Gemma 4 26b A4B · 44.8s) + +_Thinking:_ + + +I am repeatedly failing to edit correctly because of my `oldString`/`newString` strategy when the file is already corrupted. I will just use `write` to clear everything out and start fresh with the correct code. + + +**Tool: write** + +**Input:** +```json +{ + "content": "import { test, expect } from '@playwright/test';\n\ntest.describe('Pagination', () => {\n test.beforeEach(async ({ page }) => {\n // Mock the sessions API to return multiple sessions\n await page.route('**/api/sessions', async (route) => {\n const mockSessions = Array.from({ length: 15 }, (_, i) => ({\n id: i + 1,\n start_time: new Date(2024, 0, 1 + i).toISOString(),\n end_time: new Date(2024, 0, 1 + i, 8, 0).toISOString(),\n }));\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({ sessions: mockSessions, total: 15 }),\n });\n });\n\n await page.goto('http://localhost:5173/stats');\n });\n\n test('Pagination component is visible when sessions exceed page size', async ({ page }) => {\n // Wait for one of the mocked dates to appear to ensure data is loaded\n await expect(page.locator('body')).toContainText('Jan 1, 2024', { timeout: 10000 });\n await page.screenshot({ path: 'screenshots/pagination_visible.png' });\n\n const pagination = page.locator('nav').filter({ hasText: /Previous|Next/i });\n await expect(pagination).toBeVisible();\n\n await expect(page.getByText('1')).toBeVisible();\n });\n\n test('Changing results per page in settings updates the list', async ({ page }) => {\n // Wait for data to load first\n await expect(page.locator('body')).toContainText('Jan 1, 2024', { timeout: 10000 });\n await page.screenshot({ path: 'screenshots/before_settings_change.png' });\n\n // Open Settings\n await page.getByRole('button', { name: /Settings/i }).click();\n await expect(page.locator('text=Results per page')).toBeVisible();\n\n // Change to 5\n await page.getByLabel(/Results per page/i).selectOption('5');\n await page.getByRole('button', { name: /Save/i }).click();\n await page.waitForTimeout(500);\n\n await page.screenshot({ path: 'screenshots/after_settings_change.png' });\n\n // Verify sessions per page is 5 (only 5 should be visible)\n const sessionRows = page.locator('div[class*=\"SessionList\"] >> .border-b');\n await expect(sessionRows).toHaveCount(5);\n\n // Verify settings is saved in localStorage\n const localStorage = await page.evaluate(() => {\n return window.localStorage.getItem('baby-sleep-settings');\n });\n expect(localStorage).toContain('\"sessionsPerPage\":5');\n });\n\n test('Navigating through pages works', async ({ page }) => {\n // Wait for data to load first\n await expect(page.locator('body')).toContainText('Jan 1, 2024', { timeout: 10000 });\n \n // Set to 5 via settings first\n await page.getByRole('button', { name: /Settings/i }).click();\n await page.getByLabel(/Results per page/i).selectOption('5');\n await page.getByRole('button', { name: /Save/i }).click();\n await page.waitForTimeout(500);\n\n const nextButton = page.getByRole('button', { name: /Next/i });\n await expect(nextButton).toBeVisible();\n\n await nextButton.click();\n await page.waitForTimeout(500);\n\n // Should now show sessions from index 5 to 9 (if total is 15)\n // Let's check if the first visible session changed.\n await expect(page.getByText('Jan 6, 2024')).toBeVisible();\n });\n});\n", + "filePath": "/Users/jhag/projects/babymonitor/frontend/tests/e2e/pagination.spec.ts" +} +``` + +**Output:** +``` +Wrote file successfully. +``` + +--- + +## Assistant (Build · Gemma 4 26b A4B · 21.1s) + +_Thinking:_ + + +Now run the tests again. Hopefully it works this time! + + +**Tool: bash** + +**Input:** +```json +{ + "command": "npx playwright test frontend/tests/e2e/pagination.spec.ts --reporter=list", + "description": "Run the cleaned up pagination tests.", + "workdir": "/Users/jhag/projects/babymonitor/frontend" +} +``` + +**Output:** +``` + +Running 3 tests using 3 workers + + ✘ 3 [chromium] › tests/e2e/pagination.spec.ts:22:3 › Pagination › Pagination component is visible when sessions exceed page size (10.4s) + ✘ 1 [chromium] › tests/e2e/pagination.spec.ts:60:3 › Pagination › Navigating through pages works (10.4s) + ✘ 2 [chromium] › tests/e2e/pagination.spec.ts:33:3 › Pagination › Changing results per page in settings updates the list (10.4s) + + + 1) [chromium] › tests/e2e/pagination.spec.ts:22:3 › Pagination › Pagination component is visible when sessions exceed page size + + Error: expect(locator).toContainText(expected) failed + + Locator: locator('body') + Timeout: 10000ms + - Expected substring - 1 + + Received string + 5 + + - Jan 1, 2024 + + + + TrackerStatsStatisticsOverview of sleep patterns and trends7d14d30d90dTotal sleeps in period2Avg sleeps per day in period0.2Total sleeps (all time)2Avg sleeps per day (all time)0.3Daily Summary0h2h4h6h8h2026-05-312026-05-26Avg Total5h 0mAvg Day3h 59mAvg Night1h 1mWakeup TimesToday2d ago4d ago6d ago8d ago03:3003:4504:0004:1504:30BedtimeToday2d ago4d ago6d ago8d ago18:3018:4519:0019:1519:30Sleep Timetable SunMonTueWedThuFriSatSun00:0001:0002:0003:0004:0005:0006:0007:0008:0009:0010:0011:0012:0013:0014:0015:0016:0017:0018:0019:0020:0021:0022:0023:00Sleep Sessions2024-01-01 00:008h 0m2024-01-01 00:00 → 2024-01-01 08:008h 0m2024-01-02 00:008h 0m2024-01-02 00:00 → 2024-01-02 08:008h 0m2024-01-03 00:008h 0m2024-01-03 00:00 → 2024-01-03 08:008h 0m2024-01-04 00:008h 0m2024-01-04 00:00 → 2024-01-04 08:008h 0m2024-01-05 00:008h 0m2024-01-05 00:00 → 2024-01-05 08:008h 0m2024-01-06 00:008h 0m2024-01-06 00:00 → 2024-01-06 08:008h 0m2024-01-07 00:008h 0m2024-01-07 00:00 → 2024-01-07 08:008h 0m2024-01-08 00:008h 0m2024-01-08 00:00 → 2024-01-08 08:008h 0m2024-01-09 00:008h 0m2024-01-09 00:00 → 2024-01-09 08:008h 0m2024-01-10 00:008h 0m2024-01-10 00:00 → 2024-01-10 08:008h 0m12 + +   + +   + + 18:30 + + Call log: +  - Expect "toContainText" with timeout 10000ms +  - waiting for locator('body') +  - locator resolved to … +  - unexpected value " +  TrackerStatsStatisticsOverview of sleep patterns and trends7d14d30d90dLoading summary...Loading plot...Loading plot...Loading timetable...Loading sessions... +   +   + " +  - locator resolved to … +  - unexpected value " +  TrackerStatsStatisticsOverview of sleep patterns and trends7d14d30d90dLoading summary...Loading plot...Loading plot...Loading timetable...Sleep Sessions2024-01-01 00:008h 0m2024-01-01 00:00 → 2024-01-01 08:008h 0m2024-01-02 00:008h 0m2024-01-02 00:00 → 2024-01-02 08:008h 0m2024-01-03 00:008h 0m2024-01-03 00:00 → 2024-01-03 08:008h 0m2024-01-04 00:008h 0m2024-01-04 00:00 → 2024-01-04 08:008h 0m2024-01-05 00:008h 0m2024-01-05 00:00 → 2024-01-05 08:008h 0m2024-01-06 00:008h 0m2024-01-06 00:00 → 2024-01-06 08:008h 0m2024-01-07 00:008h 0m2024-01-07 00:00 → 2024-01-07 08:008h 0m2024-01-08 00:008h 0m2024-01-08 00:00 → 2024-01-08 08:008h 0m2024-01-09 00:008h 0m2024-01-09 00:00 → 2024-01-09 08:008h 0m2024-01-10 00:008h 0m2024-01-10 00:00 → 2024-01-10 08:008h 0m12 +   +   + " +  - locator resolved to … +  - unexpected value " +  TrackerStatsStatisticsOverview of sleep patterns and trends7d14d30d90dDaily Summary0h2h4h6h8h2026-05-312026-05-26Avg Total5h 0mAvg Day3h 59mAvg Night1h 1mWakeup TimesToday2d ago4d ago6d ago8d ago03:3003:4504:0004:1504:30BedtimeToday2d ago4d ago6d ago8d ago18:3018:4519:0019:1519:30Sleep Timetable SunMonTueWedThuFriSatSun00:0001:0002:0003:0004:0005:0006:0007:0008:0009:0010:0011:0012:0013:0014:0015:0016:0017:0018:0019:0020:0021:0022:0023:00Sleep Sessions2024-01-01 00:008h 0m2024-01-01 00:00 → 2024-01-01 08:008h 0m2024-01-02 00:008h 0m2024-01-02 00:00 → 2024-01-02 08:008h 0m2024-01-03 00:008h 0m2024-01-03 00:00 → 2024-01-03 08:008h 0m2024-01-04 00:008h 0m2024-01-04 00:00 → 2024-01-04 08:008h 0m2024-01-05 00:008h 0m2024-01-05 00:00 → 2024-01-05 08:008h 0m2024-01-06 00:008h 0m2024-01-06 00:00 → 2024-01-06 08:008h 0m2024-01-07 00:008h 0m2024-01-07 00:00 → 2024-01-07 08:008h 0m2024-01-08 00:008h 0m2024-01-08 00:00 → 2024-01-08 08:008h 0m2024-01-09 00:008h 0m2024-01-09 00:00 → 2024-01-09 08:008h 0m2024-01-10 00:008h 0m2024-01-10 00:00 → 2024-01-10 08:008h 0m12 +   +   + 18:30" +  10 × locator resolved to … +  - unexpected value " +  TrackerStatsStatisticsOverview of sleep patterns and trends7d14d30d90dTotal sleeps in period2Avg sleeps per day in period0.2Total sleeps (all time)2Avg sleeps per day (all time)0.3Daily Summary0h2h4h6h8h2026-05-312026-05-26Avg Total5h 0mAvg Day3h 59mAvg Night1h 1mWakeup TimesToday2d ago4d ago6d ago8d ago03:3003:4504:0004:1504:30BedtimeToday2d ago4d ago6d ago8d ago18:3018:4519:0019:1519:30Sleep Timetable SunMonTueWedThuFriSatSun00:0001:0002:0003:0004:0005:0006:0007:0008:0009:0010:0011:0012:0013:0014:0015:0016:0017:0018:0019:0020:0021:0022:0023:00Sleep Sessions2024-01-01 00:008h 0m2024-01-01 00:00 → 2024-01-01 08:008h 0m2024-01-02 00:008h 0m2024-01-02 00:00 → 2024-01-02 08:008h 0m2024-01-03 00:008h 0m2024-01-03 00:00 → 2024-01-03 08:008h 0m2024-01-04 00:008h 0m2024-01-04 00:00 → 2024-01-04 08:008h 0m2024-01-05 00:008h 0m2024-01-05 00:00 → 2024-01-05 08:008h 0m2024-01-06 00:008h 0m2024-01-06 00:00 → 2024-01-06 08:008h 0m2024-01-07 00:008h 0m2024-01-07 00:00 → 2024-01-07 08:008h 0m2024-01-08 00:008h 0m2024-01-08 00:00 → 2024-01-08 08:008h 0m2024-01-09 00:008h 0m2024-01-09 00:00 → 2024-01-09 08:008h 0m2024-01-10 00:008h 0m2024-01-10 00:00 → 2024-01-10 08:008h 0m12 +   +   + 18:30" + + + 22 | test('Pagination component is visible when sessions exceed page size', async ({ page }) => { + 23 | // Wait for one of the mocked dates to appear to ensure data is loaded + > 24 | await expect(page.locator('body')).toContainText('Jan 1, 2024', { timeout: 10000 }); + | ^ + 25 | await page.screenshot({ path: 'screenshots/pagination_visible.png' }); + 26 | + 27 | const pagination = page.locator('nav').filter({ hasText: /Previous|Next/i }); + at /Users/jhag/projects/babymonitor/frontend/tests/e2e/pagination.spec.ts:24:40 + + attachment #1: screenshot (image/png) ────────────────────────────────────────────────────────── + test-results/e2e-pagination-Pagination--1edf2-n-sessions-exceed-page-size-chromium/test-failed-1.png + ──────────────────────────────────────────────────────────────────────────────────────────────── + + Error Context: test-results/e2e-pagination-Pagination--1edf2-n-sessions-exceed-page-size-chromium/error-context.md + + 2) [chromium] › tests/e2e/pagination.spec.ts:33:3 › Pagination › Changing results per page in settings updates the list + + Error: expect(locator).toContainText(expected) failed + + Locator: locator('body') + Timeout: 10000ms + - Expected substring - 1 + + Received string + 5 + + - Jan 1, 2024 + + + + TrackerStatsStatisticsOverview of sleep patterns and trends7d14d30d90dTotal sleeps in period2Avg sleeps per day in period0.2Total sleeps (all time)2Avg sleeps per day (all time)0.3Daily Summary0h2h4h6h8h2026-05-312026-05-26Avg Total5h 0mAvg Day3h 59mAvg Night1h 1mWakeup TimesToday2d ago4d ago6d ago8d ago03:3003:4504:0004:1504:30BedtimeToday2d ago4d ago6d ago8d ago18:3018:4519:0019:1519:30Sleep Timetable SunMonTueWedThuFriSatSun00:0001:0002:0003:0004:0005:0006:0007:0008:0009:0010:0011:0012:0013:0014:0015:0016:0017:0018:0019:0020:0021:0022:0023:00Sleep Sessions2024-01-01 00:008h 0m2024-01-01 00:00 → 2024-01-01 08:008h 0m2024-01-02 00:008h 0m2024-01-02 00:00 → 2024-01-02 08:008h 0m2024-01-03 00:008h 0m2024-01-03 00:00 → 2024-01-03 08:008h 0m2024-01-04 00:008h 0m2024-01-04 00:00 → 2024-01-04 08:008h 0m2024-01-05 00:008h 0m2024-01-05 00:00 → 2024-01-05 08:008h 0m2024-01-06 00:008h 0m2024-01-06 00:00 → 2024-01-06 08:008h 0m2024-01-07 00:008h 0m2024-01-07 00:00 → 2024-01-07 08:008h 0m2024-01-08 00:008h 0m2024-01-08 00:00 → 2024-01-08 08:008h 0m2024-01-09 00:008h 0m2024-01-09 00:00 → 2024-01-09 08:008h 0m2024-01-10 00:008h 0m2024-01-10 00:00 → 2024-01-10 08:008h 0m12 + +   + +   + + 18:30 + + Call log: +  - Expect "toContainText" with timeout 10000ms +  - waiting for locator('body') +  - locator resolved to … +  - unexpected value " +  TrackerStatsStatisticsOverview of sleep patterns and trends7d14d30d90dLoading summary...Loading plot...Loading plot...Loading timetable...Loading sessions... +   +   + " +  - locator resolved to … +  - unexpected value " +  TrackerStatsStatisticsOverview of sleep patterns and trends7d14d30d90dLoading summary...Loading plot...Loading plot...Loading timetable...Sleep Sessions2024-01-01 00:008h 0m2024-01-01 00:00 → 2024-01-01 08:008h 0m2024-01-02 00:008h 0m2024-01-02 00:00 → 2024-01-02 08:008h 0m2024-01-03 00:008h 0m2024-01-03 00:00 → 2024-01-03 08:008h 0m2024-01-04 00:008h 0m2024-01-04 00:00 → 2024-01-04 08:008h 0m2024-01-05 00:008h 0m2024-01-05 00:00 → 2024-01-05 08:008h 0m2024-01-06 00:008h 0m2024-01-06 00:00 → 2024-01-06 08:008h 0m2024-01-07 00:008h 0m2024-01-07 00:00 → 2024-01-07 08:008h 0m2024-01-08 00:008h 0m2024-01-08 00:00 → 2024-01-08 08:008h 0m2024-01-09 00:008h 0m2024-01-09 00:00 → 2024-01-09 08:008h 0m2024-01-10 00:008h 0m2024-01-10 00:00 → 2024-01-10 08:008h 0m12 +   +   + " +  - locator resolved to … +  - unexpected value " +  TrackerStatsStatisticsOverview of sleep patterns and trends7d14d30d90dDaily Summary0h2h4h6h8h2026-05-312026-05-26Avg Total5h 0mAvg Day3h 59mAvg Night1h 1mWakeup TimesToday2d ago4d ago6d ago8d ago03:3003:4504:0004:1504:30BedtimeToday2d ago4d ago6d ago8d ago18:3018:4519:0019:1519:30Sleep Timetable SunMonTueWedThuFriSatSun00:0001:0002:0003:0004:0005:0006:0007:0008:0009:0010:0011:0012:0013:0014:0015:0016:0017:0018:0019:0020:0021:0022:0023:00Sleep Sessions2024-01-01 00:008h 0m2024-01-01 00:00 → 2024-01-01 08:008h 0m2024-01-02 00:008h 0m2024-01-02 00:00 → 2024-01-02 08:008h 0m2024-01-03 00:008h 0m2024-01-03 00:00 → 2024-01-03 08:008h 0m2024-01-04 00:008h 0m2024-01-04 00:00 → 2024-01-04 08:008h 0m2024-01-05 00:008h 0m2024-01-05 00:00 → 2024-01-05 08:008h 0m2024-01-06 00:008h 0m2024-01-06 00:00 → 2024-01-06 08:008h 0m2024-01-07 00:008h 0m2024-01-07 00:00 → 2024-01-07 08:008h 0m2024-01-08 00:008h 0m2024-01-08 00:00 → 2024-01-08 08:008h 0m2024-01-09 00:008h 0m2024-01-09 00:00 → 2024-01-09 08:008h 0m2024-01-10 00:008h 0m2024-01-10 00:00 → 2024-01-10 08:008h 0m12 +   +   + 18:30" +  10 × locator resolved to … +  - unexpected value " +  TrackerStatsStatisticsOverview of sleep patterns and trends7d14d30d90dTotal sleeps in period2Avg sleeps per day in period0.2Total sleeps (all time)2Avg sleeps per day (all time)0.3Daily Summary0h2h4h6h8h2026-05-312026-05-26Avg Total5h 0mAvg Day3h 59mAvg Night1h 1mWakeup TimesToday2d ago4d ago6d ago8d ago03:3003:4504:0004:1504:30BedtimeToday2d ago4d ago6d ago8d ago18:3018:4519:0019:1519:30Sleep Timetable SunMonTueWedThuFriSatSun00:0001:0002:0003:0004:0005:0006:0007:0008:0009:0010:0011:0012:0013:0014:0015:0016:0017:0018:0019:0020:0021:0022:0023:00Sleep Sessions2024-01-01 00:008h 0m2024-01-01 00:00 → 2024-01-01 08:008h 0m2024-01-02 00:008h 0m2024-01-02 00:00 → 2024-01-02 08:008h 0m2024-01-03 00:008h 0m2024-01-03 00:00 → 2024-01-03 08:008h 0m2024-01-04 00:008h 0m2024-01-04 00:00 → 2024-01-04 08:008h 0m2024-01-05 00:008h 0m2024-01-05 00:00 → 2024-01-05 08:008h 0m2024-01-06 00:008h 0m2024-01-06 00:00 → 2024-01-06 08:008h 0m2024-01-07 00:008h 0m2024-01-07 00:00 → 2024-01-07 08:008h 0m2024-01-08 00:008h 0m2024-01-08 00:00 → 2024-01-08 08:008h 0m2024-01-09 00:008h 0m2024-01-09 00:00 → 2024-01-09 08:008h 0m2024-01-10 00:008h 0m2024-01-10 00:00 → 2024-01-10 08:008h 0m12 +   +   + 18:30" + + + 33 | test('Changing results per page in settings updates the list', async ({ page }) => { + 34 | // Wait for data to load first + > 35 | await expect(page.locator('body')).toContainText('Jan 1, 2024', { timeout: 10000 }); + | ^ + 36 | await page.screenshot({ path: 'screenshots/before_settings_change.png' }); + 37 | + 38 | // Open Settings + at /Users/jhag/projects/babymonitor/frontend/tests/e2e/pagination.spec.ts:35:40 + + attachment #1: screenshot (image/png) ────────────────────────────────────────────────────────── + test-results/e2e-pagination-Pagination--d7b84-n-settings-updates-the-list-chromium/test-failed-1.png + ──────────────────────────────────────────────────────────────────────────────────────────────── + + Error Context: test-results/e2e-pagination-Pagination--d7b84-n-settings-updates-the-list-chromium/error-context.md + + 3) [chromium] › tests/e2e/pagination.spec.ts:60:3 › Pagination › Navigating through pages works ── + + Error: expect(locator).toContainText(expected) failed + + Locator: locator('body') + Timeout: 10000ms + - Expected substring - 1 + + Received string + 5 + + - Jan 1, 2024 + + + + TrackerStatsStatisticsOverview of sleep patterns and trends7d14d30d90dTotal sleeps in period2Avg sleeps per day in period0.2Total sleeps (all time)2Avg sleeps per day (all time)0.3Daily Summary0h2h4h6h8h2026-05-312026-05-26Avg Total5h 0mAvg Day3h 59mAvg Night1h 1mWakeup TimesToday2d ago4d ago6d ago8d ago03:3003:4504:0004:1504:30BedtimeToday2d ago4d ago6d ago8d ago18:3018:4519:0019:1519:30Sleep Timetable SunMonTueWedThuFriSatSun00:0001:0002:0003:0004:0005:0006:0007:0008:0009:0010:0011:0012:0013:0014:0015:0016:0017:0018:0019:0020:0021:0022:0023:00Sleep Sessions2024-01-01 00:008h 0m2024-01-01 00:00 → 2024-01-01 08:008h 0m2024-01-02 00:008h 0m2024-01-02 00:00 → 2024-01-02 08:008h 0m2024-01-03 00:008h 0m2024-01-03 00:00 → 2024-01-03 08:008h 0m2024-01-04 00:008h 0m2024-01-04 00:00 → 2024-01-04 08:008h 0m2024-01-05 00:008h 0m2024-01-05 00:00 → 2024-01-05 08:008h 0m2024-01-06 00:008h 0m2024-01-06 00:00 → 2024-01-06 08:008h 0m2024-01-07 00:008h 0m2024-01-07 00:00 → 2024-01-07 08:008h 0m2024-01-08 00:008h 0m2024-01-08 00:00 → 2024-01-08 08:008h 0m2024-01-09 00:008h 0m2024-01-09 00:00 → 2024-01-09 08:008h 0m2024-01-10 00:008h 0m2024-01-10 00:00 → 2024-01-10 08:008h 0m12 + +   + +   + + 18:30 + + Call log: +  - Expect "toContainText" with timeout 10000ms +  - waiting for locator('body') +  - locator resolved to … +  - unexpected value " +  TrackerStatsStatisticsOverview of sleep patterns and trends7d14d30d90dLoading summary...Loading plot...Loading plot...Loading timetable...Loading sessions... +   +   + " +  - locator resolved to … +  - unexpected value " +  TrackerStatsStatisticsOverview of sleep patterns and trends7d14d30d90dLoading summary...Loading plot...Loading plot...Loading timetable...Sleep Sessions2024-01-01 00:008h 0m2024-01-01 00:00 → 2024-01-01 08:008h 0m2024-01-02 00:008h 0m2024-01-02 00:00 → 2024-01-02 08:008h 0m2024-01-03 00:008h 0m2024-01-03 00:00 → 2024-01-03 08:008h 0m2024-01-04 00:008h 0m2024-01-04 00:00 → 2024-01-04 08:008h 0m2024-01-05 00:008h 0m2024-01-05 00:00 → 2024-01-05 08:008h 0m2024-01-06 00:008h 0m2024-01-06 00:00 → 2024-01-06 08:008h 0m2024-01-07 00:008h 0m2024-01-07 00:00 → 2024-01-07 08:008h 0m2024-01-08 00:008h 0m2024-01-08 00:00 → 2024-01-08 08:008h 0m2024-01-09 00:008h 0m2024-01-09 00:00 → 2024-01-09 08:008h 0m2024-01-10 00:008h 0m2024-01-10 00:00 → 2024-01-10 08:008h 0m12 +   +   + " +  - locator resolved to … +  - unexpected value " +  TrackerStatsStatisticsOverview of sleep patterns and trends7d14d30d90dDaily Summary0h2h4h6h8h2026-05-312026-05-26Avg Total5h 0mAvg Day3h 59mAvg Night1h 1mWakeup TimesToday2d ago4d ago6d ago8d ago03:3003:4504:0004:1504:30BedtimeToday2d ago4d ago6d ago8d ago18:3018:4519:0019:1519:30Sleep Timetable SunMonTueWedThuFriSatSun00:0001:0002:0003:0004:0005:0006:0007:0008:0009:0010:0011:0012:0013:0014:0015:0016:0017:0018:0019:0020:0021:0022:0023:00Sleep Sessions2024-01-01 00:008h 0m2024-01-01 00:00 → 2024-01-01 08:008h 0m2024-01-02 00:008h 0m2024-01-02 00:00 → 2024-01-02 08:008h 0m2024-01-03 00:008h 0m2024-01-03 00:00 → 2024-01-03 08:008h 0m2024-01-04 00:008h 0m2024-01-04 00:00 → 2024-01-04 08:008h 0m2024-01-05 00:008h 0m2024-01-05 00:00 → 2024-01-05 08:008h 0m2024-01-06 00:008h 0m2024-01-06 00:00 → 2024-01-06 08:008h 0m2024-01-07 00:008h 0m2024-01-07 00:00 → 2024-01-07 08:008h 0m2024-01-08 00:008h 0m2024-01-08 00:00 → 2024-01-08 08:008h 0m2024-01-09 00:008h 0m2024-01-09 00:00 → 2024-01-09 08:008h 0m2024-01-10 00:008h 0m2024-01-10 00:00 → 2024-01-10 08:008h 0m12 +   +   + 18:30" +  10 × locator resolved to … +  - unexpected value " +  TrackerStatsStatisticsOverview of sleep patterns and trends7d14d30d90dTotal sleeps in period2Avg sleeps per day in period0.2Total sleeps (all time)2Avg sleeps per day (all time)0.3Daily Summary0h2h4h6h8h2026-05-312026-05-26Avg Total5h 0mAvg Day3h 59mAvg Night1h 1mWakeup TimesToday2d ago4d ago6d ago8d ago03:3003:4504:0004:1504:30BedtimeToday2d ago4d ago6d ago8d ago18:3018:4519:0019:1519:30Sleep Timetable SunMonTueWedThuFriSatSun00:0001:0002:0003:0004:0005:0006:0007:0008:0009:0010:0011:0012:0013:0014:0015:0016:0017:0018:0019:0020:0021:0022:0023:00Sleep Sessions2024-01-01 00:008h 0m2024-01-01 00:00 → 2024-01-01 08:008h 0m2024-01-02 00:008h 0m2024-01-02 00:00 → 2024-01-02 08:008h 0m2024-01-03 00:008h 0m2024-01-03 00:00 → 2024-01-03 08:008h 0m2024-01-04 00:008h 0m2024-01-04 00:00 → 2024-01-04 08:008h 0m2024-01-05 00:008h 0m2024-01-05 00:00 → 2024-01-05 08:008h 0m2024-01-06 00:008h 0m2024-01-06 00:00 → 2024-01-06 08:008h 0m2024-01-07 00:008h 0m2024-01-07 00:00 → 2024-01-07 08:008h 0m2024-01-08 00:008h 0m2024-01-08 00:00 → 2024-01-08 08:008h 0m2024-01-09 00:008h 0m2024-01-09 00:00 → 2024-01-09 08:008h 0m2024-01-10 00:008h 0m2024-01-10 00:00 → 2024-01-10 08:008h 0m12 +   +   + 18:30" + + + 60 | test('Navigating through pages works', async ({ page }) => { + 61 | // Wait for data to load first + > 62 | await expect(page.locator('body')).toContainText('Jan 1, 2024', { timeout: 10000 }); + | ^ + 63 | + 64 | // Set to 5 via settings first + 65 | await page.getByRole('button', { name: /Settings/i }).click(); + at /Users/jhag/projects/babymonitor/frontend/tests/e2e/pagination.spec.ts:62:40 + + attachment #1: screenshot (image/png) ────────────────────────────────────────────────────────── + test-results/e2e-pagination-Pagination-Navigating-through-pages-works-chromium/test-failed-1.png + ──────────────────────────────────────────────────────────────────────────────────────────────── + + Error Context: test-results/e2e-pagination-Pagination-Navigating-through-pages-works-chromium/error-context.md + + 3 failed + [chromium] › tests/e2e/pagination.spec.ts:22:3 › Pagination › Pagination component is visible when sessions exceed page size + [chromium] › tests/e2e/pagination.spec.ts:33:3 › Pagination › Changing results per page in settings updates the list + [chromium] › tests/e2e/pagination.spec.ts:60:3 › Pagination › Navigating through pages works ─── + +``` + +--- + +## Assistant (Build · Gemma 4 26b A4B · 66.3s) + +_Thinking:_ + + +The mock is definitely not working. `toContainText('Jan 1, 2024')` failed and the received text shows `2024-01-01 00:00`. +Wait, `Jan 1, 2024` is how I'd expect a date to look if formatted with `formatDateTime`. +Let me check `frontend/src/utils/time.ts` to see how it formats. + + +**Tool: read** + +**Input:** +```json +{ + "filePath": "/Users/jhag/projects/babymonitor/frontend/src/utils/time.ts" +} +``` + +**Output:** +``` +/Users/jhag/projects/babymonitor/frontend/src/utils/time.ts +file + +1: /** +2: * Time formatting utilities for baby sleep tracker +3: */ +4: +5: /** +6: * Convert minutes after a reference time to clock time string +7: * @param minutes - minutes after reference time (can be negative) +8: * @param reference - reference time as HH:MM string +9: * @returns formatted time string like "07:15" +10: */ +11: export function formatMinutesAfter(minutes: number | null, reference: string = "04:00"): string { +12: if (minutes === null) return "N/A"; +13: +14: const [refHours, refMinutes] = reference.split(":").map(Number); +15: const totalRefMinutes = refHours * 60 + refMinutes; +16: const targetMinutes = totalRefMinutes + minutes; +17: // Handle wrap around midnight +18: const normalizedMinutes = ((targetMinutes % 1440) + 1440) % 1440; +19: const hours = Math.floor(normalizedMinutes / 60); +20: const mins = normalizedMinutes % 60; +21: +22: return `${String(hours).padStart(2, "0")}:${String(mins).padStart(2, "0")}`; +23: } +24: +25: /** +26: * Adjust minutes by the local timezone offset +27: * @param utcMinutes - minutes from UTC midnight +28: * @returns adjusted minutes for local time +29: */ +30: export function adjustForTimezone(utcMinutes: number): number { +31: const offsetInMinutes = new Date().getTimezoneOffset(); +32: return utcMinutes - offsetInMinutes; +33: } +34: +35: /** +36: * Convert a Date to datetime-local input value format (local time) +37: * @param date - Date object +38: * @returns string in "YYYY-MM-DDTHH:MM" format +39: */ +40: export function toLocalDatetimeInputValue(date: Date): string { +41: const offset = date.getTimezoneOffset() * 60 * 1000; +42: const localDate = new Date(date.getTime() - offset); +43: return localDate.toISOString().slice(0, 16); +44: } +45: +46: export function formatElapsedTime(utcTimestamp: string): string { +47: let isoString = utcTimestamp; +48: if (utcTimestamp.includes(' ')) { +49: isoString = utcTimestamp.replace(' ', 'T'); +50: } +51: if (!isoString.endsWith('Z')) { +52: isoString += 'Z'; +53: } +54: const then = new Date(isoString); +55: const now = new Date(); +56: const diffMs = now.getTime() - then.getTime(); +57: const diffMins = Math.floor(diffMs / 60000); +58: const hours = Math.floor(diffMins / 60); +59: const mins = diffMins % 60; +60: +61: if (hours > 0) return `${hours}h ${mins}m`; +62: return `${mins}m`; +63: } +64: +65: /** +66: * Format a Date or ISO string to a human-readable datetime string (YYYY-MM-DD HH:mm) +67: * @param date - Date object or ISO string +68: * @returns formatted datetime string +69: */ +70: export function formatDateTime(date: Date | string): string { +71: let dateToParse = date; +72: if (typeof date === 'string' && !date.endsWith('Z') && !date.includes('+')) { +73: dateToParse = date + 'Z'; +74: } +75: const d = typeof dateToParse === 'string' ? new Date(dateToParse) : dateToParse; +76: return d.toLocaleString('sv-SE', { +77: year: 'numeric', +78: month: '2-digit', +79: day: '2-digit', +80: hour: '2-digit', +81: minute: '2-digit', +82: hour12: false +83: }).replace(',', ' '); +84: } +85: +86: /** +87: * Convert a datetime-local input value to an ISO UTC string +88: * @param inputValue - datetime-local string (YYYY-MM-DDTHH:mm) +89: * @returns ISO UTC string +90: */ +91: export function toUTCFromLocalInput(inputValue: string): string { +92: const d = new Date(inputValue); +93: return d.toISOString(); +94: } +95: +96: /** +97: * Calculate duration in minutes between two dates or ISO strings +98: * @param start - Start date object or ISO string +99: * @param end - End date object or ISO string +100: * @returns duration in minutes +101: */ +102: export function getDurationMinutes(start: Date | string, end: Date | string): number { +103: const startDate = typeof start === 'string' ? new Date(start) : start; +104: const endDate = typeof end === 'string' ? new Date(end) : end; +105: return Math.round((endDate.getTime() - startDate.getTime()) / 60000); +106: } +107: +108: /** +109: * Clamp a date to day boundaries (start or end of day) +110: * @param date - Date to clamp +111: * @param bound - 'start' for 00:00, 'end' for 23:59:59.999 +112: * @returns new Date clamped to day boundary +113: */ +114: export function clampToDay(date: Date, bound: "start" | "end"): Date { +115: const result = new Date(date); +116: if (bound === "start") { +117: result.setHours(0, 0, 0, 0); +118: } else { +119: result.setHours(23, 59, 59, 999); +120: } +121: return result; +122: } +123: +124: /** +125: * Parse HH:MM time string to Date on a given day +126: * @param timeStr - HH:MM format +127: * @param baseDate - base date (defaults to today) +128: * @returns Date object +129: * @throws Error if timeStr is invalid +130: */ +131: export function parseTime(timeStr: string, baseDate: Date = new Date()): Date { +132: const [hours, minutes] = timeStr.split(":").map(Number); +133: const result = new Date(baseDate); +134: result.setHours(hours, minutes, 0, 0); +135: return result; +136: } +137: +138: /** +139: * Get date string for N days ago +140: * @param days - number of days ago +141: * @returns ISO date string (YYYY-MM-DD) +142: */ +143: export function getDaysAgo(days: number): string { +144: const date = new Date(); +145: date.setDate(date.getDate() - days); +146: return date.toISOString().split("T")[0]; +147: } +148: +149: /** +150: * Get date string for today +151: * @returns ISO date string (YYYY-MM-DD) +152: */ +153: export function getToday(): string { +154: return new Date().toISOString().split("T")[0]; +155: } +156: +157: /** +158: * Convert HH:MM time string to total minutes from start of day +159: * @param timeStr - HH:MM format +160: * @returns total minutes +161: */ +162: export function timeToMinutes(timeStr: string): number { +163: const [hours, minutes] = timeStr.split(":").map(Number); +164: return hours * 60 + minutes; +165: } +166: +167: /** +168: * Get the offset from a reference time in local minutes +169: * @param utcMinutesFromMidnight - minutes from UTC midnight +170: * @param referenceHHMM - reference time as HH:MM string +171: * @returns local offset from reference in minutes +172: */ +173: export function getLocalOffsetFromReference(utcMinutesFromMidnight: number, referenceHHMM: string): number { +174: const localMinsFromMidnight = adjustForTimezone(utcMinutesFromMidnight); +175: return localMinsFromMidnight - timeToMinutes(referenceHHMM); +176: } +177: +178: /** +179: * Convert total minutes to a human-readable string (e.g., "1h 30m" or "45m") +180: * @param totalMinutes - Total number of minutes +181: * @returns Formatted string +182: */ +183: export function formatMinutes(totalMinutes: number): string { +184: const hours = Math.floor(totalMinutes / 60); +185: const mins = totalMinutes % 60; +186: +187: if (hours > 0) { +188: return `${hours}h ${mins}m`; +189: } +190: return `${mins}m`; +191: } + +(End of file - total 191 lines) + +``` + +--- + +## Assistant (Build · Gemma 4 26b A4B · 106.2s) + +_Thinking:_ + + +`formatDateTime` uses `toLocaleString('sv-SE', ...)`: +```typescript +70: export function formatDateTime(date: Date | string): string { +... +76: const d = typeof dateToParse === 'string' ? new Date(dateToParse) : dateToParse; +77: return d.toLocaleString('sv-SE', { +78: year: 'numeric', +79: month: '2-digit', +80: day: '2-digit', +81: hour: '2-digit', +82: minute: '2-digit', +83: hour12: false +84: }).replace(',', ' '); +85: } +``` + +`sv-SE` format is `YYYY-MM-DD HH:mm`. +So for `2024-01-01`, it should be `2024-01-01 00:00`. + +In my test, I was looking for `Jan 1, 2024`. That's why it failed. I should use `2024-01-01`. + +Let me fix the test again. + + +**Tool: edit** + +**Input:** +```json +{ + "filePath": "/Users/jhag/projects/babymonitor/frontend/tests/e2e/pagination.spec.ts", + "newString": "import { test, expect } from '@playwright/test';\n\ntest.describe('Pagination', () => {\n test.beforeEach(async ({ page }) => {\n // Mock the sessions API to return multiple sessions\n await page.route('**/api/sessions', async (route) => {\n const mockSessions = Array.from({ length: 15 }, (_, i) => ({\n id: i + 1,\n start_time: new Date(2024, 0, 1 + i).toISOString(),\n end_time: new Date(2024, 0, 1 + i, 8, 0).toISOString(),\n }));\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({ sessions: mockSessions, total: 15 }),\n });\n });\n\n await page.goto('http://localhost:5173/stats');\n });\n\n test('Pagination component is visible when sessions exceed page size', async ({ page }) => {\n // Wait for one of the mocked dates to appear to ensure data is loaded\n // sv-SE format: 2024-01-01 00:00\n await expect(page.locator('body')).toContainText('2024-01-01', { timeout: 10000 });\n await page.screenshot({ path: 'screenshots/pagination_visible.png' });\n\n const pagination = page.locator('nav').filter({ hasText: /Previous|Next/i });\n await expect(pagination).toBeVisible();\n\n await expect(page.getByText('1')).toBeVisible();\n });\n\n test('Changing results per page in settings updates the list', async ({ page }) => {\n // Wait for data to load first\n await expect(page.locator('body')).toContainText('2024-01-01', { timeout: 10000 });\n await page.screenshot({ path: 'screenshots/before_settings_change.png' });\n\n // Open Settings\n await page.getByRole('button', { name: /Settings/i }).click();\n await expect(page.locator('text=Results per page')).toBeVisible();\n\n // Change to 5\n await page.getByLabel(/Results per page/i).selectOption('5');\n await page.getByRole('button', { name: /Save/i }).click();\n await page.waitForTimeout(500);\n\n await page.screenshot({ path: 'screenshots/after_settings_change.png' });\n\n // Verify sessions per page is 5 (only 5 should be visible)\n const sessionRows = page.locator('div[class*=\"SessionList\"] >> .border-b');\n await expect(sessionRows).toHaveCount(5);\n\n // Verify settings is saved in localStorage\n const localStorage = await page.evaluate(() => {\n return window.localStorage.getItem('baby-sleep-settings');\n });\n expect(localStorage).toContain('\"sessionsPerPage\":5');\n });\n\n test('Navigating through pages works', async ({ page }) => {\n // Wait for data to load first\n await expect(page.locator('body')).toContainText('2024-01-01', { timeout: 10000 });\n \n // Set to 5 via settings first\n await page.getByRole('button', { name: /Settings/i }).click();\n await page.getByLabel(/Results per page/i).selectOption('5');\n await page.getByRole('button', { name: /Save/i }).click();\n await page.waitForTimeout(500);\n\n const nextButton = page.getByRole('button', { name: /Next/i });\n await expect(nextButton).toBeVisible();\n\n await nextButton.click();\n await page.waitForTimeout(500);\n\n // Should now show sessions from index 5 to 9 (if total is 15)\n // Mocked start_time for i=5 is 2024-01-06\n await expect(page.getByText('2024-01-06')).toBeVisible();\n });\n});\n", + "oldString": "import { test, expect } from '@playwright/test';\n\ntest.describe('Pagination', () => {\n test.beforeEach(async ({ page }) => {\n // Mock the sessions API to return multiple sessions\n await page.route('**/api/sessions', async (route) => {\n const mockSessions = Array.from({ length: 15 }, (_, i) => ({\n id: i + 1,\n start_time: new Date(2024, 0, 1 + i).toISOString(),\n end_time: new Date(2024, 0, 1 + i, 8, 0).toISOString(),\n }));\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({ sessions: mockSessions, total: 15 }),\n });\n });\n\n await page.goto('http://localhost:5173/stats');\n await page.waitForLoadState('networkidle');\n });\n\n test('Pagination component is visible when sessions exceed page size', async ({ page }) => {\n await page.screenshot({ path: 'screenshots/pagination_visible.png' });\n\n // We look for the pagination container or buttons\n const pagination = page.locator('nav').filter({ hasText: /Previous|Next/i });\n await expect(pagination).toBeVisible();\n\n // Check if page 1 is active/displayed\n await expect(page.getByText('1')).toBeVisible();\n });\n\n test('Changing results per page in settings updates the list', async ({ page }) => {\n await page.screenshot({ path: 'screenshots/before_settings_change.png' });\n\n // Open Settings\n await page.getByRole('button', { name: /Settings/i }).click();\n await expect(page.locator('text=Results per page')).toBeVisible();\n\n // Change to 5\n await page.getByLabel('Results per page').selectOption('5');\n await page.getByRole('button', { name: /Save/i }).click();\n await page.waitForTimeout(500);\n\n await page.screenshot({ path: 'screenshots/after_settings_change.png' });\n\n // Verify sessions per page is 5 (only 5 should be visible)\n const sessionRows = page.locator('div[class*=\"SessionList\"] >> .border-b');\n await expect(sessionRows).toHaveCount(5);\n\n // Verify settings is saved in localStorage\n const localStorage = await page.evaluate(() => {\n return window.localStorage.getItem('baby-sleep-settings');\n });\n expect(localStorage).toContain('\"sessionsPerPage\":5');\n });\n\n test('Navigating through pages works', async ({ page }) => {\n // With 15 sessions and default (or mocked) size of 10, there should be 2 pages.\n // Let's set it to 5 via settings first or just use the default if we can.\n // Actually, let's change it to 5 via settings to be sure.\n await page.getByRole('button', { name: /Settings/i }).click();\n await page.getByLabel('Results per page').selectOption('5');\n await page.getByRole('button', { name: /Save/i }).click();\n await page.waitForTimeout(500);\n\n const nextButton = page.getByRole('button', { name: /Next/i });\n await expect(nextButton).toBeVisible();\n\n await nextButton.click();\n await page.waitForTimeout(500);\n\n // Should now show sessions from index 5 to 9 (if total is 15)\n // Let's check if the first visible session changed.\n const sessions = page.locator('div[class*=\"SessionList\"] >> .border-b');\n await expect(sessions).toHaveCount(5);\n });\n});\n" +} +``` + +**Output:** +``` +Edit applied successfully. +``` + +--- + +## Assistant (Build · Gemma 4 26b A4B · 18.1s) + +_Thinking:_ + + +Now run the tests again. + + +**Tool: bash** + +**Input:** +```json +{ + "command": "npx playwright test frontend/tests/e2e/pagination.spec.ts --reporter=list", + "description": "Run the fixed pagination tests with corrected date format.", + "workdir": "/Users/jhag/projects/babymonitor/frontend" +} +``` + +**Output:** +``` +SyntaxError: /Users/jhag/projects/babymonitor/frontend/tests/e2e/pagination.spec.ts: Unexpected token (83:4) + + 81 | }); + 82 | +> 83 | }); + | ^ + 84 | + 85 | await page.goto('http://localhost:5173/stats'); + 86 | }); + + at e2e/pagination.spec.ts:83 + + 81 | }); + 82 | +> 83 | }); + | ^ + 84 | + 85 | await page.goto('http://localhost:5173/stats'); + 86 | }); +Error: No tests found. +Make sure that arguments are regular expressions matching test files. +You may need to escape symbols like "$" or "*" and quote the arguments. + + +``` + +--- + +## Assistant (Build · Gemma 4 26b A4B · 48.8s) + +_Thinking:_ + + +I am seriously struggling with `edit`. I will use `write` to replace the whole file and stop trying to `edit` it. + + +**Tool: write** + +**Input:** +```json +{ + "content": "import { test, expect } from '@playwright/test';\n\ntest.describe('Pagination', () => {\n test.beforeEach(async ({ page }) => {\n // Mock the sessions API to return multiple sessions\n await page.route('**/api/sessions', async (route) => {\n const mockSessions = Array.from({ length: 15 }, (_, i) => ({\n id: i + 1,\n start_time: new Date(2024, 0, 1 + i).toISOString(),\n end_time: new Date(2024, 0, 1 + i, 8, 0).toISOString(),\n }));\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({ sessions: mockSessions, total: 15 }),\n });\n });\n\n await page.goto('http://localhost:5173/stats');\n });\n\n test('Pagination component is visible when sessions exceed page size', async ({ page }) => {\n // Wait for one of the mocked dates to appear to ensure data is loaded\n await expect(page.locator('body')).toContainText('2024-01-01', { timeout: 10000 });\n await page.screenshot({ path: 'screenshots/pagination_visible.png' });\n\n const pagination = page.locator('nav').filter({ hasText: /Previous|Next/i });\n await expect(pagination).toBeVisible();\n\n await expect(page.getByText('1')).toBeVisible();\n });\n\n test('Changing results per page in settings updates the list', async ({ page }) => {\n // Wait for data to load first\n await expect(page.locator('body')).toContainText('2024-01-01', { timeout: 10000 });\n await page.screenshot({ path: 'screenshots/before_settings_change.png' });\n\n // Open Settings\n await page.getByRole('button', { name: /Settings/i }).click();\n await expect(page.locator('text=Results per page')).toBeVisible();\n\n // Change to 5\n await page.getByLabel(/Results per page/i).selectOption('5');\n await page.getByRole('button', { name: /Save/i }).click();\n await page.waitForTimeout(500);\n\n await page.screenshot({ path: 'screenshots/after_settings_change.png' });\n\n // Verify sessions per page is 5 (only 5 should be visible)\n const sessionRows = page.locator('div[class*=\"SessionList\"] >> .border-b');\n await expect(sessionRows).toHaveCount(5);\n\n // Verify settings is saved in localStorage\n const localStorage = await page.evaluate(() => {\n return window.localStorage.getItem('baby-sleep-settings');\n });\n expect(localStorage).toContain('\"sessionsPerPage\":5');\n });\n\n test('Navigating through pages works', async ({ page }) => {\n // Wait for data to load first\n await expect(page.locator('body')).toContainText('2024-01-01', { timeout: 10000 });\n \n // Set to 5 via settings first\n await page.getByRole('button', { name: /Settings/i }).click();\n await page.getByLabel(/Results per page/i).selectOption('5');\n await page.getByRole('button', { name: /Save/i }).click();\n await page.waitForTimeout(500);\n\n const nextButton = page.getByRole('button', { name: /Next/i });\n await expect(nextButton).toBeVisible();\n\n await nextButton.click();\n await page.waitForTimeout(500);\n\n // Should now show sessions from index 5 to 9 (if total is 15)\n // Mocked start_time for i=5 is 2024-01-06\n await expect(page.getByText('2024-01-06')).toBeVisible();\n });\n});\n", + "filePath": "/Users/jhag/projects/babymonitor/frontend/tests/e2e/pagination.spec.ts" +} +``` + +**Output:** +``` +Wrote file successfully. +``` + +--- + +## Assistant (Build · Gemma 4 26b A4B · 22.4s) + +_Thinking:_ + + +Now run the tests again. + + +**Tool: bash** + +**Input:** +```json +{ + "command": "npx playwright test frontend/tests/e2e/pagination.spec.ts --reporter=list", + "description": "Run the cleaned up pagination tests.", + "workdir": "/Users/jhag/projects/babymonitor/frontend" +} +``` + +**Output:** +``` + +Running 3 tests using 3 workers + + ✘ 1 [chromium] › tests/e2e/pagination.spec.ts:22:3 › Pagination › Pagination component is visible when sessions exceed page size (6.0s) + ✘ 2 [chromium] › tests/e2e/pagination.spec.ts:33:3 › Pagination › Changing results per page in settings updates the list (8.2s) + ✘ 3 [chromium] › tests/e2e/pagination.spec.ts:60:3 › Pagination › Navigating through pages works (8.7s) + + + 1) [chromium] › tests/e2e/pagination.spec.ts:22:3 › Pagination › Pagination component is visible when sessions exceed page size + + Error: expect(locator).toBeVisible() failed + + Locator: locator('nav').filter({ hasText: /Previous|Next/i }) + Expected: visible + Timeout: 5000ms + Error: element(s) not found + + Call log: +  - Expect "toBeVisible" with timeout 5000ms +  - waiting for locator('nav').filter({ hasText: /Previous|Next/i }) + + + 26 | + 27 | const pagination = page.locator('nav').filter({ hasText: /Previous|Next/i }); + > 28 | await expect(pagination).toBeVisible(); + | ^ + 29 | + 30 | await expect(page.getByText('1')).toBeVisible(); + 31 | }); + at /Users/jhag/projects/babymonitor/frontend/tests/e2e/pagination.spec.ts:28:30 + + attachment #1: screenshot (image/png) ────────────────────────────────────────────────────────── + test-results/e2e-pagination-Pagination--1edf2-n-sessions-exceed-page-size-chromium/test-failed-1.png + ──────────────────────────────────────────────────────────────────────────────────────────────── + + Error Context: test-results/e2e-pagination-Pagination--1edf2-n-sessions-exceed-page-size-chromium/error-context.md + + 2) [chromium] › tests/e2e/pagination.spec.ts:33:3 › Pagination › Changing results per page in settings updates the list + + Error: expect(locator).toHaveCount(expected) failed + + Locator: locator('div[class*="SessionList"]').locator('.border-b') + Expected: 5 + Received: 0 + Timeout: 5000ms + + Call log: +  - Expect "toHaveCount" with timeout 5000ms +  - waiting for locator('div[class*="SessionList"]').locator('.border-b') +  9 × locator resolved to 0 elements +  - unexpected value "0" + + + 49 | // Verify sessions per page is 5 (only 5 should be visible) + 50 | const sessionRows = page.locator('div[class*="SessionList"] >> .border-b'); + > 51 | await expect(sessionRows).toHaveCount(5); + | ^ + 52 | + 53 | // Verify settings is saved in localStorage + 54 | const localStorage = await page.evaluate(() => { + at /Users/jhag/projects/babymonitor/frontend/tests/e2e/pagination.spec.ts:51:31 + + attachment #1: screenshot (image/png) ────────────────────────────────────────────────────────── + test-results/e2e-pagination-Pagination--d7b84-n-settings-updates-the-list-chromium/test-failed-1.png + ──────────────────────────────────────────────────────────────────────────────────────────────── + + Error Context: test-results/e2e-pagination-Pagination--d7b84-n-settings-updates-the-list-chromium/error-context.md + + 3) [chromium] › tests/e2e/pagination.spec.ts:60:3 › Pagination › Navigating through pages works ── + + Error: expect(locator).toBeVisible() failed + + Locator: getByText('2024-01-06') + Expected: visible + Timeout: 5000ms + Error: element(s) not found + + Call log: +  - Expect "toBeVisible" with timeout 5000ms +  - waiting for getByText('2024-01-06') + + + 76 | // Should now show sessions from index 5 to 9 (if total is 15) + 77 | // Mocked start_time for i=5 is 2024-01-06 + > 78 | await expect(page.getByText('2024-01-06')).toBeVisible(); + | ^ + 79 | }); + 80 | }); + 81 | + at /Users/jhag/projects/babymonitor/frontend/tests/e2e/pagination.spec.ts:78:48 + + attachment #1: screenshot (image/png) ────────────────────────────────────────────────────────── + test-results/e2e-pagination-Pagination-Navigating-through-pages-works-chromium/test-failed-1.png + ──────────────────────────────────────────────────────────────────────────────────────────────── + + Error Context: test-results/e2e-pagination-Pagination-Navigating-through-pages-works-chromium/error-context.md + + 3 failed + [chromium] › tests/e2e/pagination.spec.ts:22:3 › Pagination › Pagination component is visible when sessions exceed page size + [chromium] › tests/e2e/pagination.spec.ts:33:3 › Pagination › Changing results per page in settings updates the list + [chromium] › tests/e2e/pagination.spec.ts:60:3 › Pagination › Navigating through pages works ─── + +``` + +--- + +## Assistant (Build · Gemma 4 26b A4B · 135.9s) + +_Thinking:_ + + +The tests are failing with the same error: `Received: 0 elements` and `toContainText('2024-01-01')` failing. +This means the sessions are NOT being loaded, even with the mock. + +Wait, look at the `page.route` call: +```javascript + await page.route('**/api/sessions', async (route) => { +``` + +Is it possible that the request is NOT actually hitting `**/api/sessions`? +Let's look at the console output from my previous run (it had `console.log` in the route handler): +``` +>> GET http://localhost:5173/stats +<< 200 http://localhost:5173/stats +>> GET http://localhost:5173/@vite/client +... +>> GET http://localhost:5173/api/sessions +``` + +Wait, the console output DOES show `>> GET http://localhost:5173/api/sessions` but it does NOT show `Intercepted request to: http://localhost:5173/api/sessions` which I added in the last attempt! + +This means my `page.route` is not matching the request. +Why? Maybe it's because of the `**/api/sessions` pattern or how Playwright handles it. + +Let's try to match it with a regex instead: `/\/api\/sessions/`. +Or even better, I'll check what the exact URL is from the logs. It says `http://localhost:5173/api/sessions`. + +Wait, I also see that `fetchSessions` in `client.ts` uses `${BASE_URL}/api/sessions`. +If `BASE_URL` is empty, it's `/api/sessions`. + +Let me try to match everything with `page.route('**/api/sessions', ...)` but I will also add a fallback mock for `/**/api/*` to see what's happening. + +Actually, I suspect the issue is that `fetchSessions()` might be calling something else if there's a prefix. +Wait, I see `import { fetchSessions } from '../api/client';` in `useSleepSession.ts`. + +Let's look at the logs again carefully. +``` +>> GET http://localhost:5173/api/sessions +``` +There is NO `Intercepted request to:` in the log. This means the route was not matched. + +Wait, I see why! In my last `write`, I removed the `console.log` that was in the interceptor? No, I didn't. +Ah! I see it now. In my `write` command, I used: +```javascript + await page.route('**/api/sessions', async (route) => { + const mockSessions = Array.from({ length: 15 }, (_, i) => ({ + id: i + 1, + start_time: new Date(2024, 0, 1 + i).toISOString(), + end_time: new Date(2024, 0, 1 + i, 8, 0).toISOString(), + })); + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ sessions: mockSessions, total: 15 }), + }); + }); +``` +I DON'T have the `console.log` in there in my latest version! My mistake, I must have accidentally deleted it when I was replacing the content. + +Let's add it back and use a regex for better matching. + + +**Tool: edit** + +**Input:** +```json +{ + "filePath": "/Users/jhag/projects/babymonitor/frontend/tests/e2e/pagination.spec.ts", + "newString": "import { test, expect } from '@playwright/test';\n\ntest.describe('Pagination', () => {\n test.beforeEach(async ({ page }) => {\n // Monitor network requests to see if our mock is hit\n page.on('request', request => console.log('>>', request.method(), request.url()));\n page.on('response', response => console.log('<<', response.status(), response.url()));\n\n // Mock the sessions API with a regex for better matching\n await page.route(/\\/api\\/sessions/, async (route) => {\n console.log('Intercepted request to:', route.request().url());\n const mockSessions = Array.from({ length: 15 }, (_, i) => ({\n id: i + 1,\n start_time: new Date(2024, 0, 1 + i).toISOString(),\n end_time: new Date(2024, 0, 1 + i, 8, 0).toISOString(),\n }));\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({ sessions: mockSessions, total: 15 }),\n });\n });\n\n await page.goto('http://localhost:5173/stats');\n });\n\n test('Pagination component is visible when sessions exceed page size', async ({ page }) => {\n // Wait for one of the mocked dates to appear to ensure data is loaded\n await expect(page.locator('body')).toContainText('2024-01-01', { timeout: 10000 });\n await page.screenshot({ path: 'screenshots/pagination_visible.png' });\n\n const pagination = page.locator('nav').filter({ hasText: /Previous|Next/i });\n await expect(pagination).toBeVisible();\n\n await expect(page.getByText('1')).toBeVisible();\n });\n\n test('Changing results per page in settings updates the list', async ({ page }) => {\n // Wait for data to load first\n await expect(page.locator('body')).toContainText('2024-01-01', { timeout: 10000 });\n await page.screenshot({ path: 'screenshots/before_settings_change.png' });\n\n // Open Settings\n await page.getByRole('button', { name: /Settings/i }).click();\n await expect(page.locator('text=Results per page')).toBeVisible();\n\n // Change to 5\n await page.getByLabel(/Results per page/i).selectOption('5');\n await page.getByRole('button', { name: /Save/i }).click();\n await page.waitForTimeout(500);\n\n await page.screenshot({ path: 'screenshots/after_settings_change.png' });\n\n // Verify sessions per page is 5 (only 5 should be visible)\n const sessionRows = page.locator('div[class*=\"SessionList\"] >> .border-b');\n await expect(sessionRows).toHaveCount(5);\n\n // Verify settings is saved in localStorage\n const localStorage = await page.evaluate(() => {\n return window.localStorage.getItem('baby-sleep-settings');\n });\n expect(localStorage).toContain('\"sessionsPerPage\":5');\n });\n\n test('Navigating through pages works', async ({ page }) => {\n // Wait for data to load first\n await expect(page.locator('body')).toContainText('2024-01-01', { timeout: 10000 });\n \n // Set to 5 via settings first\n await page.getByRole('button', { name: /Settings/i }).click();\n await page.getByLabel(/Results per page/i).selectOption('5');\n await page.getByRole('button', { name: /Save/i }).click();\n await page.waitForTimeout(500);\n\n const nextButton = page.getByRole('button', { name: /Next/i });\n await expect(nextButton).toBeVisible();\n\n await nextButton.click();\n await page.waitForTimeout(500);\n\n // Should now show sessions from index 5 to 9 (if total is 15)\n // Mocked start_time for i=5 is 2024-01-06\n await expect(page.getByText('2024-01-06')).toBeVisible();\n });\n});\n", + "oldString": "import { test, expect } from '@playwright/test';\n\ntest.describe('Pagination', () => {\n test.beforeEach(async ({ page }) => {\n // Mock the sessions API to return multiple sessions\n await page.route('**/api/sessions', async (route) => {\n const mockSessions = Array.from({ length: 15 }, (_, i) => ({\n id: i + 1,\n start_time: new Date(2024, 0, 1 + i).toISOString(),\n end_time: new Date(2024, 0, 1 + i, 8, 0).toISOString(),\n }));\n await route.fulfill({\n status: 200,\n contentType: 'application/json',\n body: JSON.stringify({ sessions: mockSessions, total: 15 }),\n });\n });\n\n await page.goto('http://localhost:5173/stats');\n });\n\n test('Pagination component is visible when sessions exceed page size', async ({ page }) => {\n // Wait for one of the mocked dates to appear to ensure data is loaded\n await expect(page.locator('body')).toContainText('2024-01-01', { timeout: 10000 });\n await page.screenshot({ path: 'screenshots/pagination_visible.png' });\n\n const pagination = page.locator('nav').filter({ hasText: /Previous|Next/i });\n await expect(pagination).toBeVisible();\n\n await expect(page.getByText('1')).toBeVisible();\n });\n\n test('Changing results per page in settings updates the list', async ({ page }) => {\n // Wait for data to load first\n await expect(page.locator('body')).toContainText('2024-01-01', { timeout: 10000 });\n await page.screenshot({ path: 'screenshots/before_settings_change.png' });\n\n // Open Settings\n await page.getByRole('button', { name: /Settings/i }).click();\n await expect(page.locator('text=Results per page')).toBeVisible();\n\n // Change to 5\n await page.getByLabel(/Results per page/i).selectOption('5');\n await page.getByRole('button', { name: /Save/i }).click();\n await page.waitForTimeout(500);\n\n await page.screenshot({ path: 'screenshots/after_settings_change.png' });\n\n // Verify sessions per page is 5 (only 5 should be visible)\n const sessionRows = page.locator('div[class*=\"SessionList\"] >> .border-b');\n await expect(sessionRows).toHaveCount(5);\n\n // Verify settings is saved in localStorage\n const localStorage = await page.evaluate(() => {\n return window.localStorage.getItem('baby-sleep-settings');\n });\n expect(localStorage).toContain('\"sessionsPerPage\":5');\n });\n\n test('Navigating through pages works', async ({ page }) => {\n // Wait for data to load first\n await expect(page.locator('body')).toContainText('2024-01-01', { timeout: 10000 });\n \n // Set to 5 via settings first\n await page.getByRole('button', { name: /Settings/i }).click();\n await page.getByLabel(/Results per page/i).selectOption('5');\n await page.getByRole('button', { name: /Save/i }).click();\n await page.waitForTimeout(500);\n\n const nextButton = page.getByRole('button', { name: /Next/i });\n await expect(nextButton).toBeVisible();\n\n await nextButton.click();\n await page.waitForTimeout(500);\n\n // Should now show sessions from index 5 to 9 (if total is 15)\n // Mocked start_time for i=5 is 2024-01-06\n await expect(page.getByText('2024-01-06')).toBeVisible();\n });\n});\n" +} +``` + +**Output:** +``` +Edit applied successfully. +``` + +--- + +## Assistant (Build · Gemma 4 26b A4B · 28.2s) + +_Thinking:_ + + +Now run the tests again. + + +**Tool: bash** + +**Input:** +```json +{ + "command": "npx playwright test frontend/tests/e2e/pagination.spec.ts --reporter=list", + "description": "Run the fixed pagination tests with regex mock and debugging.", + "workdir": "/Users/jhag/projects/babymonitor/frontend" +} +``` + +**Output:** +``` + +Running 3 tests using 3 workers + +>> GET http://localhost:5173/stats +>> GET http://localhost:5173/stats +>> GET http://localhost:5173/stats +<< 200 http://localhost:5173/stats +<< 200 http://localhost:5173/stats +<< 200 http://localhost:5173/stats +>> GET http://localhost:5173/@vite/client +>> GET http://localhost:5173/@vite/client +>> GET http://localhost:5173/src/main.tsx?t=1780218805882 +>> GET http://localhost:5173/@vite/client +>> GET http://localhost:5173/src/main.tsx?t=1780218805882 +>> GET http://localhost:5173/src/main.tsx?t=1780218805882 +>> GET http://localhost:5173/@react-refresh +>> GET http://localhost:5173/@react-refresh +>> GET http://localhost:5173/@react-refresh +<< 200 http://localhost:5173/@vite/client +>> GET http://localhost:5173/node_modules/vite/dist/client/env.mjs +<< 200 http://localhost:5173/@vite/client +>> GET http://localhost:5173/node_modules/vite/dist/client/env.mjs +<< 200 http://localhost:5173/@react-refresh +<< 200 http://localhost:5173/node_modules/vite/dist/client/env.mjs +<< 200 http://localhost:5173/src/main.tsx?t=1780218805882 +<< 200 http://localhost:5173/src/main.tsx?t=1780218805882 +<< 200 http://localhost:5173/src/main.tsx?t=1780218805882 +<< 200 http://localhost:5173/@vite/client +>> GET http://localhost:5173/node_modules/.vite/deps/react.js?v=a0c06352 +>> GET http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=a0c06352 +>> GET http://localhost:5173/src/index.css?t=1780218805882 +>> GET http://localhost:5173/src/App.tsx?t=1780218805882 +>> GET http://localhost:5173/node_modules/.vite/deps/@tanstack_react-query.js?v=a0c06352 +>> GET http://localhost:5173/node_modules/.vite/deps/react_jsx-dev-runtime.js?v=a0c06352 +>> GET http://localhost:5173/node_modules/.vite/deps/react.js?v=a0c06352 +>> GET http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=a0c06352 +>> GET http://localhost:5173/src/index.css?t=1780218805882 +>> GET http://localhost:5173/src/App.tsx?t=1780218805882 +>> GET http://localhost:5173/node_modules/.vite/deps/@tanstack_react-query.js?v=a0c06352 +>> GET http://localhost:5173/node_modules/.vite/deps/react_jsx-dev-runtime.js?v=a0c06352 +>> GET http://localhost:5173/node_modules/.vite/deps/react.js?v=a0c06352 +>> GET http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=a0c06352 +>> GET http://localhost:5173/src/index.css?t=1780218805882 +>> GET http://localhost:5173/src/App.tsx?t=1780218805882 +>> GET http://localhost:5173/node_modules/.vite/deps/@tanstack_react-query.js?v=a0c06352 +>> GET http://localhost:5173/node_modules/.vite/deps/react_jsx-dev-runtime.js?v=a0c06352 +>> GET http://localhost:5173/node_modules/vite/dist/client/env.mjs +<< 200 http://localhost:5173/@react-refresh +<< 200 http://localhost:5173/node_modules/.vite/deps/react.js?v=a0c06352 +>> GET http://localhost:5173/node_modules/.vite/deps/react-BDWiK9rz.js?v=a0c06352 +<< 200 http://localhost:5173/node_modules/.vite/deps/react_jsx-dev-runtime.js?v=a0c06352 +<< 200 http://localhost:5173/@react-refresh +<< 200 http://localhost:5173/node_modules/.vite/deps/react.js?v=a0c06352 +>> GET http://localhost:5173/node_modules/.vite/deps/react-BDWiK9rz.js?v=a0c06352 +<< 200 http://localhost:5173/node_modules/.vite/deps/react.js?v=a0c06352 +>> GET http://localhost:5173/node_modules/.vite/deps/react-BDWiK9rz.js?v=a0c06352 +<< 200 http://localhost:5173/src/index.css?t=1780218805882 +<< 200 http://localhost:5173/node_modules/vite/dist/client/env.mjs +<< 200 http://localhost:5173/node_modules/vite/dist/client/env.mjs +<< 200 http://localhost:5173/node_modules/.vite/deps/react-BDWiK9rz.js?v=a0c06352 +<< 200 http://localhost:5173/node_modules/.vite/deps/react-BDWiK9rz.js?v=a0c06352 +<< 200 http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=a0c06352 +<< 200 http://localhost:5173/src/index.css?t=1780218805882 +>> GET http://localhost:5173/node_modules/.vite/deps/react-dom.js?v=a0c06352 +<< 200 http://localhost:5173/node_modules/.vite/deps/react-BDWiK9rz.js?v=a0c06352 +<< 200 http://localhost:5173/node_modules/.vite/deps/react_jsx-dev-runtime.js?v=a0c06352 +<< 200 http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=a0c06352 +>> GET http://localhost:5173/node_modules/.vite/deps/react-dom.js?v=a0c06352 +<< 200 http://localhost:5173/src/App.tsx?t=1780218805882 +>> GET http://localhost:5173/node_modules/.vite/deps/react-router-dom.js?v=a0c06352 +>> GET http://localhost:5173/src/components/tracker/TrackerPage.tsx?t=1780161383326 +>> GET http://localhost:5173/src/components/stats/StatsPage.tsx?t=1780218805882 +>> GET http://localhost:5173/src/components/Navbar.tsx +<< 200 http://localhost:5173/src/index.css?t=1780218805882 +>> GET http://localhost:5173/src/components/Playground.tsx?t=1780218805882 +<< 200 http://localhost:5173/src/App.tsx?t=1780218805882 +>> GET http://localhost:5173/node_modules/.vite/deps/react-router-dom.js?v=a0c06352 +<< 200 http://localhost:5173/src/App.tsx?t=1780218805882 +>> GET http://localhost:5173/node_modules/.vite/deps/react-router-dom.js?v=a0c06352 +>> GET http://localhost:5173/src/components/tracker/TrackerPage.tsx?t=1780161383326 +>> GET http://localhost:5173/src/components/stats/StatsPage.tsx?t=1780218805882 +<< 200 http://localhost:5173/node_modules/.vite/deps/@tanstack_react-query.js?v=a0c06352 +>> GET http://localhost:5173/node_modules/.vite/deps/react_jsx-runtime.js?v=a0c06352 +<< 200 http://localhost:5173/node_modules/.vite/deps/react-dom.js?v=a0c06352 +>> GET http://localhost:5173/src/components/Navbar.tsx +>> GET http://localhost:5173/src/components/Playground.tsx?t=1780218805882 +>> GET http://localhost:5173/src/components/tracker/TrackerPage.tsx?t=1780161383326 +>> GET http://localhost:5173/src/components/stats/StatsPage.tsx?t=1780218805882 +>> GET http://localhost:5173/src/components/Navbar.tsx +>> GET http://localhost:5173/src/components/Playground.tsx?t=1780218805882 +<< 200 http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=a0c06352 +>> GET http://localhost:5173/node_modules/.vite/deps/react-dom.js?v=a0c06352 +<< 200 http://localhost:5173/node_modules/.vite/deps/react-dom.js?v=a0c06352 +<< 200 http://localhost:5173/src/components/tracker/TrackerPage.tsx?t=1780161383326 +<< 200 http://localhost:5173/src/components/Playground.tsx?t=1780218805882 +<< 200 http://localhost:5173/node_modules/.vite/deps/@tanstack_react-query.js?v=a0c06352 +>> GET http://localhost:5173/src/components/tracker/StatusBanner.tsx?t=1780161383326 +>> GET http://localhost:5173/node_modules/.vite/deps/react_jsx-runtime.js?v=a0c06352 +<< 200 http://localhost:5173/src/components/stats/StatsPage.tsx?t=1780218805882 +>> GET http://localhost:5173/src/components/tracker/StatusBanner.tsx?t=1780161383326 +>> GET http://localhost:5173/src/components/tracker/SleepButton.tsx?t=1780161383326 +>> GET http://localhost:5173/src/components/tracker/TimeAdjuster.tsx?t=1780160413016 +>> GET http://localhost:5173/src/components/tracker/TimeAdjuster.tsx?t=1780160413016 +>> GET http://localhost:5173/src/components/tracker/SleepButton.tsx?t=1780161383326 +>> GET http://localhost:5173/src/components/stats/DailySummary.tsx?t=1780161394841 +>> GET http://localhost:5173/src/components/stats/WakeupPlot.tsx?t=1780209705683 +>> GET http://localhost:5173/src/components/stats/BedtimePlot.tsx?t=1780209713826 +>> GET http://localhost:5173/src/components/stats/SleepTimetable.tsx?t=1780161394841 +>> GET http://localhost:5173/src/components/stats/SessionList.tsx?t=1780218805882 +>> GET http://localhost:5173/src/components/stats/IntervalPicker.tsx +>> GET http://localhost:5173/src/components/stats/SettingsPopover.tsx?t=1780218145201 +<< 200 http://localhost:5173/src/components/tracker/TrackerPage.tsx?t=1780161383326 +<< 200 http://localhost:5173/src/components/Navbar.tsx +>> GET http://localhost:5173/src/components/stats/IntervalPicker.tsx +>> GET http://localhost:5173/src/components/stats/SettingsPopover.tsx?t=1780218145201 +>> GET http://localhost:5173/src/components/stats/SummaryCards.tsx?t=1780210381087 +>> GET http://localhost:5173/src/components/stats/DailySummary.tsx?t=1780161394841 +>> GET http://localhost:5173/src/components/stats/SleepTimetable.tsx?t=1780161394841 +>> GET http://localhost:5173/src/components/stats/WakeupPlot.tsx?t=1780209705683 +>> GET http://localhost:5173/src/components/stats/BedtimePlot.tsx?t=1780209713826 +>> GET http://localhost:5173/src/components/stats/SessionList.tsx?t=1780218805882 +>> GET http://localhost:5173/src/components/ErrorBoundary.tsx +>> GET http://localhost:5173/src/hooks/useSettings.ts?t=1780218037679 +>> GET http://localhost:5173/src/hooks/useStats.ts?t=1780161394841 +>> GET http://localhost:5173/src/components/tracker/StatusBanner.tsx?t=1780161383326 +<< 200 http://localhost:5173/src/components/Playground.tsx?t=1780218805882 +>> GET http://localhost:5173/src/components/tracker/SleepButton.tsx?t=1780161383326 +>> GET http://localhost:5173/src/components/tracker/TimeAdjuster.tsx?t=1780160413016 +<< 200 http://localhost:5173/node_modules/.vite/deps/react_jsx-runtime.js?v=a0c06352 +<< 200 http://localhost:5173/src/components/tracker/TrackerPage.tsx?t=1780161383326 +<< 200 http://localhost:5173/src/components/stats/StatsPage.tsx?t=1780218805882 +>> GET http://localhost:5173/src/components/stats/IntervalPicker.tsx +>> GET http://localhost:5173/src/components/stats/SettingsPopover.tsx?t=1780218145201 +<< 200 http://localhost:5173/src/components/Navbar.tsx +>> GET http://localhost:5173/src/components/stats/SummaryCards.tsx?t=1780210381087 +>> GET http://localhost:5173/src/components/stats/DailySummary.tsx?t=1780161394841 +<< 200 http://localhost:5173/src/components/stats/StatsPage.tsx?t=1780218805882 +>> GET http://localhost:5173/src/components/stats/SleepTimetable.tsx?t=1780161394841 +>> GET http://localhost:5173/src/components/stats/WakeupPlot.tsx?t=1780209705683 +>> GET http://localhost:5173/src/components/stats/BedtimePlot.tsx?t=1780209713826 +>> GET http://localhost:5173/src/components/stats/SessionList.tsx?t=1780218805882 +>> GET http://localhost:5173/src/components/ErrorBoundary.tsx +>> GET http://localhost:5173/src/hooks/useSettings.ts?t=1780218037679 +<< 200 http://localhost:5173/node_modules/.vite/deps/react-router-dom.js?v=a0c06352 +>> GET http://localhost:5173/src/hooks/useStats.ts?t=1780161394841 +<< 200 http://localhost:5173/src/components/tracker/StatusBanner.tsx?t=1780161383326 +>> GET http://localhost:5173/src/components/stats/SummaryCards.tsx?t=1780210381087 +<< 200 http://localhost:5173/node_modules/.vite/deps/react-dom.js?v=a0c06352 +>> GET http://localhost:5173/src/components/ErrorBoundary.tsx +>> GET http://localhost:5173/src/hooks/useSettings.ts?t=1780218037679 +>> GET http://localhost:5173/src/hooks/useStats.ts?t=1780161394841 +<< 200 http://localhost:5173/node_modules/.vite/deps/react_jsx-runtime.js?v=a0c06352 +>> GET http://localhost:5173/src/hooks/useSleepSession.ts?t=1780161383326 +>> GET http://localhost:5173/src/utils/time.ts?t=1780152909168 +<< 200 http://localhost:5173/src/components/Navbar.tsx +<< 200 http://localhost:5173/src/components/stats/SettingsPopover.tsx?t=1780218145201 +<< 200 http://localhost:5173/src/components/tracker/StatusBanner.tsx?t=1780161383326 +>> GET http://localhost:5173/src/hooks/useSleepSession.ts?t=1780161383326 +>> GET http://localhost:5173/src/utils/time.ts?t=1780152909168 +<< 200 http://localhost:5173/src/components/tracker/SleepButton.tsx?t=1780161383326 +<< 200 http://localhost:5173/src/components/tracker/TimeAdjuster.tsx?t=1780160413016 +<< 200 http://localhost:5173/src/components/stats/IntervalPicker.tsx +<< 200 http://localhost:5173/src/components/Playground.tsx?t=1780218805882 +<< 200 http://localhost:5173/node_modules/.vite/deps/@tanstack_react-query.js?v=a0c06352 +>> GET http://localhost:5173/node_modules/.vite/deps/react_jsx-runtime.js?v=a0c06352 +<< 200 http://localhost:5173/src/components/tracker/StatusBanner.tsx?t=1780161383326 +>> GET http://localhost:5173/src/hooks/useSleepSession.ts?t=1780161383326 +>> GET http://localhost:5173/src/utils/time.ts?t=1780152909168 +<< 200 http://localhost:5173/src/components/stats/DailySummary.tsx?t=1780161394841 +>> GET http://localhost:5173/node_modules/.vite/deps/recharts.js?v=a0c06352 +<< 200 http://localhost:5173/node_modules/.vite/deps/react-router-dom.js?v=a0c06352 +<< 200 http://localhost:5173/node_modules/.vite/deps/react_jsx-dev-runtime.js?v=a0c06352 +<< 200 http://localhost:5173/src/components/tracker/SleepButton.tsx?t=1780161383326 +<< 200 http://localhost:5173/src/components/stats/SleepTimetable.tsx?t=1780161394841 +<< 200 http://localhost:5173/src/components/tracker/TimeAdjuster.tsx?t=1780160413016 +<< 200 http://localhost:5173/src/components/tracker/TimeAdjuster.tsx?t=1780160413016 +<< 200 http://localhost:5173/src/components/tracker/SleepButton.tsx?t=1780161383326 +<< 200 http://localhost:5173/src/components/stats/IntervalPicker.tsx +<< 200 http://localhost:5173/src/components/stats/DailySummary.tsx?t=1780161394841 +>> GET http://localhost:5173/node_modules/.vite/deps/recharts.js?v=a0c06352 +<< 200 http://localhost:5173/node_modules/.vite/deps/react-router-dom.js?v=a0c06352 +<< 200 http://localhost:5173/src/components/stats/BedtimePlot.tsx?t=1780209713826 +<< 200 http://localhost:5173/src/components/stats/SleepTimetable.tsx?t=1780161394841 +<< 200 http://localhost:5173/src/components/stats/SessionList.tsx?t=1780218805882 +>> GET http://localhost:5173/src/components/stats/Pagination.tsx +<< 200 http://localhost:5173/src/components/stats/SettingsPopover.tsx?t=1780218145201 +<< 200 http://localhost:5173/src/components/stats/WakeupPlot.tsx?t=1780209705683 +>> GET http://localhost:5173/node_modules/.vite/deps/lucide-react.js?v=a0c06352 +<< 200 http://localhost:5173/src/components/stats/IntervalPicker.tsx +<< 200 http://localhost:5173/src/components/stats/BedtimePlot.tsx?t=1780209713826 +<< 200 http://localhost:5173/src/components/stats/SummaryCards.tsx?t=1780210381087 +<< 200 http://localhost:5173/src/components/stats/WakeupPlot.tsx?t=1780209705683 +>> GET http://localhost:5173/node_modules/.vite/deps/lucide-react.js?v=a0c06352 +<< 200 http://localhost:5173/src/components/stats/SummaryCards.tsx?t=1780210381087 +<< 200 http://localhost:5173/src/components/ErrorBoundary.tsx +<< 200 http://localhost:5173/src/hooks/useSettings.ts?t=1780218037679 +<< 200 http://localhost:5173/src/components/ErrorBoundary.tsx +<< 200 http://localhost:5173/src/hooks/useSettings.ts?t=1780218037679 +<< 200 http://localhost:5173/src/hooks/useStats.ts?t=1780161394841 +>> GET http://localhost:5173/src/api/client.ts?t=1780161383326 +<< 200 http://localhost:5173/src/hooks/useStats.ts?t=1780161394841 +<< 200 http://localhost:5173/node_modules/.vite/deps/react_jsx-runtime.js?v=a0c06352 +<< 200 http://localhost:5173/src/hooks/useSleepSession.ts?t=1780161383326 +<< 200 http://localhost:5173/src/components/stats/SleepTimetable.tsx?t=1780161394841 +<< 200 http://localhost:5173/src/components/stats/WakeupPlot.tsx?t=1780209705683 +>> GET http://localhost:5173/node_modules/.vite/deps/recharts.js?v=a0c06352 +<< 200 http://localhost:5173/src/components/stats/SettingsPopover.tsx?t=1780218145201 +<< 200 http://localhost:5173/src/components/stats/BedtimePlot.tsx?t=1780209713826 +<< 200 http://localhost:5173/src/components/stats/SessionList.tsx?t=1780218805882 +>> GET http://localhost:5173/src/api/client.ts?t=1780161383326 +>> GET http://localhost:5173/src/components/stats/Pagination.tsx +>> GET http://localhost:5173/node_modules/.vite/deps/lucide-react.js?v=a0c06352 +<< 200 http://localhost:5173/src/utils/time.ts?t=1780152909168 +<< 200 http://localhost:5173/src/hooks/useSleepSession.ts?t=1780161383326 +<< 200 http://localhost:5173/src/components/stats/SummaryCards.tsx?t=1780210381087 +<< 200 http://localhost:5173/src/components/stats/DailySummary.tsx?t=1780161394841 +<< 200 http://localhost:5173/src/components/stats/SessionList.tsx?t=1780218805882 +>> GET http://localhost:5173/src/components/stats/Pagination.tsx +<< 200 http://localhost:5173/src/components/ErrorBoundary.tsx +<< 200 http://localhost:5173/src/hooks/useSettings.ts?t=1780218037679 +<< 200 http://localhost:5173/src/utils/time.ts?t=1780152909168 +<< 200 http://localhost:5173/src/hooks/useStats.ts?t=1780161394841 +>> GET http://localhost:5173/src/api/client.ts?t=1780161383326 +<< 200 http://localhost:5173/src/hooks/useSleepSession.ts?t=1780161383326 +<< 200 http://localhost:5173/src/components/stats/Pagination.tsx +<< 200 http://localhost:5173/src/api/client.ts?t=1780161383326 +<< 200 http://localhost:5173/src/utils/time.ts?t=1780152909168 +<< 200 http://localhost:5173/src/components/stats/Pagination.tsx +<< 200 http://localhost:5173/src/api/client.ts?t=1780161383326 +<< 200 http://localhost:5173/src/api/client.ts?t=1780161383326 +<< 200 http://localhost:5173/src/components/stats/Pagination.tsx +<< 200 http://localhost:5173/node_modules/.vite/deps/recharts.js?v=a0c06352 +<< 200 http://localhost:5173/node_modules/.vite/deps/lucide-react.js?v=a0c06352 +<< 200 http://localhost:5173/node_modules/.vite/deps/lucide-react.js?v=a0c06352 +<< 200 http://localhost:5173/node_modules/.vite/deps/lucide-react.js?v=a0c06352 +<< 200 http://localhost:5173/node_modules/.vite/deps/recharts.js?v=a0c06352 +<< 200 http://localhost:5173/node_modules/.vite/deps/recharts.js?v=a0c06352 +>> GET http://localhost:5173/api/stats/daily-summary?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z&day_start=07%3A00&day_end=20%3A00 +>> GET http://localhost:5173/api/stats/daily-summary?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z&day_start=07%3A00&day_end=20%3A00 +>> GET http://localhost:5173/api/stats/timetable?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z&day_start=07%3A00&day_end=20%3A00 +>> GET http://localhost:5173/api/stats/timetable?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z&day_start=07%3A00&day_end=20%3A00 +>> GET http://localhost:5173/api/stats/wake-sleep-times?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z&wakeup_ref=02%3A00&bedtime_ref=17%3A00 +>> GET http://localhost:5173/api/sessions +>> GET http://localhost:5173/api/stats/wake-sleep-times?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z&wakeup_ref=02%3A00&bedtime_ref=17%3A00 +>> GET http://localhost:5173/api/sessions +Intercepted request to: http://localhost:5173/api/sessions +Intercepted request to: http://localhost:5173/api/sessions +>> GET http://localhost:5173/api/stats/daily-summary?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z&day_start=07%3A00&day_end=20%3A00 +>> GET http://localhost:5173/api/stats/timetable?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z&day_start=07%3A00&day_end=20%3A00 +>> GET http://localhost:5173/api/stats/wake-sleep-times?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z&wakeup_ref=02%3A00&bedtime_ref=17%3A00 +>> GET http://localhost:5173/api/sessions +Intercepted request to: http://localhost:5173/api/sessions +>> GET http://localhost:5173/api/stats/summary?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z +>> GET http://localhost:5173/api/stats/summary?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z +>> GET http://localhost:5173/api/stats/summary?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z +<< 200 http://localhost:5173/api/sessions +<< 200 http://localhost:5173/api/sessions +<< 200 http://localhost:5173/api/sessions +<< 200 http://localhost:5173/api/stats/daily-summary?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z&day_start=07%3A00&day_end=20%3A00 +<< 200 http://localhost:5173/api/stats/timetable?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z&day_start=07%3A00&day_end=20%3A00 +<< 200 http://localhost:5173/api/stats/daily-summary?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z&day_start=07%3A00&day_end=20%3A00 +<< 200 http://localhost:5173/api/stats/wake-sleep-times?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z&wakeup_ref=02%3A00&bedtime_ref=17%3A00 +<< 200 http://localhost:5173/api/stats/daily-summary?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z&day_start=07%3A00&day_end=20%3A00 +<< 200 http://localhost:5173/api/stats/timetable?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z&day_start=07%3A00&day_end=20%3A00 +<< 200 http://localhost:5173/api/stats/wake-sleep-times?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z&wakeup_ref=02%3A00&bedtime_ref=17%3A00 +<< 200 http://localhost:5173/api/stats/wake-sleep-times?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z&wakeup_ref=02%3A00&bedtime_ref=17%3A00 +<< 200 http://localhost:5173/api/stats/timetable?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z&day_start=07%3A00&day_end=20%3A00 +<< 200 http://localhost:5173/api/stats/summary?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z +<< 200 http://localhost:5173/api/stats/summary?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z +<< 200 http://localhost:5173/api/stats/summary?from_date=2026-05-23T22%3A00%3A00.000Z&to_date=2026-05-31T21%3A59%3A59.000Z + ✘ 2 [chromium] › tests/e2e/pagination.spec.ts:27:3 › Pagination › Pagination component is visible when sessions exceed page size (5.4s) + ✘ 3 [chromium] › tests/e2e/pagination.spec.ts:38:3 › Pagination › Changing results per page in settings updates the list (6.3s) + ✘ 1 [chromium] › tests/e2e/pagination.spec.ts:65:3 › Pagination › Navigating through pages works (6.8s) + + + 1) [chromium] › tests/e2e/pagination.spec.ts:27:3 › Pagination › Pagination component is visible when sessions exceed page size + + Error: expect(locator).toBeVisible() failed + + Locator: locator('nav').filter({ hasText: /Previous|Next/i }) + Expected: visible + Timeout: 5000ms + Error: element(s) not found + + Call log: +  - Expect "toBeVisible" with timeout 5000ms +  - waiting for locator('nav').filter({ hasText: /Previous|Next/i }) + + + 31 | + 32 | const pagination = page.locator('nav').filter({ hasText: /Previous|Next/i }); + > 33 | await expect(pagination).toBeVisible(); + | ^ + 34 | + 35 | await expect(page.getByText('1')).toBeVisible(); + 36 | }); + at /Users/jhag/projects/babymonitor/frontend/tests/e2e/pagination.spec.ts:33:30 + + attachment #1: screenshot (image/png) ────────────────────────────────────────────────────────── + test-results/e2e-pagination-Pagination--1edf2-n-sessions-exceed-page-size-chromium/test-failed-1.png + ──────────────────────────────────────────────────────────────────────────────────────────────── + + Error Context: test-results/e2e-pagination-Pagination--1edf2-n-sessions-exceed-page-size-chromium/error-context.md + + 2) [chromium] › tests/e2e/pagination.spec.ts:38:3 › Pagination › Changing results per page in settings updates the list + + Error: expect(locator).toHaveCount(expected) failed + + Locator: locator('div[class*="SessionList"]').locator('.border-b') + Expected: 5 + Received: 0 + Timeout: 5000ms + + Call log: +  - Expect "toHaveCount" with timeout 5000ms +  - waiting for locator('div[class*="SessionList"]').locator('.border-b') +  9 × locator resolved to 0 elements +  - unexpected value "0" + + + 54 | // Verify sessions per page is 5 (only 5 should be visible) + 55 | const sessionRows = page.locator('div[class*="SessionList"] >> .border-b'); + > 56 | await expect(sessionRows).toHaveCount(5); + | ^ + 57 | + 58 | // Verify settings is saved in localStorage + 59 | const localStorage = await page.evaluate(() => { + at /Users/jhag/projects/babymonitor/frontend/tests/e2e/pagination.spec.ts:56:31 + + attachment #1: screenshot (image/png) ────────────────────────────────────────────────────────── + test-results/e2e-pagination-Pagination--d7b84-n-settings-updates-the-list-chromium/test-failed-1.png + ──────────────────────────────────────────────────────────────────────────────────────────────── + + Error Context: test-results/e2e-pagination-Pagination--d7b84-n-settings-updates-the-list-chromium/error-context.md + + 3) [chromium] › tests/e2e/pagination.spec.ts:65:3 › Pagination › Navigating through pages works ── + + Error: expect(locator).toBeVisible() failed + + Locator: getByText('2024-01-06') + Expected: visible + Timeout: 5000ms + Error: element(s) not found + + Call log: +  - Expect "toBeVisible" with timeout 5000ms +  - waiting for getByText('2024-01-06') + + + 81 | // Should now show sessions from index 5 to 9 (if total is 15) + 82 | // Mocked start_time for i=5 is 2024-01-06 + > 83 | await expect(page.getByText('2024-01-06')).toBeVisible(); + | ^ + 84 | }); + 85 | }); + 86 | + at /Users/jhag/projects/babymonitor/frontend/tests/e2e/pagination.spec.ts:83:48 + + attachment #1: screenshot (image/png) ────────────────────────────────────────────────────────── + test-results/e2e-pagination-Pagination-Navigating-through-pages-works-chromium/test-failed-1.png + ──────────────────────────────────────────────────────────────────────────────────────────────── + + Error Context: test-results/e2e-pagination-Pagination-Navigating-through-pages-works-chromium/error-context.md + + 3 failed + [chromium] › tests/e2e/pagination.spec.ts:27:3 › Pagination › Pagination component is visible when sessions exceed page size + [chromium] › tests/e2e/pagination.spec.ts:38:3 › Pagination › Changing results per page in settings updates the list + [chromium] › tests/e2e/pagination.spec.ts:65:3 › Pagination › Navigating through pages works ─── + +``` + +--- + +## Assistant (Build · Gemma 4 26b A4B) + +--- + +## User + +Please continue, you got interrupted. + +--- + +## Assistant (Build · Gemma 4 26b A4B) + +--- + +## User + +Please create a hand-off document "handoff.md". It should include + +- Your todo +- The original plan +- The progress you've made +- What is left +- A summerized progress of what you have found difficult, what you want to do next. +- ANything else you might need to be able to let a coworker continue this work. + +--- + +## Assistant (Build · Gemma 4 26b A4B) + +--- +