Closes #1442 (server-side _LOGIN_LOCALE missing ja/pt/ko) Closes #1443 (promote _isImeEnter helper to 6 other Safari Enter guards) Closes #1446 (glued-bold-heading lift for LLM thinking-block output) Closes #1447 (markdown heading visual hierarchy in chat messages) All four issues were filed by the Opus pre-release advisor on the v0.50.264 batch or by Cygnus via Discord (relayed by @AvidFuturist, May 1 2026). They share a common shape — narrow, well-scoped, independent of each other, all adding regression tests. == #1442: _LOGIN_LOCALE parity (api/routes.py + static/i18n.js) == Added entries for ja/pt/ko to the server-side _LOGIN_LOCALE dict that renders the localized login page BEFORE the JS i18n bundle loads. With v0.50.264 shipping Japanese as the 8th built-in locale, ja/pt/ko users were seeing the English login page even with their language preference set. While auditing static/i18n.js for English leakage, also fixed: - ko: 10 user-facing login/sign-out/password keys still in English - es: 3 sign-out/auth-disabled keys still in English Tests: tests/test_login_locale_parity.py (20 tests) — pins both invariants: (a) every locale in i18n.js LOCALES has a matching _LOGIN_LOCALE entry (b) every locale's login-flow keys (13 of them) are translated, not English == #1443: window._isImeEnter promotion == PR #1441 fixed the Safari IME-composition Enter race in the chat composer (`#msg`) by widening the guard from `e.isComposing` to a `_isImeEnter(e)` helper that combines three signals (isComposing || keyCode===229 || _imeComposing flag). Six other Enter-input handlers were left on the original narrow guard and would still drop IME composition Enters on Safari for Japanese/Chinese/Korean users. Promoted the helper to `window._isImeEnter` (defined in static/boot.js) and replaced the `e.isComposing` guards at all six sites: - static/sessions.js: session rename, project create, project rename - static/ui.js: app dialog (confirm/prompt), message edit, workspace rename The state-free part of the helper (`isComposing || keyCode===229`) handles Safari's race for any focused input without needing per-input composition listeners — only `#msg` keeps the local `_imeComposing` flag. Tests: - tests/test_issue1443_ime_helper_promotion.py (9 tests) — pins each site + verifies no raw `e.isComposing` Enter-guards remain in sessions.js/ui.js - tests/test_ime_composition.py — alternation regex extended to accept the windowed helper form (loosen-test-on-shape-change pattern from v0.50.264 reflection notes) == #1446: glued-bold-heading lift (static/ui.js renderMd + Python mirror) == LLMs in thinking/reasoning mode emit "section headers" glued to the end of the previous paragraph with no whitespace: Para 1 text.**Heading to Para 2** Para 2 text.**Heading to Para 3** The renderer correctly produces inline `<strong>` per CommonMark, but it looks like trailing emphasis on the body text rather than a section break. Cygnus reported this as "Markdown feedback 2 of 3." Added a single regex pre-pass in renderMd(): s.replace(/([.!?])\*\*([^*\n]{1,80})\*\*\n\n/g, '$1\n\n**$2**\n\n') Constraints chosen to avoid false positives: - Trigger only on `[.!?]` IMMEDIATELY before `**` (no space) — almost always an LLM-glued heading, not intentional emphasis - Inner text ≤80 chars, no `*` or newline (single-line only) - Trailing `\n\n` required — preserves "this is **important** to know." mid-paragraph emphasis untouched - Position: after rawPreStash restore, before fence_stash restore — fenced code blocks stay protected (their content is `\x00P` / `\x00F` tokens when the lift runs) Mirrored in tests/test_sprint16.py render_md() so both stay in sync. Tests: tests/test_issue1446_glued_heading_lift.py (17 tests, 5 of which drive the actual ui.js renderMd via node) — covers all 3 trigger forms (.!?), all 4 preserve-emphasis cases the issue spec'd, fenced/inline code protection, chained glued headings, source-level position pin, regex shape pin. == #1447: markdown heading visual hierarchy (static/style.css) == Pre-fix sizes in `.msg-body`: h1 18px, h2 16px, h3 14px (= body), h4 13px, h5 12px, h6 11px So h3 was indistinguishable from body and h4/h5/h6 were SMALLER than body. Cygnus's report: "Markdown feedback 3 of 3 — Headings seem to be missing across the board in Hermes. They're there, but all plaintext." New sizes: h1 24px (border-bottom) h2 20px (border-bottom) h3 17px h4 15px h5 14px (uppercase, tracked) h6 13px (uppercase, tracked, muted) All headings now `font-weight:700` + `color:var(--strong)` for stronger ink. h5/h6 use uppercase + letter-spacing for "label-style" affordance instead of being smaller-than-body. Synced .preview-md (file preview pane) to match exactly so a markdown file preview and a chat message render identically. Added missing h4/h5/h6 rules to .preview-md (it only had h1-h3 before). Updated data-font-size="small"/"large" h1-h6 overrides to scale proportionally with the new defaults. Hierarchy preserved at all three font-size settings. Tests: tests/test_issue1447_heading_hierarchy.py (9 tests) — pins the size hierarchy, the bottom borders on h1/h2, the uppercase affordance on h5/h6, the .preview-md sync, and the small/large override scaling. == Verification == pytest tests/ -q → 3748 passed (+56 new) bash ~/WebUI/scripts/run-browser-tests.sh → 20 + 11 PASS bash ~/WebUI/scripts/webui_qa_agent.sh 8789 → 23/23 PASS Visual confirmation in browser at port 8789: - Heading hierarchy clearly visible at all 6 levels - Glued-bold lift produces separate paragraphs as designed - window._isImeEnter accessible from any module after boot.js - Login page renders ja/pt/ko strings correctly (curl -s /login)
184 lines
7.3 KiB
Python
184 lines
7.3 KiB
Python
"""Regression tests for issue #1443 — promote `_isImeEnter` to all Safari-affected Enter guards.
|
|
|
|
PR #1441 (v0.50.264) widened the chat composer's IME-Enter guard from `e.isComposing`
|
|
to a `_isImeEnter(e)` helper in `static/boot.js`. The helper combines three signals
|
|
(`e.isComposing || e.keyCode === 229 || _imeComposing`) so it catches the Safari race
|
|
where the committing keydown for an IME composition fires AFTER `compositionend` with
|
|
`isComposing=false`.
|
|
|
|
Six other Enter-input handlers were left on the original `e.isComposing` guard:
|
|
|
|
- `static/sessions.js` — session rename (~line 1693)
|
|
- `static/sessions.js` — project create (~line 1987)
|
|
- `static/sessions.js` — project rename (~line 2015)
|
|
- `static/ui.js` — app dialog (confirm/prompt) (~line 2482)
|
|
- `static/ui.js` — message edit (Enter to save) (~line 4106)
|
|
- `static/ui.js` — workspace rename (~line 5007)
|
|
|
|
Issue #1443 promotes the helper to `window._isImeEnter` (defined in boot.js) and
|
|
replaces the 6 `e.isComposing` guards with `window._isImeEnter(e)`. These tests pin
|
|
each site so a future cleanup that strips the windowed call trips a test.
|
|
|
|
The state-free part of the helper (`e.isComposing || e.keyCode === 229`) is what the
|
|
6 non-composer sites rely on — it works for any focused input on Safari without needing
|
|
per-input composition listeners or a per-input flag.
|
|
"""
|
|
|
|
import pathlib
|
|
import re
|
|
|
|
REPO_ROOT = pathlib.Path(__file__).parent.parent.resolve()
|
|
BOOT_JS = (REPO_ROOT / "static" / "boot.js").read_text(encoding="utf-8")
|
|
UI_JS = (REPO_ROOT / "static" / "ui.js").read_text(encoding="utf-8")
|
|
SESSIONS_JS = (REPO_ROOT / "static" / "sessions.js").read_text(encoding="utf-8")
|
|
|
|
|
|
def _windowed_guard(ev: str) -> str:
|
|
"""Return regex for `if(window._isImeEnter && window._isImeEnter(<ev>))` shape."""
|
|
return (
|
|
rf"if\s*\(\s*window\._isImeEnter\s*&&\s*"
|
|
rf"window\._isImeEnter\s*\(\s*{ev}\s*\)\s*\)\s*"
|
|
)
|
|
|
|
|
|
# ── Promotion: `window._isImeEnter` is exported from boot.js ─────────────────
|
|
|
|
|
|
def test_isimeenter_helper_is_exposed_on_window():
|
|
"""boot.js must attach `_isImeEnter` to `window` so other modules can reuse it."""
|
|
assert re.search(
|
|
r"window\._isImeEnter\s*=\s*_isImeEnter\s*;?",
|
|
BOOT_JS,
|
|
), (
|
|
"boot.js must export `window._isImeEnter = _isImeEnter` so "
|
|
"static/sessions.js and static/ui.js can call the same Safari-aware "
|
|
"helper without duplicating the IIFE per input (issue #1443)."
|
|
)
|
|
|
|
|
|
# ── No raw `e.isComposing` guards remain in the 6 non-composer sites ─────────
|
|
|
|
|
|
def test_no_isComposing_guards_remain_in_sessions_js():
|
|
"""sessions.js must not contain a raw `e.isComposing` Enter-guard anymore."""
|
|
leaks = re.findall(r"\b(?:e2?)\.isComposing\b", SESSIONS_JS)
|
|
assert not leaks, (
|
|
f"sessions.js still contains {len(leaks)} raw `e.isComposing` guard(s); "
|
|
f"all Enter-input handlers should route through window._isImeEnter "
|
|
f"(issue #1443)."
|
|
)
|
|
|
|
|
|
def test_no_isComposing_guards_remain_in_ui_js():
|
|
"""ui.js must not contain a raw `e.isComposing` Enter-guard anymore."""
|
|
leaks = re.findall(r"\b(?:e2?)\.isComposing\b", UI_JS)
|
|
assert not leaks, (
|
|
f"ui.js still contains {len(leaks)} raw `e.isComposing` guard(s); "
|
|
f"all Enter-input handlers should route through window._isImeEnter "
|
|
f"(issue #1443)."
|
|
)
|
|
|
|
|
|
# ── Each of the 6 specific sites uses the promoted helper ────────────────────
|
|
|
|
|
|
def test_session_rename_uses_windowed_helper():
|
|
"""Session rename (sessions.js ~1693) must use window._isImeEnter."""
|
|
# The session rename block: `inp.onkeydown=e2=>{ if(e2.key==='Enter'){ <guard> ...
|
|
pattern = re.compile(
|
|
r"inp\.onkeydown\s*=\s*e2\s*=>\s*\{\s*"
|
|
r"if\s*\(\s*e2\.key\s*===\s*'Enter'\s*\)\s*\{\s*"
|
|
+ _windowed_guard("e2"),
|
|
re.DOTALL,
|
|
)
|
|
assert pattern.search(SESSIONS_JS), (
|
|
"Session rename Enter handler in static/sessions.js must use "
|
|
"window._isImeEnter(e2) (issue #1443)."
|
|
)
|
|
|
|
|
|
def test_project_create_and_rename_use_windowed_helper():
|
|
"""Project create + project rename (sessions.js ~1987 and ~2015) both use window._isImeEnter."""
|
|
# Both project blocks share the shape `inp.onkeydown=(e)=>{...}` (note the parens).
|
|
pattern = re.compile(
|
|
r"inp\.onkeydown\s*=\s*\(\s*e\s*\)\s*=>\s*\{\s*"
|
|
r"if\s*\(\s*e\.key\s*===\s*'Enter'\s*\)\s*\{\s*"
|
|
+ _windowed_guard("e"),
|
|
re.DOTALL,
|
|
)
|
|
matches = pattern.findall(SESSIONS_JS)
|
|
assert len(matches) >= 2, (
|
|
f"Project create AND project rename Enter handlers in static/sessions.js "
|
|
f"must both use window._isImeEnter(e); found {len(matches)} of 2 expected "
|
|
f"(issue #1443)."
|
|
)
|
|
|
|
|
|
def test_app_dialog_uses_windowed_helper():
|
|
"""App dialog confirm/prompt (ui.js ~2482) must use window._isImeEnter."""
|
|
# Pattern: `document.addEventListener('keydown',e=>{ ... if(e.key==='Enter'){
|
|
# if(window._isImeEnter && window._isImeEnter(e)) return;`
|
|
pattern = re.compile(
|
|
r"document\.addEventListener\(\s*'keydown'\s*,\s*e\s*=>\s*\{[\s\S]*?"
|
|
r"if\s*\(\s*e\.key\s*===\s*'Enter'\s*\)\s*\{\s*"
|
|
+ _windowed_guard("e"),
|
|
re.DOTALL,
|
|
)
|
|
assert pattern.search(UI_JS), (
|
|
"App dialog confirm/prompt Enter handler in static/ui.js must use "
|
|
"window._isImeEnter(e) (issue #1443)."
|
|
)
|
|
|
|
|
|
def test_message_edit_uses_windowed_helper():
|
|
"""Message edit Enter-to-save (ui.js ~4106) must use window._isImeEnter."""
|
|
# Pattern: `ta.addEventListener('keydown', e => { if(e.key==='Enter' && !e.shiftKey)
|
|
# { if(window._isImeEnter && window._isImeEnter(e)) return;`
|
|
pattern = re.compile(
|
|
r"ta\.addEventListener\(\s*'keydown'\s*,\s*e\s*=>\s*\{\s*"
|
|
r"if\s*\(\s*e\.key\s*===\s*'Enter'\s*&&\s*!\s*e\.shiftKey\s*\)\s*\{\s*"
|
|
+ _windowed_guard("e"),
|
|
re.DOTALL,
|
|
)
|
|
assert pattern.search(UI_JS), (
|
|
"Message edit Enter-to-save handler in static/ui.js must use "
|
|
"window._isImeEnter(e) (issue #1443)."
|
|
)
|
|
|
|
|
|
def test_workspace_rename_uses_windowed_helper():
|
|
"""Workspace rename (ui.js ~5007) must use window._isImeEnter."""
|
|
# Pattern: `inp.onkeydown=(e2)=>{ if(e2.key==='Enter'){ if(window._isImeEnter && ...
|
|
pattern = re.compile(
|
|
r"inp\.onkeydown\s*=\s*\(\s*e2\s*\)\s*=>\s*\{\s*"
|
|
r"if\s*\(\s*e2\.key\s*===\s*'Enter'\s*\)\s*\{\s*"
|
|
+ _windowed_guard("e2"),
|
|
re.DOTALL,
|
|
)
|
|
assert pattern.search(UI_JS), (
|
|
"Workspace rename Enter handler in static/ui.js must use "
|
|
"window._isImeEnter(e2) (issue #1443)."
|
|
)
|
|
|
|
|
|
# ── Helper still has the 3-guard shape (regression on PR #1441) ──────────────
|
|
|
|
|
|
def test_isimeenter_still_has_three_guards():
|
|
"""The helper itself must still combine all three guards. Promotion to `window`
|
|
must not have stripped any of them."""
|
|
pattern = re.compile(
|
|
r"function\s+_isImeEnter\s*\(\s*e\s*\)\s*\{[^}]*"
|
|
r"e\.isComposing"
|
|
r"[^}]*"
|
|
r"e\.keyCode\s*===\s*229"
|
|
r"[^}]*"
|
|
r"_imeComposing"
|
|
r"[^}]*\}",
|
|
re.DOTALL,
|
|
)
|
|
assert pattern.search(BOOT_JS), (
|
|
"_isImeEnter must still combine e.isComposing, keyCode===229, and "
|
|
"_imeComposing flag after promotion to window (PR #1441 + issue #1443)."
|
|
)
|