Files
hermes-webui/tests/test_issue3952_escape_blur_composer.py
T
cdcede8ce7 Release OX (v0.51.437): blur composer on Escape for j/k nav (#3952) (#4258)
* stage-3955: blur composer on Escape for j/k nav + IME guard (Opus nit)

* stage-3955: update test to match IME-guarded blur branch

* Release OX: blur composer on Escape for j/k message nav (#3952)

Self-rebased TomBanksAU's #3955 onto v0.51.436; full Codex+Opus+suite gate.
Escape now blurs the focused composer so j/k session-nav works, after higher-
priority Escape actions run; dropdown-Escape stops propagation; IME-composition
guarded (Opus nit). Also restored the v0.51.434/435 CHANGELOG headings that an
earlier batch left folded under v0.51.436 (hygiene fix).

Co-authored-by: TomBanksAU <TomBanksAU@users.noreply.github.com>

---------

Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: TomBanksAU <TomBanksAU@users.noreply.github.com>
2026-06-15 11:55:47 -07:00

75 lines
3.3 KiB
Python

"""Regression tests for #3952 composer Escape keyboard navigation."""
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
BOOT_JS = (REPO_ROOT / "static" / "boot.js").read_text(encoding="utf-8")
SESSIONS_JS = (REPO_ROOT / "static" / "sessions.js").read_text(encoding="utf-8")
def _block_from_opening_brace(src: str, brace: int, label: str) -> str:
assert brace >= 0, f"{label} opening brace must exist"
depth = 1
idx = brace + 1
while idx < len(src) and depth:
if src[idx] == "{":
depth += 1
elif src[idx] == "}":
depth -= 1
idx += 1
assert depth == 0, f"{label} block must close"
return src[brace + 1 : idx - 1]
def _escape_block() -> str:
document_keydown = BOOT_JS.index("// B14: Cmd/Ctrl+K creates a new chat from anywhere")
start = BOOT_JS.index("if(e.key==='Escape'){", document_keydown)
brace = BOOT_JS.index("{", start)
return _block_from_opening_brace(BOOT_JS, brace, "document Escape handler")
def _composer_keydown_block() -> str:
start = BOOT_JS.index("$('msg').addEventListener('keydown',e=>{")
brace = BOOT_JS.index("{", start)
return _block_from_opening_brace(BOOT_JS, brace, "composer keydown handler")
def test_escape_blurs_focused_composer_after_higher_priority_escape_actions():
"""Escape should let keyboard-only users leave the composer and use j/k nav."""
block = _escape_block()
blur_idx = block.find("document.activeElement===$('msg')")
assert blur_idx != -1, "Escape handler must detect the focused composer"
assert "$('msg').blur();" in block[blur_idx:], "Escape handler must blur the composer"
# The blur must be skipped while an IME candidate window is composing, so
# Escape dismisses the candidate (CJK input) rather than blurring the composer.
blur_line = block[blur_idx : block.find("\n", blur_idx)]
assert "!e.isComposing" in blur_line and "!_imeComposing" in blur_line, (
"Escape blur must be guarded against IME composition"
)
assert block.index("skipOnboarding") < blur_idx, "onboarding dismissal stays higher priority"
assert block.index("_closeSettingsPanel") < blur_idx, "settings dismissal stays higher priority"
assert block.index("clearSessionSearch") < blur_idx, "session-search clearing stays higher priority"
assert block.index("msg-edit-cancel") < blur_idx, "message-edit cancel stays higher priority"
def test_jk_session_navigation_still_ignores_interactive_targets():
"""Composer text entry still owns j/k until Escape blurs it."""
nav_start = SESSIONS_JS.index("// Keyboard session navigation — J/K bindings")
nav_block = SESSIONS_JS[nav_start:]
assert "if(typeof _isInteractiveSwipeTarget==='function'&&_isInteractiveSwipeTarget(e.target)) return;" in nav_block
def test_escape_dismisses_command_dropdown_without_blurring_composer():
"""Escape should close slash-command autocomplete without bubbling to blur."""
block = _composer_keydown_block()
escape_idx = block.find("if(e.key==='Escape'){")
assert escape_idx != -1, "composer keydown handler must handle dropdown Escape"
escape_line = block[escape_idx : block.find("}", escape_idx) + 1]
assert "e.preventDefault();" in escape_line
assert "e.stopPropagation();" in escape_line
assert "hideCmdDropdown();" in escape_line