phase: 157_chat_chip_cap
All verification is green. Final report: **Phase 157 — final verification pass (all 3 tasks already complete in `complete/`)** - Verified the implemented work matches the design: `CHIP_CAP = 90` constant + display-only cap in `renderChips` (`slice(0,90).trimEnd() + "…"`), `title`/`aria-label`/click payload byte-unchanged; unit pins (89/90/91 letter & space, full-text invariant, short-string guard, Maybe-try single-path guard); phase-104 E2E updated to the cap contract - Full suite `uv run pytest --cov=app --cov-report=term-missing`: **3578 passed**, app/ coverage **99%** (>90% ✓) - `uv run ruff check .` clean; `uv run pyright`: 0 errors - Dedicated E2E `uv run pytest tests/e2e/test_chat_chip_cap.py -v --no-cov`: **4/4 passed** in isolation (DB up) - Cross-phase regression: phase-104 E2E 6/6, phase 05/80/103 chip suite 8/8 (seed chips <90 chars — cap is a no-op, assertions hold) - Criteria: ≤91-char capped display ✓ (byte-for-byte pin) · click submits FULL 120-char question (user bubble) ✓ · `title` + clipped-case `aria-label` carry full text ✓ (375px viewport) · <90-char byte-identical ✓ (unit + live DOM) · commit: deferred to the harness (per harness protocol, overrides AGENTS.md rule 8 for this pass) - Documentation: README.md reviewed in full — every `.env.example` var documented, all referenced scripts/pages/commands verified against the tree, the phase's chip-cap note is in the Chat section; AGENTS.md verified (commands, test files, layout-rule citations — phase 157 adds no new layout principle) → **verified consistent, no changes needed** - No live-infrastructure changes were required or made - Next pending phase: **158_thinking_timeline**
This commit is contained in:
@@ -16,7 +16,11 @@ The contract (owner decisions A1–A6, recorded in 00_phase.md):
|
||||
multi-line "chonk") — long text ellipsizes at the row edge, and the
|
||||
FULL text is recoverable via the native ``title`` tooltip on every
|
||||
chip plus the ``aria-label`` accessible name when the visible text is
|
||||
actually clipped;
|
||||
actually clipped; phase 157 (owner 2026-10-03, A1) EXTENDS this:
|
||||
the chip's DISPLAYED text is capped at 90 characters
|
||||
(``slice(0, 90).trimEnd() + "…"``) — the cap is the PRIMARY length
|
||||
guard, the single-line CSS clip the secondary; the ``title`` /
|
||||
``aria-label`` full-text contract is unchanged;
|
||||
* **A3 — the hard cap through the input path:** the composer textarea
|
||||
carries ``maxlength="4000"`` mirroring the pre-existing server cap
|
||||
(``ChatRequest.message max_length=4000`` — the server 422s beyond);
|
||||
@@ -34,17 +38,21 @@ The contract (owner decisions A1–A6, recorded in 00_phase.md):
|
||||
|
||||
The states pinned here (six tests):
|
||||
|
||||
1. **truncated-chip** (A1/A2 core) — a saved chat whose FIRST user
|
||||
question is LONG (720 chars — a readable repeated phrase, 300+ per
|
||||
the phase) with a short follow-up → a fresh page load shows EXACTLY
|
||||
ONE onboarding chip (the phase-103 opener semantics — the follow-up
|
||||
never surfaces): computed ``white-space: nowrap`` /
|
||||
``overflow: hidden`` / ``text-overflow: ellipsis``, visually clipped
|
||||
(``scrollWidth > clientWidth``), single line (``44 <=
|
||||
offsetHeight <= 60`` — a one-line pill sits at the 44px
|
||||
``min-height`` floor, border-box; a wrapped two-liner is ~65px —
|
||||
the chonk), and
|
||||
``title`` + ``aria-label`` == the FULL long text;
|
||||
1. **capped-chip** (A1/A2 core + the phase-157 extension) — a saved
|
||||
chat whose FIRST user question is LONG (720 chars — a readable
|
||||
repeated phrase, 300+ per the phase) with a short follow-up → a
|
||||
fresh page load shows EXACTLY ONE onboarding chip (the phase-103
|
||||
opener semantics — the follow-up never surfaces): computed
|
||||
``white-space: nowrap`` / ``overflow: hidden`` /
|
||||
``text-overflow: ellipsis`` (the phase-104 secondary guard), the
|
||||
DISPLAY capped at 90 chars (``slice(0, 90).trimEnd() + "…"`` — the
|
||||
phase-157 primary guard), FITTING the 72rem column at the 1280px
|
||||
viewport (``scrollWidth <= clientWidth`` — the cap is what keeps
|
||||
the chip compact), single line (``44 <= offsetHeight <= 60`` — a
|
||||
one-line pill sits at the 44px ``min-height`` floor, border-box;
|
||||
a wrapped two-liner is ~65px — the chonk), ``title`` == the FULL
|
||||
long text, and NO ``aria-label`` (not visually clipped — the
|
||||
attribute is absent by design);
|
||||
2. **seed-contrast** — a fresh deployment's (seed) short chips have
|
||||
``title`` set AND NO ``aria-label`` (not truncated — the attribute
|
||||
is absent by design; the ``textContent`` already carries the full
|
||||
@@ -231,22 +239,26 @@ def _wait_chat_booted(page: Page) -> None:
|
||||
def test_long_chip_is_single_line_ellipsized_with_full_text_tooltip(
|
||||
page: Page, app_url: str, db_ready: None
|
||||
) -> None:
|
||||
"""A1/A2 core: a saved chat whose FIRST user question is 720 chars
|
||||
(a readable repeated phrase) with a short follow-up → a fresh page
|
||||
load shows EXACTLY ONE onboarding chip (the phase-103 opener
|
||||
semantics — the follow-up never surfaces), and it is:
|
||||
"""A1/A2 core + the phase-157 extension: a saved chat whose FIRST
|
||||
user question is 720 chars (a readable repeated phrase) with a
|
||||
short follow-up → a fresh page load shows EXACTLY ONE onboarding
|
||||
chip (the phase-103 opener semantics — the follow-up never
|
||||
surfaces), and it is:
|
||||
|
||||
* ONE line — computed ``white-space: nowrap``, ``overflow: hidden``,
|
||||
``text-overflow: ellipsis``;
|
||||
* visually clipped — ``scrollWidth > clientWidth`` (720 chars of
|
||||
~7px/char far exceeds the 46rem column);
|
||||
``text-overflow: ellipsis`` (the phase-104 secondary guard);
|
||||
* display-capped — the visible text is
|
||||
``slice(0, 90).trimEnd() + "…"`` (the phase-157 PRIMARY guard —
|
||||
a 91-char display no longer stretches the chip to the row edge);
|
||||
* FITS the 72rem column at the 1280px viewport — NOT visually
|
||||
clipped (``scrollWidth <= clientWidth``);
|
||||
* the pill, not the chonk — ``44 <= offsetHeight <= 60`` (a
|
||||
one-line pill sits at the 44px ``min-height`` floor, border-box
|
||||
— the global ``* { box-sizing: border-box }`` makes the 44px
|
||||
the OUTER height; a wrapped two-liner is ~65px);
|
||||
* the FULL text one hover away — ``title`` == the full long text;
|
||||
* the clipped-case accessible name — ``aria-label`` == the full
|
||||
long text.
|
||||
* no ``aria-label`` — the chip is not visually clipped, so the
|
||||
attribute stays absent (the phase-104 pattern).
|
||||
"""
|
||||
page.set_default_timeout(30_000)
|
||||
login(page, app_url, next="/")
|
||||
@@ -268,9 +280,11 @@ def test_long_chip_is_single_line_ellipsized_with_full_text_tooltip(
|
||||
chips = page.locator("#suggestions .suggestion-chip")
|
||||
expect(chips.first).to_be_visible(timeout=30_000)
|
||||
expect(chips).to_have_count(1, timeout=15_000)
|
||||
# The chip carries the EXACT full opener in the DOM (the clipping
|
||||
# is visual — textContent is never truncated).
|
||||
assert chips.first.inner_text().strip() == LONG_OPENER
|
||||
# Phase 157 (A1): the DISPLAY is capped at 90 chars — the 720-char
|
||||
# opener shows as slice(0, 90).trimEnd() + "…" (91 chars max);
|
||||
# the full text stays in the title (asserted below).
|
||||
assert chips.first.inner_text().strip() == LONG_OPENER[:90].rstrip() + "…"
|
||||
assert len(chips.first.inner_text().strip()) <= 91
|
||||
|
||||
chip = chips.first
|
||||
# A1 — the single-line computed contract (the phase-104 CSS).
|
||||
@@ -291,22 +305,27 @@ def test_long_chip_is_single_line_ellipsized_with_full_text_tooltip(
|
||||
dims = chip.evaluate(
|
||||
"el => ({ sw: el.scrollWidth, cw: el.clientWidth, oh: el.offsetHeight })"
|
||||
)
|
||||
assert dims["sw"] > dims["cw"], (
|
||||
"a 720-char question must be visually clipped in the 46rem column"
|
||||
assert dims["sw"] <= dims["cw"], (
|
||||
"the 91-char capped display must FIT the 72rem column at the "
|
||||
"1280px viewport — the cap is the PRIMARY guard, the CSS "
|
||||
"clip the secondary (a 720-char display is what used to "
|
||||
"stretch the chip to the row edge)"
|
||||
)
|
||||
assert 44 <= dims["oh"] <= 60, (
|
||||
f"one-line pill: 44px min-height floor (border-box), a wrapped "
|
||||
f"two-liner is ~65px — got {dims['oh']}px"
|
||||
)
|
||||
|
||||
# A2 — the full text is one hover away (the native tooltip), and
|
||||
# the accessible name of the CLIPPED chip is the full text.
|
||||
# A2 — the full text is one hover away (the native tooltip).
|
||||
assert chip.get_attribute("title") == LONG_OPENER, (
|
||||
"the title tooltip must carry the FULL question (owner A2)"
|
||||
)
|
||||
assert chip.get_attribute("aria-label") == LONG_OPENER, (
|
||||
"a clipped chip's aria-label must be the full text (the "
|
||||
"source-chip pattern)"
|
||||
# Not visually clipped (the capped display fits) → NO aria-label
|
||||
# (the phase-104 pattern: the attribute is set only when the
|
||||
# visible text is clipped).
|
||||
assert chip.get_attribute("aria-label") is None, (
|
||||
"a chip whose display FITS its pill must stay attribute-free "
|
||||
"— aria-label is set only when visually clipped (phase 104)"
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user