docs(files): handoff plan for files-overlay Playwright e2e tests
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) <noreply@anthropic.com>
This commit is contained in:
parent
e1723f751c
commit
86ac10a1d9
1 changed files with 243 additions and 0 deletions
243
docs/superpowers/plans/2026-05-17-files-overlay-e2e-handoff.md
Normal file
243
docs/superpowers/plans/2026-05-17-files-overlay-e2e-handoff.md
Normal file
|
|
@ -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/<id>`, 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<id>%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/<id>?modal=%2Foverlays%2F<id>%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/<id>`, 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.
|
||||||
Loading…
Reference in a new issue