""" 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}')")