trainUs/tests/test_step1_database.py
David 352a3d5e88
Some checks are pending
CI / Lint & Format (push) Waiting to run
CI / Build (push) Waiting to run
CI / Tests (${{ matrix.browser }}) (chromium) (push) Waiting to run
CI / Tests (${{ matrix.browser }}) (firefox) (push) Waiting to run
Initial commit
2026-06-18 13:06:57 +02:00

373 lines
14 KiB
Python

"""
Step 1 — Database Layer Tests
- test_db_initializes: Database initializes without errors
- test_uuid_generated: A UUID is generated and stored in settings
- test_app_version_stored: App version is stored in settings
- test_default_settings: Default settings (user_name, language, theme) are created
- test_exercise_crud: Full CRUD cycle on exercises
- test_exercise_search: Search exercises by title
- test_data_survives_reload: Data persists across page reload
- test_settings_crud: Settings can be read and written
- test_schema_tables_exist: All expected tables exist in the schema
- test_foreign_keys_enabled: Foreign key constraints are enforced- test_combo_none_type: Combo can be created with type NONE
- test_session_item_repetitions: Session items support repetitions field
- test_collection_crud: Full CRUD cycle on collections (renamed from groups)
- test_execution_log_recent_and_count: getRecent and getCountSince work correctly"""
from playwright.sync_api import Page, expect
def _wait_for_db(page: Page, app_url: str):
"""Navigate and wait for the database to be ready."""
page.goto(app_url)
page.wait_for_function("document.documentElement.getAttribute('data-db-ready') === 'true'", timeout=15000)
def test_db_initializes(page: Page, app_url: str):
"""Database initializes and sets the data-db-ready attribute."""
page.goto(app_url)
page.wait_for_function(
"document.documentElement.getAttribute('data-db-ready') === 'true'"
" || document.documentElement.getAttribute('data-db-error') !== null",
timeout=15000,
)
error = page.evaluate("document.documentElement.getAttribute('data-db-error')")
assert error is None, f"DB initialization failed: {error}"
def test_uuid_generated(page: Page, app_url: str):
"""A UUID is generated on first launch and stored in settings."""
_wait_for_db(page, app_url)
uuid = page.evaluate("async () => await window.__repos.settings.get('user_uuid')")
assert uuid is not None, "No user_uuid found in settings"
# UUID v4 format: 8-4-4-4-12 hex chars
import re
assert re.match(
r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$',
uuid
), f"user_uuid is not a valid UUID: {uuid}"
def test_app_version_stored(page: Page, app_url: str):
"""App version from package.json is stored in settings."""
_wait_for_db(page, app_url)
version = page.evaluate("async () => await window.__repos.settings.get('app_version')")
assert version is not None, "No app_version found in settings"
assert version == "1.0.0", f"Unexpected app_version: {version}"
def test_default_settings(page: Page, app_url: str):
"""Default settings are created on first launch."""
_wait_for_db(page, app_url)
settings = page.evaluate("async () => await window.__repos.settings.getAll()")
assert settings["user_name"] == "User", f"Unexpected user_name: {settings.get('user_name')}"
assert settings["language"] == "en", f"Unexpected language: {settings.get('language')}"
assert settings["theme"] == "light", f"Unexpected theme: {settings.get('theme')}"
assert settings["db_created_at"] is not None, "db_created_at should be set on first launch"
def test_db_created_at_is_valid_iso_timestamp(page: Page, app_url: str):
"""db_created_at is stored as a valid ISO-8601 timestamp on first launch."""
_wait_for_db(page, app_url)
created_at = page.evaluate("async () => await window.__repos.settings.get('db_created_at')")
assert created_at is not None, "db_created_at should be set"
parsed = page.evaluate(f"new Date('{created_at}').getTime()")
assert parsed > 0, f"db_created_at is not a valid date: {created_at}"
def test_exercise_crud(page: Page, app_url: str):
"""Full CRUD cycle: create, read, update, delete an exercise."""
_wait_for_db(page, app_url)
# Create
ex_id = page.evaluate("""
window.__repos.exercises.create({
title: 'Push-ups',
description: 'Basic push-up exercise',
asymmetric: false,
alternate: false,
default_reps: 10,
default_weight: 0,
created_by: 'test'
})
""")
assert ex_id is not None, "Exercise creation returned no ID"
# Read
exercise = page.evaluate(f"window.__repos.exercises.getById('{ex_id}')")
assert exercise is not None, "Exercise not found after creation"
assert exercise["title"] == "Push-ups"
assert exercise["description"] == "Basic push-up exercise"
assert exercise["asymmetric"] is False
assert exercise["alternate"] is False
assert exercise["default_reps"] == 10
# Update
page.evaluate(f"""
window.__repos.exercises.update('{ex_id}', {{
title: 'Diamond Push-ups',
description: 'Advanced push-up variation',
asymmetric: true,
alternate: true,
default_reps: 8,
default_weight: 0
}})
""")
updated = page.evaluate(f"window.__repos.exercises.getById('{ex_id}')")
assert updated["title"] == "Diamond Push-ups"
assert updated["default_reps"] == 8
assert updated["asymmetric"] is True
assert updated["alternate"] is True
# Delete
page.evaluate(f"window.__repos.exercises.delete('{ex_id}')")
deleted = page.evaluate(f"window.__repos.exercises.getById('{ex_id}')")
assert deleted is None, "Exercise still exists after deletion"
def test_exercise_search(page: Page, app_url: str):
"""Search exercises by title."""
_wait_for_db(page, app_url)
# Create a few exercises
page.evaluate("""
(async () => {
await window.__repos.exercises.create({ title: 'Squat', description: '', created_by: 'test' });
await window.__repos.exercises.create({ title: 'Bench Press', description: '', created_by: 'test' });
await window.__repos.exercises.create({ title: 'Squat Jump', description: '', created_by: 'test' });
})()
""")
results = page.evaluate("window.__repos.exercises.search('Squat')")
assert len(results) >= 2, f"Expected at least 2 results for 'Squat', got {len(results)}"
titles = [r["title"] for r in results]
assert "Squat" in titles
assert "Squat Jump" in titles
# Cleanup
page.evaluate("""
(async () => {
const all = await window.__repos.exercises.getAll();
for (const e of all) await window.__repos.exercises.delete(e.id);
})()
""")
def test_data_survives_reload(page: Page, app_url: str):
"""Data persists across page reload (OPFS persistence)."""
_wait_for_db(page, app_url)
# Create an exercise
ex_id = page.evaluate("""
window.__repos.exercises.create({
title: 'Persistent Exercise',
description: 'Should survive reload',
created_by: 'test'
})
""")
# Reload the page
page.reload()
page.wait_for_function(
"document.documentElement.getAttribute('data-db-ready') === 'true'",
timeout=15000,
)
# Verify exercise still exists
exercise = page.evaluate(f"window.__repos.exercises.getById('{ex_id}')")
assert exercise is not None, "Exercise lost after reload — persistence may not be working"
assert exercise["title"] == "Persistent Exercise"
# Cleanup
page.evaluate(f"window.__repos.exercises.delete('{ex_id}')")
def test_settings_crud(page: Page, app_url: str):
"""Settings can be read and updated."""
_wait_for_db(page, app_url)
# Read existing
original = page.evaluate("async () => await window.__repos.settings.get('user_name')")
assert original == "User"
# Update
page.evaluate("async () => { await window.__repos.settings.set('user_name', 'TestUser') }")
updated = page.evaluate("async () => await window.__repos.settings.get('user_name')")
assert updated == "TestUser", f"Expected 'TestUser', got '{updated}'"
# Restore
page.evaluate("async () => { await window.__repos.settings.set('user_name', 'User') }")
def test_schema_tables_exist(page: Page, app_url: str):
"""All expected tables exist in the database."""
_wait_for_db(page, app_url)
tables = page.evaluate("""
window.__db.selectAll(
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
)
""")
table_names = [t["name"] for t in tables]
expected = [
"collection", "collection_session", "combo", "combo_exercise",
"execution_log", "execution_log_item", "exercise",
"schema_version", "session", "session_item", "settings", "suffix"
]
for name in expected:
assert name in table_names, f"Table '{name}' not found. Found: {table_names}"
def test_foreign_keys_enabled(page: Page, app_url: str):
"""Foreign key constraints are enforced."""
_wait_for_db(page, app_url)
# Try to insert a session_item referencing a non-existent session
error = page.evaluate("""
(async () => {
try {
await window.__db.run(
"INSERT INTO session_item (session_id, item_id, item_type, position) VALUES (?, ?, ?, ?)",
['nonexistent', 'item1', 'exercise', 0]
);
return null;
} catch (e) {
return e.message;
}
})()
""")
assert error is not None, "Foreign key constraint was not enforced"
def test_combo_none_type(page: Page, app_url: str):
"""Combo can be created with type NONE."""
_wait_for_db(page, app_url)
combo_id = page.evaluate("""
window.__repos.combos.create({
title: 'Simple Combo',
description: 'A combo with no special type',
type: 'NONE',
created_by: 'test'
})
""")
assert combo_id is not None, "Combo creation returned no ID"
combo = page.evaluate(f"window.__repos.combos.getById('{combo_id}')")
assert combo is not None, "Combo not found after creation"
assert combo["type"] == "NONE"
# Cleanup
page.evaluate(f"window.__repos.combos.delete('{combo_id}')")
def test_session_item_repetitions(page: Page, app_url: str):
"""Session items support a repetitions (series) field."""
_wait_for_db(page, app_url)
# Create a session and an exercise
session_id = page.evaluate("""
window.__repos.sessions.create({
title: 'Repetitions Test Session',
description: '',
created_by: 'test'
})
""")
ex_id = page.evaluate("""
window.__repos.exercises.create({
title: 'Burpees',
description: '',
created_by: 'test'
})
""")
# Set items with repetitions
page.evaluate(f"""
window.__repos.sessions.setItems('{session_id}', [
{{ item_id: '{ex_id}', item_type: 'exercise', repetitions: 3 }}
])
""")
session = page.evaluate(f"window.__repos.sessions.getById('{session_id}')")
assert session["items"][0]["repetitions"] == 3, f"Expected repetitions=3, got {session['items'][0].get('repetitions')}"
# Default repetitions should be 1
page.evaluate(f"""
window.__repos.sessions.setItems('{session_id}', [
{{ item_id: '{ex_id}', item_type: 'exercise' }}
])
""")
session = page.evaluate(f"window.__repos.sessions.getById('{session_id}')")
assert session["items"][0]["repetitions"] == 1, "Default repetitions should be 1"
# Cleanup
page.evaluate(f"window.__repos.sessions.delete('{session_id}')")
page.evaluate(f"window.__repos.exercises.delete('{ex_id}')")
def test_collection_crud(page: Page, app_url: str):
"""Full CRUD cycle on collections (renamed from groups)."""
_wait_for_db(page, app_url)
# Create
coll_id = page.evaluate("""
window.__repos.collections.create({
label: 'Morning Routine',
created_by: 'test'
})
""")
assert coll_id is not None, "Collection creation returned no ID"
# Read
coll = page.evaluate(f"window.__repos.collections.getById('{coll_id}')")
assert coll is not None, "Collection not found after creation"
assert coll["label"] == "Morning Routine"
# Update
page.evaluate(f"""
window.__repos.collections.update('{coll_id}', {{ label: 'Evening Routine' }})
""")
updated = page.evaluate(f"window.__repos.collections.getById('{coll_id}')")
assert updated["label"] == "Evening Routine"
# Delete
page.evaluate(f"window.__repos.collections.delete('{coll_id}')")
deleted = page.evaluate(f"window.__repos.collections.getById('{coll_id}')")
assert deleted is None, "Collection still exists after deletion"
def test_execution_log_recent_and_count(page: Page, app_url: str):
"""getRecent returns last N logs with session title; getCountSince counts logs after a date."""
_wait_for_db(page, app_url)
# Create a session
session_id = page.evaluate("""
window.__repos.sessions.create({
title: 'Stats Test Session',
description: '',
created_by: 'test'
})
""")
# Create execution logs with different dates
page.evaluate(f"""
(async () => {{
await window.__repos.executionLogs.create({{ session_id: '{session_id}', started_at: '2026-03-10T10:00:00Z', finished_at: '2026-03-10T11:00:00Z' }});
await window.__repos.executionLogs.create({{ session_id: '{session_id}', started_at: '2026-03-12T10:00:00Z', finished_at: '2026-03-12T11:00:00Z' }});
await window.__repos.executionLogs.create({{ session_id: '{session_id}', started_at: '2026-03-14T10:00:00Z', finished_at: '2026-03-14T11:00:00Z' }});
}})()
""")
# getRecent
recent = page.evaluate("window.__repos.executionLogs.getRecent(2)")
assert len(recent) == 2, f"Expected 2 recent logs, got {len(recent)}"
assert recent[0]["session_title"] == "Stats Test Session"
# Most recent first
assert recent[0]["started_at"] > recent[1]["started_at"]
# getCountSince
count = page.evaluate("window.__repos.executionLogs.getCountSince('2026-03-11T00:00:00Z')")
assert count == 2, f"Expected 2 logs since March 11, got {count}"
# Cleanup
page.evaluate(f"window.__repos.sessions.delete('{session_id}')")