"""Regression tests for PR #934 UI fixes.
Four invariants this file locks in place:
1. `#composerReasoningDropdown` lives OUTSIDE `.composer-left` (as a sibling of
the other composer dropdowns), so it isn't clipped by that container's
`overflow-y: hidden`. Regresses to invisible-dropdown if moved back.
2. The reasoning chip label uses an SVG icon (`stroke="currentColor"`) instead
of the `🧠` emoji, matching every other composer chip.
3. `cmdReasoning()` calls `_applyReasoningChip(eff)` directly with the
server-confirmed effort, not `syncReasoningChip()` which re-applies the
stale cached value.
4. `attachBtwStream()` sets a `_streamDone` flag in `done`/`apperror` and
gates `onerror`'s row removal on `!_streamDone` — otherwise the browser's
post-`stream_end` error event wipes the just-rendered answer.
"""
from __future__ import annotations
import pathlib
import re
REPO = pathlib.Path(__file__).resolve().parent.parent
INDEX = (REPO / "static" / "index.html").read_text(encoding="utf-8")
UI_JS = (REPO / "static" / "ui.js").read_text(encoding="utf-8")
COMMANDS_JS = (REPO / "static" / "commands.js").read_text(encoding="utf-8")
MESSAGES_JS = (REPO / "static" / "messages.js").read_text(encoding="utf-8")
STYLE_CSS = (REPO / "static" / "style.css").read_text(encoding="utf-8")
# ── #1 dropdown escapes composer-left ─────────────────────────────────────────
class TestReasoningDropdownEscapesComposerLeft:
"""The dropdown must sit as a sibling of .composer-footer, not inside
.composer-left which has overflow-y: hidden and clips absolute children."""
def test_dropdown_lives_outside_composer_left(self):
# Find the
...
block and confirm the
# reasoning dropdown is NOT inside it.
m = re.search(
r'
]*>(?P[\s\S]*?)' in INDEX
assert 'id="composerReasoningDropdown"' in INDEX
def test_dropdown_is_sibling_of_other_composer_dropdowns(self):
# The composer-level dropdowns must appear contiguously — if one
# of them is nested inside an overflow-hidden container, this would
# typically split the group.
# NOTE (#3177): #profileDropdown was intentionally relocated to document
# root so it can be positioned from BOTH the composer chip and the
# optional titlebar profile button. It is therefore no longer part of the
# composer-footer contiguity group; the check below covers the three
# dropdowns that must still sit together in the composer footer.
positions = [
("composerWsDropdown", INDEX.find('id="composerWsDropdown"')),
("composerReasoningDropdown", INDEX.find('id="composerReasoningDropdown"')),
("composerModelDropdown", INDEX.find('id="composerModelDropdown"')),
]
for name, pos in positions:
assert pos > -1, f"{name} not found in index.html"
# They should all be in the same area of the document — within ~1.5 KB
window = [p for _, p in positions]
assert max(window) - min(window) < 2000, (
"composer dropdowns are no longer grouped — reasoning dropdown may "
"have drifted back inside a nested container"
)
# ── #2 monochrome SVG replaces emoji ──────────────────────────────────────────
class TestReasoningChipIcon:
"""The chip must render a currentColor SVG, not a 🧠emoji, for cross-platform
rendering consistency with the other composer chips."""
def test_chip_button_contains_svg_with_currentColor(self):
# Locate the chip button and confirm it contains a stroke="currentColor" SVG
m = re.search(
r'',
INDEX,
)
assert m, "composer-reasoning-chip button not found"
btn_body = m.group(1)
assert 'stroke="currentColor"' in btn_body, (
"reasoning chip must use stroke='currentColor' SVG matching other chips"
)
assert '