From aad83566137d660cded3d905908ef5b81a19e910 Mon Sep 17 00:00:00 2001 From: mwiegand Date: Sun, 17 May 2026 18:40:56 +0200 Subject: [PATCH] test(files): cover 409 askConflict keep-both path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tries to create `cfg`, which collides with the seeded directory of the same name. /files/save returns 409 "destination is not a file"; routedSaveClicked routes that through fo.askConflict, which opens the inline #files-conflict-modal on top of the still-open routed editor. Clicking keep-both triggers a second POST with the suffixed path (`cfg (1)`), the routed modal closes, and the new row materialises in the tree. This is the F4 path from 8dc14f0 ("wire askConflict into the routed new-file 409 path"). Before that commit, the routed code branch fell through to a generic alert(). With this test in place, a missing call site fails loudly instead of silently. Pins three invariants: * The conflict dialog is INLINE, not routed — it appears without a URL change (the decision tree in AGENTS.md "Modals: inline vs routed" hinges on this). * .files-conflict-path echoes the original colliding path, not the computed suffix — the suffix is internal, the user sees the collision. * withCollisionSuffix("cfg") → "cfg (1)" (no dot after the last slash → trailing-suffix branch in uploads.js). Per docs/superpowers/plans/2026-05-17-files-overlay-e2e-handoff.md. Co-Authored-By: Claude Opus 4.7 (1M context) --- l4d2web/tests/e2e/test_files_overlay.py | 59 +++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/l4d2web/tests/e2e/test_files_overlay.py b/l4d2web/tests/e2e/test_files_overlay.py index e1c9b54..ad80b01 100644 --- a/l4d2web/tests/e2e/test_files_overlay.py +++ b/l4d2web/tests/e2e/test_files_overlay.py @@ -116,3 +116,62 @@ def test_create_new_file_routed(page: Page, files_overlay_server) -> None: expect( page.locator(f'.file-tree-row-file[data-target-path="{new_filename}"]') ).to_be_visible(timeout=5000) + + +def test_create_new_file_409_askConflict_keep_both(page: Page, files_overlay_server) -> None: + """Try to create a file named `cfg`, which collides with the + seeded directory of the same name. /files/save returns 409 because + the target exists and is not a file; routedSaveClicked routes that + 409 through fo.askConflict, which opens the INLINE + #files-conflict-modal on top of the still-open routed editor. + Clicking "keep-both" causes a second POST with the suffixed path + (cfg → "cfg (1)"), the routed modal closes, and the new row + appears in the tree. + + This is the F4 path from commit 8dc14f0 ("wire askConflict into + the routed new-file 409 path"). Without coverage it can regress + silently — the legacy create-new flow went through a different code + path before the rewrite, and a missing call site would only + manifest as a confusing alert() instead of the conflict dialog. + + Key invariants this asserts: + * The conflict dialog is inline, NOT routed — it appears WITHOUT + a URL change (we don't navigate, we just wait for the dialog). + * .files-conflict-path shows the original colliding path, not + the suffixed one. + * withCollisionSuffix("cfg") returns "cfg (1)" (single-extension + path with no dot falls into the trailing-suffix branch). + """ + 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[data-action="new-file"][data-target-path=""]') + _wait_for_routed_editor(page) + + page.fill('input[data-editor-filename]', "cfg") + new_content = "STEAM_1:0:5\n" + page.evaluate("(text) => window.__filesEditor.setContent(text)", new_content) + page.click(".files-editor-save") + + # The conflict dialog should pop up because `cfg` is a directory. + conflict = page.locator("#files-conflict-modal") + expect(conflict).to_be_visible(timeout=5000) + # The dialog must echo the ORIGINAL colliding path, not the suffix. + expect(conflict.locator(".files-conflict-path")).to_have_text("cfg") + + page.click('[data-files-conflict-action="keep-both"]') + + # Both modals should close (conflict dialog first, routed modal + # after the second /files/save resolves). + expect(page.locator("#modal-container")).to_be_hidden(timeout=5000) + expect(conflict).to_be_hidden(timeout=5000) + + # The seeded `cfg/` directory must still be intact + the new + # suffixed file landed alongside it. + assert (overlay_root / "cfg").is_dir() + assert (overlay_root / "cfg (1)").read_text() == new_content + expect( + page.locator('.file-tree-row-file[data-target-path="cfg (1)"]') + ).to_be_visible(timeout=5000)