944 lines
34 KiB
Python
944 lines
34 KiB
Python
"""
|
|
Step 6 — Collection Management & Home Page Tests
|
|
- test_collection_list_empty: Shows empty state when no collections exist
|
|
- test_create_collection_minimal: Create collection with only required field (label)
|
|
- test_create_collection_with_sessions: Create collection with sessions
|
|
- test_collection_appears_in_list: Created collection appears in the list
|
|
- test_collection_detail_view: Detail view shows all collection data
|
|
- test_edit_collection: Edit a collection and verify changes
|
|
- test_delete_collection: Delete a collection with confirmation
|
|
- test_delete_collection_cancel: Cancelling delete does not remove collection
|
|
- test_search_collections: Search filters collections by label
|
|
- test_search_no_results: Search with no matches shows empty message
|
|
- test_search_clear_button: Clear button empties the search field and restores the full list
|
|
- test_validation_label_required: Empty label shows validation error
|
|
- test_duplicate_label_rejected: Creating collection with duplicate label shows error
|
|
- test_reorder_sessions: Reorder sessions in collection
|
|
- test_remove_session: Remove a session from collection
|
|
- test_action_bar_buttons: New and Search buttons appear in action bar
|
|
- test_navigate_to_session_from_collection: Click session in collection detail navigates to session
|
|
- test_home_empty_state: Home shows empty state when no executions exist
|
|
- test_home_recent_sessions: Home shows last 4 executed sessions with dates
|
|
- test_home_session_counts: Home shows session counts for last 7 and 30 days
|
|
- test_home_history_section: History section groups execution logs by calendar day
|
|
- test_home_history_date_filter: History section hides logs outside the active date range
|
|
- test_home_play_button_visible: Play button is visible on recent session cards
|
|
- test_home_export_html_button: HTML export button visible when data exists, absent otherwise
|
|
"""
|
|
from playwright.sync_api import Page, expect
|
|
from datetime import datetime, timedelta
|
|
|
|
|
|
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 _go_to_collections(page: Page, app_url: str):
|
|
"""Navigate to the collections list and wait for it to load."""
|
|
_wait_for_db(page, app_url)
|
|
page.click('a[href="#/collections"]')
|
|
page.wait_for_timeout(500)
|
|
|
|
|
|
def _go_to_home(page: Page, app_url: str):
|
|
"""Navigate to the home page and wait for it to load."""
|
|
_wait_for_db(page, app_url)
|
|
page.click('a[href="#/home"]')
|
|
page.wait_for_timeout(500)
|
|
|
|
|
|
def _clean_collections(page: Page):
|
|
"""Remove all collections via the repository."""
|
|
page.evaluate(
|
|
"""async () => {
|
|
const collections = await window.__repos.collections.getAll();
|
|
for (const c of collections) {
|
|
await window.__repos.collections.delete(c.id);
|
|
}
|
|
}"""
|
|
)
|
|
|
|
|
|
def _clean_sessions(page: Page):
|
|
"""Remove all sessions via the repository."""
|
|
page.evaluate(
|
|
"""async () => {
|
|
const sessions = await window.__repos.sessions.getAll();
|
|
for (const s of sessions) {
|
|
await window.__repos.sessions.delete(s.id);
|
|
}
|
|
}"""
|
|
)
|
|
|
|
|
|
def _clean_execution_logs(page: Page):
|
|
"""Remove all execution logs via the repository."""
|
|
page.evaluate(
|
|
"""async () => {
|
|
const logs = await window.__repos.executionLogs.getAll();
|
|
for (const log of logs) {
|
|
await window.__repos.executionLogs.delete(log.id);
|
|
}
|
|
}"""
|
|
)
|
|
|
|
|
|
def _create_session_via_repo(page: Page, title):
|
|
"""Create a session via the repository and return its ID."""
|
|
return page.evaluate(
|
|
"""async (title) => {
|
|
const uuid = await window.__repos.settings.get('user_uuid');
|
|
return window.__repos.sessions.create({
|
|
title: title, description: '', created_by: uuid
|
|
});
|
|
}""",
|
|
title,
|
|
)
|
|
|
|
|
|
def test_collection_list_empty(page: Page, app_url: str):
|
|
"""Empty state is shown when no collections exist."""
|
|
_go_to_collections(page, app_url)
|
|
_clean_collections(page)
|
|
page.reload()
|
|
page.wait_for_function(
|
|
"document.documentElement.getAttribute('data-db-ready') === 'true'",
|
|
timeout=15000,
|
|
)
|
|
page.click('a[href="#/collections"]')
|
|
page.wait_for_timeout(500)
|
|
expect(page.locator('[data-testid="collection-empty"]')).to_be_visible()
|
|
|
|
|
|
def test_create_collection_minimal(page: Page, app_url: str):
|
|
"""Create a collection with only the required label field."""
|
|
_go_to_collections(page, app_url)
|
|
_clean_collections(page)
|
|
page.reload()
|
|
page.wait_for_function(
|
|
"document.documentElement.getAttribute('data-db-ready') === 'true'",
|
|
timeout=15000,
|
|
)
|
|
page.click('a[href="#/collections"]')
|
|
page.wait_for_timeout(500)
|
|
|
|
page.click('[data-testid="collection-new-btn"]')
|
|
page.wait_for_selector('[data-testid="collection-label"]')
|
|
|
|
page.fill('[data-testid="collection-label"]', "My First Collection")
|
|
page.click('[data-testid="collection-save"]')
|
|
page.wait_for_selector('[data-testid="collection-detail-title"]', timeout=5000)
|
|
|
|
expect(page.locator('[data-testid="collection-detail-title"]')).to_have_text(
|
|
"My First Collection"
|
|
)
|
|
|
|
_clean_collections(page)
|
|
|
|
|
|
def test_create_collection_with_sessions(page: Page, app_url: str):
|
|
"""Create a collection with sessions."""
|
|
_go_to_collections(page, app_url)
|
|
_clean_collections(page)
|
|
_clean_sessions(page)
|
|
page.reload()
|
|
page.wait_for_function(
|
|
"document.documentElement.getAttribute('data-db-ready') === 'true'",
|
|
timeout=15000,
|
|
)
|
|
page.click('a[href="#/collections"]')
|
|
page.wait_for_timeout(500)
|
|
|
|
# Create sessions
|
|
session1_id = _create_session_via_repo(page, "Morning Workout")
|
|
session2_id = _create_session_via_repo(page, "Evening Stretch")
|
|
|
|
page.reload()
|
|
page.wait_for_function(
|
|
"document.documentElement.getAttribute('data-db-ready') === 'true'",
|
|
timeout=15000,
|
|
)
|
|
page.click('a[href="#/collections"]')
|
|
page.wait_for_timeout(500)
|
|
|
|
# Create collection
|
|
page.click('[data-testid="collection-new-btn"]')
|
|
page.wait_for_selector('[data-testid="collection-label"]')
|
|
|
|
page.fill('[data-testid="collection-label"]', "Weekly Plan")
|
|
|
|
# Add sessions
|
|
page.select_option('[data-testid="collection-session-select"]', session1_id)
|
|
page.click('[data-testid="collection-session-add"]')
|
|
|
|
page.select_option('[data-testid="collection-session-select"]', session2_id)
|
|
page.click('[data-testid="collection-session-add"]')
|
|
|
|
page.click('[data-testid="collection-save"]')
|
|
page.wait_for_selector('[data-testid="collection-detail-title"]', timeout=5000)
|
|
|
|
expect(page.locator('[data-testid="collection-detail-title"]')).to_have_text("Weekly Plan")
|
|
expect(page.locator('[data-testid="collection-detail-session-0"]')).to_contain_text(
|
|
"Morning Workout"
|
|
)
|
|
expect(page.locator('[data-testid="collection-detail-session-1"]')).to_contain_text(
|
|
"Evening Stretch"
|
|
)
|
|
|
|
_clean_collections(page)
|
|
_clean_sessions(page)
|
|
|
|
|
|
def test_collection_appears_in_list(page: Page, app_url: str):
|
|
"""Created collection appears in the list view."""
|
|
_go_to_collections(page, app_url)
|
|
_clean_collections(page)
|
|
page.reload()
|
|
page.wait_for_function(
|
|
"document.documentElement.getAttribute('data-db-ready') === 'true'",
|
|
timeout=15000,
|
|
)
|
|
page.click('a[href="#/collections"]')
|
|
page.wait_for_timeout(500)
|
|
|
|
page.click('[data-testid="collection-new-btn"]')
|
|
page.wait_for_selector('[data-testid="collection-label"]')
|
|
page.fill('[data-testid="collection-label"]', "Test Collection")
|
|
page.click('[data-testid="collection-save"]')
|
|
page.wait_for_selector('[data-testid="collection-detail-title"]', timeout=5000)
|
|
|
|
page.click('[data-testid="collection-back-btn"]')
|
|
page.wait_for_timeout(500)
|
|
|
|
expect(page.locator('[data-testid="collection-card-title"]')).to_have_text("Test Collection")
|
|
|
|
_clean_collections(page)
|
|
|
|
|
|
def test_collection_detail_view(page: Page, app_url: str):
|
|
"""Detail view shows all collection data correctly."""
|
|
_go_to_collections(page, app_url)
|
|
_clean_collections(page)
|
|
_clean_sessions(page)
|
|
|
|
session_id = _create_session_via_repo(page, "HIIT Session")
|
|
|
|
# Create collection via repo
|
|
collection_id = page.evaluate(
|
|
"""async ({ sessionId }) => {
|
|
const uuid = await window.__repos.settings.get('user_uuid');
|
|
const id = await window.__repos.collections.create({
|
|
label: 'Training Week', created_by: uuid
|
|
});
|
|
await window.__repos.collections.setSessions(id, [sessionId]);
|
|
return id;
|
|
}""",
|
|
{"sessionId": session_id},
|
|
)
|
|
|
|
page.goto(f"{app_url}/#/collections/{collection_id}")
|
|
page.wait_for_selector('[data-testid="collection-detail-title"]', timeout=5000)
|
|
|
|
expect(page.locator('[data-testid="collection-detail-title"]')).to_have_text("Training Week")
|
|
expect(page.locator('[data-testid="collection-detail-session-0"]')).to_contain_text(
|
|
"HIIT Session"
|
|
)
|
|
|
|
_clean_collections(page)
|
|
_clean_sessions(page)
|
|
|
|
|
|
def test_edit_collection(page: Page, app_url: str):
|
|
"""Edit a collection and verify changes are persisted."""
|
|
_go_to_collections(page, app_url)
|
|
_clean_collections(page)
|
|
page.reload()
|
|
page.wait_for_function(
|
|
"document.documentElement.getAttribute('data-db-ready') === 'true'",
|
|
timeout=15000,
|
|
)
|
|
page.click('a[href="#/collections"]')
|
|
page.wait_for_timeout(500)
|
|
|
|
page.click('[data-testid="collection-new-btn"]')
|
|
page.wait_for_selector('[data-testid="collection-label"]')
|
|
page.fill('[data-testid="collection-label"]', "Original Label")
|
|
page.click('[data-testid="collection-save"]')
|
|
page.wait_for_selector('[data-testid="collection-detail-title"]', timeout=5000)
|
|
|
|
page.click('[data-testid="collection-edit-btn"]')
|
|
page.wait_for_selector('[data-testid="collection-label"]')
|
|
|
|
page.fill('[data-testid="collection-label"]', "Updated Label")
|
|
|
|
page.click('[data-testid="collection-save"]')
|
|
page.wait_for_selector('[data-testid="collection-detail-title"]', timeout=5000)
|
|
|
|
expect(page.locator('[data-testid="collection-detail-title"]')).to_have_text("Updated Label")
|
|
|
|
_clean_collections(page)
|
|
|
|
|
|
def test_delete_collection(page: Page, app_url: str):
|
|
"""Delete a collection with confirmation dialog."""
|
|
_go_to_collections(page, app_url)
|
|
_clean_collections(page)
|
|
page.reload()
|
|
page.wait_for_function(
|
|
"document.documentElement.getAttribute('data-db-ready') === 'true'",
|
|
timeout=15000,
|
|
)
|
|
page.click('a[href="#/collections"]')
|
|
page.wait_for_timeout(500)
|
|
|
|
page.click('[data-testid="collection-new-btn"]')
|
|
page.wait_for_selector('[data-testid="collection-label"]')
|
|
page.fill('[data-testid="collection-label"]', "To Delete")
|
|
page.click('[data-testid="collection-save"]')
|
|
page.wait_for_selector('[data-testid="collection-detail-title"]', timeout=5000)
|
|
|
|
page.on("dialog", lambda dialog: dialog.accept())
|
|
page.click('[data-testid="collection-delete-btn"]')
|
|
page.wait_for_timeout(500)
|
|
|
|
expect(page.locator('[data-testid="collection-empty"]')).to_be_visible()
|
|
|
|
|
|
def test_delete_collection_cancel(page: Page, app_url: str):
|
|
"""Cancelling the delete dialog does not remove the collection."""
|
|
_go_to_collections(page, app_url)
|
|
_clean_collections(page)
|
|
page.reload()
|
|
page.wait_for_function(
|
|
"document.documentElement.getAttribute('data-db-ready') === 'true'",
|
|
timeout=15000,
|
|
)
|
|
page.click('a[href="#/collections"]')
|
|
page.wait_for_timeout(500)
|
|
|
|
page.click('[data-testid="collection-new-btn"]')
|
|
page.wait_for_selector('[data-testid="collection-label"]')
|
|
page.fill('[data-testid="collection-label"]', "Keep Me")
|
|
page.click('[data-testid="collection-save"]')
|
|
page.wait_for_selector('[data-testid="collection-detail-title"]', timeout=5000)
|
|
|
|
page.on("dialog", lambda dialog: dialog.dismiss())
|
|
page.click('[data-testid="collection-delete-btn"]')
|
|
page.wait_for_timeout(500)
|
|
|
|
expect(page.locator('[data-testid="collection-detail-title"]')).to_have_text("Keep Me")
|
|
|
|
_clean_collections(page)
|
|
|
|
|
|
def test_search_collections(page: Page, app_url: str):
|
|
"""Search filters collections by label."""
|
|
_go_to_collections(page, app_url)
|
|
_clean_collections(page)
|
|
|
|
page.evaluate(
|
|
"""async () => {
|
|
const uuid = await window.__repos.settings.get('user_uuid');
|
|
await window.__repos.collections.create({
|
|
label: 'Upper Body Week', created_by: uuid
|
|
});
|
|
await window.__repos.collections.create({
|
|
label: 'Lower Body Week', created_by: uuid
|
|
});
|
|
await window.__repos.collections.create({
|
|
label: 'Full Body Week', created_by: uuid
|
|
});
|
|
}"""
|
|
)
|
|
|
|
page.reload()
|
|
page.wait_for_function(
|
|
"document.documentElement.getAttribute('data-db-ready') === 'true'",
|
|
timeout=15000,
|
|
)
|
|
page.click('a[href="#/collections"]')
|
|
page.wait_for_timeout(500)
|
|
|
|
expect(page.locator('[data-testid="collection-card-title"]')).to_have_count(3)
|
|
|
|
page.click('[data-testid="collection-search-toggle"]')
|
|
page.wait_for_selector('[data-testid="collection-search-input"]')
|
|
|
|
page.fill('[data-testid="collection-search-input"]', "Upper")
|
|
page.wait_for_timeout(500)
|
|
|
|
expect(page.locator('[data-testid="collection-card-title"]')).to_have_count(1)
|
|
expect(page.locator('[data-testid="collection-card-title"]')).to_have_text("Upper Body Week")
|
|
|
|
_clean_collections(page)
|
|
|
|
|
|
def test_search_no_results(page: Page, app_url: str):
|
|
"""Search with no matches shows no-results message."""
|
|
_go_to_collections(page, app_url)
|
|
_clean_collections(page)
|
|
|
|
page.evaluate(
|
|
"""async () => {
|
|
const uuid = await window.__repos.settings.get('user_uuid');
|
|
await window.__repos.collections.create({
|
|
label: 'Upper Body Week', created_by: uuid
|
|
});
|
|
}"""
|
|
)
|
|
|
|
page.reload()
|
|
page.wait_for_function(
|
|
"document.documentElement.getAttribute('data-db-ready') === 'true'",
|
|
timeout=15000,
|
|
)
|
|
page.click('a[href="#/collections"]')
|
|
page.wait_for_timeout(500)
|
|
|
|
page.click('[data-testid="collection-search-toggle"]')
|
|
page.fill('[data-testid="collection-search-input"]', "zzzznotfound")
|
|
page.wait_for_timeout(500)
|
|
|
|
expect(page.locator('[data-testid="collection-no-results"]')).to_be_visible()
|
|
|
|
_clean_collections(page)
|
|
|
|
|
|
def test_search_clear_button(page: Page, app_url: str):
|
|
"""Clear button empties the search field and restores the full list."""
|
|
_go_to_collections(page, app_url)
|
|
_clean_collections(page)
|
|
|
|
page.evaluate(
|
|
"""async () => {
|
|
const uuid = await window.__repos.settings.get('user_uuid');
|
|
await window.__repos.collections.create({
|
|
label: 'Upper Body Week', created_by: uuid
|
|
});
|
|
await window.__repos.collections.create({
|
|
label: 'Lower Body Week', created_by: uuid
|
|
});
|
|
}"""
|
|
)
|
|
|
|
page.reload()
|
|
page.wait_for_function(
|
|
"document.documentElement.getAttribute('data-db-ready') === 'true'",
|
|
timeout=15000,
|
|
)
|
|
page.click('a[href="#/collections"]')
|
|
page.wait_for_timeout(500)
|
|
|
|
page.click('[data-testid="collection-search-toggle"]')
|
|
|
|
expect(page.locator('[data-testid="collection-search-clear"]')).not_to_be_visible()
|
|
|
|
page.fill('[data-testid="collection-search-input"]', "Upper")
|
|
page.wait_for_timeout(500)
|
|
expect(page.locator('[data-testid="collection-card-title"]')).to_have_count(1)
|
|
|
|
page.click('[data-testid="collection-search-clear"]')
|
|
expect(page.locator('[data-testid="collection-search-input"]')).to_have_value("")
|
|
page.wait_for_timeout(500)
|
|
expect(page.locator('[data-testid="collection-card-title"]')).to_have_count(2)
|
|
|
|
expect(page.locator('[data-testid="collection-search-input"]')).to_be_focused()
|
|
|
|
_clean_collections(page)
|
|
|
|
|
|
def test_validation_label_required(page: Page, app_url: str):
|
|
"""Empty label shows validation error and prevents save."""
|
|
_go_to_collections(page, app_url)
|
|
_clean_collections(page)
|
|
page.reload()
|
|
page.wait_for_function(
|
|
"document.documentElement.getAttribute('data-db-ready') === 'true'",
|
|
timeout=15000,
|
|
)
|
|
page.click('a[href="#/collections"]')
|
|
page.wait_for_timeout(500)
|
|
|
|
page.click('[data-testid="collection-new-btn"]')
|
|
page.wait_for_selector('[data-testid="collection-label"]')
|
|
|
|
page.click('[data-testid="collection-save"]')
|
|
|
|
expect(page.locator('[data-testid="collection-label-error"]')).to_be_visible()
|
|
expect(page.locator('[data-testid="collection-label"]')).to_be_visible()
|
|
|
|
|
|
def test_duplicate_label_rejected(page: Page, app_url: str):
|
|
"""Creating a collection with a duplicate label shows an error."""
|
|
_go_to_collections(page, app_url)
|
|
_clean_collections(page)
|
|
page.reload()
|
|
page.wait_for_function(
|
|
"document.documentElement.getAttribute('data-db-ready') === 'true'",
|
|
timeout=15000,
|
|
)
|
|
page.click('a[href="#/collections"]')
|
|
page.wait_for_timeout(500)
|
|
|
|
page.click('[data-testid="collection-new-btn"]')
|
|
page.wait_for_selector('[data-testid="collection-label"]')
|
|
page.fill('[data-testid="collection-label"]', "Unique Collection")
|
|
page.click('[data-testid="collection-save"]')
|
|
page.wait_for_selector('[data-testid="collection-detail-title"]', timeout=5000)
|
|
|
|
page.click('[data-testid="collection-back-btn"]')
|
|
page.wait_for_timeout(500)
|
|
|
|
page.click('[data-testid="collection-new-btn"]')
|
|
page.wait_for_selector('[data-testid="collection-label"]')
|
|
page.fill('[data-testid="collection-label"]', "Unique Collection")
|
|
page.click('[data-testid="collection-save"]')
|
|
page.wait_for_timeout(500)
|
|
|
|
label_error_visible = page.locator('[data-testid="collection-label-error"]').is_visible()
|
|
save_error_visible = page.locator('[data-testid="collection-save-error"]').is_visible()
|
|
assert label_error_visible or save_error_visible, "Expected duplicate label error"
|
|
|
|
_clean_collections(page)
|
|
|
|
|
|
def test_reorder_sessions(page: Page, app_url: str):
|
|
"""Reorder sessions in a collection."""
|
|
_go_to_collections(page, app_url)
|
|
_clean_collections(page)
|
|
_clean_sessions(page)
|
|
page.reload()
|
|
page.wait_for_function(
|
|
"document.documentElement.getAttribute('data-db-ready') === 'true'",
|
|
timeout=15000,
|
|
)
|
|
page.click('a[href="#/collections"]')
|
|
page.wait_for_timeout(500)
|
|
|
|
s1_id = _create_session_via_repo(page, "Session A")
|
|
s2_id = _create_session_via_repo(page, "Session B")
|
|
s3_id = _create_session_via_repo(page, "Session C")
|
|
|
|
page.reload()
|
|
page.wait_for_function(
|
|
"document.documentElement.getAttribute('data-db-ready') === 'true'",
|
|
timeout=15000,
|
|
)
|
|
page.click('a[href="#/collections"]')
|
|
page.wait_for_timeout(500)
|
|
|
|
# Create collection with 3 sessions
|
|
page.click('[data-testid="collection-new-btn"]')
|
|
page.wait_for_selector('[data-testid="collection-label"]')
|
|
page.fill('[data-testid="collection-label"]', "Reorder Test")
|
|
|
|
for s_id in [s1_id, s2_id, s3_id]:
|
|
page.select_option('[data-testid="collection-session-select"]', s_id)
|
|
page.click('[data-testid="collection-session-add"]')
|
|
|
|
page.click('[data-testid="collection-save"]')
|
|
page.wait_for_selector('[data-testid="collection-detail-title"]', timeout=5000)
|
|
|
|
# Go to edit
|
|
page.click('[data-testid="collection-edit-btn"]')
|
|
page.wait_for_selector('[data-testid="collection-label"]')
|
|
|
|
# Move Session B (index 1) up
|
|
page.click('[data-testid="collection-session-move-up-1"]')
|
|
|
|
# Save
|
|
page.click('[data-testid="collection-save"]')
|
|
page.wait_for_selector('[data-testid="collection-detail-title"]', timeout=5000)
|
|
|
|
# Verify order: Session B, Session A, Session C
|
|
expect(page.locator('[data-testid="collection-detail-session-0"]')).to_contain_text("Session B")
|
|
expect(page.locator('[data-testid="collection-detail-session-1"]')).to_contain_text("Session A")
|
|
expect(page.locator('[data-testid="collection-detail-session-2"]')).to_contain_text("Session C")
|
|
|
|
_clean_collections(page)
|
|
_clean_sessions(page)
|
|
|
|
|
|
def test_remove_session(page: Page, app_url: str):
|
|
"""Remove a session from a collection."""
|
|
_go_to_collections(page, app_url)
|
|
_clean_collections(page)
|
|
_clean_sessions(page)
|
|
page.reload()
|
|
page.wait_for_function(
|
|
"document.documentElement.getAttribute('data-db-ready') === 'true'",
|
|
timeout=15000,
|
|
)
|
|
page.click('a[href="#/collections"]')
|
|
page.wait_for_timeout(500)
|
|
|
|
s1_id = _create_session_via_repo(page, "Session A")
|
|
s2_id = _create_session_via_repo(page, "Session B")
|
|
|
|
page.reload()
|
|
page.wait_for_function(
|
|
"document.documentElement.getAttribute('data-db-ready') === 'true'",
|
|
timeout=15000,
|
|
)
|
|
page.click('a[href="#/collections"]')
|
|
page.wait_for_timeout(500)
|
|
|
|
# Create collection with 2 sessions
|
|
page.click('[data-testid="collection-new-btn"]')
|
|
page.wait_for_selector('[data-testid="collection-label"]')
|
|
page.fill('[data-testid="collection-label"]', "Remove Test")
|
|
|
|
for s_id in [s1_id, s2_id]:
|
|
page.select_option('[data-testid="collection-session-select"]', s_id)
|
|
page.click('[data-testid="collection-session-add"]')
|
|
|
|
page.click('[data-testid="collection-save"]')
|
|
page.wait_for_selector('[data-testid="collection-detail-title"]', timeout=5000)
|
|
|
|
# Go to edit
|
|
page.click('[data-testid="collection-edit-btn"]')
|
|
page.wait_for_selector('[data-testid="collection-label"]')
|
|
|
|
# Remove first session
|
|
page.click('[data-testid="collection-session-remove-0"]')
|
|
|
|
page.click('[data-testid="collection-save"]')
|
|
page.wait_for_selector('[data-testid="collection-detail-title"]', timeout=5000)
|
|
|
|
# Only Session B should remain
|
|
expect(page.locator('[data-testid="collection-detail-session-0"]')).to_contain_text("Session B")
|
|
|
|
_clean_collections(page)
|
|
_clean_sessions(page)
|
|
|
|
|
|
def test_action_bar_buttons(page: Page, app_url: str):
|
|
"""New and Search buttons are visible in the action bar on the collection list."""
|
|
_go_to_collections(page, app_url)
|
|
_clean_collections(page)
|
|
page.reload()
|
|
page.wait_for_function(
|
|
"document.documentElement.getAttribute('data-db-ready') === 'true'",
|
|
timeout=15000,
|
|
)
|
|
page.click('a[href="#/collections"]')
|
|
page.wait_for_timeout(500)
|
|
|
|
expect(page.locator('[data-testid="collection-new-btn"]')).to_be_visible()
|
|
expect(page.locator('[data-testid="collection-search-toggle"]')).to_be_visible()
|
|
|
|
|
|
def test_navigate_to_session_from_collection(page: Page, app_url: str):
|
|
"""Clicking a session in collection detail navigates to session detail."""
|
|
_go_to_collections(page, app_url)
|
|
_clean_collections(page)
|
|
_clean_sessions(page)
|
|
|
|
session_id = _create_session_via_repo(page, "Target Session")
|
|
|
|
collection_id = page.evaluate(
|
|
"""async ({ sessionId }) => {
|
|
const uuid = await window.__repos.settings.get('user_uuid');
|
|
const id = await window.__repos.collections.create({
|
|
label: 'Navigation Test', created_by: uuid
|
|
});
|
|
await window.__repos.collections.setSessions(id, [sessionId]);
|
|
return id;
|
|
}""",
|
|
{"sessionId": session_id},
|
|
)
|
|
|
|
page.goto(f"{app_url}/#/collections/{collection_id}")
|
|
page.wait_for_selector('[data-testid="collection-detail-title"]', timeout=5000)
|
|
|
|
page.click('[data-testid="collection-detail-session-link-0"]')
|
|
page.wait_for_selector('[data-testid="session-detail-title"]', timeout=5000)
|
|
|
|
expect(page.locator('[data-testid="session-detail-title"]')).to_have_text("Target Session")
|
|
|
|
_clean_collections(page)
|
|
_clean_sessions(page)
|
|
|
|
|
|
def test_home_empty_state(page: Page, app_url: str):
|
|
"""Home shows empty state when no executions exist."""
|
|
_go_to_home(page, app_url)
|
|
_clean_execution_logs(page)
|
|
page.reload()
|
|
page.wait_for_function(
|
|
"document.documentElement.getAttribute('data-db-ready') === 'true'",
|
|
timeout=15000,
|
|
)
|
|
page.click('a[href="#/home"]')
|
|
page.wait_for_timeout(500)
|
|
|
|
expect(page.locator('[data-testid="home-empty"]')).to_be_visible()
|
|
|
|
|
|
def test_home_recent_sessions(page: Page, app_url: str):
|
|
"""Home shows last 4 executed sessions with inline exercise summaries."""
|
|
_go_to_home(page, app_url)
|
|
_clean_execution_logs(page)
|
|
_clean_sessions(page)
|
|
|
|
# Create sessions
|
|
session1_id = _create_session_via_repo(page, "Session One")
|
|
session2_id = _create_session_via_repo(page, "Session Two")
|
|
session3_id = _create_session_via_repo(page, "Session Three")
|
|
session4_id = _create_session_via_repo(page, "Session Four")
|
|
|
|
# Create execution logs with different dates
|
|
page.evaluate(
|
|
"""async ({ s1, s2, s3, s4 }) => {
|
|
const now = new Date();
|
|
const logs = [
|
|
{ session_id: s1, started_at: new Date(now - 4 * 86400000).toISOString(), finished_at: new Date(now - 4 * 86400000 + 3600000).toISOString() },
|
|
{ session_id: s2, started_at: new Date(now - 3 * 86400000).toISOString(), finished_at: new Date(now - 3 * 86400000 + 3600000).toISOString() },
|
|
{ session_id: s3, started_at: new Date(now - 2 * 86400000).toISOString(), finished_at: new Date(now - 2 * 86400000 + 3600000).toISOString() },
|
|
{ session_id: s4, started_at: new Date(now - 1 * 86400000).toISOString(), finished_at: new Date(now - 1 * 86400000 + 3600000).toISOString() },
|
|
];
|
|
for (const log of logs) {
|
|
await window.__repos.executionLogs.create(log);
|
|
}
|
|
}""",
|
|
{"s1": session1_id, "s2": session2_id, "s3": session3_id, "s4": session4_id},
|
|
)
|
|
|
|
page.reload()
|
|
page.wait_for_function(
|
|
"document.documentElement.getAttribute('data-db-ready') === 'true'",
|
|
timeout=15000,
|
|
)
|
|
page.click('a[href="#/home"]')
|
|
page.wait_for_timeout(500)
|
|
|
|
# Should show the 4 most recent (Session Four, Three, Two, One)
|
|
expect(page.locator('[data-testid="home-recent-session-title"]')).to_have_count(4)
|
|
|
|
# Most recent first
|
|
expect(page.locator('[data-testid="home-recent-session-title"]').first).to_have_text(
|
|
"Session Four"
|
|
)
|
|
|
|
_clean_execution_logs(page)
|
|
_clean_sessions(page)
|
|
|
|
|
|
def test_home_session_counts(page: Page, app_url: str):
|
|
"""Home shows session counts for last 7 and 30 days."""
|
|
_go_to_home(page, app_url)
|
|
_clean_execution_logs(page)
|
|
_clean_sessions(page)
|
|
|
|
session_id = _create_session_via_repo(page, "Count Session")
|
|
|
|
# Create 2 logs within 7 days, 3 more within 30 days
|
|
page.evaluate(
|
|
"""async ({ sessionId }) => {
|
|
const now = new Date();
|
|
const logs = [
|
|
{ session_id: sessionId, started_at: new Date(now - 1 * 86400000).toISOString(), finished_at: new Date(now - 1 * 86400000 + 3600000).toISOString() },
|
|
{ session_id: sessionId, started_at: new Date(now - 3 * 86400000).toISOString(), finished_at: new Date(now - 3 * 86400000 + 3600000).toISOString() },
|
|
{ session_id: sessionId, started_at: new Date(now - 10 * 86400000).toISOString(), finished_at: new Date(now - 10 * 86400000 + 3600000).toISOString() },
|
|
{ session_id: sessionId, started_at: new Date(now - 15 * 86400000).toISOString(), finished_at: new Date(now - 15 * 86400000 + 3600000).toISOString() },
|
|
{ session_id: sessionId, started_at: new Date(now - 25 * 86400000).toISOString(), finished_at: new Date(now - 25 * 86400000 + 3600000).toISOString() },
|
|
];
|
|
for (const log of logs) {
|
|
await window.__repos.executionLogs.create(log);
|
|
}
|
|
}""",
|
|
{"sessionId": session_id},
|
|
)
|
|
|
|
page.reload()
|
|
page.wait_for_function(
|
|
"document.documentElement.getAttribute('data-db-ready') === 'true'",
|
|
timeout=15000,
|
|
)
|
|
page.click('a[href="#/home"]')
|
|
page.wait_for_timeout(500)
|
|
|
|
# 2 within 7 days
|
|
expect(page.locator('[data-testid="home-stat-7days"] .stat-value')).to_have_text("2")
|
|
# 5 within 30 days
|
|
expect(page.locator('[data-testid="home-stat-30days"] .stat-value')).to_have_text("5")
|
|
|
|
_clean_execution_logs(page)
|
|
_clean_sessions(page)
|
|
|
|
|
|
def test_home_history_section(page: Page, app_url: str):
|
|
"""History section groups execution logs by calendar day."""
|
|
_go_to_home(page, app_url)
|
|
_clean_execution_logs(page)
|
|
_clean_sessions(page)
|
|
|
|
session_id = _create_session_via_repo(page, "History Session")
|
|
|
|
# Two logs on two different days (today and yesterday)
|
|
page.evaluate(
|
|
"""async ({ sessionId }) => {
|
|
const now = new Date();
|
|
const yesterday = new Date(now - 86400000);
|
|
const logs = [
|
|
{ session_id: sessionId, started_at: now.toISOString(), finished_at: new Date(now.getTime() + 3600000).toISOString() },
|
|
{ session_id: sessionId, started_at: yesterday.toISOString(), finished_at: new Date(yesterday.getTime() + 3600000).toISOString() },
|
|
];
|
|
for (const log of logs) {
|
|
await window.__repos.executionLogs.create(log);
|
|
}
|
|
}""",
|
|
{"sessionId": session_id},
|
|
)
|
|
|
|
page.reload()
|
|
page.wait_for_function(
|
|
"document.documentElement.getAttribute('data-db-ready') === 'true'",
|
|
timeout=15000,
|
|
)
|
|
page.click('a[href="#/home"]')
|
|
page.wait_for_timeout(500)
|
|
|
|
# History section must show 2 day groups (the .history-day-label elements)
|
|
expect(page.locator(".history-day-label")).to_have_count(2)
|
|
|
|
# Both logs are visible as history cards
|
|
history_cards = page.locator('[data-testid^="home-history-session-"]')
|
|
expect(history_cards).to_have_count(2)
|
|
|
|
_clean_execution_logs(page)
|
|
_clean_sessions(page)
|
|
|
|
|
|
def test_home_history_date_filter(page: Page, app_url: str):
|
|
"""History section hides execution logs that fall outside the active date range."""
|
|
_go_to_home(page, app_url)
|
|
_clean_execution_logs(page)
|
|
_clean_sessions(page)
|
|
|
|
session_id = _create_session_via_repo(page, "Filter Session")
|
|
|
|
# One log 5 days ago (inside default 30-day window) and one 40 days ago (outside)
|
|
page.evaluate(
|
|
"""async ({ sessionId }) => {
|
|
const now = new Date();
|
|
const logs = [
|
|
{
|
|
session_id: sessionId,
|
|
started_at: new Date(now - 5 * 86400000).toISOString(),
|
|
finished_at: new Date(now - 5 * 86400000 + 3600000).toISOString()
|
|
},
|
|
{
|
|
session_id: sessionId,
|
|
started_at: new Date(now - 40 * 86400000).toISOString(),
|
|
finished_at: new Date(now - 40 * 86400000 + 3600000).toISOString()
|
|
},
|
|
];
|
|
for (const log of logs) {
|
|
await window.__repos.executionLogs.create(log);
|
|
}
|
|
}""",
|
|
{"sessionId": session_id},
|
|
)
|
|
|
|
page.reload()
|
|
page.wait_for_function(
|
|
"document.documentElement.getAttribute('data-db-ready') === 'true'",
|
|
timeout=15000,
|
|
)
|
|
page.click('a[href="#/home"]')
|
|
page.wait_for_timeout(500)
|
|
|
|
# Default filter is last 30 days: only the 5-day-ago log shows in history
|
|
history_cards = page.locator('[data-testid^="home-history-session-"]')
|
|
expect(history_cards).to_have_count(1)
|
|
|
|
_clean_execution_logs(page)
|
|
_clean_sessions(page)
|
|
|
|
|
|
def test_home_play_button_visible(page: Page, app_url: str):
|
|
"""Play button is visible on each recent session card."""
|
|
_go_to_home(page, app_url)
|
|
_clean_execution_logs(page)
|
|
_clean_sessions(page)
|
|
|
|
session_id = _create_session_via_repo(page, "Play Button Session")
|
|
|
|
page.evaluate(
|
|
"""async ({ sessionId }) => {
|
|
const now = new Date();
|
|
await window.__repos.executionLogs.create({
|
|
session_id: sessionId,
|
|
started_at: now.toISOString(),
|
|
finished_at: new Date(now.getTime() + 3600000).toISOString(),
|
|
});
|
|
}""",
|
|
{"sessionId": session_id},
|
|
)
|
|
|
|
page.reload()
|
|
page.wait_for_function(
|
|
"document.documentElement.getAttribute('data-db-ready') === 'true'",
|
|
timeout=15000,
|
|
)
|
|
page.click('a[href="#/home"]')
|
|
page.wait_for_timeout(500)
|
|
|
|
play_btn = page.locator(f'[data-testid="home-recent-session-play-{session_id}"]')
|
|
expect(play_btn).to_be_visible()
|
|
|
|
_clean_execution_logs(page)
|
|
_clean_sessions(page)
|
|
|
|
|
|
def test_home_export_html_button(page: Page, app_url: str):
|
|
"""HTML export button is visible when execution logs exist and absent on empty state."""
|
|
_go_to_home(page, app_url)
|
|
_clean_execution_logs(page)
|
|
_clean_sessions(page)
|
|
|
|
page.reload()
|
|
page.wait_for_function(
|
|
"document.documentElement.getAttribute('data-db-ready') === 'true'",
|
|
timeout=15000,
|
|
)
|
|
page.click('a[href="#/home"]')
|
|
page.wait_for_timeout(500)
|
|
|
|
# No data: export button must not be rendered
|
|
expect(page.locator('[data-testid="home-export-html-btn"]')).to_have_count(0)
|
|
|
|
# Create a session and an execution log
|
|
session_id = _create_session_via_repo(page, "Export Session")
|
|
page.evaluate(
|
|
"""async ({ sessionId }) => {
|
|
const now = new Date();
|
|
await window.__repos.executionLogs.create({
|
|
session_id: sessionId,
|
|
started_at: now.toISOString(),
|
|
finished_at: new Date(now.getTime() + 1800000).toISOString(),
|
|
});
|
|
}""",
|
|
{"sessionId": session_id},
|
|
)
|
|
|
|
page.reload()
|
|
page.wait_for_function(
|
|
"document.documentElement.getAttribute('data-db-ready') === 'true'",
|
|
timeout=15000,
|
|
)
|
|
page.click('a[href="#/home"]')
|
|
page.wait_for_timeout(500)
|
|
|
|
# With data: export button must be visible
|
|
expect(page.locator('[data-testid="home-export-html-btn"]')).to_be_visible()
|
|
|
|
_clean_execution_logs(page)
|
|
_clean_sessions(page)
|