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)