"""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}"