marionette / tests /e2e /test_ui.py
RemiFabre
Simplify recording form: Name + Duration + Record in one row
c0e8ee8
Raw
History Blame
7 kB
"""Tier 3 β€” Playwright E2E browser tests for Marionette.
Run without hardware, without daemon. Tests the frontend UI in real browsers.
Uses a real Marionette server with temp paths (no robot connection needed for
these tests β€” they only test the web UI behavior).
"""
import re
import pytest
from playwright.sync_api import Page, expect
# ──────── Page load tests ─────────────────────────────────────────────
class TestPageLoad:
def test_page_loads_successfully(self, page: Page, base_url: str):
page.goto(base_url)
expect(page).to_have_title(re.compile("Marionette", re.IGNORECASE))
def test_main_sections_visible(self, page: Page, base_url: str):
page.goto(base_url)
# Record section
expect(page.locator("#record-form")).to_be_visible()
# Moves section
expect(page.locator("#moves-list")).to_be_visible()
# Record button
expect(page.locator("#record-btn")).to_be_visible()
def test_mode_pill_shows_idle(self, page: Page, base_url: str):
page.goto(base_url)
pill = page.locator("#mode-pill")
expect(pill).to_be_visible()
# Wait for first poll to update the pill
page.wait_for_timeout(2000)
expect(pill).to_contain_text(re.compile("idle", re.IGNORECASE))
# ──────── Idle state display ──────────────────────────────────────────
class TestIdleState:
def test_record_button_enabled(self, page: Page, base_url: str):
page.goto(base_url)
page.wait_for_timeout(2000)
btn = page.locator("#record-btn")
expect(btn).to_be_enabled()
def test_stop_buttons_hidden(self, page: Page, base_url: str):
page.goto(base_url)
page.wait_for_timeout(2000)
expect(page.locator("#stop-recording-btn")).to_be_hidden()
expect(page.locator("#stop-playback-btn")).to_be_hidden()
def test_progress_bar_empty(self, page: Page, base_url: str):
page.goto(base_url)
page.wait_for_timeout(2000)
fill = page.locator("#progress-fill")
width = fill.evaluate("el => el.style.width")
assert width == "0%" or width == ""
def test_phase_display_hidden_when_idle(self, page: Page, base_url: str):
page.goto(base_url)
page.wait_for_timeout(2000)
expect(page.locator("#phase-display")).to_be_hidden()
# ──────── Form validation tests ───────────────────────────────────────
class TestSimplifiedForm:
def test_name_and_duration_visible_by_default(self, page: Page, base_url: str):
page.goto(base_url)
expect(page.locator("#label")).to_be_visible()
expect(page.locator("#duration")).to_be_visible()
expect(page.locator("#record-btn")).to_be_visible()
def test_options_collapsed_by_default(self, page: Page, base_url: str):
"""Audio source should be hidden in Options until user expands it."""
page.goto(base_url)
expect(page.locator("#audio-source-mic")).to_be_hidden()
def test_description_field_removed(self, page: Page, base_url: str):
"""P1: description field was removed to simplify the form."""
page.goto(base_url)
assert page.locator("#description").count() == 0
class TestFormValidation:
def test_duration_field_exists(self, page: Page, base_url: str):
page.goto(base_url)
duration = page.locator("#duration")
expect(duration).to_be_visible()
# Check it has a default value
value = duration.input_value()
assert float(value) > 0
def test_duration_accepts_decimal(self, page: Page, base_url: str):
page.goto(base_url)
duration = page.locator("#duration")
duration.fill("3.7")
assert duration.input_value() == "3.7"
def test_duration_accepts_non_half_second(self, page: Page, base_url: str):
"""P0-2 regression: duration field must accept values like 5.2, not just 0.5 multiples."""
page.goto(base_url)
duration = page.locator("#duration")
for val in ["5.2", "12.3", "0.7", "201.7"]:
duration.fill(val)
assert duration.input_value() == val
# Verify the field passes HTML5 validity
is_valid = duration.evaluate("el => el.checkValidity()")
assert is_valid, f"Duration {val} should be valid but HTML5 validation rejected it"
def test_audio_source_radios_exist(self, page: Page, base_url: str):
page.goto(base_url)
# Audio source is inside collapsible Options β€” open it first
page.locator("#record-options summary").click()
expect(page.locator("#audio-source-mic")).to_be_visible()
expect(page.locator("#audio-source-none")).to_be_visible()
def test_label_field_exists(self, page: Page, base_url: str):
page.goto(base_url)
expect(page.locator("#label")).to_be_visible()
# ──────── Recording submission tests ──────────────────────────────────
class TestRecordingSubmission:
def test_submit_recording_changes_mode(self, page: Page, base_url: str, test_marionette):
page.goto(base_url)
page.wait_for_timeout(2000)
# Open Options and set audio source to "none" to avoid audio backend requirement
page.locator("#record-options summary").click()
page.locator("#audio-source-none").check()
page.locator("#duration").fill("2")
page.locator("#label").fill("e2e-test")
# Submit
page.locator("#record-btn").click()
# Wait for state to update
page.wait_for_timeout(2000)
# Mode pill should show something other than "idle"
pill_text = page.locator("#mode-pill").text_content()
assert pill_text is not None
# Mode should be queued (since no robot is running to process it)
assert "queued" in pill_text.lower() or "countdown" in pill_text.lower() or "recording" in pill_text.lower()
# Reset for other tests
test_marionette._set_idle_state()
test_marionette._pending_recording = None
# ──────── Dataset UI tests ────────────────────────────────────────────
class TestDatasetUI:
def test_dataset_selector_visible(self, page: Page, base_url: str):
page.goto(base_url)
page.wait_for_timeout(2000)
expect(page.locator("#dataset-select")).to_be_visible()
def test_new_dataset_button_exists(self, page: Page, base_url: str):
page.goto(base_url)
expect(page.locator("#new-dataset-btn")).to_be_visible()