From e89dd25cdd29342f3202090e5bb25c881c5f06b0 Mon Sep 17 00:00:00 2001 From: mwiegand Date: Sun, 17 May 2026 19:18:57 +0200 Subject: [PATCH] test(files): cover internal drag row to folder move MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drags server.cfg from the overlay root onto the cfg/ folder row, asserts both that the file actually moved on disk AND that the tree reflects the move after the debounced HTMX refresh of both parents. Exercises the internal-drag path in uploads.js:332-366: the custom-MIME setData/getData contract, the POST /files/move call, and the dual scheduleRefresh(parentOf(src)) + scheduleRefresh(dst) on success. Playwright's locator.drag_to() synthesizes setData correctly — it relies on dataTransfer.getData(), NOT webkitGetAsEntry which Playwright cannot fake. Pitfalls handled inline: scoped the source/target locators to li.file-tree-row-{file,dir} because action buttons inside the row duplicate data-target-path + data-row-kind and would trip strict mode. Used to_have_count instead of to_be_visible because cfg's children div is collapsed (hidden) by default — the moved row is in the DOM but not visually rendered until cfg is expanded. Per docs/superpowers/plans/2026-05-17-files-overlay-e2e-handoff.md (Tier 2 case C). Co-Authored-By: Claude Opus 4.7 (1M context) --- l4d2web/tests/e2e/test_files_overlay.py | 47 +++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/l4d2web/tests/e2e/test_files_overlay.py b/l4d2web/tests/e2e/test_files_overlay.py index 2dbd3bc..ca19ec4 100644 --- a/l4d2web/tests/e2e/test_files_overlay.py +++ b/l4d2web/tests/e2e/test_files_overlay.py @@ -453,3 +453,50 @@ def test_modal_close_on_escape_preserves_no_state(page: Page, files_overlay_serv _wait_for_routed_editor(page) reopened = page.evaluate("() => window.__filesEditor.getValue()") assert reopened == original_content + + +def test_drag_row_to_folder_moves_file(page: Page, files_overlay_server) -> None: + """Drag the server.cfg row from the overlay root onto the cfg/ + folder row. Asserts the file actually moved on disk AND the tree + reflects the move after the debounced HTMX refresh. + + Exercises the internal-drag path in uploads.js:332-366 — the + `dataTransfer.setData("application/x-files-overlay", path)` → + `getData` round-trip → POST /files/move → schedule refresh of + both parents. Playwright's `locator.drag_to()` synthesizes the + `setData/getData` contract that this path relies on (it does NOT + require `webkitGetAsEntry`, which Playwright cannot fake). + + Tree DOM gotcha: the cfg folder is collapsed by default, so its + children div is `hidden`. `scheduleRefresh("cfg")` still swaps + HTML into the hidden div — the new row exists in the DOM, just + not visible to a `to_be_visible` assertion. Using `to_have_count` + instead of `to_be_visible` accommodates this. + """ + base = files_overlay_server["base_url"] + overlay_id = files_overlay_server["overlay_id"] + overlay_root = files_overlay_server["overlay_root"] + original_content = (overlay_root / "server.cfg").read_text() + _open_overlay(page, base, overlay_id) + + # Scope to the row
  • , not any inner button — delete-action + # buttons also carry data-target-path + data-row-kind, so a bare + # attribute selector resolves to multiple elements and trips + # strict mode. + source = page.locator('li.file-tree-row-file[data-target-path="server.cfg"]') + target = page.locator('li.file-tree-row-dir[data-target-path="cfg"]') + source.drag_to(target) + + # Wait for both refreshes to land: old row gone from root, new row + # present (though hidden) inside cfg's children div. + expect( + page.locator('.file-tree-row-file[data-target-path="server.cfg"]') + ).to_have_count(0, timeout=5000) + expect( + page.locator('.file-tree-row-file[data-target-path="cfg/server.cfg"]') + ).to_have_count(1, timeout=5000) + + # Disk is the load-bearing assertion — proves /files/move actually + # moved bytes, not just shuffled DOM rows. + assert not (overlay_root / "server.cfg").exists() + assert (overlay_root / "cfg" / "server.cfg").read_text() == original_content