test(files): cover new-folder + delete cycle

Creates a folder via the inline new-folder dialog's Enter-keydown
submit path, then deletes it via the inline delete-confirm dialog.
Each step asserts disk + tree state. The Enter path is the
direct-bound listener (not delegated), so it's the most likely to
break under future refactors — pinning it here surfaces such
regressions immediately.

Row-action buttons (`✕`) live inside `<li draggable="true">` — the
draggable ancestor confuses Playwright's hit-test even though real
browsers click through fine. The click uses force=True to skip the
hit-test (documented inline).

Per docs/superpowers/plans/2026-05-17-files-overlay-e2e-handoff.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
mwiegand 2026-05-17 18:44:58 +02:00
parent 6b0fbb75bf
commit 3ea57b2bdb
No known key found for this signature in database

View file

@ -277,3 +277,62 @@ def test_binary_replace_via_browse_writes_new_bytes(page: Page, files_overlay_se
# The filename on disk stays the same (no rename) — only the bytes
# changed.
assert (overlay_root / "icon.png").read_bytes() == new_bytes
def test_new_folder_then_delete(page: Page, files_overlay_server) -> None:
"""Create a folder via the inline new-folder dialog (Enter-to-submit
keydown path), then delete it via the inline delete-confirm dialog.
Each step asserts disk + tree state, so a regression in either the
mkdir or delete route or in the modal close + refresh wiring
fails loudly.
The new-folder dialog has TWO submit paths (click on
.files-new-folder-create + Enter keydown on the name input); this
test pins the keydown path, which is direct-bound rather than
delegated, because it's the one most likely to break under future
refactors.
"""
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)
# --- Create folder via Enter-to-submit -----------------------------
page.click('button[data-action="new-folder"][data-target-path=""]')
new_folder_modal = page.locator("#files-new-folder-modal")
expect(new_folder_modal).to_be_visible(timeout=5000)
folder_name = "sourcemod"
page.fill(".files-new-folder-name", folder_name)
page.locator(".files-new-folder-name").press("Enter")
expect(new_folder_modal).to_be_hidden(timeout=5000)
assert (overlay_root / folder_name).is_dir()
folder_row = page.locator(
f'.file-tree-row-dir[data-target-path="{folder_name}"]'
)
expect(folder_row).to_be_visible(timeout=5000)
# --- Delete folder via inline confirm ------------------------------
# `force=True`: the row-action buttons are positioned inside a
# `<li draggable="true">`, and Playwright's hit-test treats the
# draggable ancestor as intercepting pointer events even though
# real browsers dispatch the click correctly. Forcing skips the
# hit-test and dispatches on the target — matches user behavior.
page.locator(
f'button[data-action="delete"]'
f'[data-target-path="{folder_name}"]'
f'[data-row-kind="dir"]'
).click(force=True)
delete_modal = page.locator("#files-delete-modal")
expect(delete_modal).to_be_visible(timeout=5000)
# The dialog should name the row being deleted — the user must see
# what they're confirming.
expect(delete_modal.locator(".files-delete-name")).to_have_text(folder_name)
page.click(".files-delete-confirm")
expect(delete_modal).to_be_hidden(timeout=5000)
assert not (overlay_root / folder_name).exists()
# The tree refresh removes the row.
expect(folder_row).to_have_count(0, timeout=5000)