phase: 116_modal_scrollbar_theme
All verification complete — every gate re-run independently and green. Final report: **Phase 116 final verification pass — all green (work was already in the tree; verified, nothing to fix)** - **Verified** `styles.css`: `--scrollbar-thumb`/`--scrollbar-track` tokens in `:root` (color-mix derivations — follow the saved theme, not in BUILTIN_COLORS; contrast 3.73:1 ≥ 3:1, recomputed independently) + scoped rules `.doc-modal .doc-raw` (overflow-x already owned by the element) - **Verified** E2E test 8 `test_raw_code_block_themed_scrollbar`: overflow, scroll action, computed-style theme assertions, screenshot - **Screenshots visually confirmed**: headed `doc_modal_scrollbar_themed_1280.png` shows muted rounded thumb on dark track, no native light-gray bar; headless suite shot records scrolled state (headless Chromium paints no bar UI — documented in test docstring) **Test / lint / coverage (re-run by me):** - `uv run pytest tests/e2e/test_document_viewer.py -v --no-cov` → **8 passed** (isolation) - `uv run pytest --cov=app --cov-report=term` → **2457 passed, app/ 99%** (>90% ✓) - `uv run pytest tests/e2e/test_dark_tech_theme.py -v --no-cov` → **6 passed** (no token collision) - `uv run ruff check .` → clean; `uv run pyright` → 0 errors, 0 warnings **Completion criteria:** 1. Modal scrolls horizontally, scrollbar themed, screenshot shows no native bar — ✅ 2. No global scrollbar change (all new selectors under `.doc-modal`; grep-confirmed), regressions green — ✅ 3. pytest green, coverage >90%, e2e isolated green, ruff+pyright clean — ✅ 4. `--no-gpg-sign` commit + phase move — harness job (left uncommitted in working tree, per rules) **No defects found; no deviations. Next pending phase:** none — `todo/` contains only phase 116.
This commit is contained in:
@@ -29,6 +29,10 @@ Test → story mapping (Playwright Mapping Rule):
|
||||
inner content).
|
||||
7. ``test_modal_theme_and_no_cdn`` — dark page background, the panel on
|
||||
the Phase-08 surface colour, every asset same-origin or ``data:``.
|
||||
8. ``test_raw_code_block_themed_scrollbar`` — phase 116: the raw code
|
||||
block (``pre.doc-raw``) overflows horizontally, a scroll action moves
|
||||
it, the themed scrollbar tokens are in effect via computed style, and
|
||||
a screenshot records the themed (not native light-gray) bar.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -51,6 +55,7 @@ from e2e.auth_helpers import login
|
||||
|
||||
REPO = Path(__file__).resolve().parents[2]
|
||||
FIXTURES = REPO / "tests" / "fixtures" / "docs"
|
||||
SCREENSHOTS = REPO / ".agents" / "screenshots" # house convention for visual records
|
||||
QUESTION = "How is my Kubernetes cluster set up?"
|
||||
|
||||
|
||||
@@ -119,6 +124,33 @@ def _drill(page: Page, *names: str) -> None:
|
||||
page.click(f'#folders-tbody a.folder-link:text-is("{name}")')
|
||||
|
||||
|
||||
def _seed_long_line_doc() -> None:
|
||||
"""Seed the synthetic phase-116 fixture (see the test docstring):
|
||||
a non-markdown doc whose one long line is deliberately longer than
|
||||
the modal width — the shape of the TODO's quest .pl ``quest::say``
|
||||
line. Direct DB seed, the ``test_modal_xss_safe`` pattern (the
|
||||
viewer is database-only; no chunks needed)."""
|
||||
long_line = "quest::say(\"" + ("the quick brown fox jumps over the lazy dog " * 20) + "\")"
|
||||
with SessionLocal() as db:
|
||||
db.add(
|
||||
Document(
|
||||
source="docs",
|
||||
path="notes/long-line.txt",
|
||||
full_path="/tmp/long-line.txt",
|
||||
title="Long Line Fixture",
|
||||
content=(
|
||||
"# long-line-fixture: the line below is deliberately\n"
|
||||
"# longer than the document modal's width (phase 116).\n"
|
||||
+ long_line
|
||||
+ "\n"
|
||||
),
|
||||
content_hash="e" * 64,
|
||||
indexed_at=datetime.now(UTC),
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. Chat source chip → SAME-PAGE modal (no new tab)
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -456,3 +488,112 @@ def test_modal_theme_and_no_cdn(
|
||||
assert refs, "expected local asset references on the chat page"
|
||||
for ref in refs:
|
||||
assert ref.startswith(app_url) or ref.startswith("data:"), f"non-local: {ref}"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 8. Phase 116: the raw code block scrolls horizontally with a themed
|
||||
# scrollbar (task 01's scoped rules) — never the native light-gray bar
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_raw_code_block_themed_scrollbar(
|
||||
page: Page, app_url: str, mock_llm: int, db_ready: None
|
||||
) -> None:
|
||||
"""The modal's ``pre.doc-raw`` overflows horizontally, a horizontal
|
||||
scroll action actually moves it, and task 01's themed scrollbar rules
|
||||
are in effect (the screenshot is the visual record of the themed bar).
|
||||
|
||||
Document choice (the task says to check the fixture KB first): no
|
||||
fixture doc qualifies — the longest RAW (non-markdown) fixture line is
|
||||
91 chars (``homelab/networking/static-dns.json``) ≈ 742px at the
|
||||
0.85rem mono font, well inside the ≈1058px pre client width at the
|
||||
1280px fixture viewport (panel 1100px − content padding − pre border).
|
||||
So this test seeds a synthetic doc instead (the
|
||||
``test_modal_xss_safe`` direct-seed pattern): one ≈935-char
|
||||
``quest::say(...)`` line — the shape of the TODO's observed quest
|
||||
.pl line — which guarantees real horizontal overflow.
|
||||
|
||||
Screenshot note: headless Chromium paints NO scrollbars at all
|
||||
(frame UI is omitted — verified on Chromium 151), so the suite's
|
||||
screenshot (``doc_modal_scrollbar_1280.png``) is the record of the
|
||||
scrolled modal state (long line mid-content = overflow + working
|
||||
scroll, no native bar); the themed bar itself is asserted via
|
||||
computed style here and recorded in the headed capture
|
||||
``doc_modal_scrollbar_themed_1280.png`` (phase 116, task 02).
|
||||
"""
|
||||
_reset_db(mock_llm, seed=True)
|
||||
_seed_long_line_doc()
|
||||
|
||||
login(page, app_url)
|
||||
_drill(page, "docs", "notes")
|
||||
row = page.locator("#docs-tbody tr", has_text="long-line.txt")
|
||||
expect(row).to_have_count(1)
|
||||
row.locator("td:nth-child(2) a.doc-link").click()
|
||||
|
||||
expect(page.locator(".doc-modal")).to_be_visible()
|
||||
expect(page.locator("#doc-modal-title")).to_have_text("Long Line Fixture")
|
||||
pre = page.locator("#doc-modal-content pre.doc-raw")
|
||||
expect(pre).to_have_count(1)
|
||||
|
||||
# The overflow is real: the long line makes the content wider than the
|
||||
# box (no line-wrap — locked A1, phase 116).
|
||||
dims = pre.evaluate("el => ({sw: el.scrollWidth, cw: el.clientWidth})")
|
||||
assert dims["sw"] > dims["cw"], f"no horizontal overflow: {dims}"
|
||||
|
||||
# The overflow is SCROLLED, not clipped: the element owns its
|
||||
# overflow-x (task 01's comment in styles.css) and the themed rules
|
||||
# are in effect — scrollbar-width: thin + scrollbar-color as a
|
||||
# two-colour pair (the ::-webkit-scrollbar pseudos are not exposed via
|
||||
# computed style; that half is the screenshot's job).
|
||||
themed = page.evaluate(
|
||||
"""() => {
|
||||
const el = document.querySelector('#doc-modal-content pre.doc-raw');
|
||||
const cs = getComputedStyle(el);
|
||||
const probe = (name) => {
|
||||
const s = document.createElement('span');
|
||||
const hidden = 'position:absolute;visibility:hidden;';
|
||||
s.style.cssText = hidden + 'background:var(' + name + ')';
|
||||
document.body.appendChild(s);
|
||||
const bg = getComputedStyle(s).backgroundColor;
|
||||
s.remove();
|
||||
return bg;
|
||||
};
|
||||
return {
|
||||
overflowX: cs.overflowX,
|
||||
width: cs.scrollbarWidth,
|
||||
color: cs.scrollbarColor,
|
||||
thumb: probe('--scrollbar-thumb'),
|
||||
track: probe('--scrollbar-track'),
|
||||
};
|
||||
}"""
|
||||
)
|
||||
assert themed["overflowX"] == "auto", f"overflow-x is {themed['overflowX']}, not auto"
|
||||
assert themed["width"] == "thin", f"scrollbar-width is {themed['width']}, not thin"
|
||||
# The tokens are color-mix() derivations, so Chromium serializes the
|
||||
# used values as color(srgb …) (plain rgb(…) or #hex also accepted).
|
||||
colors = re.findall(
|
||||
r"#[0-9a-fA-F]{3,8}\b|rgb\([^)]*\)|color\([^)]*\)", themed["color"]
|
||||
)
|
||||
assert len(colors) == 2, f"scrollbar-color {themed['color']!r} is not a two-colour pair"
|
||||
assert colors[0] == themed["thumb"], (
|
||||
f"scrollbar thumb {colors[0]} is not the theme's --scrollbar-thumb {themed['thumb']}"
|
||||
)
|
||||
assert colors[1] == themed["track"], (
|
||||
f"scrollbar track {colors[1]} is not the theme's --scrollbar-track {themed['track']}"
|
||||
)
|
||||
|
||||
# A horizontal scroll action actually moves it (no clipping — the
|
||||
# rest of the line is reachable). The pre is freshly rendered, so
|
||||
# scrollLeft starts at 0.
|
||||
before = pre.evaluate("el => el.scrollLeft")
|
||||
assert before == 0, f"expected scrollLeft 0 before the action, got {before}"
|
||||
pre.evaluate("el => el.scrollBy({left: 300, behavior: 'instant'})")
|
||||
after = pre.evaluate("el => el.scrollLeft")
|
||||
assert after > 250, f"scrollBy(300) only moved to {after}"
|
||||
|
||||
# Visual record (house convention): the bar at the pre's bottom is the
|
||||
# themed one — scroll to mid-content so the thumb sits mid-track.
|
||||
pre.evaluate("el => el.scrollTo({left: el.scrollWidth / 2, behavior: 'instant'})")
|
||||
shot = SCREENSHOTS / "doc_modal_scrollbar_1280.png"
|
||||
page.screenshot(path=str(shot))
|
||||
assert shot.exists() and shot.stat().st_size > 0
|
||||
|
||||
Reference in New Issue
Block a user