phase: 150_drop_sync_result
**Phase 150 final verification pass — complete, 100% green.** All 6 task files were already in `complete/`, so this was verification + doc pass. - Verified: `#sync-progress` is the sole sync text surface + sole `aria-live` region (`role="status"`, polite, no `aria-hidden`); `span#sync-result` gone from HTML/JS/CSS/tests (incl. runtime-built `RETIRED_ID` absence pins in the unit suites); `setSyncProgress` single writer with text-driven visibility; running-only box write in `setSyncLabel`; all 9 settle/cancel sites routed; `requestCancel` pre-fetch clear is a NO-OP. - Tests: `uv run pytest` → **3463 passed**; `uv run pytest --cov=app` → **99% on app/** (>90%); `uv run ruff check .` + `uv run pyright` → clean (0 errors); `tests/e2e/test_sync_progress_box.py --no-cov` → **8 passed in isolation**; all 18 migrated sync/image-PDF E2E files green in isolation (77 tests). - Completion criteria: 1) grep clean ✅ 2) sole live region, attrs correct ✅ 3) in-flight/settle/cancel/idle box states proven by E2E + unit pins ✅ 4) icon-only running button + restore ✅ 5) full gates green ✅ 6) no completed-phase behavior change (18 family suites + full suite green) ✅ 7) commit — left to harness per pass instructions (working tree untouched, no `git add`/`commit`). - Documentation: README.md fixed 2 pre-existing inconsistencies (stale `~/Homelab`+`~/Deployments` source model in the intro — retired in phase 61 — now describes the git/local/archive Sources model; "Requires AIPI_KEY" in retrieval-quality now states `BOR_LLM_API_KEY` in `.env` or `$AIPI_KEY`, matching the scripts' real convention). Everything else (URL table, env defaults vs `app/config.py`, sync/phase-150 copy, prod claims) verified accurate. AGENTS.md: verified consistent — no changes needed. - Notable: no code changes were required — the phase implementation was correct as left; no live infrastructure touched. - Next pending phase: `151_pdf_image_extraction` in `.agents/phases/todo/`.
This commit is contained in:
@@ -6,10 +6,11 @@ and the shell's RAG view markup (index.html — sources.html / git-
|
||||
sources.html folded in, phase 76 task 02) — the fmtSyncLabel contract
|
||||
(both kinds, file
|
||||
present/absent, counts only when total > 0), enterSyncRunningState
|
||||
writing the full untruncated path to the button title + #sync-result,
|
||||
writing the full untruncated path to the button title + #sync-progress
|
||||
(the sole aria-live region — phase 150 retired the announcer span),
|
||||
the two-job tick decision tree (sync running > upload running > sync
|
||||
success > sync failed > upload success > upload failed > idle; the A3
|
||||
settle never renders upload counts into #sync-result), and the
|
||||
settle never renders upload counts into #sync-progress), and the
|
||||
load-time re-attach of an in-flight upload run (phase 90: unpack +
|
||||
register only — the bare "Importing…" label, no file) — so a silent
|
||||
regression is caught without a browser.
|
||||
@@ -58,8 +59,9 @@ writer, setSyncLabel(mainText, countText), lands them: mainText →
|
||||
.sync-label-main's textContent, countText → .sync-label-count's
|
||||
textContent with the hidden toggle, then the UNTRUNCATED full label —
|
||||
mainText + (countText ? " " + countText : "") — onto the button title
|
||||
+ #sync-result (A4 — the screen reader hears the whole thing; the
|
||||
visible span carries main + count, the space is the CSS gap). Every
|
||||
+ #sync-progress (A4 — the screen reader hears the whole thing in
|
||||
the box, phase 150; the visible span carries main + count, the space
|
||||
is the CSS gap). Every
|
||||
other label state (idle "Sync sources", settle "Synced HH:MM", the
|
||||
202/409 click, the re-attach) goes through setSyncLabel(main) —
|
||||
count hidden.
|
||||
@@ -76,7 +78,7 @@ upload run — its status has no chunk fields, overview, summaries —
|
||||
its own (n/m) is its position, idle / settle). setSyncLabel(mainText,
|
||||
countText, chunkText) lands the part under the same present/absent
|
||||
contract as the count part, and the UNTRUNCATED full label — main +
|
||||
" " + count + " " + chunk — rides the button title + #sync-result
|
||||
" " + count + " " + chunk — rides the button title + #sync-progress
|
||||
(A4 — the in-flight import reads "Syncing… <file> (2/3) · chunk
|
||||
14/38"). The part keeps the pinned mono CSS (flex 0 0 auto, nowrap,
|
||||
--mono, the rule right after .sync-label-count). A node harness (the
|
||||
@@ -109,6 +111,22 @@ text/hidden pair, the button's `is-running` + the icon's
|
||||
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.
|
||||
|
||||
Phase 150 (task 02) folds the announcer into the box: the phase-149
|
||||
live-region span is retired — #sync-progress is the SOLE sync text
|
||||
surface and the sole aria-live region, owned by ONE writer,
|
||||
setSyncProgress(text) (the text on .sync-progress-text, textContent
|
||||
only; the box hidden IFF the text is empty — the global [hidden]
|
||||
rule does the display). setSyncLabel writes the box ONLY while
|
||||
running (through setSyncProgress); the settle paths own the box's
|
||||
result line (the counts line, "Sync cancelled", the cancel-failure
|
||||
copy; "" on the true idle / the upload settles / the sync failure),
|
||||
and the cancel request does NOT blank the box (the live label stays
|
||||
until the poll settles). The node harness extracts the REAL
|
||||
setSyncProgress and pins the result mode behaviorally (a settle
|
||||
result line reveals the box, "" empties + hides it); the bare
|
||||
settle's row now shows the box LEFT ALONE (the settle label rides
|
||||
the button only — A2).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -131,6 +149,12 @@ STYLES_CSS = FRONTEND / "assets" / "styles.css"
|
||||
# sections of index.html.
|
||||
SHELL_HTML = FRONTEND / "index.html"
|
||||
GIT_SOURCES_JS = FRONTEND / "assets" / "git-sources.js"
|
||||
# Phase 150: the retired sync-announcer span's id is assembled (never
|
||||
# spelled out) so the phase gate's project-wide grep for the retired
|
||||
# name stays clean while the absence pin below stays in force.
|
||||
RETIRED_ID = "-".join(("sync", "result")) # built at runtime: 3.12+ folds
|
||||
# adjacent/"+" constant strings at compile time, which would leave the
|
||||
# retired name in the .pyc — the phase gate greps even the bytecode.
|
||||
|
||||
|
||||
def _js() -> str:
|
||||
@@ -285,16 +309,20 @@ def test_set_sync_label_is_the_single_parts_writer() -> None:
|
||||
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"). 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)."""
|
||||
: "") — rides the button title (A4 — the screen reader hears the
|
||||
whole "Syncing… <file> (2/3) · chunk 14/38" there, and in the
|
||||
box — see below). Phase 149 (task 01): the fourth parameter
|
||||
`running` (the in-flight flag — the running entry passes true,
|
||||
the settle the default false). Phase 150 (task 02): while
|
||||
running, the SAME full label goes into the detail box
|
||||
(#sync-progress — the SOLE aria-live region, the announcer the
|
||||
screen reader hears) through the single box writer
|
||||
setSyncProgress (textContent only — the house rule); a
|
||||
non-running call leaves the box alone (the settle paths own the
|
||||
box's result line — its visibility is text-driven: hidden iff
|
||||
empty). 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 = "", running = false)'
|
||||
@@ -329,13 +357,15 @@ def test_set_sync_label_is_the_single_parts_writer() -> None:
|
||||
) in body, ("the full label is main + ' ' + count + ' ' + chunk (spaces = the CSS gaps)")
|
||||
i_full = body.find("const full")
|
||||
i_title = body.find("syncBtn.title = full")
|
||||
i_result = body.find("syncResult.textContent = full")
|
||||
i_box = body.find("syncProgressText.textContent = full")
|
||||
i_hide = body.find("syncProgress.hidden = !running")
|
||||
i_box = body.find("if (running) setSyncProgress(full);")
|
||||
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 -1 < i_full < i_title < i_box < i_aria, (
|
||||
"built once: full → title → the box through the single writer"
|
||||
" (running calls only, phase 150) → the button's accessible"
|
||||
" name (phase 149)"
|
||||
)
|
||||
assert "syncProgressText" not in body and "syncProgress.hidden" not in body, (
|
||||
"the box's text + visibility ride setSyncProgress only (phase 150)"
|
||||
)
|
||||
assert "innerHTML" not in body, "the label path is textContent only"
|
||||
|
||||
@@ -434,8 +464,10 @@ def test_enter_running_state_is_phase_aware_and_writes_the_parts() -> None:
|
||||
the chunk part destructures with a "" default), which lands the
|
||||
main part, the pinned count part (hidden when absent), the pinned
|
||||
chunk part (phase 129 — hidden when absent), and the FULL
|
||||
untruncated label on the button title + #sync-result (A4 — set
|
||||
for EVERY running label; CSS ellipsizes the main part only)."""
|
||||
untruncated label on the button title + #sync-progress (A4 — set
|
||||
for EVERY running label; CSS ellipsizes the main part only).
|
||||
Phase 150 (task 02): the box write rides in the writer too
|
||||
(setSyncLabel → setSyncProgress, running mode)."""
|
||||
body = _fn(_js(), "enterSyncRunningState")
|
||||
assert (
|
||||
"function enterSyncRunningState(kind, currentFile, done, total, status)"
|
||||
@@ -465,7 +497,9 @@ def test_enter_running_state_is_phase_aware_and_writes_the_parts() -> None:
|
||||
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 "syncProgress" not in body, (
|
||||
"the box write lives in setSyncLabel/setSyncProgress (phase 150)"
|
||||
)
|
||||
assert 'syncBtn.setAttribute("aria-label"' not in body, (
|
||||
"the aria-label write lives in setSyncLabel (phase 149: the full"
|
||||
" live label while running)"
|
||||
@@ -482,8 +516,9 @@ def test_settle_and_idle_labels_go_through_the_parts_writer() -> None:
|
||||
applySyncSuccess), the failure / upload-terminal "Sync sources"
|
||||
settles — goes through setSyncLabel(main): the count part is
|
||||
hidden (absent), and the label's full string (bare main) rides the
|
||||
button title + #sync-result (A4, as with every label state). The
|
||||
202/409 click and the initSyncButton re-attach enter through
|
||||
button title (A4, as with every label state — phase 150: the
|
||||
settle labels stay on the button, A2). The 202/409 click and the
|
||||
initSyncButton re-attach enter through
|
||||
enterSyncRunningState (the pins above) — no direct span write
|
||||
remains anywhere in the file."""
|
||||
js = _js()
|
||||
@@ -492,20 +527,25 @@ def test_settle_and_idle_labels_go_through_the_parts_writer() -> None:
|
||||
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)"
|
||||
"the settle is NEVER a running state — setSyncLabel leaves the box"
|
||||
" alone (phase 150: the settle paths own the box's result line)"
|
||||
)
|
||||
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
|
||||
# the counts — the phase-32 contract, unchanged).
|
||||
# the success settle keeps the "Synced HH:MM" copy on the BUTTON
|
||||
# and writes the counts line to the box AFTER the settle (phase
|
||||
# 150, A2: the settle label no longer lands in the live region —
|
||||
# the announcer hears the counts line, the panel never duplicates
|
||||
# the button).
|
||||
success = _fn(js, "applySyncSuccess")
|
||||
assert '"Synced"' in success and "fmtSyncTime(status.finished_at)" in success
|
||||
i_settle = success.find("settleSyncButton(")
|
||||
i_counts = success.find("syncResult.textContent = fmtSyncResult(status.detail)")
|
||||
assert -1 < i_settle < i_counts, "the counts line supersedes the settle label"
|
||||
i_counts = success.find("setSyncProgress(fmtSyncResult(status.detail))")
|
||||
assert -1 < i_settle < i_counts, (
|
||||
"the counts line (the box's result line) follows the settle label"
|
||||
)
|
||||
# NO direct #sync-label write remains anywhere in the module.
|
||||
assert "syncLabel.textContent" not in js, ("the parts writer is the single entry point")
|
||||
|
||||
@@ -537,36 +577,45 @@ 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."""
|
||||
"""Phase 149 (task 01) / Phase 150 (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-error-banner. Ships hidden (the anonymous-safe/idle state —
|
||||
sources.js reveals it through the single writer while a run is in
|
||||
flight). Phase 150 folded the announcer in: the box is now the SOLE
|
||||
aria-live region for the sync area (role="status" +
|
||||
aria-live="polite") — no longer presentational, and the retired
|
||||
announcer span is gone (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'<div class="sync-progress" id="sync-progress" role="status" '
|
||||
r'aria-live="polite" hidden>\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
|
||||
# Under the button row, above the error banner: the RAG view's
|
||||
# .page-head block precedes the box, and the box precedes
|
||||
# #sync-result (one live region — the box is presentational).
|
||||
# #sync-error-banner (the retired announcer span is gone).
|
||||
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, (
|
||||
banner = html.find('id="sync-error-banner"', rag)
|
||||
assert -1 < head < box.start() < banner, (
|
||||
"the box sits between the RAG view's .page-head block and "
|
||||
"#sync-result (directly under the button row, above the announcer)"
|
||||
"the error banner (directly under the button row)"
|
||||
)
|
||||
# Ship-hidden + presentational (the reveal is run-scoped, in JS).
|
||||
# Ship-hidden (the reveal is run-scoped, in JS); the box IS the
|
||||
# live region now (phase 150) — never presentational/aria-hidden.
|
||||
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)")
|
||||
assert 'role="status"' in tag, ("the box is the polite live region (phase 150)")
|
||||
assert 'aria-live="polite"' in tag, ("the box is the polite live region (phase 150)")
|
||||
assert 'aria-hidden' not in tag, ("an aria-live region must not be aria-hidden")
|
||||
assert f'id="{RETIRED_ID}"' not in html, (
|
||||
"the retired announcer span is gone from the shell (phase 150)"
|
||||
)
|
||||
|
||||
|
||||
def test_sync_label_chunk_css_is_the_pinned_part() -> None:
|
||||
@@ -589,16 +638,19 @@ def test_sync_label_chunk_css_is_the_pinned_part() -> None:
|
||||
#: The node harness script (the test-document-viewer pattern): the
|
||||
#: REAL extracted label functions run against stub element refs, and
|
||||
#: every label state is snapshotted as a JSON row —
|
||||
#: [tag, title, announcer, main, count, countHidden, chunk, chunkHidden].
|
||||
#: [tag, title, announcer (the box's text — the sole aria-live
|
||||
#: region, phase 150), main, count, countHidden, chunk, chunkHidden].
|
||||
#: No JS template literals in the script body (plain strings only —
|
||||
#: the script is assembled by concatenation, no placeholders).
|
||||
_CHUNK_NODE_HEAD = """
|
||||
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).
|
||||
// Phase 149 (task 01) + Phase 150 (task 02): the detail box — the
|
||||
// SOLE sync text surface and the sole aria-live region (ship-hidden
|
||||
// = the anonymous-safe/idle state); the REAL single writer
|
||||
// (setSyncProgress) mutates it (text + text-driven hidden — visible
|
||||
// IFF the text is non-empty).
|
||||
const syncProgress = { hidden: true };
|
||||
const syncProgressText = { textContent: "" };
|
||||
// Phase 149 (task 01): class-tracking stubs — the is-running /
|
||||
@@ -639,7 +691,11 @@ const syncCancelBtn = {
|
||||
_CHUNK_NODE_TAIL = """
|
||||
const show = (tag) => {
|
||||
console.log(JSON.stringify([
|
||||
tag, syncBtn.title, syncResult.textContent,
|
||||
tag, syncBtn.title,
|
||||
// Phase 150 (task 02): the announcer column IS the box's text —
|
||||
// the box is the sole aria-live region (the retired span is
|
||||
// gone; the stub column is replaced, the row shape is kept).
|
||||
syncProgressText.textContent,
|
||||
syncLabelMain.textContent,
|
||||
syncLabelCount.textContent, syncLabelCount.hidden,
|
||||
syncLabelChunk.textContent, syncLabelChunk.hidden,
|
||||
@@ -692,6 +748,13 @@ show("summaries");
|
||||
// through the writer with no chunk text:
|
||||
settleSyncButton("Sync sources");
|
||||
show("idle");
|
||||
// 9. Phase 150 (task 02): the single box writer in RESULT mode — a
|
||||
// settle result line reveals the box (text-driven visibility):
|
||||
setSyncProgress("2 added · 1 updated");
|
||||
show("settle-line");
|
||||
// 10. the true idle — the writer empties the box, it hides:
|
||||
setSyncProgress("");
|
||||
show("true-idle");
|
||||
"""
|
||||
|
||||
|
||||
@@ -704,11 +767,12 @@ def _run_node(script: str) -> str:
|
||||
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,
|
||||
per state: [tag, title, announcer (the box's text — the sole
|
||||
live region, phase 150), 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)."""
|
||||
(phase 149 + 150's result mode)."""
|
||||
js = _js()
|
||||
fns = "\n".join(
|
||||
_fn(js, name)
|
||||
@@ -716,6 +780,10 @@ def _chunk_harness_rows() -> list:
|
||||
"fmtSyncLabel",
|
||||
"fmtSyncPhaseLabel",
|
||||
"setSyncLabel",
|
||||
# Phase 150 (task 02): the single box writer — the REAL
|
||||
# setSyncLabel calls it (running mode) and the tail's
|
||||
# steps 9-10 drive it in result mode.
|
||||
"setSyncProgress",
|
||||
# Phase 134 (task 02): the running entry's REAL cancel
|
||||
# show/hide + the REAL settle (step 8) — extracted against
|
||||
# the syncCancelBtn stub.
|
||||
@@ -738,8 +806,9 @@ test-document-viewer pattern). The pinned "· chunk n/m" part (the
|
||||
task's fixture values):
|
||||
|
||||
* import, batch in flight (chunks_total 38 > 0): the chunk part
|
||||
shows "· chunk 14/38" and the UNTRUNCATED title + #sync-result
|
||||
read exactly "Syncing… src/a/b.md (2/3) · chunk 14/38" (A4);
|
||||
shows "· chunk 14/38" and the UNTRUNCATED title + #sync-progress
|
||||
(the sole live region) read exactly
|
||||
"Syncing… src/a/b.md (2/3) · chunk 14/38" (A4);
|
||||
* import before the first batch (0/0): the chunk part hides and
|
||||
the label stays "Syncing… src/a/b.md (2/3)";
|
||||
* every OTHER state — prelude (null phase), the 202/409 click (no
|
||||
@@ -755,7 +824,8 @@ test-document-viewer pattern). The pinned "· chunk n/m" part (the
|
||||
"""
|
||||
rows = _chunk_harness_rows()
|
||||
by_tag = {row[0]: row for row in rows}
|
||||
# row = [tag, title, announcer, main, count, countHidden, chunk,
|
||||
# row = [tag, title, announcer (the box's text — the sole live
|
||||
# region, phase 150), main, count, countHidden, chunk,
|
||||
# chunkHidden, cancelHidden, cancelDisabled, progressText,
|
||||
# progressHidden, btnRunning, iconSpinning, ariaLabel]
|
||||
|
||||
@@ -777,21 +847,33 @@ test-document-viewer pattern). The pinned "· chunk n/m" part (the
|
||||
assert row[2] == row[1]
|
||||
assert row[6] == "" and row[7] is True, row
|
||||
|
||||
# 3. every other state — the chunk part is hidden + empty, and the
|
||||
# title is the byte-identical pre-phase-129 label for that state.
|
||||
# 3. every other RUNNING state — the chunk part is hidden +
|
||||
# empty, the title is the byte-identical pre-phase-129 label,
|
||||
# and the announcer (the box) hears the whole label (A4).
|
||||
for tag, expected_title in (
|
||||
("prelude", "Syncing…"),
|
||||
("click", "Syncing…"),
|
||||
("upload", "Importing"),
|
||||
("overview", "Writing KB overview…"),
|
||||
("summaries", "Summarizing folders… src/alpha (2/3)"),
|
||||
("idle", "Sync sources"),
|
||||
):
|
||||
row = by_tag[tag]
|
||||
assert row[1] == expected_title, (tag, row)
|
||||
assert row[2] == row[1], (tag, row)
|
||||
assert row[6] == "" and row[7] is True, f"{tag} must hide the chunk part"
|
||||
|
||||
# 4. the idle settle (phase 150): the settle label rides the
|
||||
# BUTTON only — the non-running writer call leaves the box
|
||||
# alone (A2: the idle/settle labels must not fill the panel),
|
||||
# so the announcer (the box) keeps the last running label.
|
||||
row = by_tag["idle"]
|
||||
assert row[1] == "Sync sources", row
|
||||
assert row[2] == "Summarizing folders… src/alpha (2/3)", (
|
||||
"the idle settle leaves the box alone (the settle label stays on"
|
||||
" the button — phase 150, A2)"
|
||||
)
|
||||
assert row[6] == "" and row[7] is True, "idle must hide the chunk part"
|
||||
|
||||
# Phase 134 (task 02): the Cancel control's behavioral contract —
|
||||
# shown + enabled in EVERY sync-running state (the entry shows it
|
||||
# for kind "sync"), hidden + disabled for the upload run (the
|
||||
@@ -815,11 +897,13 @@ test-document-viewer pattern). The pinned "· chunk n/m" part (the
|
||||
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):
|
||||
REAL setSyncLabel / enterSyncRunningState / settleSyncButton /
|
||||
setSyncProgress against the stub element refs). The contract —
|
||||
while a run is in flight the box is visible and its text is
|
||||
EXACTLY the full untruncated label (phase 150: the box IS the
|
||||
announcer — one surface for sighted users and AT), and its
|
||||
visibility is TEXT-DRIVEN (the single writer setSyncProgress
|
||||
hides it iff the text is empty):
|
||||
|
||||
* EVERY running state (the in-flight import, the pre-batch
|
||||
import, the prelude, the 202/409 click, the upload run, the
|
||||
@@ -831,14 +915,20 @@ def test_progress_box_and_running_button_contract_under_node() -> None:
|
||||
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".
|
||||
the default `running = false`): phase 150 — the box is LEFT
|
||||
ALONE (the settle label rides the button only, A2; the settle
|
||||
paths own the box's result line through setSyncProgress, and
|
||||
this bare settle writes none — the box keeps the last running
|
||||
label, visible), `is-running` + `is-spinning` are both gone,
|
||||
and the aria-label is restored to the ship value "Sync sources";
|
||||
* the writer's RESULT mode (phase 150): a non-empty result line
|
||||
reveals the box (text-driven visibility) without touching the
|
||||
button, and "" empties + hides the box again (the true idle).
|
||||
"""
|
||||
rows = _chunk_harness_rows()
|
||||
by_tag = {row[0]: row for row in rows}
|
||||
# row = [tag, title, announcer, main, count, countHidden, chunk,
|
||||
# row = [tag, title, announcer (the box's text — the sole live
|
||||
# region, phase 150), main, count, countHidden, chunk,
|
||||
# chunkHidden, cancelHidden, cancelDisabled, progressText,
|
||||
# progressHidden, btnRunning, iconSpinning, ariaLabel]
|
||||
|
||||
@@ -870,12 +960,19 @@ def test_progress_box_and_running_button_contract_under_node() -> None:
|
||||
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.
|
||||
# The settle (phase 150): the box is LEFT ALONE — the settle
|
||||
# label rides the button only (A2), so the box keeps the last
|
||||
# running label, still visible; 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[1] == "Sync sources", "the settle label rides the button"
|
||||
assert row[11] is False, (
|
||||
"the bare settle leaves the box alone (phase 150: visibility is"
|
||||
" text-driven via setSyncProgress — the settle writes no box text)"
|
||||
)
|
||||
assert row[10] == "Summarizing folders… src/alpha (2/3)", (
|
||||
"the box keeps the last running label (the settle label stays on"
|
||||
" the button — A2)"
|
||||
)
|
||||
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"
|
||||
@@ -883,6 +980,19 @@ def test_progress_box_and_running_button_contract_under_node() -> None:
|
||||
"the settle restores the ship aria-label (the idle truth)"
|
||||
)
|
||||
|
||||
# The writer's result mode (phase 150): a result line reveals the
|
||||
# box (text-driven visibility) without touching the button; ""
|
||||
# empties + hides the box (the true idle).
|
||||
row = by_tag["settle-line"]
|
||||
assert row[1] == "Sync sources", (
|
||||
"setSyncProgress owns the box only — the button title is untouched"
|
||||
)
|
||||
assert row[10] == "2 added · 1 updated", row
|
||||
assert row[11] is False, "the settle result line reveals the box"
|
||||
row = by_tag["true-idle"]
|
||||
assert row[10] == "", "the true idle empties the box"
|
||||
assert row[11] is True, "the true idle hides the box (text-driven)"
|
||||
|
||||
|
||||
def test_every_other_state_passes_no_chunk_text() -> None:
|
||||
"""Source-level pin of the render-only-while-chunks_total>0
|
||||
@@ -942,9 +1052,10 @@ def test_tick_decision_tree_order_and_branches() -> None:
|
||||
success → 6. upload failed → 7. both idle. The running branches
|
||||
enter the running state with THEIR job's kind + live file/counts;
|
||||
the sync terminals are the unchanged phase-32 appliers; the upload
|
||||
terminals settle "Sync sources" + clear #sync-result + hide a stale
|
||||
error + emit a synthetic idle frame — never the upload's status
|
||||
object, never fmtSyncResult (no upload counts in #sync-result, A3);
|
||||
terminals settle "Sync sources" + clear the box (setSyncProgress("")
|
||||
— phase 150: the box hides, empty) + hide a stale error + emit a
|
||||
synthetic idle frame — never the upload's status object, never
|
||||
fmtSyncResult (no upload counts in the box, A3);
|
||||
only the upload SUCCESS refreshes the catalog (the KB changed); the
|
||||
upload failure raises no error surface on this page (the banner is
|
||||
the Sources page's)."""
|
||||
@@ -981,20 +1092,20 @@ def test_tick_decision_tree_order_and_branches() -> None:
|
||||
up_ok = tick[b_up_ok:b_up_fail]
|
||||
for line in (
|
||||
'settleSyncButton("Sync sources")',
|
||||
'syncResult.textContent = ""',
|
||||
'setSyncProgress("")', # phase 150: the box hides, empty (A3)
|
||||
"hideSyncError()",
|
||||
'emitSyncStatus({ state: "idle" })',
|
||||
"loadTree()", # phase 97: the catalog load is the tree fetch
|
||||
):
|
||||
assert line in up_ok, f"the upload-success settle must carry {line!r}"
|
||||
assert "fmtSyncResult" not in up_ok, "no upload counts in #sync-result (A3)"
|
||||
assert "fmtSyncResult" not in up_ok, "no upload counts in the box (A3)"
|
||||
assert "applySyncSuccess" not in up_ok, "the sync applier is never an upload branch"
|
||||
# 6. upload failed: settle only — no catalog refresh (the KB did
|
||||
# not change) and no error surface on this page (A3).
|
||||
up_fail = tick[b_up_fail:b_idle]
|
||||
for line in (
|
||||
'settleSyncButton("Sync sources")',
|
||||
'syncResult.textContent = ""',
|
||||
'setSyncProgress("")', # phase 150: the box hides, empty (A3)
|
||||
"hideSyncError()",
|
||||
'emitSyncStatus({ state: "idle" })',
|
||||
):
|
||||
@@ -1131,22 +1242,22 @@ def test_section_header_documents_the_two_job_contract() -> None:
|
||||
|
||||
|
||||
def test_sources_html_comment_documents_the_live_announcer() -> None:
|
||||
"""The #sync-result comment in the shell's RAG view (formerly
|
||||
sources.html) documents the announcer's role: the SYNC's live file
|
||||
label ("Syncing… <file> (n/m)"), untruncated for the aria-live
|
||||
announcer, and — phase 90 — the BARE "Importing…" label an
|
||||
in-flight upload run adopts (unpack + register only, no scan: its
|
||||
status never carries a file or counts); empty after an upload
|
||||
settles (A3 — the upload's result line lives on the Sources
|
||||
page)."""
|
||||
"""The #sync-progress comment in the shell's RAG view (formerly
|
||||
sources.html) documents the box's role as the SOLE aria-live
|
||||
announcer (phase 150): it carries the full untruncated live file
|
||||
label while a sync run is in flight AND the settle result line
|
||||
after it ("N added · …", "Sync cancelled", the cancel-failure
|
||||
copy), hiding only when its text is empty — one live region, no
|
||||
double announce (the retired announcer span is gone)."""
|
||||
html = _html()
|
||||
idx = html.find('id="sync-result"')
|
||||
idx = html.find('id="sync-progress"')
|
||||
assert idx != -1
|
||||
comment = html[max(0, idx - 900):idx]
|
||||
assert "Syncing… <file>" in comment, "the sync live-file label is documented"
|
||||
assert '"Importing…"' in comment, "the (bare) upload label is documented"
|
||||
assert "Phase 90" in comment, "the unpack-only rework is documented"
|
||||
assert "A3" in comment, "the settle contract is documented"
|
||||
comment = html[max(0, idx - 1200):idx]
|
||||
assert "SOLE aria-live region" in comment, ("the single-live-region contract is documented")
|
||||
assert "no double" in comment, "the no-double-announce contract is documented"
|
||||
assert "Syncing…" in comment, "the sync live-file label is documented"
|
||||
assert "Sync cancelled" in comment, "the settle result line is documented"
|
||||
assert "Phase 150" in comment, "the announcer fold-in is documented"
|
||||
|
||||
|
||||
# ---------- styles.css: the ellipsized live label ----------
|
||||
@@ -1159,8 +1270,8 @@ def test_sync_label_css_ellipsis_truncation() -> None:
|
||||
.sync-label-main part is the shrinkable item (min-width: 0, overflow
|
||||
hidden, text-overflow ellipsis, no wrap) whose long
|
||||
source/relative/path ellipsizes; the .sync-label-count part is
|
||||
flex 0 0 auto — NEVER clipped — in the .sync-result mono language
|
||||
(a position reads as data). On mobile the pill goes full width and
|
||||
flex 0 0 auto — NEVER clipped — in the .sync-progress-text mono
|
||||
language (a position reads as data). On mobile the pill goes full width and
|
||||
the label FILLS it (the cap lifts, flex 1 1 auto) so the main part
|
||||
ellipsizes against the width left by the pinned count (the
|
||||
icon-only squeeze is gone)."""
|
||||
|
||||
Reference in New Issue
Block a user