mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-05-25 11:10:18 +00:00
c73f2ff387
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)
210 lines
9.1 KiB
Python
210 lines
9.1 KiB
Python
"""Regression tests for issue #1447 — markdown heading visual hierarchy.
|
|
|
|
Cygnus reported (Discord, May 1 2026, relayed by @AvidFuturist):
|
|
"Headings seem to be missing across the board in Hermes. They're there,
|
|
but all plaintext. They get lost so easily in all the plaintext."
|
|
|
|
Pre-fix sizes (smaller-than-or-equal to body 14px):
|
|
h1 18px, h2 16px, h3 14px (= body), h4 13px, h5 12px, h6 11px.
|
|
|
|
Post-fix sizes (clear hierarchy above body 14px):
|
|
h1 24px, h2 20px, h3 17px, h4 15px, h5 14px (uppercase + tracked), h6 13px (uppercase + tracked + muted).
|
|
|
|
These tests pin:
|
|
- Each heading level has a meaningful size delta from the body and from the
|
|
next-deeper level
|
|
- h1 and h2 carry a bottom border for "section title" affordance
|
|
- h5 and h6 carry uppercase + letter-spacing for "label-style" affordance
|
|
- The .preview-md (file preview pane) sizes match .msg-body so a markdown
|
|
file preview and a chat message look the same
|
|
- The data-font-size small/large overrides scale proportionally
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from pathlib import Path
|
|
|
|
|
|
REPO = Path(__file__).resolve().parent.parent
|
|
CSS = (REPO / "static" / "style.css").read_text(encoding="utf-8")
|
|
|
|
|
|
def _font_size(scope: str, level: str) -> int:
|
|
"""Extract the integer font-size (px) for the BARE `<scope> <level>` selector.
|
|
|
|
Anchors at the start of a line (after whitespace) so the data-font-size
|
|
overrides like `[data-font-size="small"] .msg-body h1` are not matched.
|
|
"""
|
|
# Match `^<whitespace><scope> <level>{...font-size:Npx...}` (whole rule on one line)
|
|
pat = re.compile(
|
|
rf"^\s*{re.escape(scope)}\s+{level}\s*\{{[^}}]*font-size:\s*(\d+)px",
|
|
re.M,
|
|
)
|
|
m = pat.search(CSS)
|
|
assert m, f"font-size not found for `{scope} {level}` (line-anchored bare selector)"
|
|
return int(m.group(1))
|
|
|
|
|
|
# ── Hierarchy: each level larger than the next ───────────────────────────────
|
|
|
|
|
|
def test_msg_body_heading_sizes_form_clear_hierarchy():
|
|
"""h1 > h2 > h3 > h4 in size, with at least 2px between adjacent levels.
|
|
|
|
h5 and h6 use uppercase + letter-spacing rather than larger size for their
|
|
visual distinction (they're "label-style" headings), so they don't strictly
|
|
need to be larger than h4 — but they must still be at least body size (14px).
|
|
"""
|
|
h1 = _font_size(".msg-body", "h1")
|
|
h2 = _font_size(".msg-body", "h2")
|
|
h3 = _font_size(".msg-body", "h3")
|
|
h4 = _font_size(".msg-body", "h4")
|
|
h5 = _font_size(".msg-body", "h5")
|
|
h6 = _font_size(".msg-body", "h6")
|
|
|
|
assert h1 >= h2 + 3, f"h1 ({h1}) must be at least 3px larger than h2 ({h2})"
|
|
assert h2 >= h3 + 2, f"h2 ({h2}) must be at least 2px larger than h3 ({h3})"
|
|
assert h3 >= h4 + 2, f"h3 ({h3}) must be at least 2px larger than h4 ({h4})"
|
|
# Body is 14px.
|
|
assert h4 >= 14, f"h4 ({h4}) must not be smaller than body (14px)"
|
|
assert h5 >= 14, f"h5 ({h5}) must not be smaller than body (14px) — uppercase compensates"
|
|
assert h6 >= 13, f"h6 ({h6}) must not be much smaller than body (uppercase compensates) — got {h6}"
|
|
# h3 must be visibly above body — Cygnus's specific complaint.
|
|
assert h3 > 14, f"h3 ({h3}) must be larger than body (14px) so it is visibly a heading"
|
|
|
|
|
|
def test_msg_body_h1_and_h2_have_bottom_border():
|
|
"""h1 and h2 carry a bottom border for visible 'section title' affordance.
|
|
|
|
This mirrors GitHub/Notion convention and the existing .preview-md h1 rule.
|
|
"""
|
|
h1_match = re.search(
|
|
r"\.msg-body\s+h1\s*\{[^}]*border-bottom:\s*1px\s+solid",
|
|
CSS,
|
|
)
|
|
assert h1_match, ".msg-body h1 must have border-bottom: 1px solid"
|
|
h2_match = re.search(
|
|
r"\.msg-body\s+h2\s*\{[^}]*border-bottom:\s*1px\s+solid",
|
|
CSS,
|
|
)
|
|
assert h2_match, ".msg-body h2 must have border-bottom: 1px solid"
|
|
|
|
|
|
def test_msg_body_h5_and_h6_use_label_style_affordance():
|
|
"""h5 and h6 use uppercase + letter-spacing rather than larger sizes."""
|
|
h5_match = re.search(
|
|
r"\.msg-body\s+h5\s*\{[^}]*text-transform:\s*uppercase[^}]*letter-spacing:",
|
|
CSS,
|
|
)
|
|
assert h5_match, ".msg-body h5 must have text-transform:uppercase + letter-spacing"
|
|
h6_match = re.search(
|
|
r"\.msg-body\s+h6\s*\{[^}]*text-transform:\s*uppercase[^}]*letter-spacing:",
|
|
CSS,
|
|
)
|
|
assert h6_match, ".msg-body h6 must have text-transform:uppercase + letter-spacing"
|
|
|
|
|
|
def test_msg_body_headings_use_strong_color_and_bold_weight():
|
|
"""All headings must use bold weight (700) and strong color, not light grey."""
|
|
base_match = re.search(
|
|
r"\.msg-body\s+h1,\s*\.msg-body\s+h2,\s*\.msg-body\s+h3,\s*\.msg-body\s+h4,"
|
|
r"\s*\.msg-body\s+h5,\s*\.msg-body\s+h6\s*\{[^}]*font-weight:\s*700",
|
|
CSS,
|
|
)
|
|
assert base_match, "Combined .msg-body h1..h6 selector must set font-weight:700"
|
|
# Color must reference --strong (with --text fallback).
|
|
color_match = re.search(
|
|
r"\.msg-body\s+h1,\s*\.msg-body\s+h2,\s*\.msg-body\s+h3,\s*\.msg-body\s+h4,"
|
|
r"\s*\.msg-body\s+h5,\s*\.msg-body\s+h6\s*\{[^}]*color:\s*var\(--strong",
|
|
CSS,
|
|
)
|
|
assert color_match, "Combined heading selector must use color:var(--strong, ...)"
|
|
|
|
|
|
# ── preview-md sync: chat and file preview render headings the same ──────────
|
|
|
|
|
|
def test_preview_md_heading_sizes_match_msg_body():
|
|
"""Per the issue's 'companion fix' note: .preview-md heading sizes must mirror .msg-body
|
|
so a markdown file preview and a chat message look identical."""
|
|
for level in ("h1", "h2", "h3", "h4", "h5", "h6"):
|
|
msg_size = _font_size(".msg-body", level)
|
|
preview_size = _font_size(".preview-md", level)
|
|
assert msg_size == preview_size, (
|
|
f".preview-md {level} ({preview_size}px) must match .msg-body {level} ({msg_size}px)"
|
|
)
|
|
|
|
|
|
def test_preview_md_has_h4_h5_h6_rules():
|
|
"""Pre-fix .preview-md only had h1-h3 rules. Post-fix must have all six."""
|
|
for level in ("h4", "h5", "h6"):
|
|
match = re.search(rf"\.preview-md\s+{level}\s*\{{[^}}]*font-size:\s*\d+px", CSS)
|
|
assert match, f".preview-md {level} rule missing"
|
|
|
|
|
|
# ── data-font-size scaling: small/large stay proportional ────────────────────
|
|
|
|
|
|
def test_data_font_size_small_overrides_scale_with_new_defaults():
|
|
"""The data-font-size='small' h1 override must NOT be ≤ body 14px (the old 15px h1
|
|
in small mode was effectively the same as body — the bug Cygnus complained about,
|
|
just at small font-size).
|
|
"""
|
|
# Walk h1..h6 small overrides
|
|
small_h_sizes = {}
|
|
for level in ("h1", "h2", "h3", "h4", "h5", "h6"):
|
|
m = re.search(
|
|
rf'data-font-size="small"\]\s*\.msg-body\s+{level}\s*\{{[^}}]*font-size:\s*(\d+)px',
|
|
CSS,
|
|
)
|
|
if m:
|
|
small_h_sizes[level] = int(m.group(1))
|
|
# Must have all six.
|
|
assert set(small_h_sizes.keys()) == {"h1", "h2", "h3", "h4", "h5", "h6"}, (
|
|
f"data-font-size='small' missing override for some levels: got {sorted(small_h_sizes)}"
|
|
)
|
|
# Body in small mode is 12px.
|
|
assert small_h_sizes["h1"] >= 18, f"small h1 too small: {small_h_sizes['h1']}"
|
|
assert small_h_sizes["h2"] >= 16, f"small h2 too small: {small_h_sizes['h2']}"
|
|
assert small_h_sizes["h3"] >= 14, f"small h3 too small: {small_h_sizes['h3']}"
|
|
# Hierarchy preserved.
|
|
assert small_h_sizes["h1"] > small_h_sizes["h2"]
|
|
assert small_h_sizes["h2"] > small_h_sizes["h3"]
|
|
assert small_h_sizes["h3"] > small_h_sizes["h4"]
|
|
|
|
|
|
def test_data_font_size_large_overrides_scale_with_new_defaults():
|
|
"""The data-font-size='large' h1 override must scale up proportionally with the new defaults."""
|
|
large_h_sizes = {}
|
|
for level in ("h1", "h2", "h3", "h4", "h5", "h6"):
|
|
m = re.search(
|
|
rf'data-font-size="large"\]\s*\.msg-body\s+{level}\s*\{{[^}}]*font-size:\s*(\d+)px',
|
|
CSS,
|
|
)
|
|
if m:
|
|
large_h_sizes[level] = int(m.group(1))
|
|
assert set(large_h_sizes.keys()) == {"h1", "h2", "h3", "h4", "h5", "h6"}
|
|
# Body in large mode is 16px. h1 must be a meaningful step above.
|
|
assert large_h_sizes["h1"] >= 26, f"large h1 too small: {large_h_sizes['h1']}"
|
|
assert large_h_sizes["h2"] >= 22, f"large h2 too small: {large_h_sizes['h2']}"
|
|
assert large_h_sizes["h3"] >= 19, f"large h3 too small: {large_h_sizes['h3']}"
|
|
# Hierarchy preserved.
|
|
assert large_h_sizes["h1"] > large_h_sizes["h2"]
|
|
assert large_h_sizes["h2"] > large_h_sizes["h3"]
|
|
assert large_h_sizes["h3"] > large_h_sizes["h4"]
|
|
|
|
|
|
# ── Specific values from the issue spec ──────────────────────────────────────
|
|
|
|
|
|
def test_specific_heading_sizes_match_issue_spec():
|
|
"""Pin the exact spec'd sizes so unrelated CSS edits don't drift them."""
|
|
assert _font_size(".msg-body", "h1") == 24
|
|
assert _font_size(".msg-body", "h2") == 20
|
|
assert _font_size(".msg-body", "h3") == 17
|
|
assert _font_size(".msg-body", "h4") == 15
|
|
# h5 and h6 use uppercase, so size is at body or just below.
|
|
assert _font_size(".msg-body", "h5") == 14
|
|
assert _font_size(".msg-body", "h6") == 13
|