trainUs/tests/test_step12_timers.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

609 lines
25 KiB
Python

"""
Step 12 — Timers & Combo Execution Tests
- test_tabata_form_inputs_shown: TABATA combo form shows work/rest time inputs
- test_tabata_form_inputs_not_shown_for_other_types: Work/rest inputs hidden for non-TABATA types
- test_tabata_timer_config_saved: TABATA work/rest times saved to timer_config
- test_amrap_label_in_session_form: AMRAP combo item shows "Duration (min)" label
- test_amrap_label_in_session_detail: AMRAP combo item shows minutes in detail view
- test_emom_timer_visible: EMOM combo steps show a timer display
- test_emom_timer_pause_resume: Pause button toggles pause state on EMOM timer
- test_emom_done_early_advances: Tapping Done during EMOM advances to next step immediately
- test_emom_auto_advance: EMOM timer auto-advances to next step after 60 seconds
- test_amrap_timer_visible: AMRAP combo steps show a timer display
- test_amrap_manual_done_cycles: Done button manually advances through AMRAP exercises
- test_amrap_timeup_screen: Time-up overlay appears when AMRAP timer expires
- test_amrap_continue_advances: Continue after time-up advances past AMRAP combo
- test_tabata_work_phase_shows_timer: TABATA shows timer on work phase
- test_tabata_rest_screen_after_work: REST screen appears after work phase timer expires
- test_tabata_advances_after_rest: Next exercise appears after rest phase timer expires
- test_tabata_pause_during_work: Pause freezes TABATA work timer
- test_tabata_pause_during_rest: Pause freezes TABATA rest timer
- test_non_timed_combo_unchanged: NONE-type combos still show final-round screen as before
"""
import pytest
from playwright.sync_api import Page, expect
# ── helpers ───────────────────────────────────────────────────────────────────
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 _wait_for_execute(page: Page):
page.wait_for_selector('[data-testid="execute-session-title"]', timeout=10000)
page.wait_for_timeout(400)
def _create_exercise(page: Page, title: str, default_reps=10, default_weight=0.0):
return page.evaluate(f"""async () => {{
const userId = await window.__repos.settings.get('user_uuid')
return window.__repos.exercises.create({{
title: {repr(title)}, description: '', asymmetric: false, alternate: false,
image_urls: '[]', video_urls: '[]',
default_reps: {default_reps}, default_weight: {default_weight},
created_by: userId
}})
}}""")
def _create_timed_combo(page: Page, title: str, combo_type: str, exercise_ids: list,
timer_config: dict = None):
if timer_config is None:
timer_config = {}
import json
tc_json = json.dumps(timer_config)
ex_json = str([{"exerciseId": eid, "defaultReps": 10, "defaultWeight": 0}
for eid in exercise_ids]).replace("'", '"')
return page.evaluate(f"""async () => {{
const userId = await window.__repos.settings.get('user_uuid')
const comboId = await window.__repos.combos.create({{
title: {repr(title)}, description: '', type: {repr(combo_type)},
timer_config: {tc_json}, created_by: userId
}})
const exercises = {ex_json}
await window.__repos.combos.setExercises(comboId, exercises)
return comboId
}}""")
def _create_session_with_items(page: Page, title: str, items: list):
import json
items_json = json.dumps(items)
return page.evaluate(f"""async () => {{
const userId = await window.__repos.settings.get('user_uuid')
const sessId = await window.__repos.sessions.create({{
title: {repr(title)}, description: '', created_by: userId
}})
const items = {items_json}
await window.__repos.sessions.setItems(sessId, items.map((it, i) => ({{
item_id: it.item_id, item_type: it.item_type,
position: i, repetitions: it.repetitions, weight: it.weight || 0
}})))
return sessId
}}""")
def _clean_all(page: Page):
page.evaluate("""async () => {
const sessions = await window.__repos.sessions.getAll()
for (const s of sessions) await window.__repos.sessions.delete(s.id)
const combos = await window.__repos.combos.getAll()
for (const c of combos) await window.__repos.combos.delete(c.id)
const exercises = await window.__repos.exercises.getAll()
for (const e of exercises) await window.__repos.exercises.delete(e.id)
}""")
def _navigate_to_execute(page: Page, app_url: str, sess_id: str):
page.goto(f"{app_url}#/sessions/{sess_id}/execute")
_wait_for_execute(page)
# ── Combo form ────────────────────────────────────────────────────────────────
def test_tabata_form_inputs_shown(page: Page, app_url: str, browser_name: str):
"""TABATA combo form shows work time and rest time inputs."""
_wait_for_db(page, app_url)
page.goto(f"{app_url}#/combos/new")
page.wait_for_selector('[data-testid="combo-type"]', timeout=5000)
# Before selecting TABATA, inputs should not be visible
expect(page.locator('[data-testid="combo-tabata-work-time"]')).not_to_be_visible()
expect(page.locator('[data-testid="combo-tabata-rest-time"]')).not_to_be_visible()
# Select TABATA
page.select_option('[data-testid="combo-type"]', "TABATA")
expect(page.locator('[data-testid="combo-tabata-work-time"]')).to_be_visible()
expect(page.locator('[data-testid="combo-tabata-rest-time"]')).to_be_visible()
def test_tabata_form_inputs_not_shown_for_other_types(page: Page, app_url: str, browser_name: str):
"""Work/rest inputs are hidden for EMOM and AMRAP types."""
_wait_for_db(page, app_url)
page.goto(f"{app_url}#/combos/new")
page.wait_for_selector('[data-testid="combo-type"]', timeout=5000)
for combo_type in ["EMOM", "AMRAP", "NONE"]:
page.select_option('[data-testid="combo-type"]', combo_type)
expect(page.locator('[data-testid="combo-tabata-work-time"]')).not_to_be_visible()
expect(page.locator('[data-testid="combo-tabata-rest-time"]')).not_to_be_visible()
def test_tabata_timer_config_saved(page: Page, app_url: str, browser_name: str):
"""TABATA work/rest times are persisted in timer_config."""
_wait_for_db(page, app_url)
_clean_all(page)
page.goto(f"{app_url}#/combos/new")
page.wait_for_selector('[data-testid="combo-type"]', timeout=5000)
page.fill('[data-testid="combo-title"]', "My Tabata")
page.select_option('[data-testid="combo-type"]', "TABATA")
page.fill('[data-testid="combo-tabata-work-time"]', "15")
page.fill('[data-testid="combo-tabata-rest-time"]', "5")
page.click('[data-testid="combo-save"]')
page.wait_for_selector('[data-testid="combo-detail-title"]', timeout=8000)
combo_id = page.url.split("/")[-1]
timer_config = page.evaluate(f"""async () => {{
const c = await window.__repos.combos.getById({repr(combo_id)})
return c.timer_config
}}""")
assert timer_config.get("workTime") == 15
assert timer_config.get("restTime") == 5
# ── Session form / detail labels for AMRAP ────────────────────────────────────
def test_amrap_label_in_session_form(page: Page, app_url: str, browser_name: str):
"""AMRAP combo items show 'Duration (min)' instead of 'Reps' in session form."""
_wait_for_db(page, app_url)
_clean_all(page)
ex_id = _create_exercise(page, "Jump")
amrap_id = _create_timed_combo(page, "AMRAP Combo", "AMRAP", [ex_id])
none_id = _create_timed_combo(page, "None Combo", "NONE", [ex_id])
page.goto(f"{app_url}#/sessions/new")
page.wait_for_selector('[data-testid="session-title"]', timeout=5000)
# Add AMRAP combo
page.select_option('[data-testid="session-item-type-select"]', "combo")
page.wait_for_timeout(300)
page.select_option('[data-testid="session-item-select"]', amrap_id)
page.click('[data-testid="session-item-add"]')
page.wait_for_timeout(300)
# The reps label for AMRAP item should say "Duration (min)"
label = page.locator('[data-testid="session-item-0"] label').first.inner_text()
assert "min" in label.lower() or "duration" in label.lower()
# Add NONE combo
page.select_option('[data-testid="session-item-type-select"]', "combo")
page.wait_for_timeout(300)
page.select_option('[data-testid="session-item-select"]', none_id)
page.click('[data-testid="session-item-add"]')
page.wait_for_timeout(300)
# The reps label for NONE combo should say "Reps" (not duration)
label2 = page.locator('[data-testid="session-item-1"] label').first.inner_text()
assert "rep" in label2.lower() or label2.lower() == "reps"
def test_amrap_label_in_session_detail(page: Page, app_url: str, browser_name: str):
"""AMRAP combo items show minutes unit in session detail view."""
_wait_for_db(page, app_url)
_clean_all(page)
ex_id = _create_exercise(page, "Burpee")
amrap_id = _create_timed_combo(page, "AMRAP Detail", "AMRAP", [ex_id])
sess_id = _create_session_with_items(page, "AMRAP Session", [
{"item_id": amrap_id, "item_type": "combo", "repetitions": 10, "weight": 0}
])
page.goto(f"{app_url}#/sessions/{sess_id}")
page.wait_for_selector('[data-testid="session-detail-title"]', timeout=5000)
detail_text = page.locator('[data-testid="session-detail-item-details-0"]').inner_text()
# Should show "10 minutes" (not "10 reps")
assert "10" in detail_text
assert "min" in detail_text.lower()
# ── EMOM timer ────────────────────────────────────────────────────────────────
def test_emom_timer_visible(page: Page, app_url: str, browser_name: str):
"""EMOM combo steps show a timer display."""
_wait_for_db(page, app_url)
_clean_all(page)
ex_id = _create_exercise(page, "EMOM Press")
combo_id = _create_timed_combo(page, "EMOM Combo", "EMOM", [ex_id])
sess_id = _create_session_with_items(page, "EMOM Session", [
{"item_id": combo_id, "item_type": "combo", "repetitions": 1, "weight": 0}
])
_navigate_to_execute(page, app_url, sess_id)
timer = page.locator('[data-testid="execute-timer"]')
expect(timer).to_be_visible()
# Should show countdown format MM:SS
timer_text = timer.inner_text()
assert ":" in timer_text
def test_emom_timer_pause_resume(page: Page, app_url: str, browser_name: str):
"""Pause button toggles pause state — button label changes."""
_wait_for_db(page, app_url)
_clean_all(page)
ex_id = _create_exercise(page, "EMOM Press P")
combo_id = _create_timed_combo(page, "EMOM Pause Combo", "EMOM", [ex_id])
sess_id = _create_session_with_items(page, "EMOM Pause Session", [
{"item_id": combo_id, "item_type": "combo", "repetitions": 1, "weight": 0}
])
_navigate_to_execute(page, app_url, sess_id)
timer = page.locator('[data-testid="execute-timer"]')
expect(timer).to_be_visible()
# Initially timer is running — button should say Pause
pause_btn = timer.locator('button')
initial_label = pause_btn.inner_text()
assert "pause" in initial_label.lower() or "pause" in pause_btn.get_attribute("aria-label", "").lower()
# Click pause
pause_btn.click()
page.wait_for_timeout(300)
# Button should now say Resume
resume_label = pause_btn.inner_text()
assert "resume" in resume_label.lower() or "resume" in pause_btn.get_attribute("aria-label", "").lower()
# Click again to resume
pause_btn.click()
page.wait_for_timeout(300)
after_resume = pause_btn.inner_text()
assert "pause" in after_resume.lower() or "pause" in pause_btn.get_attribute("aria-label", "").lower()
def test_emom_done_early_advances(page: Page, app_url: str, browser_name: str):
"""Tapping Done during EMOM advances to the next step immediately."""
_wait_for_db(page, app_url)
_clean_all(page)
ex1 = _create_exercise(page, "EMOM Ex1")
ex2 = _create_exercise(page, "EMOM Ex2")
combo_id = _create_timed_combo(page, "EMOM Two", "EMOM", [ex1, ex2])
sess_id = _create_session_with_items(page, "EMOM Two Session", [
{"item_id": combo_id, "item_type": "combo", "repetitions": 1, "weight": 0}
])
_navigate_to_execute(page, app_url, sess_id)
# We're on step 1 (EMOM Ex1)
expect(page.locator('[data-testid="execute-exercise-title"]')).to_have_text("EMOM Ex1")
# Tap Done early (should advance without waiting 60 seconds)
page.click('[data-testid="execute-done-btn"]')
page.wait_for_timeout(500)
# Now we should be on step 2
expect(page.locator('[data-testid="execute-exercise-title"]')).to_have_text("EMOM Ex2")
def test_emom_auto_advance(page: Page, app_url: str, browser_name: str):
"""EMOM timer auto-advances to next step after 60 seconds."""
page.clock.install()
_wait_for_db(page, app_url)
_clean_all(page)
ex1 = _create_exercise(page, "EMOM Auto1")
ex2 = _create_exercise(page, "EMOM Auto2")
combo_id = _create_timed_combo(page, "EMOM Auto", "EMOM", [ex1, ex2])
sess_id = _create_session_with_items(page, "EMOM Auto Session", [
{"item_id": combo_id, "item_type": "combo", "repetitions": 1, "weight": 0}
])
_navigate_to_execute(page, app_url, sess_id)
expect(page.locator('[data-testid="execute-exercise-title"]')).to_have_text("EMOM Auto1")
# Advance 61 seconds — timer should fire and auto-advance
page.clock.run_for(61000)
page.wait_for_timeout(500)
expect(page.locator('[data-testid="execute-exercise-title"]')).to_have_text("EMOM Auto2")
# ── AMRAP timer ───────────────────────────────────────────────────────────────
def test_amrap_timer_visible(page: Page, app_url: str, browser_name: str):
"""AMRAP combo steps show a timer display."""
_wait_for_db(page, app_url)
_clean_all(page)
ex_id = _create_exercise(page, "AMRAP Ex")
combo_id = _create_timed_combo(page, "AMRAP Combo Vis", "AMRAP", [ex_id])
sess_id = _create_session_with_items(page, "AMRAP Vis Session", [
{"item_id": combo_id, "item_type": "combo", "repetitions": 5, "weight": 0}
])
_navigate_to_execute(page, app_url, sess_id)
timer = page.locator('[data-testid="execute-timer"]')
expect(timer).to_be_visible()
# Timer should show 05:00 (5 minutes)
timer_text = timer.inner_text()
assert "05:00" in timer_text or "5:" in timer_text
def test_amrap_manual_done_cycles(page: Page, app_url: str, browser_name: str):
"""Done button manually cycles through AMRAP exercises (auto-adds next round)."""
_wait_for_db(page, app_url)
_clean_all(page)
ex1 = _create_exercise(page, "AMRAP Cycle1")
ex2 = _create_exercise(page, "AMRAP Cycle2")
combo_id = _create_timed_combo(page, "AMRAP Cycle", "AMRAP", [ex1, ex2])
sess_id = _create_session_with_items(page, "AMRAP Cycle Session", [
{"item_id": combo_id, "item_type": "combo", "repetitions": 3, "weight": 0}
])
_navigate_to_execute(page, app_url, sess_id)
# Step 1: AMRAP Cycle1
expect(page.locator('[data-testid="execute-exercise-title"]')).to_have_text("AMRAP Cycle1")
# Done → advances to AMRAP Cycle2
page.click('[data-testid="execute-done-btn"]')
page.wait_for_timeout(400)
expect(page.locator('[data-testid="execute-exercise-title"]')).to_have_text("AMRAP Cycle2")
# Done → auto-cycles back to AMRAP Cycle1 (round 2)
page.click('[data-testid="execute-done-btn"]')
page.wait_for_timeout(400)
expect(page.locator('[data-testid="execute-exercise-title"]')).to_have_text("AMRAP Cycle1")
def test_amrap_timeup_screen(page: Page, app_url: str, browser_name: str):
"""Time-up overlay appears when AMRAP timer expires."""
page.clock.install()
_wait_for_db(page, app_url)
_clean_all(page)
ex_id = _create_exercise(page, "AMRAP TimeUp Ex")
combo_id = _create_timed_combo(page, "AMRAP TimeUp", "AMRAP", [ex_id])
sess_id = _create_session_with_items(page, "AMRAP TimeUp Session", [
{"item_id": combo_id, "item_type": "combo", "repetitions": 1, "weight": 0}
])
_navigate_to_execute(page, app_url, sess_id)
# Advance 61 seconds (1 minute AMRAP)
page.clock.run_for(61000)
page.wait_for_timeout(500)
timeup = page.locator('[data-testid="execute-amrap-timeup"]')
expect(timeup).to_be_visible()
def test_amrap_continue_advances(page: Page, app_url: str, browser_name: str):
"""Continue after AMRAP time-up advances to the next session item."""
page.clock.install()
_wait_for_db(page, app_url)
_clean_all(page)
ex_amrap = _create_exercise(page, "AMRAP Cont Ex")
ex_next = _create_exercise(page, "After AMRAP")
combo_id = _create_timed_combo(page, "AMRAP Cont", "AMRAP", [ex_amrap])
sess_id = _create_session_with_items(page, "AMRAP Cont Session", [
{"item_id": combo_id, "item_type": "combo", "repetitions": 1, "weight": 0},
{"item_id": ex_next, "item_type": "exercise", "repetitions": 5, "weight": 0}
])
_navigate_to_execute(page, app_url, sess_id)
# Expire the AMRAP timer
page.clock.run_for(61000)
page.wait_for_timeout(500)
expect(page.locator('[data-testid="execute-amrap-timeup"]')).to_be_visible()
# Click Continue
page.click('[data-testid="execute-amrap-continue-btn"]')
page.wait_for_timeout(400)
# Should now be on the next exercise
expect(page.locator('[data-testid="execute-exercise-title"]')).to_have_text("After AMRAP")
# ── TABATA timer ──────────────────────────────────────────────────────────────
def test_tabata_work_phase_shows_timer(page: Page, app_url: str, browser_name: str):
"""TABATA step shows timer during work phase."""
_wait_for_db(page, app_url)
_clean_all(page)
ex_id = _create_exercise(page, "TABATA Work Ex")
combo_id = _create_timed_combo(page, "TABATA Work", "TABATA", [ex_id],
timer_config={"workTime": 30, "restTime": 10})
sess_id = _create_session_with_items(page, "TABATA Work Session", [
{"item_id": combo_id, "item_type": "combo", "repetitions": 1, "weight": 0}
])
_navigate_to_execute(page, app_url, sess_id)
timer = page.locator('[data-testid="execute-timer"]')
expect(timer).to_be_visible()
timer_text = timer.inner_text()
assert ":" in timer_text
def test_tabata_rest_screen_after_work(page: Page, app_url: str, browser_name: str):
"""REST screen appears after TABATA work phase timer expires (using short times)."""
_wait_for_db(page, app_url)
_clean_all(page)
ex_id = _create_exercise(page, "TABATA Rest Ex")
combo_id = _create_timed_combo(page, "TABATA Rest", "TABATA", [ex_id],
timer_config={"workTime": 2, "restTime": 10})
sess_id = _create_session_with_items(page, "TABATA Rest Session", [
{"item_id": combo_id, "item_type": "combo", "repetitions": 1, "weight": 0}
])
_navigate_to_execute(page, app_url, sess_id)
# Wait for work phase to expire (2 seconds + margin)
page.wait_for_timeout(3500)
rest_card = page.locator('[data-testid="execute-tabata-rest"]')
expect(rest_card).to_be_visible()
def test_tabata_advances_after_rest(page: Page, app_url: str, browser_name: str):
"""Next exercise appears after TABATA rest phase timer expires."""
_wait_for_db(page, app_url)
_clean_all(page)
ex1 = _create_exercise(page, "TABATA Adv1")
ex2 = _create_exercise(page, "TABATA Adv2")
combo_id = _create_timed_combo(page, "TABATA Adv", "TABATA", [ex1, ex2],
timer_config={"workTime": 2, "restTime": 1})
sess_id = _create_session_with_items(page, "TABATA Adv Session", [
{"item_id": combo_id, "item_type": "combo", "repetitions": 1, "weight": 0}
])
_navigate_to_execute(page, app_url, sess_id)
# Step 1 is TABATA Adv1
expect(page.locator('[data-testid="execute-exercise-title"]')).to_have_text("TABATA Adv1")
# Wait for work (2s) + rest (1s) + margin → Playwright polls until ex2 appears
expect(page.locator('[data-testid="execute-exercise-title"]')).to_have_text(
"TABATA Adv2", timeout=8000
)
def test_tabata_done_starts_rest(page: Page, app_url: str, browser_name: str):
"""Tapping Done during TABATA work phase starts the rest phase immediately."""
_wait_for_db(page, app_url)
_clean_all(page)
ex1 = _create_exercise(page, "TABATA Done1")
ex2 = _create_exercise(page, "TABATA Done2")
combo_id = _create_timed_combo(page, "TABATA Done", "TABATA", [ex1, ex2],
timer_config={"workTime": 60, "restTime": 60})
sess_id = _create_session_with_items(page, "TABATA Done Session", [
{"item_id": combo_id, "item_type": "combo", "repetitions": 1, "weight": 0}
])
_navigate_to_execute(page, app_url, sess_id)
expect(page.locator('[data-testid="execute-exercise-title"]')).to_have_text("TABATA Done1")
# Tap Done early — should start rest phase
page.click('[data-testid="execute-done-btn"]')
page.wait_for_timeout(400)
rest_card = page.locator('[data-testid="execute-tabata-rest"]')
expect(rest_card).to_be_visible()
def test_tabata_pause_during_work(page: Page, app_url: str, browser_name: str):
"""Pause freezes TABATA work timer — REST screen does not appear while paused."""
_wait_for_db(page, app_url)
_clean_all(page)
ex_id = _create_exercise(page, "TABATA Pause Ex")
combo_id = _create_timed_combo(page, "TABATA Pause", "TABATA", [ex_id],
timer_config={"workTime": 2, "restTime": 1})
sess_id = _create_session_with_items(page, "TABATA Pause Session", [
{"item_id": combo_id, "item_type": "combo", "repetitions": 1, "weight": 0}
])
_navigate_to_execute(page, app_url, sess_id)
timer = page.locator('[data-testid="execute-timer"]')
expect(timer).to_be_visible()
# Pause immediately
timer.locator('button').click()
page.wait_for_timeout(3500) # Wait longer than workTime (2s)
# Timer is paused — REST screen should NOT appear
expect(page.locator('[data-testid="execute-tabata-rest"]')).not_to_be_visible()
expect(page.locator('[data-testid="execute-current-step"]')).to_be_visible()
def test_tabata_pause_during_rest(page: Page, app_url: str, browser_name: str):
"""Pause freezes TABATA rest timer — next exercise does not appear while paused."""
_wait_for_db(page, app_url)
_clean_all(page)
ex1 = _create_exercise(page, "TABATA PauseR1")
ex2 = _create_exercise(page, "TABATA PauseR2")
combo_id = _create_timed_combo(page, "TABATA PauseR", "TABATA", [ex1, ex2],
timer_config={"workTime": 2, "restTime": 10})
sess_id = _create_session_with_items(page, "TABATA PauseR Session", [
{"item_id": combo_id, "item_type": "combo", "repetitions": 1, "weight": 0}
])
_navigate_to_execute(page, app_url, sess_id)
# Wait for work phase to complete (2s)
page.wait_for_timeout(3000)
rest_card = page.locator('[data-testid="execute-tabata-rest"]')
expect(rest_card).to_be_visible()
# Pause during rest
rest_card.locator('button').click()
page.wait_for_timeout(3000) # Wait well beyond rest time (paused)
# Should still be on rest screen (not advanced to ex2)
expect(rest_card).to_be_visible()
expect(page.locator('[data-testid="execute-exercise-title"]')).not_to_be_visible()
# ── Non-timed combo unchanged ─────────────────────────────────────────────────
def test_non_timed_combo_unchanged(page: Page, app_url: str, browser_name: str):
"""NONE-type combos still show the final-round screen — no timer displayed."""
_wait_for_db(page, app_url)
_clean_all(page)
ex_id = _create_exercise(page, "None Combo Ex")
combo_id = _create_timed_combo(page, "None Combo", "NONE", [ex_id])
sess_id = _create_session_with_items(page, "None Session", [
{"item_id": combo_id, "item_type": "combo", "repetitions": 1, "weight": 0}
])
_navigate_to_execute(page, app_url, sess_id)
# No timer should be shown
expect(page.locator('[data-testid="execute-timer"]')).not_to_be_visible()
# Tap Done
page.click('[data-testid="execute-done-btn"]')
page.wait_for_timeout(400)
# Final-round screen should appear (unchanged from Step 8 behaviour)
expect(page.locator('[data-testid="execute-final-round"]')).to_be_visible()
expect(page.locator('[data-testid="execute-continue-btn"]')).to_be_visible()
expect(page.locator('[data-testid="execute-one-more-btn"]')).to_be_visible()