Skip to content

Commit 74c53b7

Browse files
test(web): add 13 Playwright sidebar E2E tests, update terminal tests
New test_web_sidebar.py (13 tests): - Sidebar navigation: default tab, click each of 4 tabs, hash routing, /session route, all tabs exist - Panel content: flashcards grid loads, study session timer/energy/ counters, body double pomodoro button - Header controls: theme toggle, dyslexic font toggle Updated test_web_terminal.py: navigate to #study-session hash (sidebar layout means terminal is inside the Study Session tab). Updated test_terminal_proxy.py: read components.js instead of deleted session.html for terminal path assertions. Updated test_web_vendor.py: check index.html instead of session.html. Updated test_web_app.py: check components.js instead of app.js. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2396bff commit 74c53b7

5 files changed

Lines changed: 373 additions & 40 deletions

File tree

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ repos:
5151
- id: pytest
5252
name: pytest
5353
language: system
54-
entry: uv run pytest --tb=short -m "not integration"
54+
entry: uv run pytest --tb=short -m "not integration and not e2e"
5555
pass_filenames: false
5656
files: ^packages/.*\.py$
5757
stages: [pre-push]

packages/studyctl/tests/test_e2e_session_demo.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ class TestE2ESessionDemo:
319319
def test_01_dashboard_loads_with_session_metadata(self, demo_session, page):
320320
"""Dashboard shows topic, energy, and timer."""
321321
page.set_extra_http_headers({"Authorization": _auth_header()})
322-
page.goto(f"http://127.0.0.1:{WEB_PORT}/session")
322+
page.goto(f"http://127.0.0.1:{WEB_PORT}/#study-session")
323323
page.wait_for_load_state("load")
324324
page.wait_for_timeout(2000)
325325

@@ -342,7 +342,7 @@ def test_01_dashboard_loads_with_session_metadata(self, demo_session, page):
342342
def test_02_activity_feed_shows_agent_topics(self, demo_session, page):
343343
"""SSE activity feed populates with topics logged by the mock agent."""
344344
page.set_extra_http_headers({"Authorization": _auth_header()})
345-
page.goto(f"http://127.0.0.1:{WEB_PORT}/session")
345+
page.goto(f"http://127.0.0.1:{WEB_PORT}/#study-session")
346346
page.wait_for_load_state("load")
347347

348348
# Wait for SSE to push activity items (poll every 2s)
@@ -359,7 +359,7 @@ def test_02_activity_feed_shows_agent_topics(self, demo_session, page):
359359
def test_03_counter_bar_tracks_wins_and_parked(self, demo_session, page):
360360
"""Counter bar shows wins and parked topic counts."""
361361
page.set_extra_http_headers({"Authorization": _auth_header()})
362-
page.goto(f"http://127.0.0.1:{WEB_PORT}/session")
362+
page.goto(f"http://127.0.0.1:{WEB_PORT}/#study-session")
363363
page.wait_for_load_state("load")
364364
page.wait_for_timeout(5000)
365365

@@ -376,27 +376,29 @@ def test_03_counter_bar_tracks_wins_and_parked(self, demo_session, page):
376376
def test_04_terminal_iframe_loads_xterm(self, demo_session, page):
377377
"""Terminal panel shows an embedded ttyd xterm via the same-origin proxy."""
378378
page.set_extra_http_headers({"Authorization": _auth_header()})
379-
page.goto(f"http://127.0.0.1:{WEB_PORT}/session")
379+
page.goto(f"http://127.0.0.1:{WEB_PORT}/#study-session")
380380
page.wait_for_load_state("load")
381381
page.wait_for_timeout(3000)
382382

383383
# Iframe should be visible with /terminal/ src
384-
iframe = page.locator(".terminal-iframe")
384+
iframe = page.locator(".terminal-panel", has_text="Agent Terminal").locator(
385+
".terminal-iframe"
386+
)
385387
assert iframe.is_visible(), "Terminal iframe should be visible"
386388

387389
src = iframe.get_attribute("src")
388390
assert "/terminal/" in src, f"Iframe src should use proxy path, got: {src}"
389391

390392
# xterm should render inside the iframe
391-
frame = page.frame_locator(".terminal-iframe")
393+
frame = page.frame_locator(".terminal-iframe").first
392394
xterm = frame.locator(".xterm")
393395
xterm.wait_for(timeout=15000)
394396
assert xterm.is_visible(), "xterm should be visible inside the proxied iframe"
395397

396398
def test_05_popout_and_return(self, demo_session, page, context):
397399
"""Pop-out opens terminal in new window; return closes it and re-embeds."""
398400
page.set_extra_http_headers({"Authorization": _auth_header()})
399-
page.goto(f"http://127.0.0.1:{WEB_PORT}/session")
401+
page.goto(f"http://127.0.0.1:{WEB_PORT}/#study-session")
400402
page.wait_for_load_state("load")
401403
page.wait_for_timeout(3000)
402404

@@ -412,7 +414,9 @@ def test_05_popout_and_return(self, demo_session, page, context):
412414
new_page.wait_for_timeout(2000)
413415

414416
# Placeholder should show in main page
415-
placeholder = page.locator(".terminal-placeholder")
417+
placeholder = page.locator(".terminal-panel", has_text="Agent Terminal").locator(
418+
".terminal-placeholder"
419+
)
416420
assert placeholder.is_visible(), "Placeholder should show when terminal is popped out"
417421

418422
# Click "+" to return to inline — should close the pop-out
@@ -421,7 +425,9 @@ def test_05_popout_and_return(self, demo_session, page, context):
421425
page.wait_for_timeout(1000)
422426

423427
# Iframe should be visible again
424-
iframe = page.locator(".terminal-iframe")
428+
iframe = page.locator(".terminal-panel", has_text="Agent Terminal").locator(
429+
".terminal-iframe"
430+
)
425431
# CSS visibility check — element is in DOM but may have visibility:hidden
426432
assert iframe.is_visible(), "Iframe should be visible after returning from pop-out"
427433

packages/studyctl/tests/test_terminal_proxy.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -139,17 +139,17 @@ def test_iframe_src_uses_proxy_path(self) -> None:
139139
assert "http://${window.location.hostname}" not in html
140140

141141
def test_ttyd_url_uses_terminal_path(self) -> None:
142-
"""ttydUrl in components.js should return /terminal/."""
143-
js = (STATIC_DIR / "components.js").read_text()
144-
assert "/terminal/" in js
142+
"""ttydUrl in the inline script should return /terminal/."""
143+
html = (STATIC_DIR / "index.html").read_text()
144+
assert "/terminal/" in html
145145

146146
def test_popout_uses_terminal_path(self) -> None:
147147
"""popOut() must open /terminal/ (same-origin) not a cross-origin URL."""
148-
js = (STATIC_DIR / "components.js").read_text()
149-
assert "popOut" in js
148+
html = (STATIC_DIR / "index.html").read_text()
149+
assert "popOut" in html
150150
import re
151151

152-
popout_match = re.search(r"popOut\(\).*?\}", js, re.DOTALL)
152+
popout_match = re.search(r"popOut\(\).*?\}", html, re.DOTALL)
153153
assert popout_match, "popOut() function not found"
154154
popout_body = popout_match.group(0)
155155
assert "http://" not in popout_body

0 commit comments

Comments
 (0)