203 lines
9 KiB
Python
203 lines
9 KiB
Python
"""
|
|
Step 14 — Form Keyboard Shortcuts Tests
|
|
|
|
Tests for Ctrl+Enter (save) and Escape (cancel) shortcuts on form views.
|
|
The exercise form is used as the representative form; one test each covers
|
|
the session and combo forms to confirm the shortcuts are wired consistently.
|
|
|
|
- test_ctrl_enter_saves_from_body: Ctrl+Enter with no input focused saves the form
|
|
- test_ctrl_enter_saves_from_input: Ctrl+Enter while typing in a field saves the form
|
|
- test_ctrl_enter_validates_before_saving: Ctrl+Enter with empty title shows validation error
|
|
- test_escape_cancels_from_body: Escape with no input focused navigates back to list
|
|
- test_escape_cancels_from_input: Escape while a field is focused navigates back to list
|
|
- test_escape_cancel_edit_returns_to_detail: Escape on an edit form returns to detail view
|
|
- test_save_button_tooltip_contains_shortcut: Save button title attribute shows Ctrl+Enter hint
|
|
- test_ctrl_enter_on_session_form: Ctrl+Enter saves from the session new form
|
|
- test_ctrl_enter_on_combo_form: Ctrl+Enter saves from the combo new form
|
|
- test_ctrl_enter_on_collection_form: Ctrl+Enter saves from the collection new form
|
|
"""
|
|
from playwright.sync_api import Page, expect
|
|
|
|
|
|
def _wait_db(page: Page, app_url: str):
|
|
page.goto(app_url)
|
|
page.wait_for_function(
|
|
"document.documentElement.getAttribute('data-db-ready') === 'true'",
|
|
timeout=15000,
|
|
)
|
|
|
|
|
|
def _clean_exercises(page: Page):
|
|
page.evaluate(
|
|
"""async () => {
|
|
const all = await window.__repos.exercises.getAll();
|
|
for (const ex of all) await window.__repos.exercises.delete(ex.id);
|
|
}"""
|
|
)
|
|
|
|
|
|
def _clean_sessions(page: Page):
|
|
page.evaluate(
|
|
"""async () => {
|
|
const all = await window.__repos.sessions.getAll();
|
|
for (const s of all) await window.__repos.sessions.delete(s.id);
|
|
}"""
|
|
)
|
|
|
|
|
|
def _clean_combos(page: Page):
|
|
page.evaluate(
|
|
"""async () => {
|
|
const all = await window.__repos.combos.getAll();
|
|
for (const c of all) await window.__repos.combos.delete(c.id);
|
|
}"""
|
|
)
|
|
|
|
|
|
def _clean_collections(page: Page):
|
|
page.evaluate(
|
|
"""async () => {
|
|
const all = await window.__repos.collections.getAll();
|
|
for (const c of all) await window.__repos.collections.delete(c.id);
|
|
}"""
|
|
)
|
|
|
|
|
|
def _open_exercise_new_form(page: Page, app_url: str):
|
|
_wait_db(page, app_url)
|
|
_clean_exercises(page)
|
|
page.click('a[href="#/exercises"]')
|
|
page.wait_for_timeout(300)
|
|
page.click('[data-testid="exercise-new-btn"]')
|
|
page.wait_for_selector('[data-testid="exercise-title"]')
|
|
|
|
|
|
def test_ctrl_enter_saves_from_body(page: Page, app_url: str):
|
|
"""Ctrl+Enter with focus outside any input saves the form and navigates to detail view."""
|
|
_open_exercise_new_form(page, app_url)
|
|
page.fill('[data-testid="exercise-title"]', "Shortcut Save Test")
|
|
# Move focus away from the input (Escape would cancel the form, so blur instead)
|
|
page.evaluate("document.activeElement.blur()")
|
|
page.keyboard.press("Control+Enter")
|
|
page.wait_for_selector('[data-testid="exercise-detail-title"]', timeout=5000)
|
|
expect(page.locator('[data-testid="exercise-detail-title"]')).to_have_text("Shortcut Save Test")
|
|
_clean_exercises(page)
|
|
|
|
|
|
def test_ctrl_enter_saves_from_input(page: Page, app_url: str):
|
|
"""Ctrl+Enter while a text input is focused saves the form."""
|
|
_open_exercise_new_form(page, app_url)
|
|
page.fill('[data-testid="exercise-title"]', "Shortcut From Input")
|
|
# Keep focus inside the title input and press Ctrl+Enter
|
|
page.locator('[data-testid="exercise-title"]').press("Control+Enter")
|
|
page.wait_for_selector('[data-testid="exercise-detail-title"]', timeout=5000)
|
|
expect(page.locator('[data-testid="exercise-detail-title"]')).to_have_text("Shortcut From Input")
|
|
_clean_exercises(page)
|
|
|
|
|
|
def test_ctrl_enter_validates_before_saving(page: Page, app_url: str):
|
|
"""Ctrl+Enter with an empty title triggers validation rather than saving."""
|
|
_open_exercise_new_form(page, app_url)
|
|
# Do not fill the title — press Ctrl+Enter immediately
|
|
page.locator('[data-testid="exercise-title"]').press("Control+Enter")
|
|
expect(page.locator('[data-testid="exercise-title-error"]')).to_be_visible()
|
|
# Still on the form
|
|
expect(page.locator('[data-testid="exercise-title"]')).to_be_visible()
|
|
|
|
|
|
def test_escape_cancels_from_body(page: Page, app_url: str):
|
|
"""Escape with focus on the page body (no input focused) cancels the form and goes back to the list."""
|
|
_open_exercise_new_form(page, app_url)
|
|
page.fill('[data-testid="exercise-title"]', "Should Not Be Saved")
|
|
# Click outside the form to move focus away from any input
|
|
page.locator("h1").first.click()
|
|
page.keyboard.press("Escape")
|
|
page.wait_for_url("**#/exercises", timeout=5000)
|
|
expect(page.locator('[data-testid="exercise-new-btn"]')).to_be_visible()
|
|
|
|
|
|
def test_escape_cancels_from_input(page: Page, app_url: str):
|
|
"""Escape while a text input is focused cancels the form and goes back to the list."""
|
|
_open_exercise_new_form(page, app_url)
|
|
page.fill('[data-testid="exercise-title"]', "Should Not Be Saved Either")
|
|
page.locator('[data-testid="exercise-title"]').press("Escape")
|
|
page.wait_for_url("**#/exercises", timeout=5000)
|
|
expect(page.locator('[data-testid="exercise-new-btn"]')).to_be_visible()
|
|
|
|
|
|
def test_escape_cancel_edit_returns_to_detail(page: Page, app_url: str):
|
|
"""Escape on an edit form returns to the detail view, not the list."""
|
|
_wait_db(page, app_url)
|
|
_clean_exercises(page)
|
|
# Create an exercise first
|
|
page.click('a[href="#/exercises"]')
|
|
page.wait_for_timeout(300)
|
|
page.click('[data-testid="exercise-new-btn"]')
|
|
page.wait_for_selector('[data-testid="exercise-title"]')
|
|
page.fill('[data-testid="exercise-title"]', "Edit Escape Test")
|
|
page.click('[data-testid="exercise-save"]')
|
|
page.wait_for_selector('[data-testid="exercise-detail-title"]', timeout=5000)
|
|
# Open the edit form
|
|
page.click('[data-testid="exercise-edit-btn"]')
|
|
page.wait_for_selector('[data-testid="exercise-title"]')
|
|
# Press Escape — should go back to the detail view, not the list
|
|
page.keyboard.press("Escape")
|
|
page.wait_for_selector('[data-testid="exercise-detail-title"]', timeout=5000)
|
|
expect(page.locator('[data-testid="exercise-detail-title"]')).to_have_text("Edit Escape Test")
|
|
_clean_exercises(page)
|
|
|
|
|
|
def test_save_button_tooltip_contains_shortcut(page: Page, app_url: str):
|
|
"""The save button has a title attribute that includes 'Ctrl+Enter'."""
|
|
_open_exercise_new_form(page, app_url)
|
|
title_attr = page.locator('[data-testid="exercise-save"]').get_attribute("title")
|
|
assert title_attr is not None, "Save button should have a title attribute"
|
|
assert "Ctrl+Enter" in title_attr or "Ctrl+Entrée" in title_attr, (
|
|
f"Expected Ctrl+Enter hint in title, got: {title_attr}"
|
|
)
|
|
|
|
|
|
def test_ctrl_enter_on_session_form(page: Page, app_url: str):
|
|
"""Ctrl+Enter saves the session new form from within the title input."""
|
|
_wait_db(page, app_url)
|
|
_clean_sessions(page)
|
|
page.click('a[href="#/sessions"]')
|
|
page.wait_for_timeout(300)
|
|
page.click('[data-testid="session-new-btn"]')
|
|
page.wait_for_selector('[data-testid="session-title"]')
|
|
page.fill('[data-testid="session-title"]', "Session Shortcut Test")
|
|
page.locator('[data-testid="session-title"]').press("Control+Enter")
|
|
page.wait_for_selector('[data-testid="session-detail-title"]', timeout=5000)
|
|
expect(page.locator('[data-testid="session-detail-title"]')).to_have_text("Session Shortcut Test")
|
|
_clean_sessions(page)
|
|
|
|
|
|
def test_ctrl_enter_on_combo_form(page: Page, app_url: str):
|
|
"""Ctrl+Enter saves the combo new form from within the title input."""
|
|
_wait_db(page, app_url)
|
|
_clean_combos(page)
|
|
page.click('a[href="#/combos"]')
|
|
page.wait_for_timeout(300)
|
|
page.click('[data-testid="combo-new-btn"]')
|
|
page.wait_for_selector('[data-testid="combo-title"]')
|
|
page.fill('[data-testid="combo-title"]', "Combo Shortcut Test")
|
|
page.locator('[data-testid="combo-title"]').press("Control+Enter")
|
|
page.wait_for_selector('[data-testid="combo-detail-title"]', timeout=5000)
|
|
expect(page.locator('[data-testid="combo-detail-title"]')).to_have_text("Combo Shortcut Test")
|
|
_clean_combos(page)
|
|
|
|
|
|
def test_ctrl_enter_on_collection_form(page: Page, app_url: str):
|
|
"""Ctrl+Enter saves the collection new form from within the label input."""
|
|
_wait_db(page, app_url)
|
|
_clean_collections(page)
|
|
page.click('a[href="#/collections"]')
|
|
page.wait_for_timeout(300)
|
|
page.click('[data-testid="collection-new-btn"]')
|
|
page.wait_for_selector('[data-testid="collection-label"]')
|
|
page.fill('[data-testid="collection-label"]', "Collection Shortcut Test")
|
|
page.locator('[data-testid="collection-label"]').press("Control+Enter")
|
|
page.wait_for_selector('[data-testid="collection-detail-title"]', timeout=5000)
|
|
expect(page.locator('[data-testid="collection-detail-title"]')).to_have_text("Collection Shortcut Test")
|
|
_clean_collections(page)
|