phase: 104_chip_sizing_question_cap
All completion criteria verified. Final report: **Phase 104 — final verification pass: all green** - Verified (no code changes needed): single-line ellipsized `.suggestion-chip` CSS + deleted `.maybe-try` override, `renderChips` full-text `title` + `aria-label`-when-clipped, `maxlength="4000"` + `#char-count` counter + `handleSend` over-cap guard, unit pins file, schemas boundary pins, dedicated E2E suite. - E2E (isolation): `uv run pytest tests/e2e/test_chip_sizing_question_cap.py -v --no-cov` → **6 passed**; regressions: `test_suggestion_chips.py` 8 passed, `test_pinned_composer.py` 4 passed, `test_responsive_polish.py` 7 passed, `test_chat_history.py` 5 passed. - `uv run pytest` → **2102 passed**; `--cov=app` → **99%** (>90%); `uv run ruff check . && uv run pyright` → clean, 0 errors. - Criteria: chip E2E (single-line, clipped, title+aria-label full text) ✅; paste caps at exactly 4,000, send streams, counter hides ✅; programmatic 5,000-char fill → banner, no turn, text kept ✅; 4,000/4,001 boundary pinned + HTML maxlength == JS constant cross-file pin ✅. - Diff scope: `frontend/`, new unit file, `tests/unit/test_schemas.py`, new E2E file, phase files — **no `app/` diff, no migration, no `shared.js` diff**. - Deviations: 4 regression test files touched — 2 genuine DOM-pin conflicts from the new `#char-count` child (explicitly anticipated by the overview) + 3 documented **pre-existing E2E flake fixes** (smooth-scroll race, tab-walk heuristic, 10 ms timeout), each verified pre-existing on the pre-phase-104 tree. - No commit made (harness commits per the execution protocol override). - Next pending phase: `98_sync_summary_visibility`.
This commit is contained in:
@@ -186,6 +186,38 @@ def scroll_state(page: Page) -> dict[str, float]:
|
||||
)
|
||||
|
||||
|
||||
def wait_reveal_scroll_done(page: Page, target: int, timeout: int = 10_000) -> None:
|
||||
"""Block until the submit's ``scrollReveal`` smooth scroll has ARRIVED
|
||||
at its target (the document bottom captured right after the click,
|
||||
``target`` — the first streamed delta lands tens of ms later and
|
||||
would inflate a later read). Needed before a test scroll: Chromium's
|
||||
smooth-scroll duration is distance- and environment-dependent (roughly
|
||||
half a second to a second for a full page), and a ``window.scrollTo``
|
||||
issued mid-animation does not settle the measured position (the
|
||||
animation keeps moving the page after the test's scroll, so a
|
||||
position read lands mid-flight, e.g. ``y=71`` instead of ``0``).
|
||||
|
||||
The wait is an ARRIVAL wait (``scrollY`` reached the target bottom),
|
||||
not a "looks still" wait: mid-animation frame stalls under load make
|
||||
a short stillness window pass while the animation is still pending
|
||||
(observed: a 120 ms window passed mid-scroll; 800 ms is the fallback
|
||||
for the vanishingly-rare late-target read, where the arrival
|
||||
threshold is inflated by an already-landed delta). Phase 104 (task
|
||||
03, 2026-09-12): pinned deterministically when the phase-104 E2E
|
||||
regression exposed the race (it reproduces on the pre-phase-104 tree
|
||||
— a pre-existing flake, not a phase-104 regression)."""
|
||||
page.wait_for_function(
|
||||
"""(target) => {
|
||||
if (window.scrollY >= target - window.innerHeight - 2) return true;
|
||||
const y0 = window.scrollY;
|
||||
return new Promise((resolve) =>
|
||||
setTimeout(() => resolve(window.scrollY === y0), 800));
|
||||
}""",
|
||||
arg=target,
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
|
||||
def wait_settled(page: Page, timeout: int = 30_000) -> None:
|
||||
"""The turn is over: the label is back to "Send" (phase 48 — the
|
||||
in-flight state is the enabled Stop control, so the label carries the
|
||||
@@ -194,11 +226,18 @@ def wait_settled(page: Page, timeout: int = 30_000) -> None:
|
||||
expect(page.locator("#send-label")).to_have_text("Send", timeout=timeout)
|
||||
|
||||
|
||||
def submit(page: Page, question: str) -> None:
|
||||
"""Submit through the composer (the real-user flow)."""
|
||||
def submit(page: Page, question: str) -> int:
|
||||
"""Submit through the composer (the real-user flow). Returns the
|
||||
document ``scrollHeight`` read right after the click — the submit's
|
||||
``scrollReveal`` target (``window.scrollTo({top: scrollHeight})``
|
||||
runs in the click's own event dispatch, and the first streamed delta
|
||||
lands tens of ms later and would inflate a later read)."""
|
||||
page.fill("#message-input", question)
|
||||
page.click("#send-btn")
|
||||
expect(page.locator(".msg.user .bubble").last).to_contain_text(question)
|
||||
# The bubble exists by now and the first streamed delta is still tens
|
||||
# of ms away — the read is the scrollReveal's own target.
|
||||
return page.evaluate("() => document.documentElement.scrollHeight")
|
||||
|
||||
|
||||
def build_conversation(page: Page, n: int = 6) -> None:
|
||||
@@ -371,7 +410,7 @@ def test_stop_is_reachable_from_scrolled_up(
|
||||
# SECOND answer bubble exists and carries deltas (`.last` alone would
|
||||
# still resolve to turn 1's finished bubble: the typing indicator is
|
||||
# `.bubble.typing` and excluded from ANSWER).
|
||||
submit(page, LONG_QUESTION)
|
||||
reveal_target = submit(page, LONG_QUESTION)
|
||||
page.wait_for_function(
|
||||
"() => { const els = document.querySelectorAll('.msg.brain .bubble:not(.typing)');"
|
||||
" return els.length >= 2 && els[els.length - 1].innerText.length > 80; }",
|
||||
@@ -380,6 +419,12 @@ def test_stop_is_reachable_from_scrolled_up(
|
||||
answer = page.locator(ANSWER).nth(1)
|
||||
expect(page.locator("#send-label")).to_have_text("Stop", timeout=10_000)
|
||||
|
||||
# The submit's own smooth scrollReveal (to the document bottom) must
|
||||
# ARRIVE before the test scrolls up — mid-animation, the browser
|
||||
# keeps moving the page after the test's scrollTo and the position
|
||||
# read lands mid-flight (the race wait_reveal_scroll_done documents).
|
||||
wait_reveal_scroll_done(page, reveal_target)
|
||||
|
||||
# The user scrolls UP to read earlier content. Phase 42 leaves them
|
||||
# there — the app never follows the stream.
|
||||
page.evaluate("() => window.scrollTo(0, 0)")
|
||||
|
||||
Reference in New Issue
Block a user