phase: 149_sync_progress_box
All verification complete. The `ink-soft`/`--surface` pair the box uses is already pinned AA by the existing theme E2E suites (8.2:1 dark, ~6.9:1 light). Everything is green — no fixes were needed.
**Phase 149 final verification report**
- Verified (no re-work needed): `#sync-progress` box wiring (single `setSyncLabel` writer with `running` flag; `is-running` class travels with the spin; aria-label owned by the writer), the panel/icon-only-button CSS, and the 5-test dedicated E2E; all 6 pre-existing sync E2Es' running pins migrated to the box, settled pins untouched.
- Dedicated E2E: `uv run pytest tests/e2e/test_sync_progress_box.py -v --no-cov` → **5 passed in 198.6 s** (isolation, slow-proxy mock; under the 600 s budget).
- Full suite: `uv run pytest --cov=app --cov-report=term-missing` → **3463 passed, app/ coverage 99%** (>90% gate).
- Migrated sync E2Es, each in isolation: `test_sync_button` 3 passed, `test_sync_cancel` 3, `test_sync_chunk_progress` 2, `test_sync_label_mobile` 4, `test_sync_summary_visibility` 3, `test_sync_upload_progress` 4 — all green.
- Lint/types: `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors.
- Criteria: in-flight spinner-only button + full untruncated wrapped box (desktop & mobile 390×844, `scrollWidth ≤ clientWidth`) ✓; settle byte-identical ("Synced HH:MM" + `3 added`, box hidden, aria restored) ✓; `#sync-result` sole live region (box `aria-hidden`), mid-run reload re-attaches ✓; WCAG (AA contrast both themes via the pinned ink-soft/surface pair, focus-visible, reduced-motion kill intact at styles.css L2201) ✓; cancel/409/error/upload/status API unchanged (green suites above) ✓.
- Documentation: README.md reviewed in full (sync-states paragraph now matches the shipped UI; env-var table matches `app/config.py` and covers all `.env.example` vars; pages/commands verified) and AGENTS.md reviewed in full — **verified consistent, no changes needed**.
- Notable: no defects found in this or prior work; no live infrastructure touched.
- Next pending phase: none — `todo/` will be empty once the harness moves 149 (only 149 was pending).
This commit is contained in:
@@ -727,16 +727,17 @@ def test_sync_button_names_the_summary_phase(
|
||||
page: Page, app_url: str, syncsum_dir: Path, db_ready: None
|
||||
) -> None:
|
||||
"""A fresh admin page on ``/sources.html`` clicks **Sync sources**;
|
||||
the page's OWN 2 s poll renders ``#sync-label`` as
|
||||
``Summarizing folders… [source/folder]`` + the pinned ``(n/3)``
|
||||
count span (phase 128: the label is PARTS — the folder part is
|
||||
optional — the phase's first poll may precede the first hook — and
|
||||
the button ``title`` carries the full untruncated string, main +
|
||||
" " + count, A4); on settle the button shows the terminal
|
||||
``Synced HH:MM`` (the phase-32 contract) and ``#sync-result``
|
||||
carries the counts line. The prelude/import labels stay the
|
||||
phase-64 shape (the endpoint test pins those phases; the UI pins
|
||||
the new summaries label)."""
|
||||
the page's OWN 2 s poll renders the summaries label —
|
||||
``Summarizing folders… [source/folder] (n/3)`` (the folder part is
|
||||
optional — the phase's first poll may precede the first hook) — in
|
||||
the detail box (phase 149: the running label rides #sync-progress
|
||||
under the button row; the button is the spinner) with the button
|
||||
``title`` carrying the same full untruncated string (A4 — one
|
||||
source, two surfaces); on settle the box hides, the button shows
|
||||
the terminal ``Synced HH:MM`` (the phase-32 contract) and
|
||||
``#sync-result`` carries the counts line. The prelude/import labels
|
||||
stay the phase-64 shape (the endpoint test pins those phases; the
|
||||
UI pins the new summaries label)."""
|
||||
page.set_default_timeout(30_000)
|
||||
_register_source(app_url, str(syncsum_dir))
|
||||
login(page, app_url) # lands on /sources.html (the button's home)
|
||||
@@ -759,38 +760,35 @@ def test_sync_button_names_the_summary_phase(
|
||||
|
||||
# The click's immediate state (the 202 moment — the bare prefix,
|
||||
# the phase-64 contract): disabled, aria-busy, spinning icon, no
|
||||
# error…
|
||||
# error, and the detail box revealed with the bare label
|
||||
# (phase 149 — the running label rides the box)…
|
||||
expect(btn).to_be_disabled()
|
||||
expect(btn).to_have_attribute("aria-busy", "true")
|
||||
expect(btn.locator(".sync-icon")).to_have_class(re.compile(r"\bis-spinning\b"))
|
||||
expect(page.locator("#sync-label")).to_have_text("Syncing…")
|
||||
expect(page.locator("#sync-progress")).to_have_text("Syncing…")
|
||||
expect(page.locator("#sync-error-banner")).to_be_hidden()
|
||||
|
||||
# UI layer (D2): the page's own 2 s poll names the summaries phase
|
||||
# — the RENDERED label matches the D2 PARTS shape (phase 128:
|
||||
# the MAIN part carries "Summarizing folders… [folder]" — the
|
||||
# folder part is optional, the phase's first poll may precede the
|
||||
# first hook — and the pinned COUNT part carries the (n/3)
|
||||
# counts), and the button ``title`` carries the full untruncated
|
||||
# string (main + " " + count, A4). ``wait_for_function`` checks
|
||||
# BOTH in ONE in-browser evaluation: a poll tick updates the parts
|
||||
# + the title together in a single JS task, so the atomic check
|
||||
# can never straddle one (the JS regexes are the D2 shapes,
|
||||
# unit-pinned in tests/unit/test_frontend_sync_upload.py).
|
||||
# — the RENDERED label in the detail box (phase 149: #sync-progress
|
||||
# — the button is the spinner) matches the D2 shape —
|
||||
# "Summarizing folders… [folder] (n/3)" (the folder part is
|
||||
# optional, the phase's first poll may precede the first hook — the
|
||||
# (n/3) counts always present) — and the button ``title`` carries
|
||||
# the same full untruncated string (A4 — one source, two surfaces).
|
||||
# ``wait_for_function`` checks BOTH in ONE in-browser evaluation:
|
||||
# a poll tick updates the box + the title together in a single JS
|
||||
# task (the single setSyncLabel writer), so the atomic check can
|
||||
# never straddle one (the D2 shapes are unit-pinned in
|
||||
# tests/unit/test_frontend_sync_upload.py).
|
||||
page.wait_for_function(
|
||||
"""() => {
|
||||
const label = document.querySelector('#sync-label');
|
||||
const box = document.querySelector('#sync-progress');
|
||||
const textEl = document.querySelector('#sync-progress-text');
|
||||
const btn = document.querySelector('#sync-btn');
|
||||
if (!label || !btn) return false;
|
||||
const main = label.querySelector('.sync-label-main');
|
||||
const count = label.querySelector('.sync-label-count');
|
||||
if (!main || !count || count.hidden) return false;
|
||||
const reMain = /^Summarizing folders…(?: \\S+)?$/;
|
||||
const reCount = /^\\(\\d+\\/3\\)$/;
|
||||
return reMain.test(main.textContent)
|
||||
&& reCount.test(count.textContent)
|
||||
&& btn.getAttribute('title')
|
||||
=== main.textContent + ' ' + count.textContent;
|
||||
if (!box || !textEl || !btn || box.hidden) return false;
|
||||
const t = textEl.textContent;
|
||||
const reFull = /^Summarizing folders…(?: \\S+)? \\(\\d+\\/3\\)$/;
|
||||
return reFull.test(t) && btn.getAttribute('title') === t;
|
||||
}""",
|
||||
timeout=SETTLE_TIMEOUT_MS,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user