File size: 6,995 Bytes
4a04219
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c0e8ee8
 
 
 
 
4a04219
 
 
 
c0e8ee8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4a04219
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b480153
 
 
 
 
 
 
 
 
 
 
4a04219
 
c0e8ee8
 
4a04219
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c0e8ee8
 
4a04219
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
"""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()