From 0307416b92c2de5bb985442c0439f4463cf99be0 Mon Sep 17 00:00:00 2001 From: mwiegand Date: Thu, 21 May 2026 09:29:05 +0200 Subject: [PATCH] test(e2e): console transcript pinned to bottom on tab + submit Adds server_with_console_history fixture (30 seeded CommandHistory rows) and two Playwright tests that verify the inline Console transcript is scrolled to its bottom when the Console tab is activated and after a command is submitted. Co-Authored-By: Claude Opus 4.7 (1M context) --- l4d2web/tests/e2e/conftest.py | 24 +++++++++++ l4d2web/tests/e2e/test_server_detail.py | 53 +++++++++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/l4d2web/tests/e2e/conftest.py b/l4d2web/tests/e2e/conftest.py index e0b95ac..ef12e99 100644 --- a/l4d2web/tests/e2e/conftest.py +++ b/l4d2web/tests/e2e/conftest.py @@ -269,3 +269,27 @@ def server_with_files(tmp_path, monkeypatch): } finally: shutdown() + + +@pytest.fixture(scope="function") +def server_with_console_history(server_with_files): + """server_with_files + 30 seeded CommandHistory rows for that server, + so the inline Console transcript exceeds its visible height and the + autoscroll behaviour is observable.""" + from datetime import UTC, datetime, timedelta + from l4d2web.models import CommandHistory + + sid = server_with_files["server_id"] + uid = server_with_files["user_id"] + with session_scope() as session: + for i in range(30): + session.add(CommandHistory( + user_id=uid, + server_id=sid, + command=f"seed_{i:02d}", + reply=f"reply {i}", + is_error=False, + created_at=datetime.now(UTC) - timedelta(minutes=35 - i), + )) + + return server_with_files diff --git a/l4d2web/tests/e2e/test_server_detail.py b/l4d2web/tests/e2e/test_server_detail.py index 7a1e2d2..766b2d6 100644 --- a/l4d2web/tests/e2e/test_server_detail.py +++ b/l4d2web/tests/e2e/test_server_detail.py @@ -108,3 +108,56 @@ def test_expand_opens_matching_modal(page: Page, server_with_files) -> None: dialog = page.locator("dialog#files-modal") expect(dialog).to_have_attribute("open", "") + + +def test_console_tab_pinned_to_bottom_on_activation(page: Page, server_with_console_history) -> None: + """Clicking the Console tab leaves the transcript scrolled to its + bottom — the newest seeded command must be visible, not the oldest.""" + base = server_with_console_history["base_url"] + sid = server_with_console_history["server_id"] + login(page, base) + page.goto(f"{base}/servers/{sid}") + + strip = page.locator("[data-tab-strip]") + strip.locator('[role="tab"][data-tab="console"]').click() + + transcript = page.locator(f"#console-transcript-inline-{sid}") + expect(transcript).to_be_visible() + + bottom_distance = transcript.evaluate( + "(el) => el.scrollHeight - el.scrollTop - el.clientHeight" + ) + assert abs(bottom_distance) < 2, f"transcript not pinned to bottom: {bottom_distance}px" + + line_count = transcript.locator(".console-line").count() + assert line_count == 20, f"inline expected 20 lines, got {line_count}" + + +def test_console_pane_pinned_after_command_submit(page: Page, server_with_console_history) -> None: + """After submitting a command, the transcript scrolls so the new line + is visible at the bottom. + + The dev server has no live RCON, but the POST still records a + CommandHistory row and HTMX appends a console-line to the transcript; + that's enough to exercise the autoscroll path. + """ + base = server_with_console_history["base_url"] + sid = server_with_console_history["server_id"] + login(page, base) + page.goto(f"{base}/servers/{sid}") + + strip = page.locator("[data-tab-strip]") + strip.locator('[role="tab"][data-tab="console"]').click() + + transcript = page.locator(f"#console-transcript-inline-{sid}") + pane = page.locator('[role="tabpanel"][data-tab="console"]') + cmd_input = pane.locator('input[name="command"]') + cmd_input.fill("verify_submit") + cmd_input.press("Enter") + + expect(transcript.locator(".console-line", has_text="verify_submit")).to_be_visible() + + bottom_distance = transcript.evaluate( + "(el) => el.scrollHeight - el.scrollTop - el.clientHeight" + ) + assert abs(bottom_distance) < 2, f"transcript not pinned after submit: {bottom_distance}px"