From 86ac10a1d99f558a58ebb2ce743da11a6094057a Mon Sep 17 00:00:00 2001 From: mwiegand Date: Sun, 17 May 2026 17:20:37 +0200 Subject: [PATCH] docs(files): handoff plan for files-overlay Playwright e2e tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Companion to the just-shipped files-overlay rewrite. The rewrite verified each step's behavior live in Chromium but left no automated browser regression net. This handoff plan specifies what to add: * Fixture extension (conftest.py): a files_overlay_server fixture that seeds a files-type Overlay with one text file, one binary file, and a nested folder under tmp_path-rooted LEFT4ME_ROOT. * 11 test cases in three tiers — Tier 1 covers the critical paths (text edit save, create-new, 409→askConflict, binary replace, new-folder + delete, rename-on-save), Tier 2 rounds out drag / upload / deep-link, Tier 3 hits the server-detail download button. * Patterns to follow + pitfalls (the SESSION_COOKIE_SECURE=0 gotcha, the data-rel-path location split between text and binary modes, the htmx.ajax async wait, why os-drag-with-folders can't be synthesized). Pinned references at the bottom point at the existing test_editor.py pattern model and the relevant module-header comments. Estimated half-day for the critical 7 cases, full day for the full 11. Lives under docs/superpowers/plans/ per the project's planning- artifact convention. Move to specs/ if it's executed and turned into shipped tests. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../2026-05-17-files-overlay-e2e-handoff.md | 243 ++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-17-files-overlay-e2e-handoff.md diff --git a/docs/superpowers/plans/2026-05-17-files-overlay-e2e-handoff.md b/docs/superpowers/plans/2026-05-17-files-overlay-e2e-handoff.md new file mode 100644 index 0000000..5146aa3 --- /dev/null +++ b/docs/superpowers/plans/2026-05-17-files-overlay-e2e-handoff.md @@ -0,0 +1,243 @@ +# Files-overlay E2E test handoff + +## Context + +The files-overlay rewrite (commits `4fa3964..8dc14f0`, May 2026) +moved all editor flows behind URL-addressable modals and split the +1091-line `files-overlay.js` monolith into four focused modules under +`l4d2web/l4d2web/static/js/files-overlay/`. Behavior was verified +step-by-step in Chromium during the rewrite, but there is no automated +browser regression coverage for the editor / dialog / upload flows. + +The existing Playwright suite (`l4d2web/tests/e2e/test_editor.py`) +covers only the CodeMirror 6 controller — autocomplete, form-bridge, +copy/paste — invoked through a blueprint detail page. Nothing +exercises the file manager UI. + +This handoff specifies what to add: fixture extensions, the test +cases worth writing, and the patterns / pitfalls a future implementer +should know before starting. Estimated effort: a focused half-day for +the seven critical cases, a full day for the full matrix. + +## Goal + +Lock down the user-visible behavior of the four files-overlay modules +against future regressions. The rewrite proved each module works in +isolation; e2e proves they cooperate over real DOM, real HTTP, real +HTMX, and real CodeMirror. + +## Out of scope + +- Re-testing pure CodeMirror behavior (the existing `test_editor.py` + covers this on a non-files page; the controller is the same one). +- Replacing the existing pytest route tests (`tests/test_overlay_files_routes.py`, + `tests/test_url_addressable_modals.py`). E2E adds *integration* + coverage on top of those, not in place. +- Performance / load testing of the upload queue (concurrency 3 is + the current behavior; testing it would need 4+ simultaneous uploads + and is high-flake low-value). +- The drag-drop-from-OS path. Playwright can't synthesize a real OS + drag (`webkitGetAsEntry` returns `null` for synthetic drops, so the + fallback `getAsFile` branch always runs). The internal-drag path + (row → folder) is testable; the external drag fallback is covered + enough by the route tests. + +## Fixture work + +`l4d2web/tests/e2e/conftest.py` currently seeds only a `User` and a +`Blueprint`. The files-overlay tests need a files-type overlay with a +working filesystem root. Add a new fixture (or extend `live_server`): + +```python +# tests/e2e/conftest.py + +@pytest.fixture(scope="function") +def files_overlay_server(tmp_path, monkeypatch): + """live_server + a files-type Overlay seeded with a small fixture + set: one editable text file, one binary file, one nested folder + with one file inside. + + Returns {base_url, user_id, overlay_id, overlay_root: Path}. + """ + # Same boot as live_server (extract a helper to avoid duplication). + # Set LEFT4ME_ROOT to tmp_path before create_app() so the files + # overlay's path resolution lands under tmp_path. + monkeypatch.setenv("LEFT4ME_ROOT", str(tmp_path)) + ... + + with session_scope() as session: + user = User(username="alice", password_digest=hash_password("secret"), admin=False) + session.add(user); session.flush() + overlay = Overlay(name="cfgs", path="", type="files", user_id=user.id) + session.add(overlay); session.flush() + overlay.path = str(overlay.id) + overlay_root = tmp_path / "overlays" / str(overlay.id) + overlay_root.mkdir(parents=True) + (overlay_root / "server.cfg").write_text("hostname \"left4me\"\n") + (overlay_root / "icon.png").write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * 60) + (overlay_root / "cfg").mkdir() + (overlay_root / "cfg" / "admins.txt").write_text("STEAM_1:0:1\n") + user_id, overlay_id = user.id, overlay.id + ... + yield { + "base_url": ..., + "user_id": user_id, + "overlay_id": overlay_id, + "overlay_root": overlay_root, + } +``` + +The `LEFT4ME_ROOT` env-var monkey-patch is critical — without it, +`overlay_files.resolve_overlay_root` falls back to the production +`/var/lib/left4me` path (per the `AGENTS.md` "symptom-to-cause" +note) and every route returns 404. Set it BEFORE `create_app()`. + +## Test cases to add + +Suggested file: `l4d2web/tests/e2e/test_files_overlay.py`. Pattern +each test like the existing `test_editor.py`: log in via the form, +navigate to `/overlays/`, drive the UI through Playwright `page` +locators, assert on DOM state + filesystem state under +`overlay_root`. + +### Tier 1 — critical paths (write these first) + +1. **`test_edit_text_file_save_round_trip`** + - Click `server.cfg` filename. Wait for `#modal-content + textarea[data-rel-path="server.cfg"]`. URL should contain + `?modal=%2Foverlays%2F%2Ffiles%2Fedit%3Fpath%3Dserver.cfg`. + - Modify content via Playwright `page.fill` on the textarea (or + via the `__filesEditor.setContent` controller for the CM6 case + — the existing `test_editor.py` shows both approaches). + - Click `.files-editor-save`. Modal closes (modal-container + `aria-modal` gone / `open` false). + - Assert `overlay_root / "server.cfg"` on disk has the new content. + +2. **`test_create_new_file_routed`** + - Click `+ new file` on the overlay-root row. Wait for + `#modal-content textarea[data-rel-path=""]` and save button + labeled `Create`. + - Type a filename and content. Click Create. + - Assert file appears on disk + the file tree refreshes to show + the new row. + +3. **`test_create_new_file_409_askConflict_keep_both`** + - Click `+ new file`. Type `cfg` as the filename (collides with + the seeded directory). Click Create. + - Wait for `#files-conflict-modal[open]`. Its + `.files-conflict-path` should read `cfg`. + - Click `[data-files-conflict-action="keep-both"]`. + - Assert the file `cfg (1)` appears on disk and the routed modal + closes. + - This is the path F4 (`8dc14f0`) added; without coverage it can + regress silently. + +4. **`test_open_binary_file_renders_replace_ui`** + - Click `icon.png`. Modal opens. + - Assert `#modal-content .files-editor-binary[data-rel-path="icon.png"]` + exists, save button reads `Replace` and is disabled, + `.files-editor-replace-zone` and the download anchor are present. + +5. **`test_binary_replace_via_browse_writes_new_bytes`** + - Open `icon.png` editor (as above). + - Click `.files-editor-replace-browse`. Use Playwright's + `page.expect_file_chooser()` to attach a small File buffer. + - Save button enables. Click it. Modal closes. + - Assert the file's bytes on disk are the new content. + +6. **`test_new_folder_then_delete`** + - Click `+ new folder` on the overlay root. Inline dialog opens. + - Type a name, press Enter (keydown path). Dialog closes. + - Assert folder exists on disk + appears in tree. + - Click the folder's `✕`. Delete-confirm dialog opens with the + folder name. Click `.files-delete-confirm`. + - Assert folder gone from disk + from tree. + +7. **`test_filename_rename_on_save`** + - Open `server.cfg`. Change the filename input to + `server-renamed.cfg`. Click Save. + - Assert disk has the new name + old name gone + tree row updated. + +### Tier 2 — round out the matrix + +8. **`test_drag_row_to_folder_moves_file`** — internal drag. + Playwright's `locator.drag_to()` can move a row onto a folder. + Assert the move via `/files/move` succeeded and disk reflects it. + +9. **`test_upload_queue_progress`** — drop a single file onto the + tree root. The progress panel becomes visible; the row enters + `data-state="active"`, then `data-state="done"`. Assert the + uploaded file is on disk. (Skip the 409 / conflict / cancel + permutations — they're covered by the route tests.) + +10. **`test_modal_close_on_escape_preserves_no_state`** — open the + routed editor, type some content but don't save, press Escape. + Modal closes. Reopen — content is fresh (no stale buffer), + `routedReplacement` cleared. + +11. **`test_share_url_deep_link_reopens_editor`** — navigate + directly to `/overlays/?modal=%2Foverlays%2F%2Ffiles%2Fedit%3Fpath%3Dserver.cfg`. + Modal should auto-open on DOMContentLoaded (the bootstrap path + from `modals.js`). This is the URL-addressable spec's central + promise; without coverage it regresses easily. + +### Tier 3 — nice to have + +12. Server detail page hover-download button (the F0 prefactor): + seed a server, navigate to `/servers/`, hover a file row, + click the `⬇` button, assert a file download initiates. + +## Patterns to follow / pitfalls + +- **The existing `test_editor.py` is the closest pattern.** Read it + end-to-end before starting. The login helper, the `live_server` + fixture shape, the `expect`-based assertions, and the way + Playwright interacts with the CM6 controller (`page.evaluate(...)` + on `window.__filesEditor`) all transfer. +- **Run with `uv run pytest -m e2e tests/e2e/test_files_overlay.py`.** + Anything else crashes Chromium under macOS sandbox. + `uv run playwright install chromium` once per fresh checkout. +- **Routed modals load via `htmx.ajax` — they're async.** Don't assert + immediately after the click. Use `expect(page.locator(...)).to_be_visible()` + with a timeout (Playwright's default 5s is fine). +- **Reading the file tree after a refresh is also async.** The JS + `scheduleRefresh` debounces by 50ms then fetches the directory + partial via HTMX. Use `expect(page.locator(".file-tree-row-file[data-target-path='...']")).to_be_visible()` + rather than polling DOM directly. +- **`data-rel-path` lives on the textarea in text mode and on the + binary panel in binary mode.** Tests asserting "the editor opened + for X" should query whichever matches — or use the fragment + wrapper `#files-editor-fragment` as a stable container. +- **The conflict dialog is inline, not routed.** Don't expect URL + changes when it opens. The decision tree: + - "Did the URL change?" → routed modal (editor) vs. inline modal + (new-folder, conflict, delete-confirm). +- **`SESSION_COOKIE_SECURE=0` is non-optional.** The fixture must set + it; otherwise the browser drops the session cookie over http and + every test redirects back to `/login`. The existing `conftest.py` + has the right pattern at line 39. + +## Verification + +Per AGENTS.md: `uv run pytest -m e2e tests/e2e/test_files_overlay.py -v`. +The tier-1 seven cases should pass green in <60s on a warm Chromium. +The full matrix (12 cases) target <2 minutes. + +When wiring CI / pre-push hooks: the e2e marker is excluded from the +default fast suite, so the existing 580-passing `uv run pytest tests/` +run remains the quick check. The e2e suite runs explicitly when +`-m e2e` is set. + +## References + +- `l4d2web/tests/e2e/test_editor.py` — pattern model +- `l4d2web/tests/e2e/conftest.py:39` — `SESSION_COOKIE_SECURE` note +- `l4d2web/tests/test_url_addressable_modals.py` — non-browser route + tests that already cover the server-side contract (200/404/415/400 + on edit, new, save). E2E shouldn't duplicate these. +- `l4d2web/l4d2web/static/js/files-overlay/{core,editor,dialogs,uploads}.js` + — read each file's module header comment for the listener layout + before writing assertions. +- `AGENTS.md` "Files overlay: module layout" — high-level orientation. +- `AGENTS.md` "Modals: inline vs routed" — decision tree the test + matrix follows.