That last `--co` (collect-only) run was my probe mistake — it shows 34% because no tests execute under collect-only. The valid coverage run was the real gate command executed earlier: `uv run pytest --cov=app --cov-report=term-missing` → **TOTAL 99%** (exit 0, all 3544 tests executed). All gates are verified. ## Phase 153 — final verification pass report **Verified (all tasks 01–04 complete in `complete/`):** - Modal `fullPageUrl(source, path, back = null)` + `openDocumentModal(…, back = null)` with module-level `currentBack`; 5 chat call sites pass `"/"`, sources.js call site unchanged (byte-identical Sources path); `documentUrl` comment now true - Copy-path control (shared `renderDocument` core, icon+text, text = accessible name) on both surfaces; per-page clipboard helpers with the fallback-field + status-line contract; `#doc-copy-announcer` live region; styles + 44px touch floor - Unit pins: `&back=%2F` / no-`back` byte-identity, `currentBack` single-writer guard, copy strings byte-pinned, phase-13 regression pins untouched **Test / lint / coverage results:** - `uv run pytest` → **3544 passed**, 0 failed (full unit + integration) - `uv run pytest --cov=app --cov-report=term-missing` → **TOTAL 99%** (>90% gate) - `uv run pytest tests/e2e/test_document_viewer_context.py -v --no-cov` → **8 passed** in 47.33s, in isolation (DB up, mock LLM) - `uv run ruff check . && uv run pyright` → clean (0 errors/warnings) **Completion criteria:** all met — (1) chat→modal→Full page: `href="/"` + "Chat"; Sources→modal→Full page: `href="/sources.html"` + "Sources" with `back=` asserted absent; (2) no-`back` URLs byte-identical to pre-phase; (3) Copy path: "Path copied." on success (clipboard readback), prefilled select-on-focus fallback field + "Path ready — copy it from the field." on rejected clipboard, both surfaces parameterized; (4) copied string exactly `docs/homelab/kubernetes.md`; (5) full suite green, E2E isolated green, lint clean, coverage 99%; (6) commit by harness. **Documentation:** README.md reviewed in full — phase-153 feature already named in the Chat section; routes, env reference, workflows, deployment all consistent — no changes needed. AGENTS.md reviewed in full — rules, gates, and quick-reference commands verified against the repo (incl. `test_smoke.py`, `.env.example`, gitignore entries) — consistent, no changes needed. **Notable:** pytest 9.1.1 quirk — a doubled `-q` (pyproject `addopts` + explicit `-q`) swallows the final "N passed" summary line while still exiting 0; the plain `uv run pytest` gate command prints it. No defects found in prior work. **Next pending phase:** `154_sync_status_freshness`.
620 lines
29 KiB
Python
620 lines
29 KiB
Python
"""Unit: the document modal's history-entry lifecycle (phase 144, task 02).
|
|
|
|
Back while the ``#doc-modal`` document viewer is open must CLOSE the modal
|
|
— not navigate the view behind it (TODO.md L6). The mechanics live in
|
|
``frontend/assets/document-modal.js``: the open owns ONE state-only
|
|
history entry (``{ modal: "doc", source, path }`` — the second arg
|
|
``""`` keeps the URL where it is, so the router's pathname-only
|
|
popstate is a no-op and the Sources drill re-adopts the state it was
|
|
already showing); Back closes the modal (remembering the landed state
|
|
as ``lastPrev``); a button/Escape/backdrop close removes the entry IN
|
|
PLACE (``replaceState(entry.prev)`` — the next Back behaves as if the
|
|
modal never opened); Forward onto the entry re-opens the same document
|
|
(the browser owns its entries — the phase-99 D2 convention).
|
|
|
|
The browser behavior itself is E2E-gated by the phase-144 story suite
|
|
(``tests/e2e/test_modal_back_button.py`` — task 03 drives the real
|
|
history via ``page.goBack()``/``page.goForward()``); like
|
|
``test_remove_confirm_modal.py`` (the house pattern for a frontend
|
|
module whose real behavior is E2E-gated), this module pins the
|
|
source-level contract a silent regression would break:
|
|
|
|
* a second push on the forward re-open (the adopt branch must assign
|
|
``entry`` WITHOUT a ``pushState``);
|
|
* a ``replaceState`` on a non-modal current entry (the close's
|
|
in-place removal is guarded by ``!closingViaBack`` AND
|
|
``history.state.modal === "doc"`` — the fail-safe);
|
|
* a double-registered popstate listener;
|
|
* a ``history.back()``/``history.go()`` introduction (the browser owns
|
|
its entries — the closed set of ``history.*`` sites is the one push,
|
|
the two replaces, and the read-only ``history.state`` accesses);
|
|
* a lost ``lastPrev`` write (it must land BEFORE the
|
|
``closingViaBack``-wrapped close in the back branch);
|
|
* a fallback-path push (the ``window.open`` no-skeleton fallback's
|
|
early ``return`` precedes the first ``history.`` use in the open).
|
|
|
|
Phase 153 (task 01) — the "Full page" escape hatch carries the origin
|
|
``back``; the module's added pins:
|
|
|
|
* a second ``currentBack`` declaration / a reset that does not land at
|
|
the top of EVERY open (before the no-skeleton guard — the fallback
|
|
path reads the stored value);
|
|
* ``fullPageUrl`` appending ``&back=`` unconditionally (a null/absent
|
|
``back`` must keep the URL byte-identical — the Sources path);
|
|
* the ``openDocumentModal`` ``back`` default drifting from ``null``
|
|
(the Sources table's 3-arg call must stay byte-identical);
|
|
* a ``fullPageUrl`` call site dropping the stored ``back`` (the
|
|
no-skeleton fallback and the anchor must both read ``currentBack``).
|
|
|
|
Phase 153 (task 03) — unit pins for the URL contract (additions on top
|
|
of task 01's source pins; the pre-phase contract is the load-bearing
|
|
byte-identity):
|
|
|
|
* ``fullPageUrl`` behavior under node — ``back = null`` and an ABSENT
|
|
third arg are byte-identical to each other AND to the exact
|
|
pre-phase string (no ``&back=`` anywhere — the Sources path);
|
|
``back = "/"`` appends exactly ``&back=%2F`` to that same string;
|
|
* ``openDocumentModal`` behavior under node — the no-skeleton
|
|
``window.open`` fallback carries the stored ``back`` (a chat open's
|
|
``"/"`` lands in the URL, an omitted ``back`` keeps it byte-
|
|
identical) and the ``#doc-modal-open`` ("Full page") anchor carries
|
|
it on a skeleton open (chat → ``&back=%2F``; a Sources-style open
|
|
→ byte-identical, no param).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
FRONTEND = Path(__file__).resolve().parents[2] / "frontend"
|
|
MODAL_JS = FRONTEND / "assets" / "document-modal.js"
|
|
|
|
HAVE_NODE = shutil.which("node") is not None
|
|
|
|
|
|
def _run_node(script: str) -> str:
|
|
proc = subprocess.run(["node", "-e", script], capture_output=True, text=True, timeout=60)
|
|
assert proc.returncode == 0, f"node failed: {proc.stderr}"
|
|
return proc.stdout
|
|
|
|
|
|
def _js() -> str:
|
|
return MODAL_JS.read_text(encoding="utf-8")
|
|
|
|
|
|
def _fn(js: str, name: str) -> str:
|
|
"""The source of a top-level function via balanced-brace counting
|
|
(the test_remove_confirm_modal.py helper — ``export function``
|
|
matches the plain ``function name(`` search)."""
|
|
start = js.find(f"function {name}(")
|
|
assert start != -1, f"{name}() must exist in document-modal.js"
|
|
depth = 0
|
|
for i in range(js.find("{", start), len(js)):
|
|
if js[i] == "{":
|
|
depth += 1
|
|
elif js[i] == "}":
|
|
depth -= 1
|
|
if depth == 0:
|
|
return js[start : i + 1]
|
|
raise AssertionError(f"unbalanced braces in {name}()")
|
|
|
|
|
|
def _block(source: str, from_i: int) -> str:
|
|
"""The ``{ … }`` block opening at the first ``{`` at/after
|
|
``from_i`` — balanced-brace walk, braces included."""
|
|
brace = source.find("{", from_i)
|
|
assert brace != -1, "no block opener found"
|
|
depth = 0
|
|
for i in range(brace, len(source)):
|
|
if source[i] == "{":
|
|
depth += 1
|
|
elif source[i] == "}":
|
|
depth -= 1
|
|
if depth == 0:
|
|
return source[brace : i + 1]
|
|
raise AssertionError("unbalanced braces in the entry/popstate block")
|
|
|
|
|
|
def _popstate_handler(js: str) -> str:
|
|
"""The module-top-level popstate handler's ``{ … }`` body."""
|
|
i = js.find('window.addEventListener("popstate"')
|
|
assert i != -1, "the popstate listener must be registered at module top level"
|
|
arrow = js.find("=> {", i)
|
|
assert arrow != -1, "the popstate listener must be an arrow function"
|
|
return _block(js, arrow)
|
|
|
|
|
|
# ---------- the module state: one declaration, no second writer site ----------
|
|
|
|
|
|
def test_history_state_is_declared_exactly_once() -> None:
|
|
"""REGRESSION: a duplicated module-level declaration / second
|
|
writer site for the entry state. ``entry`` / ``lastPrev`` /
|
|
``closingViaBack`` are each declared by EXACTLY ONE ``let`` (the
|
|
single writer sites — open/close, the popstate handler, the
|
|
popstate handler — are pinned by the per-branch tests below)."""
|
|
js = _js()
|
|
assert js.count("let entry = null;") == 1
|
|
assert len(re.findall(r"let\s+entry\s*=", js)) == 1
|
|
assert js.count("let lastPrev = null;") == 1
|
|
assert len(re.findall(r"let\s+lastPrev\s*=", js)) == 1
|
|
assert js.count("let closingViaBack = false;") == 1
|
|
assert len(re.findall(r"let\s+closingViaBack\s*=", js)) == 1
|
|
|
|
|
|
# ---------- the open: one state-only entry, adopt vs push vs replace ----------
|
|
|
|
|
|
def test_open_pushes_exactly_one_state_only_entry() -> None:
|
|
"""REGRESSION: a double push (two entries per open) or a
|
|
URL-MOVING push. The push is exactly ONE
|
|
``history.pushState({ modal: "doc", source, path }, "")`` — the
|
|
``""`` second arg is what keeps the URL where it is (state-only;
|
|
the router's pathname-only popstate stays a no-op)."""
|
|
open_body = _fn(_js(), "openDocumentModal")
|
|
assert open_body.count("history.pushState(") == 1, (
|
|
"exactly one pushState in the open — a second push stacks a dead history step"
|
|
)
|
|
assert 'history.pushState({ modal: "doc", source, path }, "")' in open_body, (
|
|
"the push must be state-only: the state key modal: \"doc\" + the "
|
|
"doc name, and the \"\" second arg (the URL must not move)"
|
|
)
|
|
|
|
|
|
def test_open_entry_chain_adopts_on_forward_and_replaces_on_defensive_reopen() -> None:
|
|
"""REGRESSION: a second push on the forward re-open. The chain is
|
|
``if (entry) → replaceState IN PLACE (keep entry, update
|
|
source/path — the defensive re-open-while-open never stacks)
|
|
→ else if the current entry is already a modal entry → ADOPT
|
|
(assign ``entry = { prev: lastPrev, source, path }`` WITHOUT any
|
|
pushState — the browser already sits on the entry) → else push.
|
|
The push lives ONLY in the final else."""
|
|
open_body = _fn(_js(), "openDocumentModal")
|
|
st_i = open_body.find("const st = history.state;")
|
|
def_i = open_body.find("if (entry) {", st_i)
|
|
adopt_i = open_body.find('else if (st && st.modal === "doc") {', def_i)
|
|
else_i = open_body.find("} else {", adopt_i)
|
|
push_i = open_body.find('history.pushState({ modal: "doc", source, path }, "");', else_i)
|
|
assert -1 < st_i < def_i < adopt_i < else_i < push_i, (
|
|
"chain order: const st → if (entry) → else if (adopt) → else → push"
|
|
)
|
|
# Defensive re-open: replaceState in place, entry KEPT (updated).
|
|
defensive = open_body[def_i:adopt_i]
|
|
assert "entry.source = source;" in defensive
|
|
assert "entry.path = path;" in defensive
|
|
assert 'history.replaceState({ modal: "doc", source, path }, "")' in defensive
|
|
assert "pushState" not in defensive, "a defensive re-open must never push"
|
|
# Forward re-open: ADOPT — assign entry from lastPrev, no push.
|
|
adopt = open_body[adopt_i:else_i]
|
|
assert "entry = { prev: lastPrev, source, path };" in adopt, (
|
|
"the adopt must take lastPrev (the below-entry remembered at the back-close)"
|
|
)
|
|
assert "pushState" not in adopt, (
|
|
"a second push on the forward re-open stacks a dead history step"
|
|
)
|
|
# Normal open: push with prev = the entry we sat on, in the final else.
|
|
assert "entry = { prev: st, source, path };" in open_body[else_i:push_i]
|
|
assert open_body.count("history.pushState(") == 1
|
|
|
|
|
|
def test_window_open_fallback_precedes_the_entry_logic() -> None:
|
|
"""REGRESSION: a fallback-path push. A page WITHOUT the modal
|
|
skeleton falls back to ``window.open(fullPageUrl…)`` in a new tab —
|
|
that path must not touch the history AT ALL: the fallback's early
|
|
``return`` precedes the first ``history.`` use in the open."""
|
|
open_body = _fn(_js(), "openDocumentModal")
|
|
guard_i = open_body.find("if (!modalEl || !titleEl || !metaEl || !contentEl) {")
|
|
open_i = open_body.find(
|
|
'window.open(fullPageUrl(source, path, currentBack), "_blank", "noopener");'
|
|
)
|
|
ret_i = open_body.find("return;", open_i)
|
|
first_hist_i = open_body.find("history.")
|
|
assert -1 < guard_i < open_i < ret_i < first_hist_i, (
|
|
"the window.open fallback must early-return before any history use"
|
|
)
|
|
|
|
|
|
# ---------- the close: in-place removal, guarded; entry always nulled ----------
|
|
|
|
|
|
def test_close_removes_the_entry_in_place_and_only_on_a_modal_entry() -> None:
|
|
"""REGRESSION: a ``replaceState`` on the wrong entry. The in-place
|
|
removal ``history.replaceState(entry.prev, "")`` is guarded by
|
|
BOTH ``!closingViaBack`` (a back close already moved us off the
|
|
entry — the browser owns that pop) AND the current entry still
|
|
being a modal entry (the fail-safe: a non-modal current entry is
|
|
NEVER overwritten). Exactly one replaceState in the close."""
|
|
close_body = _fn(_js(), "closeDocumentModal")
|
|
assert close_body.count("history.replaceState(") == 1
|
|
guard = 'if (entry && !closingViaBack && history.state && history.state.modal === "doc") {'
|
|
guard_i = close_body.find(guard)
|
|
assert guard_i != -1, (
|
|
"the close's in-place removal must be guarded by !closingViaBack AND "
|
|
"history.state.modal === \"doc\""
|
|
)
|
|
# The single replace sits INSIDE the guard block.
|
|
block = _block(close_body, guard_i)
|
|
assert 'history.replaceState(entry.prev, "");' in block, (
|
|
"the removal must restore entry.prev (byte-for-byte view state)"
|
|
)
|
|
|
|
|
|
def test_close_nulls_the_entry_unconditionally() -> None:
|
|
"""REGRESSION: a lost ``entry = null`` — the adopt's memory
|
|
surviving a close (a later popstate would then close/adopt against
|
|
a stale entry). ``entry = null`` runs OUTSIDE the guarded if,
|
|
between the removal and the hide, on EVERY close (back or not)."""
|
|
close_body = _fn(_js(), "closeDocumentModal")
|
|
guard_i = close_body.find('if (entry && !closingViaBack && history.state')
|
|
brace_i = close_body.find("{", guard_i)
|
|
block = _block(close_body, guard_i)
|
|
assert "entry = null" not in block, "entry = null must not sit inside the guard"
|
|
null_i = close_body.find("entry = null;", brace_i + len(block))
|
|
hide_i = close_body.find("modalEl.hidden = true;", null_i)
|
|
assert -1 < null_i < hide_i, "entry = null must run (before the hide) on every close"
|
|
|
|
|
|
# ---------- the popstate listener: once, mutually exclusive branches ----------
|
|
|
|
|
|
def test_popstate_listener_registered_exactly_once() -> None:
|
|
"""REGRESSION: a double-registered popstate listener (two closes /
|
|
two re-opens per navigation). The module registers EXACTLY ONE
|
|
``window.addEventListener("popstate"`` at top level."""
|
|
js = _js()
|
|
assert js.count('window.addEventListener("popstate"') == 1
|
|
|
|
|
|
def test_popstate_forward_branch_reopens_and_returns() -> None:
|
|
"""REGRESSION: the forward branch falling THROUGH into the
|
|
back-close branch. Forward = the modal is closed and we landed on
|
|
a modal entry — re-open the doc the entry names (trigger null —
|
|
focus goes to the content target) and RETURN before the back
|
|
branch (the two branches are mutually exclusive)."""
|
|
handler = _popstate_handler(_js())
|
|
const_i = handler.find("const st = event.state;")
|
|
flag_i = handler.find('const isModalEntry = st && st.modal === "doc";')
|
|
fwd_i = handler.find("if (isModalEntry && modalEl && modalEl.hidden) {")
|
|
reopen_i = handler.find("openDocumentModal(st.source, st.path, null);", fwd_i)
|
|
ret_i = handler.find("return;", reopen_i)
|
|
back_i = handler.find("if (!isModalEntry && modalEl && !modalEl.hidden && entry) {", ret_i)
|
|
assert -1 < const_i < flag_i < fwd_i < reopen_i < ret_i < back_i, (
|
|
"forward: land on a modal entry with the modal closed → re-open the "
|
|
"named doc (null trigger) → RETURN before the back-close branch"
|
|
)
|
|
|
|
|
|
def test_popstate_back_branch_remembers_lastprev_then_closes_guarded() -> None:
|
|
"""REGRESSION: a lost ``lastPrev`` write (the forward re-open's
|
|
adopt would then restore ``prev: null`` and corrupt the entry
|
|
below). Back = the modal is open and we landed on a NON-modal
|
|
state: write ``lastPrev = st`` FIRST, then the close wrapped in
|
|
``closingViaBack = true`` … ``= false`` (keeps the close from also
|
|
replaceState-ing the entry the browser already popped). The flag
|
|
and the below-entry have their single writer sites here."""
|
|
js = _js()
|
|
handler = _popstate_handler(js)
|
|
back_i = handler.find("if (!isModalEntry && modalEl && !modalEl.hidden && entry) {")
|
|
assert back_i != -1, (
|
|
"the back branch (non-modal land + modal open + entry) is gone"
|
|
)
|
|
lastprev_i = handler.find("lastPrev = st;", back_i)
|
|
setflag_i = handler.find("closingViaBack = true;", lastprev_i)
|
|
close_i = handler.find("closeDocumentModal();", setflag_i)
|
|
resetflag_i = handler.find("closingViaBack = false;", close_i)
|
|
assert -1 < lastprev_i < setflag_i < close_i < resetflag_i, (
|
|
"back: lastPrev = st → closingViaBack = true → close → = false"
|
|
)
|
|
# The single writer sites (no second writer anywhere in the module).
|
|
assert js.count("lastPrev = st;") == 1
|
|
assert js.count("closingViaBack = true;") == 1
|
|
assert handler.count("closingViaBack = false;") == 1
|
|
|
|
|
|
def test_popstate_passes_through_everything_else() -> None:
|
|
"""REGRESSION: a handler branch firing on entries that are not the
|
|
modal's (the router's view entries, the drill's ``kb`` entries) or
|
|
on a modal entry with the modal still open. The forward branch
|
|
requires ``isModalEntry`` (and the modal hidden) and the back
|
|
branch requires ``entry`` (falsy when the modal is closed) — and
|
|
AFTER the back branch closes the handler does NOTHING (the tail is
|
|
empty): every other popstate falls through untouched."""
|
|
handler = _popstate_handler(_js())
|
|
back_i = handler.find("if (!isModalEntry && modalEl && !modalEl.hidden && entry) {")
|
|
assert back_i != -1, "the back branch (non-modal land + modal open + entry) is gone"
|
|
brace_i = handler.find("{", back_i)
|
|
back_block = _block(handler, back_i)
|
|
tail = handler[brace_i + len(back_block) :]
|
|
# The only thing after the back branch is the handler's own closing
|
|
# brace — no code runs on any other popstate (the pass-through).
|
|
assert tail.strip() in ("", "}"), (
|
|
"no code after the back branch — the pass-through must be a no-op"
|
|
)
|
|
|
|
|
|
# ---------- the no-leak guarantee: the closed set of history.* sites ----------
|
|
|
|
|
|
def test_history_call_sites_are_the_closed_set() -> None:
|
|
"""REGRESSION: a ``history.back()``/``history.go()`` introduction,
|
|
or ANY untracked ``history.*`` call site. The browser owns its
|
|
entries (the phase-99 D2 convention): the module's ONLY
|
|
``history.`` sites are the one ``pushState`` (the open), the two
|
|
``replaceState`` (the defensive re-open, the non-back close), and
|
|
the read-only ``history.state`` accesses."""
|
|
js = _js()
|
|
sites = re.findall(r"history\.\w+", js)
|
|
assert sorted(set(sites)) == [
|
|
"history.pushState",
|
|
"history.replaceState",
|
|
"history.state",
|
|
], f"an untracked history.* site slipped in: {sorted(set(sites))}"
|
|
assert sites.count("history.pushState") == 1
|
|
assert sites.count("history.replaceState") == 2
|
|
assert "history.back(" not in js, "the browser owns its entries — no history.back()"
|
|
assert "history.go(" not in js, "the browser owns its entries — no history.go()"
|
|
|
|
|
|
# ---------- the pre-phase contract a regression could plausibly break ----------
|
|
|
|
|
|
def test_pre_phase_contract_pins_still_hold() -> None:
|
|
"""The phase-26 contracts the entry logic sits beside (a regression
|
|
in the rework must not break them): the Escape capture closes via
|
|
``closeDocumentModal()``; the per-open keydown capture is added on
|
|
open and removed on close (document-level, capture phase); the
|
|
``fetchSeq`` stale-guard lines survive (both checkpoints in the
|
|
fetch chain + the close bump); the trigger-focus restore in the
|
|
close (best-effort, contained in the DOM)."""
|
|
js = _js()
|
|
keydown = _fn(js, "onModalKeydown")
|
|
esc_i = keydown.find('if (e.key === "Escape") {')
|
|
prevent_i = keydown.find("e.preventDefault();", esc_i)
|
|
close_i = keydown.find("closeDocumentModal();", prevent_i)
|
|
assert -1 < esc_i < prevent_i < close_i, "Escape: prevent + closeDocumentModal()"
|
|
open_body = _fn(js, "openDocumentModal")
|
|
close_body = _fn(js, "closeDocumentModal")
|
|
assert (
|
|
'document.addEventListener("keydown", onModalKeydown, true);' in open_body
|
|
), "the per-open capture is added on open (document-level, capture)"
|
|
assert (
|
|
'document.removeEventListener("keydown", onModalKeydown, true);' in close_body
|
|
), "the per-open capture is removed on close"
|
|
# The fetchSeq stale guard: the sequence, both checkpoints, the
|
|
# catch checkpoint, and the close bump (any in-flight fetch is
|
|
# stale after a close).
|
|
assert open_body.count("const seq = ++fetchSeq;") == 1
|
|
assert open_body.count("if (seq !== fetchSeq) return;") == 2, (
|
|
"both render checkpoints (pre- and post-json) must stay stale-guarded"
|
|
)
|
|
assert "if (seq === fetchSeq) renderNotFound(path);" in open_body
|
|
assert "fetchSeq += 1;" in close_body
|
|
# The trigger-focus restore (best-effort: contained in the DOM).
|
|
assert (
|
|
"triggerEl && typeof triggerEl.focus === \"function\" && document.contains(triggerEl)"
|
|
in close_body
|
|
)
|
|
assert "triggerEl.focus({ preventScroll: true });" in close_body
|
|
assert "triggerEl = null;" in close_body
|
|
|
|
|
|
# ---------- phase 153 (task 01): the "Full page" hatch carries the origin ----------
|
|
|
|
|
|
def test_currentback_declared_exactly_once_and_reset_at_the_top_of_every_open() -> None:
|
|
"""REGRESSION: a second ``currentBack`` writer, or a reset that
|
|
does not land at the top of EVERY open. ``currentBack`` is
|
|
declared by EXACTLY ONE ``let``, and the open resets it
|
|
(``currentBack = back;``) BEFORE the no-skeleton guard — so even
|
|
the ``window.open`` fallback path (which early-returns there)
|
|
reads the stored value of THIS open."""
|
|
js = _js()
|
|
assert js.count("let currentBack = null;") == 1
|
|
assert len(re.findall(r"let\s+currentBack\s*=", js)) == 1
|
|
open_body = _fn(js, "openDocumentModal")
|
|
reset_i = open_body.find("currentBack = back;")
|
|
guard_i = open_body.find("if (!modalEl || !titleEl || !metaEl || !contentEl) {")
|
|
assert -1 < reset_i < guard_i, (
|
|
"the reset must land at the TOP of every open — before the "
|
|
"no-skeleton guard (the fallback path reads the stored value)"
|
|
)
|
|
# Exactly one reset site (no second writer anywhere in the module).
|
|
assert js.count("currentBack = back;") == 1
|
|
|
|
|
|
def test_fullpageurl_appends_encoded_back_only_when_truthy() -> None:
|
|
"""REGRESSION: the ``back`` param leaking into a null/absent open
|
|
(the Sources path must stay byte-identical — the viewer's
|
|
/sources.html default) or the encoding drifting from the
|
|
app.js::documentUrl contract. The signature is
|
|
``fullPageUrl(source, path, back = null)`` and the ONLY writer
|
|
after the base URL is the guarded ``if (back)`` append —
|
|
percent-encoded, exactly the way documentUrl documents it."""
|
|
js = _js()
|
|
assert "function fullPageUrl(source, path, back = null)" in js
|
|
fn = _fn(js, "fullPageUrl")
|
|
assert 'if (back) url += "&back=" + encodeURIComponent(back);' in fn
|
|
# The base URL is built first; the guarded append is the only
|
|
# ``&back=`` writer (a null back bakes no param into the URL).
|
|
base_i = fn.find('"/document.html?source=" + encodeURIComponent(')
|
|
back_i = fn.find('"&back=" + encodeURIComponent(back)', base_i)
|
|
assert -1 < base_i < back_i, "the back append must follow the base URL"
|
|
base_part = fn.split('"&back="')[0]
|
|
assert "back=" not in base_part, (
|
|
"no &back= may be baked into the base URL (null back = no param)"
|
|
)
|
|
|
|
|
|
def test_open_document_modal_defaults_back_to_null() -> None:
|
|
"""REGRESSION: a non-null default for the ``back`` argument.
|
|
``openDocumentModal(source, path, trigger = null, back = null)`` —
|
|
a call WITHOUT ``back`` (the Sources table's
|
|
``openDocumentModal(d.source, d.path, link)``) must leave the
|
|
"Full page" URL byte-identical to pre-phase (the viewer's
|
|
/sources.html default — the byte-identity is the contract)."""
|
|
js = _js()
|
|
assert (
|
|
"export function openDocumentModal(source, path, trigger = null, back = null)"
|
|
in js
|
|
)
|
|
|
|
|
|
def test_fallback_and_anchor_both_read_the_stored_back() -> None:
|
|
"""REGRESSION: a call site passing ``back`` positionally from a
|
|
caller, or dropping the third argument (a dropped arg loses the
|
|
origin — the chat path would land on "/sources.html"). BOTH the
|
|
no-skeleton ``window.open`` fallback and the ``#doc-modal-open``
|
|
("Full page") anchor read ``currentBack``: exactly three
|
|
``fullPageUrl(`` sites in the module (one definition + two call
|
|
sites), and no call site without the stored variable."""
|
|
js = _js()
|
|
assert 'window.open(fullPageUrl(source, path, currentBack), "_blank", "noopener");' in js
|
|
assert "openEl.href = fullPageUrl(source, path, currentBack);" in js
|
|
assert "fullPageUrl(source, path)" not in js, "a dropped third argument loses the back"
|
|
assert "fullPageUrl(source, path, back)" not in js, ("call sites must read the stored variable")
|
|
assert js.count("fullPageUrl(") == 3, "1 definition + 2 call sites"
|
|
|
|
|
|
# ---------- phase 153 (task 03): the URL contract, behavior-pinned under node ----------
|
|
|
|
|
|
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
|
|
def test_fullpageurl_null_back_is_byte_identical_to_the_absent_and_pre_phase() -> None:
|
|
"""The URL contract's load-bearing byte-identity, executed under
|
|
node (the real function, not a re-implementation):
|
|
|
|
* ``back = null`` and an ABSENT third argument produce the same
|
|
string, and that string is byte-identical to the EXACT pre-phase
|
|
string (no ``&back=`` anywhere — the Sources path must never gain
|
|
the param);
|
|
* ``back = "/"`` (a chat open) appends exactly ``&back=%2F`` to
|
|
that same pre-phase string (the encoding pinned byte-for-byte).
|
|
|
|
A refactor that bakes a ``back=`` into the base, reorders the
|
|
param, or re-encodes the values breaks one of the exact strings."""
|
|
fn = _fn(_js(), "fullPageUrl")
|
|
out = _run_node(
|
|
fn
|
|
+ "\nconsole.log(fullPageUrl('H omelab', 'a/b.md'));"
|
|
+ "\nconsole.log(fullPageUrl('H omelab', 'a/b.md', null));"
|
|
+ "\nconsole.log(fullPageUrl('H omelab', 'a/b.md', '/'));"
|
|
)
|
|
absent, null_back, chat = out.splitlines()
|
|
pre_phase = "/document.html?source=H%20omelab&path=a%2Fb.md"
|
|
assert null_back == absent, "back = null must be byte-identical to an absent back"
|
|
assert absent == pre_phase, (
|
|
"a null/absent back must keep the URL byte-identical to pre-phase"
|
|
)
|
|
assert "back=" not in absent, "no &back= may be present for a null/absent back"
|
|
assert chat == pre_phase + "&back=%2F", (
|
|
"back = '/' must append exactly &back=%2F to the pre-phase string"
|
|
)
|
|
|
|
|
|
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
|
|
def test_open_document_modal_no_skeleton_fallback_carries_the_stored_back() -> None:
|
|
"""The no-skeleton ``window.open`` fallback, executed under node
|
|
(the real ``openDocumentModal`` with the module state stubbed):
|
|
a chat open (``back = "/"``) lands in the URL exactly (``%2F``),
|
|
and a Sources-style open (``back`` omitted — the 3-arg call)
|
|
keeps the URL byte-identical (no ``&back=`` at all). The
|
|
fallback is the ``window.open(url, "_blank", "noopener")`` shape,
|
|
and the early ``return`` means neither open touches the
|
|
history."""
|
|
fn = _fn(_js(), "openDocumentModal")
|
|
url_fn = _fn(_js(), "fullPageUrl")
|
|
script = (
|
|
"let currentBack = null;\n"
|
|
"const modalEl = null, titleEl = null, metaEl = null, contentEl = null, openEl = null;\n"
|
|
"const opened = [];\n"
|
|
"globalThis.window = { open: (u, t, f) => opened.push([u, t, f]) };\n"
|
|
+ url_fn
|
|
+ "\n"
|
|
+ fn
|
|
+ "\nopenDocumentModal('H omelab', 'a/b c.md', null, '/');\n"
|
|
+ "\nopenDocumentModal('S2', 'p2.md', null);\n"
|
|
+ "\nconsole.log(JSON.stringify(opened));\n"
|
|
)
|
|
out = json.loads(_run_node(script))
|
|
assert out == [
|
|
[
|
|
"/document.html?source=H%20omelab&path=a%2Fb%20c.md&back=%2F",
|
|
"_blank",
|
|
"noopener",
|
|
],
|
|
# the omitted back (the Sources table's 3-arg call) stays byte-identical
|
|
["/document.html?source=S2&path=p2.md", "_blank", "noopener"],
|
|
]
|
|
|
|
|
|
@pytest.mark.skipif(not HAVE_NODE, reason="node not available")
|
|
def test_open_document_modal_full_page_anchor_carries_the_stored_back() -> None:
|
|
"""The ``#doc-modal-open`` ("Full page") anchor on a skeleton
|
|
open, executed under node (the real ``openDocumentModal`` with the
|
|
modal elements + history + fetch chain stubbed): a chat open
|
|
(``back = "/"``) sets the anchor's ``href`` to the pre-phase string
|
|
+ exactly ``&back=%2F``; a follow-up Sources-style open (``back``
|
|
omitted — the defensive re-open-while-open branch, which updates
|
|
the SAME anchor) resets it to the byte-identical no-param string.
|
|
The open's first push is a single state-only entry (the phase-144
|
|
contract stands); the defensive re-open replaces in place (no
|
|
second push)."""
|
|
fn = _fn(_js(), "openDocumentModal")
|
|
url_fn = _fn(_js(), "fullPageUrl")
|
|
script = (
|
|
"let currentBack = null;\n"
|
|
"let triggerEl = null;\n"
|
|
"let fetchSeq = 0;\n"
|
|
"let entry = null;\n"
|
|
"let lastPrev = null;\n"
|
|
"let closingViaBack = false;\n"
|
|
"const modalEl = { hidden: true };\n"
|
|
"const titleEl = {}, metaEl = { replaceChildren() {} };\n"
|
|
"const contentEl = { replaceChildren() {}, focus() {} };\n"
|
|
"const anchor = { href: '', hidden: true };\n"
|
|
"const openEl = anchor;\n"
|
|
"const hist = [];\n"
|
|
"globalThis.history = { state: null,"
|
|
" pushState(s, t) { hist.push(['push', s.modal === 'doc', s.source, s.path, t]); },"
|
|
" replaceState(s, t) { hist.push(['replace', s.modal === 'doc', s.source, s.path]); } };\n"
|
|
"globalThis.document = { addEventListener() {}, removeEventListener() {} };\n"
|
|
"globalThis.fetch = () => Promise.resolve({ ok: true, json: async () => ({}) });\n"
|
|
"function onModalKeydown() {}\n"
|
|
"function showLoading() {}\n"
|
|
"function setDesc() {}\n"
|
|
"function renderNotFound() {}\n"
|
|
"function wireCopyPath() {}\n"
|
|
"function renderDocument() {}\n"
|
|
'function contentUrl(s, p) { return ""; }\n'
|
|
+ url_fn
|
|
+ "\n"
|
|
+ fn
|
|
+ "\nopenDocumentModal('H omelab', 'a/b c.md', null, '/');\n"
|
|
+ "\nconst chatHref = anchor.href;\n"
|
|
+ "\nopenDocumentModal('S2', 'p2.md', null);\n"
|
|
+ "\nconsole.log(JSON.stringify({ chatHref, sourcesHref: anchor.href,"
|
|
+ " visible: !modalEl.hidden, hist }));\n"
|
|
)
|
|
out = json.loads(_run_node(script))
|
|
pre_phase = "/document.html?source=H%20omelab&path=a%2Fb%20c.md"
|
|
assert out["chatHref"] == pre_phase + "&back=%2F", (
|
|
"a chat open must set the Full-page anchor to the pre-phase string + exactly &back=%2F"
|
|
)
|
|
assert out["sourcesHref"] == "/document.html?source=S2&path=p2.md", (
|
|
"a Sources-style open (back omitted) must reset the anchor to the byte-"
|
|
"identical no-param string"
|
|
)
|
|
assert out["visible"] is True
|
|
# phase 144 stands: one state-only push, then the defensive
|
|
# re-open replaces IN PLACE (no second push).
|
|
assert out["hist"] == [
|
|
["push", True, "H omelab", "a/b c.md", ""],
|
|
["replace", True, "S2", "p2.md"],
|
|
]
|
|
|