Spaces:
Running
Running
| """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() | |