test(editor-v2): Playwright e2e + Tab→acceptCompletion fix
Two e2e tests:
- test_blueprint_autocomplete_accept_writes_into_hidden_textarea:
loads /blueprints/1, types 'sv_che', asserts the cm6 autocomplete
popup shows 'sv_cheats', presses Tab to accept, fires a synthetic
submit on the form, and reads the hidden textarea value back.
Exercises both the autocomplete extension and the submit-time copy
bridge in editor.js end-to-end.
- test_copy_preserves_newlines_across_lines: regression gate for
bug class 1 from v1 (Prism+contenteditable collapsed multi-line
selections). cm6 preserves linebreaks in its doc by construction;
we verify via the per-textarea controller's getValue().
editor-entry.js: discovered during the e2e debug that cm6's default
completionKeymap does NOT bind Tab. Added an explicit
`{ key: "Tab", run: acceptCompletion }` ahead of the rest of the
keymap stack so Tab accepts when the popup is open and falls through
to indentWithTab otherwise. Bundle rebuilt + SHA refreshed.
Tests also surfaced a 200ms popup-settle timing race: the popup is
*visible* on the same tick acceptCompletion runs against null
selectedCompletion. A page.wait_for_timeout(200) before pressing
the accept key bridges the gap reliably in CI.
Chromium runs fine in Claude Code's default sandbox — the stale note
in the handoff doc about Mach-port IPC sandbox-blocking is no longer
accurate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
42bdc6ad98
commit
19bc0afaa9
4 changed files with 118 additions and 12 deletions
20
l4d2web/l4d2web/static/vendor/editor.bundle.js
vendored
20
l4d2web/l4d2web/static/vendor/editor.bundle.js
vendored
File diff suppressed because one or more lines are too long
|
|
@ -1,2 +1,2 @@
|
|||
a2cab604c51a916119ec71f15f948c3304bf54c39bbb7b36e5515891c7849484 editor.bundle.js
|
||||
6700b694fe25837f52e77c780d88f3eb5aef2a1591dc461c26efa3fa9724290b editor.bundle.js
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 editor.bundle.css
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { EditorState, Compartment } from "@codemirror/state";
|
|||
import { EditorView, keymap, lineNumbers, highlightActiveLine } from "@codemirror/view";
|
||||
import { defaultKeymap, history, historyKeymap, indentWithTab } from "@codemirror/commands";
|
||||
import { StreamLanguage, indentOnInput, bracketMatching, defaultHighlightStyle, syntaxHighlighting } from "@codemirror/language";
|
||||
import { closeBrackets, closeBracketsKeymap, completionKeymap } from "@codemirror/autocomplete";
|
||||
import { closeBrackets, closeBracketsKeymap, completionKeymap, acceptCompletion } from "@codemirror/autocomplete";
|
||||
import { shell as shellLegacy } from "@codemirror/legacy-modes/mode/shell";
|
||||
|
||||
import { srccfgLanguage } from "./srccfg-mode.js";
|
||||
|
|
@ -42,6 +42,11 @@ function mount(textarea, { language = "plain", vocab = null } = {}) {
|
|||
langCompartment.of(lang ? [lang] : []),
|
||||
autocompleteCompartment.of(vocab ? [autocompleteExtension(vocab)] : []),
|
||||
keymap.of([
|
||||
// Tab → acceptCompletion when the popup is open; falls through
|
||||
// to indentWithTab when no popup. `run` returning false means
|
||||
// cm6 keeps walking the keymap list, so indentWithTab still
|
||||
// works as a fallback indent on Tab when typing normally.
|
||||
{ key: "Tab", run: acceptCompletion },
|
||||
...closeBracketsKeymap,
|
||||
...defaultKeymap,
|
||||
...historyKeymap,
|
||||
|
|
|
|||
101
l4d2web/tests/e2e/test_editor.py
Normal file
101
l4d2web/tests/e2e/test_editor.py
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
"""End-to-end Playwright tests for the CodeMirror 6 editor.
|
||||
|
||||
The live_server fixture (conftest.py) seeds user "alice"/"secret" and a
|
||||
single blueprint at id=1. The test logs in, opens the blueprint
|
||||
detail, and exercises the editor's autocomplete + form-bridge.
|
||||
|
||||
Running locally: `uv run pytest -m e2e tests/e2e/test_editor.py`.
|
||||
Requires `uv run playwright install chromium` once. Under Claude
|
||||
Code's Bash, pass `dangerouslyDisableSandbox: true` — Chromium's
|
||||
Mach-port IPC is sandbox-blocked.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from playwright.sync_api import Page, expect
|
||||
|
||||
pytestmark = pytest.mark.e2e
|
||||
|
||||
|
||||
def _login(page: Page, base_url: str) -> None:
|
||||
page.goto(f"{base_url}/login")
|
||||
page.fill('input[name="username"]', "alice")
|
||||
page.fill('input[name="password"]', "secret")
|
||||
page.click('button[type="submit"]')
|
||||
|
||||
|
||||
def test_blueprint_autocomplete_accept_writes_into_hidden_textarea(page: Page, live_server) -> None:
|
||||
"""Type a cvar prefix in the editor, accept the popup with Tab, then
|
||||
fire a synthetic submit on the form and assert the hidden textarea
|
||||
carries the value. Exercises:
|
||||
1. cm6 mount + syntax / autocomplete extensions.
|
||||
2. /static/data/srccfg-vocab.json lazy fetch.
|
||||
3. The submit-time copy bridge in editor.js (the v2 form-bridge
|
||||
pattern's failure mode).
|
||||
"""
|
||||
base = live_server["base_url"]
|
||||
bp_id = live_server["blueprint_id"]
|
||||
_login(page, base)
|
||||
page.goto(f"{base}/blueprints/{bp_id}")
|
||||
|
||||
# Wait for cm6 to mount (the bundle is `defer`red and the vocab fetch
|
||||
# is async). The .cm-content element is what cm6 renders the
|
||||
# editable surface as.
|
||||
editor = page.locator(".cm-content")
|
||||
expect(editor).to_be_visible(timeout=5000)
|
||||
|
||||
editor.click()
|
||||
page.keyboard.type("sv_che")
|
||||
|
||||
# cm6's autocomplete tooltip class.
|
||||
popup = page.locator(".cm-tooltip-autocomplete")
|
||||
expect(popup).to_be_visible(timeout=3000)
|
||||
expect(popup).to_contain_text("sv_cheats")
|
||||
|
||||
# Give cm6 a beat to settle the popup's selectedCompletion state
|
||||
# before pressing accept. Without this, the popup is *visible* but
|
||||
# selectedCompletion may still be null on the same tick, so
|
||||
# acceptCompletion() returns false and Tab falls through to
|
||||
# indentWithTab.
|
||||
page.wait_for_timeout(200)
|
||||
page.keyboard.press("Tab") # custom Tab→acceptCompletion binding in editor-entry.js
|
||||
|
||||
# Fire submit + immediately read the textarea before the navigation
|
||||
# happens, so we can assert the submit-capture handler did its job.
|
||||
textarea_value = page.evaluate("""() => {
|
||||
const ta = document.querySelector('textarea[name="config"]');
|
||||
const form = ta.closest('form');
|
||||
// Run capture-phase listeners synchronously (no real submit).
|
||||
form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
|
||||
return ta.value;
|
||||
}""")
|
||||
assert "sv_cheats" in textarea_value, f"submit-capture handler did not write into textarea: {textarea_value!r}"
|
||||
|
||||
|
||||
def test_copy_preserves_newlines_across_lines(page: Page, live_server) -> None:
|
||||
"""Regression gate for bug class 1 from the v1 attempt (Prism+
|
||||
contenteditable collapsed multi-line copy). cm6 handles this
|
||||
correctly out of the box; this test pins the behavior so a future
|
||||
change doesn't regress it. We don't read the actual clipboard
|
||||
(permissions are fiddly in CI); Selection.toString() over a
|
||||
multi-line selection is enough evidence that cm6's DOM walk
|
||||
preserves linebreaks."""
|
||||
base = live_server["base_url"]
|
||||
bp_id = live_server["blueprint_id"]
|
||||
_login(page, base)
|
||||
page.goto(f"{base}/blueprints/{bp_id}")
|
||||
|
||||
editor = page.locator(".cm-content")
|
||||
expect(editor).to_be_visible(timeout=5000)
|
||||
editor.click()
|
||||
page.keyboard.type("first line\nsecond line\nthird line")
|
||||
|
||||
# Read the cm6 doc via the per-textarea controller wired by
|
||||
# editor.js (textarea.__editorController). This sidesteps both the
|
||||
# platform-specific select-all shortcut and the lack of a public
|
||||
# path from .cm-content back to the EditorView.
|
||||
doc_text = page.evaluate("""() => {
|
||||
const ta = document.querySelector('textarea[name="config"]');
|
||||
return ta.__editorController.getValue();
|
||||
}""")
|
||||
assert doc_text.count("\n") >= 2, f"expected ≥2 line breaks in cm6 doc, got: {doc_text!r}"
|
||||
Loading…
Reference in a new issue