""" No `prompt`, `confirm` or `alert` in the SPA — `ask.js` draws them instead. The same `static/` tree is the web page and the application (CLAUDE.md's "one UI source"), and in the desktop client none of the three browser dialogs can be used. Measured against this repo's own Electron 44: prompt('name?') -> Error: prompt() is not supported. confirm('sure?') -> opens a real modal, and breaks typing once it closes alert('hi') -> same as confirm `prompt` throws, which made the Files toolbar's New folder button do nothing whatsoever: the call sat above its own try, so the click produced no folder, no error, and nothing on screen to react to. `confirm` and `alert` fail later and more quietly. After one closes, `document.hasFocus()` stays false: a click still moves `activeElement` into a field, but every keystroke goes nowhere until the window loses and regains focus. It was reported as "the Create group fields are frozen" after removing a member, and it was confirmed with real X input events under Xvfb (click and type into an input and a textarea, before and after a `confirm()`): three runs out of three, typing lands before the dialog and nothing lands after it. A test that dispatches DOM events cannot see this, since the focus is lost below the page, so the guard is on the source. """ import re from pathlib import Path import pytest STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static" pytestmark = pytest.mark.skipif(not STATIC.exists(), reason="SPA sources unavailable") # A call, not the word inside a comment or a longer identifier like # `mkdir_prompt` / `promptForName` / `role="alert"`. CALL = re.compile(r"(? str: source = re.sub(r"/\*.*?\*/", "", source, flags=re.S) return re.sub(r"^\s*//.*$", "", source, flags=re.M) def test_nothing_calls_a_native_dialog(): offenders = [] for path in sorted(STATIC.glob("*.js")): if path.name == "sw.js": continue for i, line in enumerate(_code_only( path.read_text(encoding="utf-8")).splitlines(), 1): if CALL.search(line): offenders.append(f"{path.name}:{i}: {line.strip()}") assert not offenders, ( "prompt() throws in the desktop client and confirm()/alert() leave it " "unable to type; use ask()/tell() from ask.js:\n " + "\n ".join(offenders)) def test_the_pattern_would_catch_a_real_call(): """ A guard that matches nothing passes over an empty set, which looks exactly like success. Both spellings, and the near-misses it must not flag. """ assert CALL.search("const n = prompt('x');") assert CALL.search("const n = window.prompt('x');") assert CALL.search("if (!confirm(t('x'))) return;") assert CALL.search("if (!window.confirm(q)) return;") assert CALL.search("alert(err.message);") assert not CALL.search("
") assert not CALL.search("t('members.remove_confirm')") assert not CALL.search("await ask(q)") assert not CALL.search("t('group.mkdir_prompt')") assert not CALL.search("promptForName();") assert not CALL.search("this.prompt(1);") def test_creating_a_folder_uses_a_field(): """ The control the ban is about. A typed name needs somewhere to type it, and an inline field can show the node's refusal beside the input rather than after a dialog has closed. """ source = (STATIC / "files-app.js").read_text(encoding="utf-8") assert "tb-mkdir-input" in source assert "newDirName" in source assert "group.mkdir_prompt" in source, "the field has no label or placeholder" def test_leaving_the_folder_drops_a_half_typed_name(): """ Otherwise the folder is created where the person is no longer looking — they navigated away, the draft came along, and the name lands in a directory they were not thinking about. """ source = (STATIC / "files-app.js").read_text(encoding="utf-8") assert "useEffect(() => { setNewDirName(null); }, [currentPath]);" in source