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:
@@ -1,35 +0,0 @@
|
||||
# Phase 149 — sync detail lives in a text box under the button; the button is just the spinner
|
||||
|
||||
**Source:** `TODO.md` L6 — "The "Sync Sources" button now has too much detail and you can't see all of it - it gets cut off. I get that this was the solution to show information on mobile, but can't we just show that information in a nice text box under the button instead to give us more breathing room? Then the button can just be used as a loading spinner."
|
||||
**Story:** n/a (TODO-derived phase, Protocol B append).
|
||||
**Context:** the Sources page (`frontend/index.html`, the `view-rag` section): `#sync-btn` (L465 — the pill: `.sync-icon` + `.sync-label` with the three PARTS — `.sync-label-main` (the shrinks-to-fit prefix + file/folder, the part CSS ellipsizes), `.sync-label-count` (the pinned "(n/m)"), `.sync-label-chunk` (the phase-129 pinned "· chunk n/m")) + `#sync-cancel-btn` (L477, the phase-134 Cancel control) in the `.page-head-row`; `#sync-result` (L500 — the `role="status"` aria-live announcer: the LIVE untruncated label while a sync runs, the settle counts after); `#sync-error-banner` (L502). The single label writer: `frontend/assets/sources.js` `setSyncLabel(mainText, countText, chunkText)` (~L759 — writes the parts, then the UNTRUNCATED full label rides the button `title` + `#sync-result` — the A4 announcer contract; EVERY label state goes through it: the running builders `fmtSyncLabel`/`fmtSyncPhaseLabel` (L~810–850: "Syncing… <file> (n/m) · chunk n/m", "Writing KB overview…", "Summarizing folders… <folder> (n/m)"), the idle/settle labels, the 202/409 click, the re-attach). The running state: `sources.js` ~L909 adds `.is-spinning` to the icon (the CSS spin at 1s — `styles.css` L2200, with the `prefers-reduced-motion: none` kill at L2202) and ~L928 removes it on settle; the label's ellipsis lives in `styles.css` (L2216–2239 — `.sync-label-main` min-width 0 + ellipsis; the mobile media query at L5516+ makes the button full-width and the label fill the pill). The owner's complaint: the FULL live label (a long `source/relative/path` + counts + chunk) is clipped in the pill even on desktop — the mobile solution (stuffing the parts in the button) is now the permanent surface.
|
||||
**Locked (owner delegation 2026-09-30, chat — "pick the best choices"):** while a sync/upload run is in flight, the button shows ONLY the spinning icon (the label parts hidden — the button IS the loading spinner, exactly as the owner asked; its `aria-label` carries the full live label for AT), and a NEW bordered text box directly under the button row (`#sync-progress`) shows the SAME full untruncated live label — wrapping, mono, breathing room — hidden at idle/settle. `#sync-result` REMAINS the sole aria-live announcer (the box is presentational, `aria-hidden` — one live region, no double announce); the settle counts line still lands on `#sync-result` exactly as today, and the idle "Sync sources" label returns to the button when the run ends. The box appears for BOTH run kinds (sync + the phase-90 upload "Importing…" run — the shared writer covers both) and re-attaches (a page load mid-run reveals it, the never-stale contract).
|
||||
|
||||
## Objective
|
||||
The live sync detail (file, position, chunk, phase labels) is fully visible in a calm text box under the button at every viewport, the button reduces to a clean spinner while running, and the accessibility contract (one live region, the button's accessible name, focus-visible, contrast) is preserved.
|
||||
|
||||
## Dependencies
|
||||
- `148_pdf_pages_in_sources` (todo) — pipeline order only; no shared code (this phase touches the Sources-page sync UI in `index.html`/`sources.js`/`styles.css`; 148 touches the doc viewer + the importer/docs API).
|
||||
|
||||
## Design (shared by all tasks — the executor reads this, not the chat)
|
||||
- **The box (task 01):** `#sync-progress` — a `<div class="sync-progress" hidden aria-hidden="true">` with an inner `<span class="sync-progress-text" id="sync-progress-text"></span>`, placed in `index.html` BETWEEN the `.page-head` block and `#sync-result` (directly under the button row — the owner's "under the button"). `sources.js`: element constants beside the existing ones (~L707–714); `setSyncLabel` (the SINGLE writer — the invariant: every label state, running or not, flows through here) writes the full label into the box's text AND reveals the box for the RUNNING label states (the running builders' outputs + the re-attach running entry) and HIDES it for every non-running state (idle "Sync sources", the settle "Synced HH:MM" + counts, the 202/409 clicks, the error state) — the reveal/hide decision lives at the `setSyncLabel` callsites via a small `running` flag parameter (or a dedicated `setSyncProgress(full, running)` called from the same sites — the executor picks the minimal clean shape; the CONTRACT is: the box is visible iff a run is in flight, and its text is exactly the full label `setSyncLabel` computes). The button: `#sync-btn` gains `is-running` class while in flight (added where `.is-spinning` is added, ~L909; removed where it is removed, ~L928 — including the error settle and the 409 path); its `aria-label` is set to the full live label while running (the icon-only button needs an accessible name — the `title` already carries it for hover, the aria-label for AT) and restored to `"Sync sources"` on every settle. The Cancel button is untouched (it already rides the running state).
|
||||
- **The styling (task 02):** `styles.css` — `.sync-progress`: the bordered panel (1px border in the theme border var, the theme surface/background var, the house radius, mono text in `var(--mono)` — the `.sync-label-count`/`.sync-result` mono language: a position reads as data, `overflow-wrap: anywhere` for the long `source/relative/path` (it must WRAP, never clip — the whole point), a comfortable min-height (~2 lines' breathing room, no jump when empty→full because it is hidden when idle), margin below the button row, inside the existing 72rem centered container (the page already rides it — no new width logic); `.sync-btn.is-running .sync-label { display: none; }` (the pill shrinks to an icon-only control — the width adjusts naturally; the `.sync-btn:disabled`/cursor behavior while running is preserved as today); the mobile media query (L5516+): the button stays full-width, the box full-width, the label-hidden rule identical (no mobile special-case — the box REPLACES the old mobile-stuffing reason, which is exactly what the owner asked); `prefers-reduced-motion` untouched (the box is static text; the spinner kill already exists); contrast AA in both themes (the ink vars — the muted `.sync-result` language is ≥4.5:1 already; the box reuses it); focus-visible on the button + Cancel unchanged.
|
||||
- **The E2E (task 03):** the `slow_llm` proxy idiom (the `test_sync_chunk_progress.py` recipe — a module app with `BOR_LLM_BASE_URL` = the `tests/e2e/slow_llm.py` delay proxy in front of the mock, ~1.5s per LLM call, the fixture repo with a few docs so the import phase has many polls of observable running state) — observe the in-flight state deterministically.
|
||||
|
||||
## Tasks
|
||||
1. `01_progress_box_wiring.md` — the box markup + the `setSyncLabel` wiring (reveal/hide contract) + the button's `is-running` + aria-label.
|
||||
2. `02_progress_box_styles.md` — the panel + the icon-only running button + mobile + the AA/motion gates.
|
||||
3. `03_e2e_progress_box.md` — the dedicated E2E: in-flight (spinner-only button + full untruncated box), settled (box hidden, counts line, label restored).
|
||||
|
||||
## Testing & Quality
|
||||
- E2E: one dedicated Playwright file, run in isolation, mock-only (slow proxy).
|
||||
- Frontend pins: the pre-existing sync E2Es (label parts, chunk progress, mobile label, cancel, summary visibility, upload progress) pass UNCHANGED in their settled/idle assertions; their RUNNING-state assertions that pinned the button label text are updated to the box (the executor finds them via the `sync-label` selectors and migrates the expectation to `#sync-progress` — the A4 announcer (`#sync-result`) is asserted unchanged).
|
||||
- Coverage: n/a (frontend phase — `app/` coverage stays >90%).
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] while a sync (or upload) run is in flight: the button shows only the spinning icon (no visible label text, at any viewport), its `aria-label` carries the full live label, and `#sync-progress` is visible with the EXACT full untruncated label (a long `source/relative/path` wraps — never clipped)
|
||||
- [ ] at settle/idle: the box is hidden, the button shows "Sync sources" (the settle "Synced HH:MM" + counts line on `#sync-result` is byte-identical to pre-phase), and the label parts are restored
|
||||
- [ ] `#sync-result` remains the sole aria-live region (the box is `aria-hidden`); a page load mid-run re-attaches with the box visible (the never-stale contract)
|
||||
- [ ] WCAG: focus-visible on the button + Cancel, contrast ≥4.5:1 both themes, `prefers-reduced-motion` respected (the spinner kill intact)
|
||||
- [ ] `uv run pytest` green (all pre-existing E2Es — running-state label pins migrated to the box, settled-state pins untouched); `uv run pytest tests/e2e/test_sync_progress_box.py -v --no-cov` green in isolation; `uv run ruff check . && uv run pyright` clean
|
||||
- [ ] no behavior change in completed phases (the cancel control, the 409 double-trigger, the error banner/modal, the upload run's status, the sync status API)
|
||||
@@ -1,41 +0,0 @@
|
||||
# Task 01 — the `#sync-progress` box: markup + the single-writer wiring + the icon-only running button
|
||||
|
||||
**Phase:** `149_sync_progress_box` · **Source:** `TODO.md:6` — "The "Sync Sources" button now has too much detail and you can't see all of it - it gets cut off. … can't we just show that information in a nice text box under the button instead to give us more breathing room? Then the button can just be used as a loading spinner."
|
||||
**Story:** n/a
|
||||
|
||||
## Objective
|
||||
The full live sync label is written — by the same single writer that owns the button label — into a text box directly under the button, visible only while a run is in flight; the button reduces to the spinning icon while running (label parts hidden, `aria-label` carrying the full label) and restores its idle label on every settle.
|
||||
|
||||
## Work
|
||||
1. `frontend/index.html` (the `view-rag` section, L458–500): insert BETWEEN the closing `</div>` of `.page-head` and the `#sync-result` span:
|
||||
```html
|
||||
<!-- Phase 149 (task 01): the sync detail box — the full UNTRUNCATED live
|
||||
label ("Syncing… <source/rel/path> (n/m) · chunk n/m") in a bordered
|
||||
panel under the button row, visible ONLY while a sync/upload run is
|
||||
in flight (sources.js reveals/hides it through the single setSyncLabel
|
||||
writer). Presentational (aria-hidden) — #sync-result stays the SOLE
|
||||
aria-live announcer (one live region, no double announce). The button
|
||||
itself is the spinner while running (task 02's .is-running CSS hides
|
||||
the label parts). -->
|
||||
<div class="sync-progress" id="sync-progress" hidden aria-hidden="true">
|
||||
<span class="sync-progress-text" id="sync-progress-text"></span>
|
||||
</div>
|
||||
```
|
||||
(the comment style matches the file's pinned HTML comments; the box is ship-hidden — the anonymous-safe/idle state).
|
||||
2. `frontend/assets/sources.js`:
|
||||
- element constants beside the existing block (~L707–714): `const syncProgress = root.querySelector("#sync-progress");` + `const syncProgressText = syncProgress ? syncProgress.querySelector(".sync-progress-text") : null;` (the null-guarded query pattern of the surrounding constants — the elements are only present on the Sources view);
|
||||
- `setSyncLabel(mainText, countText = "", chunkText = "")` (~L759 — the SINGLE point that writes `#sync-label`): gain a fourth parameter `running = false`. It now ALSO: when `syncProgress` — `syncProgressText.textContent = full` (the SAME untruncated string that rides the title + `#sync-result`; textContent only — the house rule, no HTML) and `syncProgress.hidden = !running` (reveal for a run-in-flight label, hide for everything else). The contract: the box is visible IFF a run is in flight, and its text is EXACTLY the full label;
|
||||
- update EVERY `setSyncLabel` callsite with the correct `running` flag (the executor enumerates them — they are all in this file): the running builders' outputs (the poll tick's `fmtSyncPhaseLabel`/`fmtSyncLabel` application, the re-attach running entry in `initSyncButton`) → `running=true`; the idle label ("Sync sources"), the settle labels ("Synced HH:MM" + counts), the 202-accepted click label, the 409 double-trigger label, the upload's running labels (the phase-90 "Importing…" run — the box serves it too) → `running=true` while in flight / `false` when that run settles; the error-state label → `running=false`;
|
||||
- the running-state toggle sites (~L909 the `is-spinning` add / ~L928 the remove — plus every settle path that removes the spin, including the 409 and error settles): add `syncBtn.classList.add("is-running")` / `remove("is-running")` at the SAME sites (the class and the spin travel together — one in-flight state);
|
||||
- the button's accessible name: while running, set `syncBtn.setAttribute("aria-label", full)` (the full live label — the icon-only button keeps an accessible name; do it in `setSyncLabel` while `running` — the writer already computes `full`); on every non-running `setSyncLabel` call, restore `syncBtn.setAttribute("aria-label", "Sync sources")` (the ship value in the HTML — the static attribute is the idle truth, the JS never removes the attribute, it only overwrites/restores it).
|
||||
3. `#sync-result`, `#sync-error-banner`, the Cancel button, and the sync modal are UNTOUCHED (the A4 announcer contract: `#sync-result` hears every label exactly as before — the box is a visual duplicate for sighted users).
|
||||
|
||||
## Testing & Quality
|
||||
- The behavioral pins come from task 03's E2E; this task's own gate: the pre-existing sync E2Es' SETTLED-state assertions (idle label, the "Synced HH:MM" + counts line, the 409, the error banner, the cancel lifecycle) pass UNCHANGED — and their running-state assertions that read `#sync-label` text while in flight are migrated to read `#sync-progress` (the executor runs each of the six pre-existing sync E2E files and migrates exactly the running-state selectors — the settled pins must not be touched).
|
||||
- Coverage: n/a (frontend task).
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `#sync-progress` sits under the button row, ship-hidden, `aria-hidden`, with the full untruncated live label while any run (sync or upload) is in flight — and hidden in every non-running state (idle, settled, 202/409, error)
|
||||
- [ ] `#sync-btn` carries `is-running` exactly while in flight; its `aria-label` is the full live label while running and "Sync sources" otherwise
|
||||
- [ ] `#sync-result` (the sole live region) and every settled-state rendering are byte-identical to pre-phase
|
||||
- [ ] `uv run pytest` green with the migrated running-state pins; `uv run ruff check . && uv run pyright` clean
|
||||
@@ -1,28 +0,0 @@
|
||||
# Task 02 — the box styling: the panel, the icon-only running button, mobile, the AA/motion gates
|
||||
|
||||
**Phase:** `149_sync_progress_box` · **Source:** `TODO.md:6` — "… can't we just show that information in a nice text box under the button instead to give us more breathing room? Then the button can just be used as a loading spinner."
|
||||
**Story:** n/a
|
||||
|
||||
## Objective
|
||||
The box reads as a calm, bordered, mono detail panel with breathing room (long paths WRAP — never clip), and the running button is a clean icon-only spinner control at every viewport — with the WCAG 2.1 AA basics intact in both themes.
|
||||
|
||||
## Work
|
||||
1. `frontend/assets/styles.css` — beside the existing sync-button block (L2138–2240 region):
|
||||
- `.sync-progress { margin-top: <house spacing>; border: 1px solid <theme border var>; background: <theme surface var>; border-radius: <house radius>; padding: <comfortable — the "breathing room">; min-height: <~2 lines>; }` — the panel language of the house's existing bordered surfaces (mirror the nearest precedent — e.g. the `.kb-banner`/token-card border treatment — so it belongs to the theme);
|
||||
- `.sync-progress-text { font-family: var(--mono); font-size: <the .sync-result size — the mono data language>; color: <the .sync-result ink — ≥4.5:1 in BOTH themes, the house muted-ink var>; overflow-wrap: anywhere; display: block; line-height: <house>; }` — `overflow-wrap: anywhere` is the point: a long `source/relative/path` wraps inside the panel instead of clipping (the pre-phase ellipsis is retired for the live detail);
|
||||
- **the running button:** `.sync-btn.is-running .sync-label { display: none; }` (the pill shrinks to the icon — the spinner is the whole button while in flight; the pill's padding/border stay, so the control does not jump to zero size — check the `.sync-btn` box model at L2138 and keep a sane icon-only footprint, e.g. the icon centered with the pill's padding — the executor verifies visually via the E2E screenshots); the button keeps its `:disabled` styling/cursor while running (the pre-phase double-trigger posture — whatever disabled state it has in flight today is preserved, only the label is hidden);
|
||||
- **focus-visible + hover** on the running (icon-only) button are unchanged (the pre-phase rules apply to the button element — hiding the label span touches neither);
|
||||
- **mobile media query** (L5516+ region, the `.sync-btn { width: 100% }` block): the box inherits full width naturally (it is a block in the same container — assert no width override is needed); the `.is-running` label-hidden rule is viewport-agnostic (defined once, applies to both) — DELETE nothing from the mobile label rules unless they become dead (the `.sync-label` parts are still rendered at idle — the mobile ellipsis rules for the IDLE label stay; if a rule references only the running state, the executor notes it in the commit, no deletion of idle rules);
|
||||
- **`prefers-reduced-motion`:** untouched (the box is static; the spinner's existing `animation: none` kill at L2202 stands);
|
||||
- **both themes:** every color via the CSS vars (the dark/light theme system — the phase-91/92/93 theme variables; a hardcoded hex would break the semantic theme completion pins).
|
||||
2. Verify (via the task-03 E2E, not here): the panel's contrast in both themes (the E2E can assert the computed color contrast is ≥4.5:1 against the panel background — the house contrast-check idiom if one exists in the theme E2Es; else a visual pin + the var-based construction argument in the commit).
|
||||
|
||||
## Testing & Quality
|
||||
- The visual/behavioral pins live in task 03's E2E (running: icon-only button + the full wrapped label in the panel, desktop AND mobile viewports; settled: idle pill restored). The pre-existing sync E2Es' settled assertions must pass UNCHANGED.
|
||||
- Coverage: n/a (frontend task).
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] the box is a bordered mono panel with the full label WRAPPING (no ellipsis, no horizontal clip) at desktop and mobile widths
|
||||
- [ ] the running button shows only the spinner (label span `display: none` via `.is-running`), with an icon-only footprint that does not collapse, and restored "Sync sources" pill at idle
|
||||
- [ ] focus-visible, hover, disabled, and `prefers-reduced-motion` behaviors unchanged; contrast ≥4.5:1 both themes (var-based)
|
||||
- [ ] `uv run pytest` green (all sync E2Es); `uv run ruff check . && uv run pyright` clean
|
||||
@@ -1,39 +0,0 @@
|
||||
# Task 03 — the dedicated E2E: spinner-only button + the full untruncated box while running, the restored pill at settle
|
||||
|
||||
**Phase:** `149_sync_progress_box` · **Source:** `TODO.md:6` — "The "Sync Sources" button now has too much detail and you can't see all of it - it gets cut off. … can't we just show that information in a nice text box under the button instead to give us more breathing room? Then the button can just be used as a loading spinner."
|
||||
**Story:** n/a
|
||||
|
||||
## Objective
|
||||
End-to-end proof through the real sync path (real `git clone` of a local `file://` fixture repo, the slowed mock LLM so the in-flight state is observable): while running, the button is the spinner and the box carries the FULL untruncated live detail (a long path included); on settle, the box is gone and the pre-phase settle rendering stands.
|
||||
|
||||
## Work
|
||||
1. `tests/e2e/test_sync_progress_box.py` — the dedicated suite (one Playwright file, run in isolation, mock-only; the `test_sync_chunk_progress.py` module-app recipe — the module app with `BOR_GIT_SOURCES=file://<fixture repo>` + its own `BOR_SOURCES_DIR` + its own port (env-overridable house constant) + `BOR_LLM_BASE_URL` = the `tests/e2e/slow_llm.py` delay proxy in front of the mock (`SLOW_DELAY_S = "1.5"` — the proxy delays EVERY LLM call, so the import phase yields many 2s-poll ticks of observable running state)):
|
||||
- **Fixture:** the house fixture-repo builder with a doc whose RELATIVE PATH IS LONG (e.g. `notes/a-rather-deeply-nested-folder/structure/for/the/sync/fixture-document.md` — the clipping trigger the owner hit: a `source/relative/path` that overflows the pill at desktop width) + one or two short-path docs (the sync has several files → the counts/chunk parts are exercised too).
|
||||
- **Test 1 — `test_running_button_is_spinner_only`:** admin login; click `#sync-btn`; poll (the house `expect(...).to_be_visible` with a generous timeout — the slow proxy guarantees a long in-flight window) until `#sync-progress` is visible (a run is in flight) → assert:
|
||||
- `#sync-btn .is-running` present + `#sync-btn .sync-icon.is-spinning` present (the spinner — the loading state);
|
||||
- `#sync-label` is NOT visible (`locator("#sync-label").is_visible() == false` — the label parts are hidden: the button is the icon);
|
||||
- `#sync-btn`'s `aria-label` attribute equals the current full live label (read `#sync-progress-text`'s text — they are written by the same call; assert equality, never staleness);
|
||||
- the Cancel button is visible (the phase-134 contract unchanged).
|
||||
- **Test 2 — `test_box_carries_the_full_untruncated_label`:** during the in-flight window of the LONG-path file (poll until `#sync-progress-text` contains the long fixture path — the slow proxy makes this deterministic):
|
||||
- `#sync-progress-text`'s textContent contains the FULL label shape `Syncing… <full long path> (<d>/<t>)` — the path string in FULL (assert the complete relative path is a substring — the pre-phase ellipsis would have truncated it; the wrap means nothing is dropped);
|
||||
- the box is NOT clipped: `syncProgress.scrollWidth <= syncProgress.clientWidth` (no horizontal overflow — the wrap engaged) AND `syncProgressText.scrollWidth <= syncProgressText.clientWidth` (the text itself did not overflow its container);
|
||||
- `#sync-result` (the announcer) carries the SAME full string (the A4 contract — one source, two surfaces);
|
||||
- at some tick the chunk part is present (`· chunk <n>/<m>` — the phase-129 part, now in the box) — assert with a retry loop over the slow embed window (the chunk part is only present while an embed batch is in flight — the slow proxy's 1.5s delay makes the window wide).
|
||||
- **Test 3 — `test_mobile_box_full_width` (the viewport half of the owner's ask):** a fresh context at the mobile viewport (the house mobile-width constant the `test_sync_label_mobile.py` uses) → same in-flight assertions: the box is full-width (its bounding box width ≈ the container width minus margins — no horizontal clipping, `scrollWidth <= clientWidth`), the button is icon-only, the label wraps (the old mobile-stuffing ellipsis is gone — assert the full path substring again).
|
||||
- **Test 4 — `test_settled_state_restores_the_pill`:** let the run settle (the `Synced HH:MM` pattern on `#sync-result` — the house settle idiom):
|
||||
- `#sync-progress` is HIDDEN (`is_visible() == false`) — the box is run-scoped only;
|
||||
- `#sync-btn` has NO `is-running`, the icon has NO `is-spinning`, and `#sync-label` is VISIBLE with the settle label ("Synced HH:MM" — the pre-phase byte-identical settle copy); the `aria-label` is back to "Sync sources";
|
||||
- the counts line on `#sync-result` matches the pre-phase shape (`N added` etc. — the `fmtSyncResult` pin).
|
||||
- **Test 5 — `test_reattach_shows_the_box`:** trigger a sync, wait until in-flight (the box visible), then RELOAD the page (the admin session persists) → the load-time re-attach (`initSyncButton`) finds the RUNNING sync → the box is visible with the current label and the button is icon-only again (the never-stale contract — a page load mid-run shows the truth, not an idle pill).
|
||||
2. Teardown per the house module-app fixture (app stopped, tmp dirs disposable); hermetic (own port + dirs) so `--no-cov` isolation holds.
|
||||
|
||||
## Testing & Quality
|
||||
- E2E: the five tests above, mock-only (slow proxy), real git + real import; the load-bearing assertions are the FULL-path substring (no clipping) + the wrap proof (`scrollWidth <= clientWidth`) + the settle byte-identity.
|
||||
- Coverage: n/a (E2E task).
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] in-flight: the button is the spinner only (label hidden, `is-running`, `aria-label` == the full label), the Cancel button rides along, and the box shows the EXACT full untruncated label — long path included, wrapped, `scrollWidth <= clientWidth`, desktop AND mobile
|
||||
- [ ] the announcer (`#sync-result`) carries the same full string; the chunk part appears in the box during the embed window
|
||||
- [ ] settled: the box hidden, the "Synced HH:MM" pill restored byte-identical, `aria-label` back to "Sync sources", the counts line unchanged
|
||||
- [ ] a page load mid-run re-attaches with the box visible (never stale)
|
||||
- [ ] `uv run pytest tests/e2e/test_sync_progress_box.py -v --no-cov` green in isolation (DB up, git on PATH — the documented prerequisite); `uv run pytest` still green (the six pre-existing sync E2Es — migrated running-state pins + untouched settled pins)
|
||||
Reference in New Issue
Block a user