test(files): cover text-file edit round trip
Opens server.cfg through the file-row name button, drives the CM6 controller via window.__filesEditor.setContent, clicks save, and asserts both that the routed modal closes and that the new bytes landed on disk under overlay_root. Guards against four classes of regression at once: the /files/edit fragment delivering the wrong data-rel-path, editor.js's htmx:afterSwap re-init failing to wire __filesEditor, routedSaveClicked stopping short of closeRouted(), and the /files/save endpoint failing to persist. Adds a `_wait_for_routed_editor` helper centered on `.cm-content` inside `#files-editor-fragment` — the textarea itself is display:none after CM6 mounts, so to_be_visible on the textarea would always fail; the cm-content surface is the real "editor is ready" signal. Per docs/superpowers/plans/2026-05-17-files-overlay-e2e-handoff.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
19357124f4
commit
3cafdba2cc
1 changed files with 75 additions and 0 deletions
75
l4d2web/tests/e2e/test_files_overlay.py
Normal file
75
l4d2web/tests/e2e/test_files_overlay.py
Normal file
|
|
@ -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/<id>, 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/<id>/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
|
||||||
Loading…
Reference in a new issue