Add migration script and work on tests

This commit is contained in:
2026-05-17 19:15:55 +02:00
parent 81120547ae
commit e23f8904ef
12 changed files with 2300 additions and 2658 deletions
+108
View File
@@ -0,0 +1,108 @@
import sqlite3
from datetime import datetime, timedelta
SOURCE_DB = 'backend/data/upload.db'
DEST_DB = 'backend/data/sleep.db'
def migrate():
try:
src_conn = sqlite3.connect(SOURCE_DB)
src_cur = src_conn.cursor()
dest_conn = sqlite3.connect(DEST_DB)
dest_cur = dest_conn.cursor()
print(f"Connecting to {SOURCE_DB} and {DEST_DB}...")
# 1. Truncate the destination table
print("Truncating sleep_session table...")
dest_cur.execute("DELETE FROM sleep_session")
dest_conn.commit()
# Fetch all records from the Sleep table
print("Fetching records from source...")
src_cur.execute('SELECT "Time", "Duration", "Note" FROM "Sleep"')
rows = src_cur.fetchall()
if not rows:
print("No records found in source Sleep table.")
return
migrated_count = 0
error_count = 0
now = datetime.now()
for row in rows:
timestamp_seconds, duration_minutes, note = row
# Convert Unix timestamp to datetime object
try:
start_time = datetime.fromtimestamp(timestamp_seconds)
except Exception as e:
print(f"Error converting timestamp {timestamp_seconds}: {e}")
error_count += 1
continue
# Calculate end time based on duration (NOW ASSUMED MINUTES)
end_time = start_time + timedelta(minutes=duration_minutes)
# VERIFICATION 1: Check if conversion matches source duration
actual_duration_minutes = (end_time - start_time).total_seconds() / 60.0
if abs(actual_duration_minutes - duration_minutes) > 0.1:
print(f"CRITICAL ERROR: Duration mismatch for timestamp {timestamp_seconds}. Source (min): {duration_minutes}, Calculated (min): {actual_duration_minutes}")
error_count += 1
continue
notes = note if note is not None else ""
# Insert the new record
try:
dest_cur.execute(
"""
INSERT INTO sleep_session (start_time, end_time, notes, created_at, updated_at)
VALUES (?, ?, ?, ?, ?)
""",
(
start_time.strftime('%Y-%m-%d %H:%M:%S.%f'),
end_time.strftime('%Y-%m-%d %H:%M:%S.%f'),
notes,
now.strftime('%Y-%m-%d %H:%M:%S'),
now.strftime('%Y-%m-%d %H:%M:%S')
)
)
migrated_count += 1
except Exception as e:
print(f"Failed to insert record for timestamp {timestamp_seconds}: {e}")
error_count += 1
dest_conn.commit()
print(f"Migration complete.")
print(f"Successfully migrated: {migrated_count}")
print(f"Errors encountered: {error_count}")
# 2. FINAL VERIFICATION
print("Running final verification...")
dest_cur.execute("SELECT start_time, end_time FROM sleep_session")
dest_rows = dest_cur.fetchall()
if len(dest_rows) != migrated_count:
print(f"VERIFICATION FAILED: Count mismatch. Source attempted: {migrated_count}, Destination count: {len(dest_rows)}")
else:
print("VERIFICATION SUCCESS: Count matches.")
# Sample duration verification in destination
print("Verifying sample durations from destination...")
for i, (st_str, et_str) in enumerate(dest_rows[:5]):
st = datetime.strptime(st_str, '%Y-%m-%d %H:%M:%S.%f')
et = datetime.strptime(et_str, '%Y-%m-%d %H:%M:%S.%f')
duration_sec = (et - st).total_seconds()
print(f" Sample {i+1}: Start={st_str}, End={et_str}, Duration={duration_sec/60.0:.1f}m")
except Exception as e:
print(f"An error occurred during migration: {e}")
finally:
if 'src_conn' in locals(): src_conn.close()
if 'dest_conn' in locals(): dest_conn.close()
if __name__ == "__main__":
migrate()
+685 -2577
View File
File diff suppressed because it is too large Load Diff
+23 -22
View File
@@ -7,35 +7,36 @@
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"test": "vitest run",
"test": "DEBUG_PRINT_LIMIT=0 vitest run --reporter=dot --no-color",
"test:all": "vitest run && npx playwright test",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
},
"dependencies": {
"@tanstack/react-query": "^5.0.0",
"lucide-react": "^0.470.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.1.0",
"recharts": "^2.15.0"
"@tanstack/react-query": "^5.100.5",
"lucide-react": "^1.11.0",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"react-router-dom": "^7.14.2",
"recharts": "^3.8.1"
},
"devDependencies": {
"@playwright/test": "^1.59.1",
"@tailwindcss/vite": "^4.0.0",
"@testing-library/jest-dom": "^6.6.0",
"@testing-library/react": "^16.1.0",
"@testing-library/user-event": "^14.5.0",
"@tailwindcss/vite": "^4.2.4",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/lodash": "^4.17.24",
"@types/react": "^18.3.0",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.0",
"@vitest/ui": "^2.1.0",
"autoprefixer": "^10.4.20",
"jsdom": "^29.0.2",
"postcss": "^8.4.49",
"tailwindcss": "^4.0.0",
"typescript": "^5.7.0",
"vite": "^6.0.0",
"vitest": "2.1.9"
"@types/node": "^25.6.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"@vitest/ui": "^4.1.5",
"autoprefixer": "^10.5.0",
"jsdom": "^29.1.0",
"postcss": "^8.5.12",
"tailwindcss": "^4.2.4",
"typescript": "^6.0.3",
"vite": "^8.0.10",
"vitest": "^4.1.5"
}
}
+14
View File
@@ -0,0 +1,14 @@
> baby-sleep-tracker-frontend@0.1.0 test
> DEBUG_PRINT_LIMIT=0 vitest run --reporter=dot --no-color
RUN v4.1.5 /Users/jhag/projects/babymonitor/frontend
··································xxx····
Test Files 1 failed | 6 passed (7)
Tests 3 failed | 38 passed (41)
Start at 15:55:21
Duration 924ms (transform 166ms, setup 0ms, import 600ms, tests 173ms, environment 4.31s)
@@ -0,0 +1,102 @@
import { render, screen } from '@testing-library/react';
import { vi, describe, it, expect } from 'vitest';
import Timetable from './SleepTimetable';
import { useStats } from '../../hooks/useStats';
vi.mock('../../hooks/useStats', () => ({
useStats: vi.fn(),
}));
describe('SleepTimetable', () => {
const defaultProps = {
from_date: '2025-04-20',
to_date: '2025-04-21',
day_start: '22:00',
day_end: '08:00',
};
it('renders loading state', () => {
(useStats as any).mockReturnValue({ timetable: null, isLoading: true });
render(<Timetable {...defaultProps} />);
expect(screen.getByText(/Loading timetable.../i)).toBeInTheDocument();
});
it('renders empty state when no data is available', () => {
(useStats as any).mockReturnValue({ timetable: { slots: [] }, isLoading: false });
render(<Timetable {...defaultProps} />);
expect(screen.getByText(/No data available/i)).toBeInTheDocument();
});
it('renders the grid correctly with hourly rows and biennial labels', () => {
(useStats as any).mockReturnValue({
timetable: {
slots: [
{
date: '2025-04-20',
sleeping: true,
slot_start: '01:00',
slot_end: '05:00',
},
],
},
isLoading: false,
});
render(<Timetable {...defaultProps} />);
// Check for day headers
expect(screen.getByText(/SUN/i)).toBeInTheDocument(); // 2025-04-20 is Sunday
expect(screen.getByText(/MON/i)).toBeInTheDocument(); // 2025-04-21 is Monday
// Check for time labels (every 2 hours: 00, 02, 04...)
expect(screen.getByText('00:00')).toBeInTheDocument();
expect(screen.getByText('02:00')).toBeInTheDocument();
expect(screen.getByText('04:00')).toBeInTheDocument();
expect(screen.getByText('22:00')).toBeInTheDocument();
// Verify non-labeled hours (every hour that isn't %120 == 0)
// We can't easily check for empty text nodes in Vitest/RTL without more precision
// but we can check that 01:00 is NOT present as a text label
expect(screen.queryByText('01:00')).not.toBeInTheDocument();
});
it('colors sleeping intervals correctly', () => {
(useStats as any).mockReturnValue({
timetable: {
slots: [
{
date: '2025-04-20',
sleeping: true,
slot_start: '01:00',
slot_end: '03:00', // Covers 01:00 and 02:00
},
],
},
isLoading: false,
});
const { container } = render(<Timetable {...defaultProps} />);
// We are looking for the grid cells.
// Since they don't have text, we look at their classes/backgrounds.
const cells = container.querySelectorAll('.h-8');
// Total cells = 24 rows * (1 label col + 2 days) = 72? No, the structure is:
// Header row has cells. Time grid rows have (1 label + 2 days) = 3 cells each.
// Total elements with .h-8: 1 (header corner) + days.length * 1 (day headers) + 24 * 3 = 75?
// Actually, the code has:
// Header row: header corner + days.map -> 1 + 2 = 3 cells
// Time Rows: (label cell) + days.map -> 24 * 3 = 72 cells
// Total = 75.
// The sleeping cell for 01:00 on 2025-04-20 should be bg-green-500
// The cell for 02:00 on 2025-04-20 should be bg-green-500
// The cell for 03:00 on 2025-04-20 should be bg-gray-50
// Let's just check if ANY cell has bg-green-500 and ALL non-sleeping have bg-gray-50
const greenCells = container.querySelectorAll('.bg-green-500');
expect(greenCells.length).toBeGreaterThan(0);
const grayCells = container.querySelectorAll('.bg-gray-50');
expect(grayCells.length).toBeGreaterThan(0);
});
});
@@ -1,6 +1,6 @@
import React, { useMemo } from 'react';
import { useStats } from '../../hooks/useStats';
import { timeToMinutes, formatMinutesAfter } from '../../utils/time';
import { timeToMinutes } from '../../utils/time';
interface TimetableProps {
from_date: string;
@@ -25,7 +25,7 @@ const Timetable: React.FC<TimetableProps> = ({ from_date, to_date, day_start, da
}, [from_date, to_date]);
const timeSteps = useMemo(() => {
return Array.from({ length: 12 }, (_, i) => i * 120);
return Array.from({ length: 24 }, (_, i) => i * 60);
}, []);
if (isLoading) return <div className="text-center p-4">Loading timetable...</div>;
@@ -62,58 +62,56 @@ const Timetable: React.FC<TimetableProps> = ({ from_date, to_date, day_start, da
const currentMins = new Date().getHours() * 60 + new Date().getMinutes();
// Total rows: 1 (header) + 12 (time steps) = 13
const totalRows = 13;
return (
<div className="bg-white p-4 rounded-xl shadow-sm border border-gray-200 overflow-x-auto">
<h3 className="text-lg font-semibold mb-4 text-gray-800">Sleep Timetable</h3>
<div className="min-w-[600px] flex flex-col gap-px">
{/* Header Row */}
<div
className="grid"
style={{
gridTemplateColumns: `4rem repeat(${days.length}, minmax(40px, 1fr))`
}}
>
<div className="h-8" /> {/* Empty corner cell */}
{days.map((day) => (
<div key={day} className="text-center text-[10px] font-medium text-gray-500 uppercase flex items-center justify-center h-8">
{getDayName(day)}
</div>
))}
</div>
return (
<div className="bg-white p-4 rounded-xl shadow-sm border border-gray-200 overflow-x-auto">
<h3 className="text-lg font-semibold mb-4 text-gray-800">Sleep Timetable</h3>
<div className="min-w-[600px] flex flex-col gap-px">
{/* Header Row */}
<div
className="grid"
style={{
gridTemplateColumns: `4rem repeat(${days.length}, minmax(40px, 1fr))`
}}
>
<div className="h-8" /> {/* Empty corner cell */}
{days.map((day) => (
<div key={day} className="text-center text-[10px] font-medium text-gray-500 uppercase flex items-center justify-center h-8">
{getDayName(day)}
</div>
))}
</div>
{/* Time Grid Container */}
<div className="relative">
<div
className="grid"
style={{
gridTemplateColumns: `4rem repeat(${days.length}, minmax(40px, 1fr))`
}}
>
{/* Time Rows */}
{timeSteps.map((step) => (
<React.Fragment key={step}>
{/* Time Label */}
<div className="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8">
{step % 120 === 0 ? formatTimeLabel(step) : ''}
</div>
{/* Grid Cells */}
{days.map((day) => {
const sleeping = isSleepingInInterval(day, step, step + 60);
return (
<div
key={`${day}-${step}`}
className={`h-8 w-full ${sleeping ? 'bg-green-500' : 'bg-gray-50'}`}
/>
);
})}
</React.Fragment>
))}
{/* Time Grid Container */}
<div className="relative">
<div
className="grid gap-px"
style={{
gridTemplateColumns: `4rem repeat(${days.length}, minmax(40px, 1fr))`
}}
>
{/* Time Rows */}
{timeSteps.map((step) => (
<React.Fragment key={step}>
{/* Time Label */}
<div className="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8">
{formatTimeLabel(step)}
</div>
{/* Grid Cells */}
{days.map((day) => {
const sleeping = isSleepingInInterval(day, step, step + 120);
return (
<div
key={`${day}-${step}`}
className={`h-8 w-full ${sleeping ? 'bg-green-500' : 'bg-gray-50'}`}
/>
);
})}
</React.Fragment>
))}
{/* Red Marker Line */}
{/* Red Marker Line */}
<div
className="absolute left-[4rem] right-0 h-0.5 bg-red-500 pointer-events-none z-10"
style={{
+650
View File
@@ -0,0 +1,650 @@
> baby-sleep-tracker-frontend@0.1.0 test
> vitest run --reporter=dot
RUN v2.1.9 /Users/jhag/projects/babymonitor/frontend
✓ tests/simple.test.ts (1 test) 1ms
✓ tests/unit/chartHelpers.test.ts (9 tests) 2ms
✓ tests/unit/time.test.ts (16 tests) 3ms
✓ tests/unit/StatusBanner.test.tsx (5 tests) 19ms
✓ tests/unit/TimeAdjuster.test.tsx (3 tests) 24ms
❯ src/components/stats/SleepTimetable.test.tsx (4 tests | 3 failed) 42ms
× SleepTimetable > renders loading state 17ms
→ Invalid Chai property: toBeInTheDocument
× SleepTimetable > renders empty state when no data is available 1ms
→ Invalid Chai property: toBeInTheDocument
× SleepTimetable > renders the grid correctly with hourly rows and biennial labels 19ms
→ Unable to find an element with the text: SUN. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.
Ignored nodes: comments, script, style
<body>
<div>
<div
class="bg-white p-4 rounded-xl shadow-sm border border-gray-200 overflow-x-auto"
>
<h3
class="text-lg font-semibold mb-4 text-gray-800"
>
Sleep Timetable
</h3>
<div
class="min-w-[600px] flex flex-col gap-px"
>
<div
class="grid"
style="grid-template-columns: 4rem repeat(2, minmax(40px, 1fr));"
>
<div
class="h-8"
/>
 
<div
class="text-center text-[10px] font-medium text-gray-500 uppercase flex items-center justify-center h-8"
>
Sun
</div>
<div
class="text-center text-[10px] font-medium text-gray-500 uppercase flex items-center justify-center h-8"
>
Mon
</div>
</div>
<div
class="relative"
>
<div
class="grid"
style="grid-template-columns: 4rem repeat(2, minmax(40px, 1fr));"
>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
00:00
</div>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-green-500"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
02:00
</div>
<div
class="h-8 w-full bg-green-500"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-green-500"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
04:00
</div>
<div
class="h-8 w-full bg-green-500"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
06:00
</div>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
08:00
</div>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
10:00
</div>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
12:00
</div>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
14:00
</div>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
16:00
</div>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
18:00
</div>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
20:00
</div>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
22:00
</div>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="absolute left-[4rem] right-0 h-0.5 bg-red-500 pointer-events-none z-10"
style="top: 84.02777777777779%; transform: translateY(-50%);"
/>
</div>
</div>
</div>
</div>
</div>
</body>
✓ tests/unit/SleepButton.test.tsx (3 tests) 62ms
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 3 ⎯⎯⎯⎯⎯⎯⎯
FAIL src/components/stats/SleepTimetable.test.tsx > SleepTimetable > renders loading state
Error: Invalid Chai property: toBeInTheDocument
❯ src/components/stats/SleepTimetable.test.tsx:21:53
19| (useStats as any).mockReturnValue({ timetable: null, isLoading: tr…
20| render(<Timetable {...defaultProps} />);
21| expect(screen.getByText(/Loading timetable.../i)).toBeInTheDocumen…
| ^
22| });
23|
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/3]⎯
FAIL src/components/stats/SleepTimetable.test.tsx > SleepTimetable > renders empty state when no data is available
Error: Invalid Chai property: toBeInTheDocument
❯ src/components/stats/SleepTimetable.test.tsx:27:50
25| (useStats as any).mockReturnValue({ timetable: { slots: [] }, isLo…
26| render(<Timetable {...defaultProps} />);
27| expect(screen.getByText(/No data available/i)).toBeInTheDocument();
| ^
28| });
29|
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/3]⎯
FAIL src/components/stats/SleepTimetable.test.tsx > SleepTimetable > renders the grid correctly with hourly rows and biennial labels
TestingLibraryElementError: Unable to find an element with the text: SUN. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.
Ignored nodes: comments, script, style
<body>
<div>
<div
class="bg-white p-4 rounded-xl shadow-sm border border-gray-200 overflow-x-auto"
>
<h3
class="text-lg font-semibold mb-4 text-gray-800"
>
Sleep Timetable
</h3>
<div
class="min-w-[600px] flex flex-col gap-px"
>
<div
class="grid"
style="grid-template-columns: 4rem repeat(2, minmax(40px, 1fr));"
>
<div
class="h-8"
/>
 
<div
class="text-center text-[10px] font-medium text-gray-500 uppercase flex items-center justify-center h-8"
>
Sun
</div>
<div
class="text-center text-[10px] font-medium text-gray-500 uppercase flex items-center justify-center h-8"
>
Mon
</div>
</div>
<div
class="relative"
>
<div
class="grid"
style="grid-template-columns: 4rem repeat(2, minmax(40px, 1fr));"
>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
00:00
</div>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-green-500"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
02:00
</div>
<div
class="h-8 w-full bg-green-500"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-green-500"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
04:00
</div>
<div
class="h-8 w-full bg-green-500"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
06:00
</div>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
08:00
</div>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
10:00
</div>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
12:00
</div>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
14:00
</div>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
16:00
</div>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
18:00
</div>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
20:00
</div>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
22:00
</div>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="absolute left-[4rem] right-0 h-0.5 bg-red-500 pointer-events-none z-10"
style="top: 84.02777777777779%; transform: translateY(-50%);"
/>
</div>
</div>
</div>
</div>
</div>
</body>
❯ Object.getElementError node_modules/@testing-library/dom/dist/config.js:37:19
❯ node_modules/@testing-library/dom/dist/query-helpers.js:76:38
❯ node_modules/@testing-library/dom/dist/query-helpers.js:52:17
❯ node_modules/@testing-library/dom/dist/query-helpers.js:95:19
❯ src/components/stats/SleepTimetable.test.tsx:48:19
46|
47| // Check for day headers
48| expect(screen.getByText('SUN')).toBeInTheDocument(); // 2025-04-20…
| ^
49| expect(screen.getByText('MON')).toBeInTheDocument(); // 2025-04-21…
50|
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[3/3]⎯
Test Files 1 failed | 6 passed (7)
Tests 3 failed | 38 passed (41)
Start at 20:10:38
Duration 718ms (transform 173ms, setup 0ms, collect 475ms, tests 155ms, environment 2.78s, prepare 242ms)
+650
View File
@@ -0,0 +1,650 @@
> baby-sleep-tracker-frontend@0.1.0 test
> vitest run --reporter=dot
RUN v2.1.9 /Users/jhag/projects/babymonitor/frontend
✓ tests/simple.test.ts (1 test) 1ms
✓ tests/unit/chartHelpers.test.ts (9 tests) 2ms
✓ tests/unit/time.test.ts (16 tests) 2ms
✓ tests/unit/StatusBanner.test.tsx (5 tests) 20ms
✓ tests/unit/TimeAdjuster.test.tsx (3 tests) 24ms
❯ src/components/stats/SleepTimetable.test.tsx (4 tests | 3 failed) 41ms
× SleepTimetable > renders loading state 15ms
→ Invalid Chai property: toBeInTheDocument
× SleepTimetable > renders empty state when no data is available 1ms
→ Invalid Chai property: toBeInTheDocument
× SleepTimetable > renders the grid correctly with hourly rows and biennial labels 20ms
→ Unable to find an element with the text: SUN. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.
Ignored nodes: comments, script, style
<body>
<div>
<div
class="bg-white p-4 rounded-xl shadow-sm border border-gray-200 overflow-x-auto"
>
<h3
class="text-lg font-semibold mb-4 text-gray-800"
>
Sleep Timetable
</h3>
<div
class="min-w-[600px] flex flex-col gap-px"
>
<div
class="grid"
style="grid-template-columns: 4rem repeat(2, minmax(40px, 1fr));"
>
<div
class="h-8"
/>
<div
class="text-center text-[10px] font-medium text-gray-500 uppercase flex items-center justify-center h-8"
>
Sun
</div>
<div
class="text-center text-[10px] font-medium text-gray-500 uppercase flex items-center justify-center h-8"
>
Mon
</div>
</div>
<div
class="relative"
>
<div
class="grid"
style="grid-template-columns: 4rem repeat(2, minmax(40px, 1fr));"
>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
00:00
</div>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-green-500"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
02:00
</div>
<div
class="h-8 w-full bg-green-500"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-green-500"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
04:00
</div>
<div
class="h-8 w-full bg-green-500"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
06:00
</div>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
08:00
</div>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
10:00
</div>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
12:00
</div>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
14:00
</div>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
16:00
</div>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
18:00
</div>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
20:00
</div>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
22:00
</div>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="absolute left-[4rem] right-0 h-0.5 bg-red-500 pointer-events-none z-10"
style="top: 84.16666666666667%; transform: translateY(-50%);"
/>
</div>
</div>
</div>
</div>
</div>
</body>
✓ tests/unit/SleepButton.test.tsx (3 tests) 62ms
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 3 ⎯⎯⎯⎯⎯⎯⎯
FAIL src/components/stats/SleepTimetable.test.tsx > SleepTimetable > renders loading state
Error: Invalid Chai property: toBeInTheDocument
❯ src/components/stats/SleepTimetable.test.tsx:21:53
19| (useStats as any).mockReturnValue({ timetable: null, isLoading: tr…
20| render(<Timetable {...defaultProps} />);
21| expect(screen.getByText(/Loading timetable.../i)).toBeInTheDocumen…
| ^
22| });
23|
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/3]⎯
FAIL src/components/stats/SleepTimetable.test.tsx > SleepTimetable > renders empty state when no data is available
Error: Invalid Chai property: toBeInTheDocument
❯ src/components/stats/SleepTimetable.test.tsx:27:50
25| (useStats as any).mockReturnValue({ timetable: { slots: [] }, isLo…
26| render(<Timetable {...defaultProps} />);
27| expect(screen.getByText(/No data available/i)).toBeInTheDocument();
| ^
28| });
29|
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/3]⎯
FAIL src/components/stats/SleepTimetable.test.tsx > SleepTimetable > renders the grid correctly with hourly rows and biennial labels
TestingLibraryElementError: Unable to find an element with the text: SUN. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.
Ignored nodes: comments, script, style
<body>
<div>
<div
class="bg-white p-4 rounded-xl shadow-sm border border-gray-200 overflow-x-auto"
>
<h3
class="text-lg font-semibold mb-4 text-gray-800"
>
Sleep Timetable
</h3>
<div
class="min-w-[600px] flex flex-col gap-px"
>
<div
class="grid"
style="grid-template-columns: 4rem repeat(2, minmax(40px, 1fr));"
>
<div
class="h-8"
/>
<div
class="text-center text-[10px] font-medium text-gray-500 uppercase flex items-center justify-center h-8"
>
Sun
</div>
<div
class="text-center text-[10px] font-medium text-gray-500 uppercase flex items-center justify-center h-8"
>
Mon
</div>
</div>
<div
class="relative"
>
<div
class="grid"
style="grid-template-columns: 4rem repeat(2, minmax(40px, 1fr));"
>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
00:00
</div>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-green-500"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
02:00
</div>
<div
class="h-8 w-full bg-green-500"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-green-500"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
04:00
</div>
<div
class="h-8 w-full bg-green-500"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
06:00
</div>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
08:00
</div>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
10:00
</div>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
12:00
</div>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
14:00
</div>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
16:00
</div>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
18:00
</div>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
20:00
</div>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
>
22:00
</div>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="text-[10px] text-gray-400 flex items-center justify-end pr-2 h-8"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="h-8 w-full bg-gray-50"
/>
<div
class="absolute left-[4rem] right-0 h-0.5 bg-red-500 pointer-events-none z-10"
style="top: 84.16666666666667%; transform: translateY(-50%);"
/>
</div>
</div>
</div>
</div>
</div>
</body>
❯ Object.getElementError node_modules/@testing-library/dom/dist/config.js:37:19
❯ node_modules/@testing-library/dom/dist/query-helpers.js:76:38
❯ node_modules/@testing-library/dom/dist/query-helpers.js:52:17
❯ node_modules/@testing-library/dom/dist/query-helpers.js:95:19
❯ src/components/stats/SleepTimetable.test.tsx:48:19
46|
47| // Check for day headers
48| expect(screen.getByText('SUN')).toBeInTheDocument(); // 2025-04-20…
| ^
49| expect(screen.getByText('MON')).toBeInTheDocument(); // 2025-04-21…
50|
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[3/3]⎯
Test Files 1 failed | 6 passed (7)
Tests 3 failed | 38 passed (41)
Start at 20:12:08
Duration 722ms (transform 112ms, setup 0ms, collect 483ms, tests 153ms, environment 2.74s, prepare 226ms)
+1 -1
View File
@@ -9,7 +9,7 @@
"paths": {
"@/*": ["src/*"]
},
"types": ["vite/client"]
"types": ["vite/client", "vitest/globals"]
},
"include": ["src"]
}
+1 -7
View File
@@ -5,12 +5,6 @@ import path from 'path'
export default defineConfig({
plugins: [react(), tailwindcss()],
test: {
globals: true,
environment: 'jsdom',
include: ['src/**/*.{test,spec}.{ts,tsx}', 'tests/**/*.{test,spec}.{ts,tsx}'],
exclude: ['**/tests/e2e/**', '**/node_modules/**'],
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
@@ -24,4 +18,4 @@ export default defineConfig({
},
},
},
})
})
+16
View File
@@ -0,0 +1,16 @@
import { defineConfig, mergeConfig } from 'vitest/config'
import viteConfig from './vite.config'
export default mergeConfig(
viteConfig,
defineConfig({
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./vitest.setup.ts'],
include: ['src/**/*.{test,spec}.{ts,tsx}', 'tests/**/*.{test,spec}.{ts,tsx}'],
exclude: ['**/tests/e2e/**', '**/node_modules/**'],
reporters: ['dot'],
},
})
)
+1
View File
@@ -0,0 +1 @@
import '@testing-library/jest-dom/vitest';