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:
@@ -92,6 +92,23 @@ contract: shown + enabled in EVERY sync-running state (the entry
|
||||
shows it for kind "sync"), hidden + disabled for the upload run
|
||||
(the upload job is NOT cancellable in this phase) and after the
|
||||
settle (settleSyncButton always hides it).
|
||||
|
||||
Phase 149 (task 01) adds the `running` flag to the single writer:
|
||||
setSyncLabel(mainText, countText = "", chunkText = "", running =
|
||||
false) also writes the SAME untruncated full label into the detail
|
||||
box (#sync-progress's .sync-progress-text — textContent only) and
|
||||
reveals the box IFF running (the running entry passes true, the
|
||||
settle passes the default false), and owns the button's accessible
|
||||
name (full live label while running, the ship value "Sync sources"
|
||||
restored on every non-running call). The running entry passes
|
||||
`running = true` + adds the `is-running` class (with the spin), and
|
||||
no longer sets aria-label (the writer owns it); the settle removes
|
||||
`is-running` (with the spin). The node harness rows gain the box's
|
||||
text/hidden pair, the button's `is-running` + the icon's
|
||||
`is-spinning` flags, and the button's aria-label — the behavioral
|
||||
contract: box visible + full label + is-running + spinning + the
|
||||
aria-label equal to the full label in EVERY running state, box
|
||||
hidden + the idle flags + the restored aria-label after the settle.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -256,22 +273,33 @@ def test_fmt_sync_label_returns_the_parts_contract() -> None:
|
||||
|
||||
|
||||
def test_set_sync_label_is_the_single_parts_writer() -> None:
|
||||
"""setSyncLabel(mainText, countText = "", chunkText = "") — Phase
|
||||
128 (task 01) + Phase 129 (task 04): the SINGLE point that writes
|
||||
#sync-label. The MAIN part gets mainText (textContent only — the
|
||||
house rule, no HTML strings); the COUNT part gets countText when
|
||||
present (textContent + hidden=false — the "(n/m)" position is
|
||||
visible, NEVER clipped) and is emptied + hidden=true when absent;
|
||||
the CHUNK part (phase 129) gets chunkText under the SAME contract
|
||||
(present → the pinned "· chunk n/m" is visible, absent → emptied
|
||||
+ hidden=true — the part renders only while a file's embed batch
|
||||
"""setSyncLabel(mainText, countText = "", chunkText = "",
|
||||
running = false) — Phase 128 (task 01) + Phase 129 (task 04):
|
||||
the SINGLE point that writes #sync-label. The MAIN part gets
|
||||
mainText (textContent only — the house rule, no HTML strings);
|
||||
the COUNT part gets countText when present (textContent +
|
||||
hidden=false — the "(n/m)" position is visible, NEVER clipped)
|
||||
and is emptied + hidden=true when absent; the CHUNK part
|
||||
(phase 129) gets chunkText under the SAME contract (present →
|
||||
the pinned "· chunk n/m" is visible, absent → emptied +
|
||||
hidden=true — the part renders only while a file's embed batch
|
||||
is in flight). Then the UNTRUNCATED full label — mainText +
|
||||
(countText ? " " + countText : "") + (chunkText ? " " + chunkText
|
||||
: "") — rides the button title + #sync-result (the aria-live
|
||||
announcer) (A4 — the screen reader hears the whole "Syncing…
|
||||
<file> (2/3) · chunk 14/38")."""
|
||||
<file> (2/3) · chunk 14/38"). Phase 149 (task 01): the fourth
|
||||
parameter `running` (the in-flight flag — the running entry
|
||||
passes true, the settle the default false): the SAME full label
|
||||
lands in the detail box (.sync-progress-text, textContent only)
|
||||
and the box (#sync-progress) is visible IFF running (hidden in
|
||||
every non-running state), and the button's accessible name is
|
||||
the full label while running / the ship value "Sync sources"
|
||||
otherwise (overwrites/restores only — never removes)."""
|
||||
body = _fn(_js(), "setSyncLabel")
|
||||
assert 'function setSyncLabel(mainText, countText = "", chunkText = "")' in body
|
||||
assert (
|
||||
'function setSyncLabel(mainText, countText = "", chunkText = "", running = false)'
|
||||
in body
|
||||
), "phase 149: the in-flight flag is the fourth parameter"
|
||||
# the parts: textContent only (the house rule), null-guarded refs
|
||||
assert "syncLabelMain.textContent = mainText" in body
|
||||
assert "syncLabelCount.textContent = countText" in body
|
||||
@@ -302,7 +330,13 @@ def test_set_sync_label_is_the_single_parts_writer() -> None:
|
||||
i_full = body.find("const full")
|
||||
i_title = body.find("syncBtn.title = full")
|
||||
i_result = body.find("syncResult.textContent = full")
|
||||
assert -1 < i_full < i_title < i_result, ("built once: full → title → announcer")
|
||||
i_box = body.find("syncProgressText.textContent = full")
|
||||
i_hide = body.find("syncProgress.hidden = !running")
|
||||
i_aria = body.find('syncBtn.setAttribute("aria-label", running ? full : "Sync sources")')
|
||||
assert -1 < i_full < i_title < i_result < i_box < i_hide < i_aria, (
|
||||
"built once: full → title → announcer → the detail box (revealed IFF"
|
||||
" running) → the button's accessible name (phase 149)"
|
||||
)
|
||||
assert "innerHTML" not in body, "the label path is textContent only"
|
||||
|
||||
|
||||
@@ -421,16 +455,25 @@ def test_enter_running_state_is_phase_aware_and_writes_the_parts() -> None:
|
||||
# the parts are handed to the single writer (A4 lives in it) —
|
||||
# phase 129: the chunk part destructures with a "" default (the
|
||||
# bare builders — the upload job, the 202/409 click — return no
|
||||
# chunk part, so the part stays hidden there).
|
||||
# chunk part, so the part stays hidden there); phase 149: the
|
||||
# call passes `running = true` (the detail box reveals + the
|
||||
# full-label aria-label ride in the writer).
|
||||
assert "const { main, count, chunk = \"\" } =" in body, (
|
||||
"the chunk part destructures with the '' default (bare builders return none)"
|
||||
)
|
||||
i_set = body.find("setSyncLabel(main, count, chunk)")
|
||||
assert -1 < i_fall < i_set, "the parts are written after the split"
|
||||
i_set = body.find("setSyncLabel(main, count, chunk, true)")
|
||||
assert -1 < i_fall < i_set, "the parts are written after the split (running = true)"
|
||||
assert body.count("setSyncLabel(") == 1, "one writer call per entry"
|
||||
assert "syncBtn.title =" not in body, ("the title write lives in setSyncLabel (A4)")
|
||||
assert "syncResult" not in body, ("the announcer write lives in setSyncLabel (A4)")
|
||||
assert 'syncBtn.setAttribute("aria-label"' not in body, (
|
||||
"the aria-label write lives in setSyncLabel (phase 149: the full"
|
||||
" live label while running)"
|
||||
)
|
||||
assert "syncLabel.textContent" not in body, ("no direct span write — the parts")
|
||||
assert "syncBtn.classList.add(\"is-running\")" in body, (
|
||||
"the icon-only running button class is added with the spin (phase 149)"
|
||||
)
|
||||
|
||||
|
||||
def test_settle_and_idle_labels_go_through_the_parts_writer() -> None:
|
||||
@@ -448,6 +491,12 @@ def test_settle_and_idle_labels_go_through_the_parts_writer() -> None:
|
||||
assert "function settleSyncButton(label)" in settle
|
||||
assert "setSyncLabel(label)" in settle, "the settle label goes through the parts writer"
|
||||
assert settle.count("setSyncLabel(") == 1
|
||||
assert "setSyncLabel(label, true)" not in settle, (
|
||||
"the settle is NEVER a running state — the detail box hides (phase 149)"
|
||||
)
|
||||
assert "syncBtn.classList.remove(\"is-running\")" in settle, (
|
||||
"the running class is removed with the spin on EVERY settle (phase 149)"
|
||||
)
|
||||
assert "syncLabel.textContent" not in settle, ("no direct span write — the parts")
|
||||
# the success settle keeps the "Synced HH:MM" copy + the counts line
|
||||
# AFTER the settle (the settle's announcer write is superseded by
|
||||
@@ -487,6 +536,39 @@ def test_sync_label_markup_ships_the_hidden_chunk_part() -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_sync_progress_box_markup_ships_under_the_button_row() -> None:
|
||||
"""Phase 149 (task 01): #sync-progress — the sync detail box under
|
||||
the button row (the shell's RAG view): between the .page-head block
|
||||
(the button row + the page sub) and #sync-result. Ships hidden +
|
||||
aria-hidden (the anonymous-safe/idle state — sources.js reveals it
|
||||
through the single setSyncLabel writer only while a run is in
|
||||
flight; presentational, so #sync-result stays the SOLE aria-live
|
||||
announcer — one live region, no double announce). It carries one
|
||||
.sync-progress-text span (id-pinned) — the writer's textContent
|
||||
target, the full untruncated live label."""
|
||||
html = _html()
|
||||
box = re.search(
|
||||
r'<div class="sync-progress" id="sync-progress" hidden aria-hidden="true">\s*'
|
||||
r'<span class="sync-progress-text" id="sync-progress-text"></span>\s*</div>',
|
||||
html,
|
||||
)
|
||||
assert box, "the shell must carry the #sync-progress detail box (phase 149)"
|
||||
# Under the button row, above the announcer: the RAG view's
|
||||
# .page-head block precedes the box, and the box precedes
|
||||
# #sync-result (one live region — the box is presentational).
|
||||
rag = html.find('id="view-rag"')
|
||||
head = html.find('class="page-head"', rag)
|
||||
result = html.find('id="sync-result"', rag)
|
||||
assert -1 < head < box.start() < result, (
|
||||
"the box sits between the RAG view's .page-head block and "
|
||||
"#sync-result (directly under the button row, above the announcer)"
|
||||
)
|
||||
# Ship-hidden + presentational (the reveal is run-scoped, in JS).
|
||||
tag = html[box.start() : html.find(">", box.start())]
|
||||
assert re.search(r"\bhidden\b", tag), "the box ships hidden (idle / anonymous-safe)"
|
||||
assert 'aria-hidden="true"' in tag, ("the box is presentational (no double announce)")
|
||||
|
||||
|
||||
def test_sync_label_chunk_css_is_the_pinned_part() -> None:
|
||||
""".sync-label-chunk (phase 129 task 04): the same pinned-part
|
||||
language as .sync-label-count — flex 0 0 auto (NEVER clipped),
|
||||
@@ -515,15 +597,33 @@ const syncLabelMain = { textContent: "" };
|
||||
const syncLabelCount = { textContent: "", hidden: true };
|
||||
const syncLabelChunk = { textContent: "", hidden: true };
|
||||
const syncResult = { textContent: "" };
|
||||
// Phase 149 (task 01): the detail box — presentational, ship-hidden;
|
||||
// the REAL writer mutates it (full label + the running reveal).
|
||||
const syncProgress = { hidden: true };
|
||||
const syncProgressText = { textContent: "" };
|
||||
// Phase 149 (task 01): class-tracking stubs — the is-running /
|
||||
// is-spinning travel-together contract is behavioral, not source.
|
||||
const syncBtn = {
|
||||
disabled: false,
|
||||
title: "",
|
||||
attrs: {},
|
||||
setAttribute(k, v) { this.attrs[k] = v; },
|
||||
removeAttribute(k) { delete this.attrs[k]; },
|
||||
classList: { add() {}, remove() {} },
|
||||
classes: new Set(),
|
||||
classList: {
|
||||
add(c) { syncBtn.classes.add(c); },
|
||||
remove(c) { syncBtn.classes.delete(c); },
|
||||
has(c) { return syncBtn.classes.has(c); },
|
||||
},
|
||||
};
|
||||
const syncIcon = {
|
||||
classes: new Set(),
|
||||
classList: {
|
||||
add(c) { syncIcon.classes.add(c); },
|
||||
remove(c) { syncIcon.classes.delete(c); },
|
||||
has(c) { return syncIcon.classes.has(c); },
|
||||
},
|
||||
};
|
||||
const syncIcon = { classList: { add() {}, remove() {} } };
|
||||
// Phase 134 (task 02): the Cancel control — ships hidden + disabled
|
||||
// (the markup's state); the extracted REAL show/hide helpers mutate
|
||||
// it (show also sets the aria-label — the attrs pocket records it).
|
||||
@@ -544,6 +644,11 @@ const show = (tag) => {
|
||||
syncLabelCount.textContent, syncLabelCount.hidden,
|
||||
syncLabelChunk.textContent, syncLabelChunk.hidden,
|
||||
syncCancelBtn.hidden, syncCancelBtn.disabled,
|
||||
// Phase 149 (task 01): the detail box (text + hidden), the
|
||||
// is-running / is-spinning flags, the button's accessible name.
|
||||
syncProgressText.textContent, syncProgress.hidden,
|
||||
syncBtn.classes.has('is-running'), syncIcon.classes.has('is-spinning'),
|
||||
syncBtn.attrs['aria-label'] || '',
|
||||
]));
|
||||
};
|
||||
// 1. import, batch in flight (the task's fixture values, 14/38):
|
||||
@@ -596,6 +701,34 @@ def _run_node(script: str) -> str:
|
||||
return proc.stdout
|
||||
|
||||
|
||||
def _chunk_harness_rows() -> list:
|
||||
"""The node harness's rows — the REAL extracted label path against
|
||||
stub element refs (the test-document-viewer pattern), one JSON row
|
||||
per state: [tag, title, announcer, main, count, countHidden,
|
||||
chunk, chunkHidden, cancelHidden, cancelDisabled, progressText,
|
||||
progressHidden, btnRunning, iconSpinning, ariaLabel]. Shared by
|
||||
the chunk-part test (phase 129) and the detail-box test
|
||||
(phase 149)."""
|
||||
js = _js()
|
||||
fns = "\n".join(
|
||||
_fn(js, name)
|
||||
for name in (
|
||||
"fmtSyncLabel",
|
||||
"fmtSyncPhaseLabel",
|
||||
"setSyncLabel",
|
||||
# Phase 134 (task 02): the running entry's REAL cancel
|
||||
# show/hide + the REAL settle (step 8) — extracted against
|
||||
# the syncCancelBtn stub.
|
||||
"showSyncCancelBtn",
|
||||
"hideSyncCancelBtn",
|
||||
"enterSyncRunningState",
|
||||
"settleSyncButton",
|
||||
)
|
||||
)
|
||||
script = _CHUNK_NODE_HEAD + fns + _CHUNK_NODE_TAIL
|
||||
return [json.loads(line) for line in _run_node(script).splitlines() if line]
|
||||
|
||||
|
||||
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
|
||||
def test_chunk_part_renders_only_in_the_import_phase_under_node() -> None:
|
||||
"""Behavioral check (node) of the REAL label path — the extracted
|
||||
@@ -620,27 +753,11 @@ test-document-viewer pattern). The pinned "· chunk n/m" part (the
|
||||
for the upload run (the upload job is NOT cancellable in this
|
||||
phase) and after the (real) settle.
|
||||
"""
|
||||
js = _js()
|
||||
fns = "\n".join(
|
||||
_fn(js, name)
|
||||
for name in (
|
||||
"fmtSyncLabel",
|
||||
"fmtSyncPhaseLabel",
|
||||
"setSyncLabel",
|
||||
# Phase 134 (task 02): the running entry's REAL cancel
|
||||
# show/hide + the REAL settle (step 8) — extracted against
|
||||
# the syncCancelBtn stub.
|
||||
"showSyncCancelBtn",
|
||||
"hideSyncCancelBtn",
|
||||
"enterSyncRunningState",
|
||||
"settleSyncButton",
|
||||
)
|
||||
)
|
||||
script = _CHUNK_NODE_HEAD + fns + _CHUNK_NODE_TAIL
|
||||
rows = [json.loads(line) for line in _run_node(script).splitlines() if line]
|
||||
rows = _chunk_harness_rows()
|
||||
by_tag = {row[0]: row for row in rows}
|
||||
# row = [tag, title, announcer, main, count, countHidden, chunk,
|
||||
# chunkHidden, cancelHidden, cancelDisabled]
|
||||
# chunkHidden, cancelHidden, cancelDisabled, progressText,
|
||||
# progressHidden, btnRunning, iconSpinning, ariaLabel]
|
||||
|
||||
# 1. the in-flight import — the chunk part renders, the full
|
||||
# untruncated label rides the title + the announcer (A4).
|
||||
@@ -694,6 +811,79 @@ test-document-viewer pattern). The pinned "· chunk n/m" part (the
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
|
||||
def test_progress_box_and_running_button_contract_under_node() -> None:
|
||||
"""Phase 149 (task 01) — the detail box + the icon-only running
|
||||
button, behaviorally pinned through the SAME node harness (the
|
||||
REAL setSyncLabel / enterSyncRunningState / settleSyncButton
|
||||
against the stub element refs). The contract — the box is
|
||||
visible IFF a run is in flight, and its text is EXACTLY the full
|
||||
untruncated label (one source, two surfaces: the box for sighted
|
||||
users, the announcer for AT):
|
||||
|
||||
* EVERY running state (the in-flight import, the pre-batch
|
||||
import, the prelude, the 202/409 click, the upload run, the
|
||||
overview, the summaries): the box is REVEALED (hidden=false)
|
||||
and carries the full label (the same string as the button
|
||||
title / announcer — the writer's `full`), the button carries
|
||||
`is-running` + the icon `is-spinning` (the class travels with
|
||||
the spin), and the button's accessible name (aria-label) is
|
||||
the FULL live label (the icon-only running button keeps a
|
||||
name);
|
||||
* the settle (the REAL settleSyncButton through the writer with
|
||||
the default `running = false`): the box HIDES, the settle
|
||||
label is written into the box text ("Sync sources" — hidden,
|
||||
so inert), `is-running` + `is-spinning` are both gone, and
|
||||
the aria-label is restored to the ship value "Sync sources".
|
||||
"""
|
||||
rows = _chunk_harness_rows()
|
||||
by_tag = {row[0]: row for row in rows}
|
||||
# row = [tag, title, announcer, main, count, countHidden, chunk,
|
||||
# chunkHidden, cancelHidden, cancelDisabled, progressText,
|
||||
# progressHidden, btnRunning, iconSpinning, ariaLabel]
|
||||
|
||||
# Every running state: box visible + the EXACT full label +
|
||||
# is-running + spinning + the full-label accessible name.
|
||||
for tag in (
|
||||
"import-inflight",
|
||||
"import-prebatch",
|
||||
"prelude",
|
||||
"click",
|
||||
"upload",
|
||||
"overview",
|
||||
"summaries",
|
||||
):
|
||||
row = by_tag[tag]
|
||||
assert row[11] is False, (
|
||||
f"{tag}: the detail box must be VISIBLE while the run is in flight"
|
||||
)
|
||||
assert row[10] == row[1], (
|
||||
f"{tag}: the box carries the EXACT full label (title == box)"
|
||||
)
|
||||
assert row[12] is True, (
|
||||
f"{tag}: the button must carry is-running while in flight"
|
||||
)
|
||||
assert row[13] is True, (
|
||||
f"{tag}: is-running travels with the spin (the icon spins)"
|
||||
)
|
||||
assert row[14] == row[1], (
|
||||
f"{tag}: the running button's accessible name is the full live label"
|
||||
)
|
||||
|
||||
# The settle: the box hides, the running state is fully cleared,
|
||||
# and the accessible name is back to the ship value.
|
||||
row = by_tag["idle"]
|
||||
assert row[11] is True, "the settle must HIDE the detail box (run-scoped only)"
|
||||
assert row[10] == "Sync sources", (
|
||||
"the writer wrote the settle label into the box (hidden — inert)"
|
||||
)
|
||||
assert row[12] is False, "is-running is removed on the settle (with the spin)"
|
||||
assert row[13] is False, "the spin is removed on the settle"
|
||||
assert row[14] == "Sync sources", (
|
||||
"the settle restores the ship aria-label (the idle truth)"
|
||||
)
|
||||
|
||||
|
||||
def test_every_other_state_passes_no_chunk_text() -> None:
|
||||
"""Source-level pin of the render-only-while-chunks_total>0
|
||||
contract: the import fall-through is the ONLY place in sources.js
|
||||
@@ -701,7 +891,8 @@ def test_every_other_state_passes_no_chunk_text() -> None:
|
||||
branches and the bare builders never touch them), and every
|
||||
NON-running label emission passes no chunk text (the part hides):
|
||||
the settle/idle labels go through setSyncLabel(label) — the only
|
||||
three-arg call site is the running entry."""
|
||||
running-flagged call site (phase 149: `running = true`) is the
|
||||
running entry."""
|
||||
js = _js()
|
||||
body = _fn(js, "fmtSyncPhaseLabel")
|
||||
i_fall = body.find("fmtSyncLabel(")
|
||||
@@ -711,7 +902,9 @@ def test_every_other_state_passes_no_chunk_text() -> None:
|
||||
)
|
||||
fall = body[i_fall:]
|
||||
assert "status.chunks_total" in fall and "status.current_chunk" in fall
|
||||
assert js.count("setSyncLabel(main, count, chunk)") == 1
|
||||
assert js.count("setSyncLabel(main, count, chunk, true)") == 1, (
|
||||
"the ONLY running-flagged call site is the running entry (phase 149)"
|
||||
)
|
||||
assert "setSyncLabel(label)" in js, "the settle/idle labels stay two-arg (no chunk)"
|
||||
|
||||
|
||||
|
||||
@@ -49,6 +49,17 @@ flight (the import phase after the first batch — hidden in every
|
||||
other state); the markup pin below pins the exact three-part ship
|
||||
shape.
|
||||
|
||||
Phase 149 (task 01): the running button becomes the SPINNER — the
|
||||
writer gains the `running` flag (the running entry passes `true`,
|
||||
the settle the default `false`), which reveals the detail box
|
||||
(#sync-progress — the full untruncated label, presentational
|
||||
aria-hidden; #sync-result stays the sole announcer) and owns the
|
||||
button's accessible name (the full live label while running, the
|
||||
ship value "Sync sources" restored on every settle); the
|
||||
`is-running` class travels with the spin (added in the running
|
||||
entry, removed in the settle). The pins below move onto that shape
|
||||
(the aria-label write leaves the running entry for the writer).
|
||||
|
||||
Phase 129 (task 03): the status contract gains the in-flight file's
|
||||
EMBED-chunk position — ``current_chunk`` (the 1-based first chunk of
|
||||
the in-flight token-budgeted batch) + ``chunks_total`` (the file's
|
||||
@@ -372,7 +383,12 @@ def test_sources_js_running_state_is_never_stale() -> None:
|
||||
(phase 129 — 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)."""
|
||||
setSyncLabel). Phase 149 (task 01): the writer call passes
|
||||
`running = true` (the detail box reveals with the full label),
|
||||
and the button's accessible name rides in the writer too (the
|
||||
full live label — the icon-only running button keeps a name);
|
||||
the entry no longer sets aria-label at all. The `is-running`
|
||||
class is added here, with the spin."""
|
||||
js = _text(SOURCES_JS)
|
||||
body = _body(js, "enterSyncRunningState")
|
||||
assert "syncBtn.disabled = true" in body
|
||||
@@ -381,15 +397,22 @@ def test_sources_js_running_state_is_never_stale() -> None:
|
||||
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, chunk)" in body
|
||||
# set for EVERY running label: the file-only rule is adjusted
|
||||
# away) with the phase-149 `running` flag (the box reveals).
|
||||
assert "setSyncLabel(main, count, chunk, true)" 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.setAttribute("aria-label"' not in body, (
|
||||
"the aria-label write lives in setSyncLabel (phase 149: the full"
|
||||
" live label while running)"
|
||||
)
|
||||
assert "syncBtn.classList.remove(\"is-error\")" in body
|
||||
assert "syncIcon.classList.add(\"is-spinning\")" in body
|
||||
assert "syncBtn.classList.add(\"is-running\")" in body, (
|
||||
"the icon-only running button class travels with the spin"
|
||||
)
|
||||
|
||||
|
||||
def test_sources_js_terminal_states() -> None:
|
||||
@@ -439,7 +462,11 @@ def test_sources_js_settles_the_button_on_terminal() -> None:
|
||||
(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)."""
|
||||
string riding the title + #sync-result (A4, every label state).
|
||||
Phase 149 (task 01): the `is-running` class is removed WITH the
|
||||
spin (every terminal + both idles pass through the settle), and
|
||||
the writer call's default `running = false` hides the detail box
|
||||
+ restores the idle accessible name."""
|
||||
js = _text(SOURCES_JS)
|
||||
body = _body(js, "settleSyncButton")
|
||||
assert "syncBtn.disabled = false" in body
|
||||
@@ -447,7 +474,13 @@ 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 "syncBtn.classList.remove(\"is-running\")" in body, (
|
||||
"the running class travels with the spin — the settle removes both"
|
||||
)
|
||||
assert "setSyncLabel(label)" in body, ("the settle label goes through the parts writer")
|
||||
assert "setSyncLabel(label, true)" not in body, (
|
||||
"the settle is NEVER a running state — the box hides (phase 149)"
|
||||
)
|
||||
assert "syncLabel.textContent" not in body, ("no direct span write — the parts")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user