trainUs/tests/test_step24_app_update.py
David 49aaa0cd31
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
Add udpate modal (step 24) and control by audio cask (step 25)
2026-07-05 21:26:21 +02:00

177 lines
7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Step 24 — App Update Modal (no more silent freeze)
- test_modal_appears_when_update_pending: Modal appears when an update is pending
- test_later_dismisses_for_session: "Later" closes the modal and it stays closed across navigation
- test_update_now_shows_spinner_and_calls_update: "Update now" switches to the spinner state and calls the update function
- test_spinner_state_ignores_escape: The updating state cannot be dismissed with Escape
- test_spinner_state_has_no_close_button: The updating state has no header close button
- test_modal_suppressed_on_execution_route: Modal does not appear during session execution
- test_modal_appears_after_leaving_execution: Modal appears once the user navigates away from execution
The pending update is forced through the dev-only hook `window.__appUpdateTest`
(see src/composables/useAppUpdate.js) — simulating a real service-worker update
in Playwright would require two production builds. The real SW flow is
validated manually.
"""
from playwright.sync_api import Page, expect
def _wait_for_db(page: Page, app_url: str):
page.goto(app_url)
page.wait_for_function(
"document.documentElement.getAttribute('data-db-ready') === 'true'",
timeout=15000,
)
def _trigger_update(page: Page):
"""Force the 'update available' state via the dev-only test hook."""
page.wait_for_function("!!window.__appUpdateTest", timeout=10000)
page.evaluate("window.__appUpdateTest.trigger()")
def _mock_update_fn(page: Page):
"""Replace the real SW update call with a recorder that never resolves
(the real call ends in a page reload, which the spinner state covers)."""
page.evaluate("""() => {
window.__updateCalls = []
window.__appUpdateTest.mockUpdateFn = (reload) => {
window.__updateCalls.push(reload)
return new Promise(() => {})
}
}""")
def _create_session(page: Page, title: str) -> str:
"""Create a one-exercise session and return its id."""
return page.evaluate(f"""async () => {{
const userId = await window.__repos.settings.get('user_uuid')
const exId = await window.__repos.exercises.create({{
title: {repr(title + ' exercise')},
description: '',
asymmetric: false,
alternate: false,
image_urls: '[]',
video_urls: '[]',
default_reps: 10,
default_weight: 0,
created_by: userId
}})
const sessId = await window.__repos.sessions.create({{
title: {repr(title)}, description: '', created_by: userId
}})
await window.__repos.sessions.setItems(sessId, [{{
item_id: exId, item_type: 'exercise',
position: 0, repetitions: 1, weight: 0
}}])
return sessId
}}""")
def test_modal_appears_when_update_pending(page: Page, app_url: str):
"""The update modal appears when an update is pending."""
_wait_for_db(page, app_url)
expect(page.locator('[data-testid="app-update-modal"]')).not_to_be_visible()
_trigger_update(page)
expect(page.locator('[data-testid="app-update-modal"]')).to_be_visible()
expect(page.locator('[data-testid="app-update-now"]')).to_be_visible()
expect(page.locator('[data-testid="app-update-later"]')).to_be_visible()
def test_later_dismisses_for_session(page: Page, app_url: str):
"""'Later' closes the modal and it does not reappear while navigating."""
_wait_for_db(page, app_url)
_trigger_update(page)
modal = page.locator('[data-testid="app-update-modal"]')
expect(modal).to_be_visible()
page.locator('[data-testid="app-update-later"]').click()
expect(modal).not_to_be_visible()
# Navigating within the session does not bring the modal back
page.evaluate("window.location.hash = '#/exercises'")
page.wait_for_selector('[data-testid="exercise-new-btn"]', timeout=10000)
expect(modal).not_to_be_visible()
def test_update_now_shows_spinner_and_calls_update(page: Page, app_url: str):
"""'Update now' switches to the spinner state and calls the update function."""
_wait_for_db(page, app_url)
_trigger_update(page)
_mock_update_fn(page)
page.locator('[data-testid="app-update-now"]').click()
expect(page.locator('[data-testid="app-update-updating"]')).to_be_visible()
# Choice buttons are gone
expect(page.locator('[data-testid="app-update-now"]')).not_to_be_visible()
expect(page.locator('[data-testid="app-update-later"]')).not_to_be_visible()
calls = page.evaluate("window.__updateCalls")
assert calls == [True], f"updateServiceWorker should be called with reload=true, got {calls}"
def test_spinner_state_ignores_escape(page: Page, app_url: str):
"""The updating state cannot be dismissed with Escape or a backdrop click."""
_wait_for_db(page, app_url)
_trigger_update(page)
_mock_update_fn(page)
page.locator('[data-testid="app-update-now"]').click()
updating = page.locator('[data-testid="app-update-updating"]')
expect(updating).to_be_visible()
page.keyboard.press("Escape")
expect(updating).to_be_visible()
# Backdrop click must not close it either
page.locator('[data-testid="modal-overlay"]').click(position={"x": 5, "y": 5})
expect(updating).to_be_visible()
def test_spinner_state_has_no_close_button(page: Page, app_url: str):
"""The updating state renders no header close (×) button."""
_wait_for_db(page, app_url)
_trigger_update(page)
_mock_update_fn(page)
expect(page.locator('[data-testid="modal-close"]')).to_be_visible()
page.locator('[data-testid="app-update-now"]').click()
expect(page.locator('[data-testid="app-update-updating"]')).to_be_visible()
expect(page.locator('[data-testid="modal-close"]')).not_to_be_visible()
def test_modal_suppressed_on_execution_route(page: Page, app_url: str):
"""The modal does not appear while a session is being executed."""
_wait_for_db(page, app_url)
sess_id = _create_session(page, "Update modal session")
page.evaluate(f"window.location.hash = '#/sessions/{sess_id}/execute'")
page.wait_for_selector('[data-testid="execute-session-title"]', timeout=10000)
_trigger_update(page)
expect(page.locator('[data-testid="app-update-modal"]')).not_to_be_visible()
def test_modal_appears_after_leaving_execution(page: Page, app_url: str):
"""The pending modal appears once the user navigates away from execution."""
_wait_for_db(page, app_url)
sess_id = _create_session(page, "Update modal deferred")
page.evaluate(f"window.location.hash = '#/sessions/{sess_id}/execute'")
page.wait_for_selector('[data-testid="execute-session-title"]', timeout=10000)
_trigger_update(page)
expect(page.locator('[data-testid="app-update-modal"]')).not_to_be_visible()
# Leave execution (accept the abandon-confirmation dialog)
page.on("dialog", lambda dialog: dialog.accept())
page.evaluate("window.location.hash = '#/home'")
expect(page.locator('[data-testid="app-update-modal"]')).to_be_visible()