phase: 128_sync_label_mobile
Everything verified and 100% green. The earlier rejection was a report-format issue, not a test failure — all work is in the working tree. Final report: **Phase 128 final pass — verified (all 3 tasks complete; no code defects found)** - Verified: `#sync-label` parts markup, `setSyncLabel` writer, part builders (A4 untruncated title/aria-live), flex-row CSS + mobile media query — all match design 1:1. - `uv run pytest --cov=app --cov-report=term`: 3024 passed; TOTAL coverage **99%** (>90% gate). - `uv run pytest tests/e2e/test_sync_label_mobile.py -v --no-cov`: 4 passed (119s, isolated). - `uv run ruff check .`: clean; `uv run pyright`: 0 errors. - Criteria: (1) mobile 390×844 label in-viewport, count visible in import+summaries — ✅ E2E; (2) desktop strings/cap/title unchanged — ✅ E2E+unit; (3) E2E isolated green — ✅; (4) full suite green — ✅. - Docs: README.md fixed 5 pre-existing gaps (PDF phase-126 section + `pdf` token, hidden-folders toggle ph.105, document dates ph.106, private git-source masked tokens ph.121, doc-draft Discard ph.115); AGENTS.md verified consistent — no changes. - Notable: double-`-q` (addopts `-q` + CLI `-q`) suppresses pytest's summary line — standard `-qq` behavior, not a bug; exit codes confirm green. - Next pending phase: `129_sync_chunk_progress`.
This commit is contained in:
@@ -34,6 +34,14 @@ FULL untruncated LABEL (the file label, "Writing KB overview…", or
|
||||
"Summarizing folders… <folder> (n/m)") rides the button title for
|
||||
EVERY running label; the phase-64 "title only when there is a current
|
||||
file" (remove-when-null) rule is gone.
|
||||
|
||||
Phase 128 (task 01): the label becomes PARTS — #sync-label ships a
|
||||
main span (the idle text pre-JS) + a hidden count span (the pinned
|
||||
"(n/m)"), the builders return { main, count }, and the single writer
|
||||
setSyncLabel(main, count) lands the parts + the untruncated full
|
||||
label (main + " " + count) on the title + #sync-result; the
|
||||
running-state / settle pins move onto that shape (their labels stay
|
||||
byte-identical as full strings).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -115,18 +123,30 @@ def test_sync_button_ships_hidden_and_labeled() -> None:
|
||||
assert 'aria-label="Sync sources"' in attrs
|
||||
|
||||
|
||||
def test_sync_button_has_icon_and_label_span() -> None:
|
||||
def test_sync_button_has_icon_and_label_parts() -> None:
|
||||
"""The button body is a refresh-cycle svg (aria-hidden — decorative,
|
||||
the spin is the visible running state) + the .sync-label span with
|
||||
the idle text, so the label can be swapped by sources.js."""
|
||||
the idle text, so the label can be swapped by sources.js.
|
||||
|
||||
Phase 128 (task 01): #sync-label is PARTS — the MAIN span
|
||||
(.sync-label-main) carries the idle text (the pre-JS label renders
|
||||
from the static markup; the part CSS may ellipsize it), and the
|
||||
COUNT span (.sync-label-count — the pinned "(n/m)" position, never
|
||||
clipped) ships hidden + empty (there is no count at idle). The
|
||||
class/id stay (the module query + the E2E label assertions)."""
|
||||
text = _text(SHELL_HTML)
|
||||
btn = text[text.find('id="sync-btn"') : text.find("</button>", text.find('id="sync-btn"'))]
|
||||
assert re.search(r'<svg[^>]*class="sync-icon"[^>]*aria-hidden="true"', btn)
|
||||
# class for the module query + id for the E2E label assertions
|
||||
# Phase 128 (task 01): the parts markup — the exact shape (the main
|
||||
# span holds the idle text, the count span is hidden + empty).
|
||||
label = re.search(
|
||||
r'<span\b[^>]*id="sync-label"[^>]*>\s*Sync sources\s*</span>', btn
|
||||
r'<span\b[^>]*id="sync-label"[^>]*>'
|
||||
r'<span class="sync-label-main">Sync sources</span>'
|
||||
r'<span class="sync-label-count" hidden></span>'
|
||||
r"</span>",
|
||||
btn,
|
||||
)
|
||||
assert label, "the #sync-label span carrying the idle text is missing"
|
||||
assert label, ("the #sync-label parts markup (main + hidden count) is missing")
|
||||
assert 'class="sync-label"' in label.group(0)
|
||||
|
||||
|
||||
@@ -275,30 +295,32 @@ def test_sources_js_running_state_is_never_stale() -> None:
|
||||
adjusted — the FULL untruncated LABEL (the file label, the overview
|
||||
label, or the summaries label — whatever the running form is) rides
|
||||
the button title for EVERY running label; the "title only when
|
||||
there is a current file" (remove-when-null) rule is gone."""
|
||||
there is a current file" (remove-when-null) rule is gone.
|
||||
Phase 128 (task 01): the entry hands the label PARTS (the
|
||||
phase-aware split — sync + status → fmtSyncPhaseLabel, the
|
||||
fall-through → the byte-identical fmtSyncLabel) to the SINGLE
|
||||
writer, setSyncLabel(main, count), which lands the main part, the
|
||||
pinned count part (hidden when absent), and the FULL untruncated
|
||||
label on the button title + #sync-result (A4) — the direct span /
|
||||
title / announcer writes are GONE from the entry (they live in
|
||||
setSyncLabel)."""
|
||||
js = _text(SOURCES_JS)
|
||||
body = _body(js, "enterSyncRunningState")
|
||||
assert "syncBtn.disabled = true" in body
|
||||
assert 'syncBtn.setAttribute("aria-busy", "true")' in body
|
||||
assert "syncBtn.title = label" in body, (
|
||||
"the full untruncated label on hover, for every running label"
|
||||
)
|
||||
assert 'syncBtn.removeAttribute("title")' not in body, (
|
||||
"the title is set for EVERY running label (phase 98: the file-only "
|
||||
"rule is adjusted away)"
|
||||
)
|
||||
title_i = body.find("syncBtn.title = label")
|
||||
span_i = body.find("syncLabel.textContent = label")
|
||||
result_i = body.find("syncResult.textContent = label")
|
||||
assert -1 < title_i < span_i < result_i, (
|
||||
"one label: built once, written to title, span AND the announcer"
|
||||
)
|
||||
# the parts split (phase 98) is intact…
|
||||
assert "fmtSyncPhaseLabel(status)" in body
|
||||
assert "fmtSyncLabel(kind, currentFile, done, total)" in body
|
||||
# …and the single writer lands the parts (A4 in it — the title is
|
||||
# set for EVERY running label: the file-only rule is adjusted away).
|
||||
assert "setSyncLabel(main, count)" in body
|
||||
assert body.count("setSyncLabel(") == 1, "the entry writes ONLY through the parts writer"
|
||||
assert "syncBtn.title =" not in body, ("the title write lives in setSyncLabel (A4)")
|
||||
assert "syncLabel.textContent" not in body, ("no direct span write — the parts")
|
||||
assert "syncResult.textContent" not in body, ("the announcer write lives in setSyncLabel (A4)")
|
||||
assert 'syncBtn.setAttribute("aria-label", "Sync sources")' in body
|
||||
assert "syncBtn.classList.remove(\"is-error\")" in body
|
||||
assert "syncIcon.classList.add(\"is-spinning\")" in body
|
||||
assert "fmtSyncLabel(kind, currentFile, done, total)" in body
|
||||
assert "syncLabel.textContent = label" in body
|
||||
assert "syncResult.textContent = label" in body, ("the announcer reads the full live path")
|
||||
|
||||
|
||||
def test_sources_js_terminal_states() -> None:
|
||||
@@ -345,7 +367,10 @@ def test_sources_js_settles_the_button_on_terminal() -> None:
|
||||
"""settleSyncButton re-enables the control, drops aria-busy, the
|
||||
title, and the failed affordances, and un-spins the icon — the
|
||||
button can never sit disabled after a run reaches a terminal state
|
||||
(failed included: retry-ready)."""
|
||||
(failed included: retry-ready). Phase 128 (task 01): the settle /
|
||||
idle label ("Sync sources", "Synced HH:MM") goes through the parts
|
||||
writer — setSyncLabel(label), count hidden (absent), the full
|
||||
string riding the title + #sync-result (A4, every label state)."""
|
||||
js = _text(SOURCES_JS)
|
||||
body = _body(js, "settleSyncButton")
|
||||
assert "syncBtn.disabled = false" in body
|
||||
@@ -353,6 +378,8 @@ def test_sources_js_settles_the_button_on_terminal() -> None:
|
||||
assert 'syncBtn.removeAttribute("title")' in body
|
||||
assert 'syncBtn.setAttribute("aria-label", "Sync sources")' in body
|
||||
assert "syncIcon.classList.remove(\"is-spinning\")" in body
|
||||
assert "setSyncLabel(label)" in body, ("the settle label goes through the parts writer")
|
||||
assert "syncLabel.textContent" not in body, ("no direct span write — the parts")
|
||||
|
||||
|
||||
def test_sources_js_never_starts_a_second_poll_loop() -> None:
|
||||
|
||||
Reference in New Issue
Block a user