diff --git a/l4d2web/tests/e2e/test_files_overlay.py b/l4d2web/tests/e2e/test_files_overlay.py new file mode 100644 index 0000000..adb538a --- /dev/null +++ b/l4d2web/tests/e2e/test_files_overlay.py @@ -0,0 +1,75 @@ +"""End-to-end Playwright tests for the files-overlay file manager. + +The files_overlay_server fixture (conftest.py) seeds user "alice"/"secret", +a files-type Overlay owned by alice, and an overlay root pre-populated +with one editable text file, one binary file, and one nested folder. +Each test logs in, opens /overlays/, drives the UI through real +clicks + HTMX swaps, and asserts on DOM + filesystem state. + +Running locally: `uv run pytest -m e2e l4d2web/tests/e2e/test_files_overlay.py`. +Requires `uv run playwright install chromium` once. + +Pattern model: test_editor.py (CM6 controller flow) + the handoff doc at +docs/superpowers/plans/2026-05-17-files-overlay-e2e-handoff.md. +""" +from __future__ import annotations + +import pytest +from playwright.sync_api import Page, expect + +from .conftest import login + +pytestmark = pytest.mark.e2e + + +def _open_overlay(page: Page, base_url: str, overlay_id: int) -> None: + login(page, base_url) + page.goto(f"{base_url}/overlays/{overlay_id}") + + +def _wait_for_routed_editor(page: Page) -> None: + """Wait until the routed modal's CM6 surface is mounted. + + The textarea itself is `display:none` (editor.css pre-hides + CM6's + mount sets `style.display = "none"`), so we cannot assert visibility + on it directly. The `.cm-content` element appearing under + `#files-editor-fragment` is the canonical "editor is ready, the + `window.__filesEditor` controller is wired" signal. + """ + expect(page.locator("#files-editor-fragment")).to_be_visible(timeout=5000) + expect(page.locator("#files-editor-fragment .cm-content")).to_be_visible(timeout=5000) + + +def test_edit_text_file_save_round_trip(page: Page, files_overlay_server) -> None: + """Open server.cfg via the file-row name button, drive the CM6 + controller to a new doc, save, and assert the disk reflects it. + Exercises the full edit path: row click → openRouted → htmx swap → + CM6 mount → save POST → modal close. Failure modes guarded against: + * /overlays//files/edit returns the wrong fragment (textarea + gets the wrong data-rel-path, or none). + * editor.js's htmx:afterSwap re-init doesn't fire / fails — then + window.__filesEditor stays unset and the save reads the stale + textarea.value instead of the user's edits. + * routedSaveClicked stops short of closeRouted() — modal stays + open and the test's hidden-assertion catches it. + * The POST /files/save endpoint doesn't actually write — covered + by reading the file back from disk. + """ + base = files_overlay_server["base_url"] + overlay_id = files_overlay_server["overlay_id"] + overlay_root = files_overlay_server["overlay_root"] + _open_overlay(page, base, overlay_id) + + page.click('button.file-tree-name-button[data-target-path="server.cfg"]') + _wait_for_routed_editor(page) + # Sanity: the modal opened for the file we asked for, not some other. + assert page.locator('textarea[data-rel-path="server.cfg"]').count() == 1 + + new_content = 'hostname "edited by e2e"\nmp_gamemode coop\n' + page.evaluate("(text) => window.__filesEditor.setContent(text)", new_content) + + page.click(".files-editor-save") + + # Modal close is async (await postJson then closeRouted()). + expect(page.locator("#modal-container")).to_be_hidden(timeout=5000) + assert (overlay_root / "server.cfg").read_text() == new_content