"""Phase 105 story E2E (Playwright): the per-source hidden-folders toggle — the "Hidden" checkbox next to the "Ignore paths" button makes a source's dot-prefixed paths indexable (``/git-sources.html``). Story source: ``TODO.md`` L3 (owner roadmap confirmation 2026-09-14 — TODO-derived, no separate user-story file). Run in isolation (DB must be up: ``podman compose up -d db``): uv run pytest tests/e2e/test_hidden_folders_toggle.py -v --no-cov The story gate proves the whole item end to end through the REAL page + REAL API + REAL sync pipeline (the in-app Sync's background task, mock LLM). **No git, no network** — the suite's single source is a local directory row (``kind="local"``, phase 38) over a fixture tree with one visible file, one hidden-folder file (the phase-105 subject), and one ``.venv`` file (``EXCLUDED_DIRS`` — never indexed in either state, A1). The module app boots with one deterministic ``BOR_GIT_SOURCES`` env URL (the ``test_source_ignore_paths.py`` module-env pattern) that is NEVER synced or cloned: every sync in this suite runs with the local DB row in place (DB rows win over the env fallback), and the one env-fallback test never triggers a sync. Contract under test: * anonymous: the ``#git-sources-gate`` sign-in gate, the manager hidden, NO ``/api/git-sources`` call on load, 403 on ``GET``/``POST /api/git-sources`` AND ``PATCH /api/git-sources/{id}`` with a bool-only body (the phase-89 anonymous pin extended to the toggle payload); * A4: with the default row, a sync indexes ``visible.md`` ONLY — ``detail.files`` counts one, ``.hidden/note.md`` has no ``documents`` row, the checkbox renders UNCHECKED and no "hidden on" tag; * A1: flipping the checkbox on (the real click) → the PATCH 200 lands (the "hidden on" tag appears, the announcer ``role=status`` fires the confirmation AFTER the reload line) → sync → ``.hidden/note.md`` IS indexed (``documents`` row present; the KB catalog lists it — the tree/catalog is DB-driven, no extra surface) and ``.venv/junk.md`` is STILL absent (EXCLUDED_DIRS in both states); the checkbox re-renders CHECKED (server state); * A2: flipping it OFF (real click) → sync → ``detail.pruned`` includes the hidden doc, the catalog no longer lists it, the tag is gone; * A3: the env-fallback view (table-empty state) renders the "from .env" tag with NO Hidden checkbox and NO "Ignore paths" button; * a11y + error surface: the checkbox has a full accessible name containing the source location (``Index hidden folders for local source: …``), is keyboard-focusable, the tag text is "hidden on" (never color alone); ``#git-sources-hidden-error`` exists with ``role="alert"`` and stays ``hidden`` through the happy path — and a network-aborted PATCH shows the canned "not changed" message and reverts the box to the server state (§7.4 never-stale). Test → contract mapping (one test per bullet, the phase-89 suite's shape): 1. ``test_anonymous_gate_and_403s`` 2. ``test_hidden_off_by_default`` 3. ``test_toggle_on_indexes_hidden_folders`` 4. ``test_toggle_off_prunes_hidden`` 5. ``test_env_fallback_rows_have_no_toggle`` 6. ``test_toggle_a11y_and_error_surface`` """ from __future__ import annotations import os import subprocess import sys import time import uuid from collections.abc import Iterator from pathlib import Path from typing import Any import pytest from playwright.sync_api import Page, expect from sqlalchemy import text from app.db import SessionLocal from e2e.auth_helpers import login from e2e.conftest import ( ADMIN_PASSWORD, SESSION_SECRET, USE_REAL_LLM, _wait_http, ) REPO = Path(__file__).resolve().parents[2] # Phase 79 (task 04, full inventory): the conftest session app owns its # port in a combined run — this module app binds its own port instead # (a same-port second uvicorn dies on bind and would drive the wrong # server). Env-overridable. APP_PORT = int(os.environ.get("E2E_APP_PORT_HIDDEN", "8141")) APP_URL = f"http://127.0.0.1:{APP_PORT}" GIT_SOURCES_URL = "/git-sources.html" #: The module app's ``BOR_GIT_SOURCES`` — one deterministic URL that is #: NEVER cloned (the env-fallback test reads it; the sync tests all run #: with the local DB row in place, so the env list never reaches a #: clone). ENV_SOURCE = "https://github.com/reese/env-alpha.git" #: The three fixture files (source-relative POSIX paths — exactly the #: strings ``documents.path`` stores). VISIBLE_MD = "visible.md" HIDDEN_NOTE = ".hidden/note.md" VENV_JUNK = ".venv/junk.md" #: ``POST /api/sync`` → terminal ``GET /api/sync/status`` (the #: test_sync_button.py polling idiom) — real import of ≤2 small files #: against the mock LLM; generous budget. SYNC_TIMEOUT_S = 60.0 SYNC_TICK_S = 2.0 # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- @pytest.fixture(scope="module") def source_dir(tmp_path_factory: pytest.TempPathFactory) -> Path: """The local source's on-disk tree (task 06 step 1). Module-scoped (the same reasoning as ``test_sync_button.py``'s module-scoped fixture note): ``tmp_path`` is function-scoped while the module-scoped app + the seeded row reference the dir for the module's lifetime, so it is built under ``tmp_path_factory`` (the same pytest-managed temp area, module-safe). Plain files — no git: a ``kind="local"`` row is walked directly by the sync (phase 38). """ root = tmp_path_factory.mktemp("hidden_src") (root / ".hidden").mkdir() (root / ".venv").mkdir() (root / VISIBLE_MD).write_text( "# Visible\n" "\n" "The visible doc — indexed in EVERY state (flag on and off).\n", encoding="utf-8", ) # The phase-105 subject: a file INSIDE a hidden (dot-prefixed) # folder — indexed only when the row's flag is on (A1). (root / HIDDEN_NOTE).write_text( "# Hidden note\n" "\n" "A file inside a hidden (dot) folder — the toggle's subject:\n" "indexed, embedded, and summarized like any visible file when\n" "the flag is on; pruned from the KB when it flips off (A2).\n", encoding="utf-8", ) # EXCLUDED_DIRS content: ``.venv`` is a cache, never content — # skipped in BOTH flag states (A1). (root / VENV_JUNK).write_text( "venv junk — EXCLUDED_DIRS content: never indexed, both states.\n", encoding="utf-8", ) return root @pytest.fixture(scope="module") def app_server(mock_llm: int, source_dir: Path) -> Iterator[str]: """The real app under test — per-module env (the conftest pattern, module-scoped): one deterministic ``BOR_GIT_SOURCES`` URL (the env fallback's subject — never cloned), module-scratch checkouts/ upload dirs, the mock LLM. No git anywhere in this suite's path.""" env = dict(os.environ) env.pop("DEBUGPY", None) env["BOR_ENVIRONMENT"] = "e2e" env["BOR_STATIC_DIR"] = str(REPO / "frontend") env["BOR_LLM_BASE_URL"] = ( "https://aipi.reeseapps.com/v1" if USE_REAL_LLM else f"http://127.0.0.1:{mock_llm}/v1" ) # Mock-calibrated threshold (conftest pattern) — no chat turn is # ever sent in this suite, but the app boots with the same env # shape. env["BOR_RELEVANCE_THRESHOLD"] = "0.30" env.setdefault( "BOR_DATABASE_URL", "postgresql+psycopg://reese:reese@localhost:5432/brain_of_reese", ) # Phase 16: admin auth must be set or create_app() refuses to boot. env["BOR_ADMIN_PASSWORD"] = ADMIN_PASSWORD env["BOR_SESSION_SECRET"] = SESSION_SECRET # The empty-table env fallback (only read while ``git_sources`` is # EMPTY — the env-fallback test; never cloned in this suite). env["BOR_GIT_SOURCES"] = ENV_SOURCE # Module-scratch dirs (never reached by this suite's local-row # syncs — kept explicit so a shared checkouts dir cannot leak rows # into the walk). scratch = source_dir.parent env["BOR_SOURCES_DIR"] = str(scratch / "checkouts") env["BOR_UPLOAD_DIR"] = str(scratch / "uploads") proc = subprocess.Popen( [sys.executable, "-m", "uvicorn", "app.main:app", "--host", "127.0.0.1", "--port", str(APP_PORT), "--log-level", "warning"], cwd=REPO, env=env, ) try: _wait_http(f"{APP_URL}/api/health") yield APP_URL finally: proc.terminate() try: proc.wait(timeout=10) except subprocess.TimeoutExpired: proc.kill() @pytest.fixture(scope="module") def app_url(app_server: str) -> str: return app_server def _truncate_all() -> None: """Fresh registry + KB per test (the E2E isolation pattern, the test_sync_button.py module-env DSN): the sync's counts and the catalog must be each test's own doing.""" with SessionLocal() as db: db.execute( text( "TRUNCATE chunks, documents, query_log, kb_overview, " "git_sources" ) ) db.commit() @pytest.fixture(autouse=True) def _clean(db_ready: None) -> Iterator[None]: """This suite owns ``git_sources`` AND the KB tables (the E2E isolation pattern): suites run in isolation but share one Postgres, and a leftover row would flip the app from the ``BOR_GIT_SOURCES`` env fallback to the DB list (and a leftover document would skew the prune counts). Empty BOTH before and after every test.""" _truncate_all() yield _truncate_all() # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _admin_git_sources_page(page: Page, app_url: str) -> None: """Real form login landing on the git sources page (admin settled: Sign out visible, the manager revealed by the page module).""" login(page, app_url, next=GIT_SOURCES_URL) expect(page).to_have_url(app_url + GIT_SOURCES_URL, timeout=30_000) expect(page.locator("#sign-out-btn")).to_be_visible(timeout=15_000) expect(page.locator("#git-sources-gate")).to_be_hidden() expect(page.locator("#git-sources-content")).to_be_visible() def _seed_local_source(page: Page, app_url: str, source_dir: Path) -> str: """(Re)seed the stored row for the module fixture dir via the REAL admin API (201) — ``kind="local"`` — with NO ``ignore_paths`` and NO ``include_hidden`` (A4: absent at create → stored ``False``) — then reload the view so the page's table re-fetches the row (the API seed is server-side: the mounted view does not know about it without a re-show). Returns the stored (expanded) path — the row's ``url`` location value, the same string the page renders and aria-labels.""" r = page.request.post( f"{app_url}/api/git-sources", data={"kind": "local", "path": str(source_dir)}, ) assert r.status == 201, r.text body = r.json() assert body["ignore_paths"] == [] # the default list, round-tripped assert body["include_hidden"] is False # A4: default off page.reload() _admin_git_sources_page(page, app_url) return body["url"] def run_sync(page: Page, app_url: str) -> dict[str, Any]: """``POST /api/sync`` → poll ``GET /api/sync/status`` to a terminal state (the test_sync_button.py polling idiom — ~2 s ticks, 60 s budget). Returns the terminal status body.""" r = page.request.post(f"{app_url}/api/sync") assert r.status == 202, r.text deadline = time.monotonic() + SYNC_TIMEOUT_S body: dict[str, Any] = {} while True: s = page.request.get(f"{app_url}/api/sync/status") assert s.status == 200, s.text body = s.json() if body["state"] in ("success", "failed"): return body assert time.monotonic() < deadline, ( f"sync did not reach a terminal state: {body}" ) time.sleep(SYNC_TICK_S) def _catalog_paths(page: Page, app_url: str) -> list[str]: """The RAG catalog's data source (``GET /api/docs`` — the Sources page's table): every ``documents.path`` (source-relative POSIX). The tree/catalog is DB-driven — a newly indexed hidden document appears here automatically (no extra surface).""" r = page.request.get(f"{app_url}/api/docs") assert r.status == 200, r.text return [d["path"] for d in r.json()["documents"]] def _db_document_paths() -> list[str]: """Every ``documents.path`` straight from the DB — the literal "no ``documents`` row" assertion (the catalog is the same table, read through the API).""" with SessionLocal() as db: rows = db.execute(text("SELECT path FROM documents")).fetchall() return [row[0] for row in rows] def _row(page: Page, value: str) -> Any: """The table row whose mono location cell shows ``value`` (re-resolved on every call — the row re-renders from the server after each successful toggle).""" return page.locator("#git-sources-tbody tr", has_text=value) def _hidden_box(page: Page, value: str) -> Any: """The row's Hidden checkbox (fresh locator — the element is re-created by the post-toggle re-render).""" return _row(page, value).locator(".git-source-hidden-box") def _flip_hidden(page: Page, value: str, wanted_on: bool) -> None: """Click the row's Hidden checkbox and wait for the §7.4 success lifecycle to settle: the PATCH 200 landed, the row re-rendered from the server, and the CONFIRMATION is the last announcer message (the reload's "N sources listed." cannot overwrite it — the phase-89 order).""" _hidden_box(page, value).click() tag = _row(page, value).locator(".git-source-hidden-count") if wanted_on: expect(tag).to_have_text("hidden on", timeout=30_000) else: expect(tag).to_have_count(0, timeout=30_000) expect( page.locator("#git-sources-announcer") ).to_have_text( f"Hidden folders {'enabled' if wanted_on else 'disabled'} for {value}." ) # --------------------------------------------------------------------------- # 1. Anonymous: gate, inert manager, no API calls, 403s (incl. the # bool-only toggle PATCH) # --------------------------------------------------------------------------- def test_anonymous_gate_and_403s(page: Page, app_url: str, db_ready: None) -> None: page.set_default_timeout(30_000) # Track every /api/git-sources request the page itself makes — the # gate must be reached WITHOUT touching the admin API (the # test_source_ignore_paths.py pattern). api_calls: list[str] = [] page.on( "request", lambda r: api_calls.append(r.url) if "/api/git-sources" in r.url else None, ) page.goto(app_url + GIT_SOURCES_URL) expect(page.locator("#sign-in-link")).to_be_visible(timeout=15_000) # The sign-in gate (the #sources-gate pattern, phase 16)… gate = page.locator("#git-sources-gate") expect(gate).to_be_visible() expect(gate).to_contain_text("Sign in to manage the git sources") expect( gate.locator("a[href='/login.html?next=/git-sources.html']") ).to_have_count(1) # …and the manager is absent/inert: table + form + env note, all # inside the hidden #git-sources-content. expect(page.locator("#git-sources-content")).to_be_hidden() expect(page.locator("#git-sources-table")).to_be_hidden() expect(page.locator("#git-source-form")).to_be_hidden() expect(page.locator("#git-sources-env-note")).to_be_hidden() # The gate never called the admin API… assert api_calls == [], f"anonymous page called the git sources API: {api_calls}" # …and the API 403s anonymous callers on GET/POST AND the # phase-105 toggle PATCH with a bool-only body (the row's list is # untouched — the toggle's exact payload is gated too). assert page.request.get(f"{app_url}/api/git-sources").status == 403 assert ( page.request.post( f"{app_url}/api/git-sources", data={"url": ENV_SOURCE} ).status == 403 ) assert ( page.request.patch( f"{app_url}/api/git-sources/{uuid.uuid4()}", data={"include_hidden": True}, ).status == 403 ) # --------------------------------------------------------------------------- # 2. A4: the default row is byte-identical to pre-phase-105 — the sync # indexes visible.md ONLY; the checkbox renders UNCHECKED, no tag # --------------------------------------------------------------------------- def test_hidden_off_by_default( page: Page, app_url: str, db_ready: None, source_dir: Path ) -> None: page.set_default_timeout(30_000) _admin_git_sources_page(page, app_url) stored_path = _seed_local_source(page, app_url, source_dir) expect(page.locator("#git-sources-tbody tr")).to_have_count(1) # A4: the default row renders the NEW control in its default state — # the checkbox UNCHECKED (checked comes only from server state) and # NO "hidden on" tag (pre-phase-105 surface + the control). box = _hidden_box(page, stored_path) expect(box).to_have_count(1) expect(box).not_to_be_checked() expect(_row(page, stored_path).locator(".git-source-hidden-count")).to_have_count(0) # The real sync: ``visible.md`` ONLY — the walk skipped the # dot-prefixed ``.hidden/`` component (A4 byte-identical default) # AND ``.venv/`` (EXCLUDED_DIRS in both states). sync = run_sync(page, app_url) assert sync["state"] == "success", sync detail = sync["detail"] assert detail["files"] == 1, detail assert detail["added"] == 1, detail assert detail["pruned"] == 0, detail assert detail["errors"] == 0, detail # The catalog holds exactly the visible file — no ``documents`` row # for the hidden note (the direct DB read makes "no row" literal)… paths = _catalog_paths(page, app_url) assert VISIBLE_MD in paths, f"visible.md missing from the catalog: {paths}" assert HIDDEN_NOTE not in paths, ( f"hidden note leaked into the catalog with the flag off: {paths}" ) assert VENV_JUNK not in paths, f"EXCLUDED_DIRS leaked into the catalog: {paths}" assert _db_document_paths() == [VISIBLE_MD] # --------------------------------------------------------------------------- # 3. A1: the real click flips the flag on — the PATCH 200 lands (tag + # last-announce confirmation), the next sync indexes the hidden # folder; EXCLUDED_DIRS stays excluded; the box re-renders CHECKED # --------------------------------------------------------------------------- def test_toggle_on_indexes_hidden_folders( page: Page, app_url: str, db_ready: None, source_dir: Path ) -> None: page.set_default_timeout(30_000) _admin_git_sources_page(page, app_url) stored_path = _seed_local_source(page, app_url, source_dir) # Baseline: the default sync indexes the visible file only. first = run_sync(page, app_url) assert first["state"] == "success", first assert first["detail"]["files"] == 1, first["detail"] # A1: flip the checkbox on (the real click) — the §7.4 lifecycle: # PATCH 200 → the "hidden on" tag appears in the source cell and # the confirmation is the LAST announcer message (the reload's # "1 source listed." landed first). _flip_hidden(page, stored_path, wanted_on=True) # The checkbox re-renders CHECKED from the server state (never a # local flip) and the API round-trips include_hidden: true. expect(_hidden_box(page, stored_path)).to_be_checked() r = page.request.get(f"{app_url}/api/git-sources") assert r.status == 200, r.text sources = r.json()["sources"] assert [s["include_hidden"] for s in sources] == [True] assert [s["ignore_paths"] for s in sources] == [[]] # the list untouched # The NEXT sync indexes the hidden-folder file like any visible # file (embedded + chunked by the mock LLM)… second = run_sync(page, app_url) assert second["state"] == "success", second detail = second["detail"] assert detail["files"] == 2, detail assert detail["added"] == 1, detail # the hidden note only assert detail["unchanged"] == 1, detail # visible.md # …and ``.venv/junk.md`` is STILL absent — EXCLUDED_DIRS content is # never indexed, in either state (A1). paths = _catalog_paths(page, app_url) assert HIDDEN_NOTE in paths, ( f"hidden note missing from the catalog after the on-sync: {paths}" ) assert VISIBLE_MD in paths, paths assert VENV_JUNK not in paths, ( f"EXCLUDED_DIRS leaked into the catalog with the flag on: {paths}" ) assert set(_db_document_paths()) == {VISIBLE_MD, HIDDEN_NOTE} # --------------------------------------------------------------------------- # 4. A2: flipping the flag OFF prunes the previously indexed hidden doc # from the KB on the next sync # --------------------------------------------------------------------------- def test_toggle_off_prunes_hidden( page: Page, app_url: str, db_ready: None, source_dir: Path ) -> None: page.set_default_timeout(30_000) _admin_git_sources_page(page, app_url) stored_path = _seed_local_source(page, app_url, source_dir) # On + sync: the hidden note is in the KB. _flip_hidden(page, stored_path, wanted_on=True) first = run_sync(page, app_url) assert first["state"] == "success", first assert first["detail"]["files"] == 2, first["detail"] assert first["detail"]["added"] == 2, first["detail"] assert HIDDEN_NOTE in _catalog_paths(page, app_url) # A2: flip it OFF (the real click) — the previously indexed hidden # file leaves the KB on the next sync (the seen-set prune — the # phase-89 A2 / A9 precedent; the tag is gone). _flip_hidden(page, stored_path, wanted_on=False) r = page.request.get(f"{app_url}/api/git-sources") assert [s["include_hidden"] for s in r.json()["sources"]] == [False] second = run_sync(page, app_url) assert second["state"] == "success", second detail = second["detail"] assert detail["pruned"] == 1, detail # the hidden doc assert detail["files"] == 1, detail # the walk is back to visible-only paths = _catalog_paths(page, app_url) assert HIDDEN_NOTE not in paths, ( f"the hidden doc survived the flag-off prune: {paths}" ) assert VISIBLE_MD in paths, f"visible.md must survive the prune: {paths}" assert _db_document_paths() == [VISIBLE_MD] # --------------------------------------------------------------------------- # 5. A3: env-fallback rows have no toggle (no DB row to store a flag on) # --------------------------------------------------------------------------- def test_env_fallback_rows_have_no_toggle( page: Page, app_url: str, db_ready: None ) -> None: page.set_default_timeout(30_000) # The autouse fixture truncated git_sources — the table is EMPTY, # so the module app's BOR_GIT_SOURCES URL is the effective list # (the test_source_ignore_paths.py module-env pattern). _admin_git_sources_page(page, app_url) expect(page.locator("#git-sources-tbody tr")).to_have_count(1) row = page.locator("#git-sources-tbody tr") expect(row).to_contain_text(ENV_SOURCE) # The env-fallback surface: the "from .env" tag (never color # alone)… expect(row.locator(".git-source-env-tag")).to_have_count(1) expect(row.locator(".git-source-env-tag")).to_have_text("from .env") # …and A3: NO per-row controls at all — neither Remove, nor the # phase-89 "Ignore paths" button, nor the phase-105 Hidden checkbox # (and no "hidden on" tag). expect(row.locator(".git-source-remove")).to_have_count(0) expect(row.locator(".git-source-ignore")).to_have_count(0) expect(row.locator(".git-source-hidden")).to_have_count(0) expect(row.locator(".git-source-hidden-box")).to_have_count(0) expect(row.locator(".git-source-hidden-count")).to_have_count(0) # The env-fallback note explains the active list's origin… expect(page.locator("#git-sources-env-note")).to_be_visible() # …and the API agrees: from_env true, null ids, the env URL, an # empty ignore list, and include_hidden False (no DB row to store a # list or a flag on — the GitSourceRow contract, task 03). r = page.request.get(f"{app_url}/api/git-sources") assert r.status == 200, r.text body = r.json() assert body["from_env"] is True assert [s["url"] for s in body["sources"]] == [ENV_SOURCE] assert all( s["id"] is None and s["ignore_paths"] == [] and s["include_hidden"] is False for s in body["sources"] ) # --------------------------------------------------------------------------- # 6. a11y + the error surface: the accessible name, keyboard focus, the # text tag, and the role=alert line (hidden on the happy path, the # canned message + revert on a network failure) # --------------------------------------------------------------------------- def test_toggle_a11y_and_error_surface( page: Page, app_url: str, db_ready: None, source_dir: Path ) -> None: page.set_default_timeout(30_000) _admin_git_sources_page(page, app_url) stored_path = _seed_local_source(page, app_url, source_dir) box = _hidden_box(page, stored_path) expect(box).to_have_count(1) # The full accessible name contains the source location (the # aria-label — the ONLY place the value appears, setAttribute, # never innerHTML)… expect(box).to_have_attribute( "aria-label", f"Index hidden folders for local source: {stored_path}" ) expect( page.get_by_role( "checkbox", name=f"Index hidden folders for local source: {stored_path}" ) ).to_have_count(1) # …it is keyboard-reachable — a plain Tab sequence from the page # top reaches it (one full focus cycle covers every focusable # element; the visible ring is the GLOBAL :focus-visible rule, # styles.css — unit-pinned, no per-control rule)… for _ in range(60): page.keyboard.press("Tab") if box.evaluate("el => el === document.activeElement"): break expect(box).to_be_focused() # …and it receives programmatic focus too (the a11y trees agree). box.focus() expect(box).to_be_focused() # …and the visible "Hidden" label wraps the box (text + control — # never color or icon alone). label = page.locator("label.git-source-hidden") expect(label).to_have_count(1) expect(label).to_contain_text("Hidden") # The page-level error line exists, is a real role=alert, and stays # hidden through the happy path. error = page.locator("#git-sources-hidden-error") expect(error).to_have_count(1) assert error.get_attribute("role") == "alert" expect(error).to_be_hidden() # The tag is TEXT (never color alone — WCAG 1.4.1): flip on… _flip_hidden(page, stored_path, wanted_on=True) expect( _row(page, stored_path).locator(".git-source-hidden-count") ).to_have_text("hidden on") expect(error).to_be_hidden() # the happy path never touches the line # §7.4 failure path: a network-level abort of the PATCH → the # canned "not changed" message in the role=alert line and the box # reverts to the SERVER state (still ON — the request never # landed) and re-enables. The tag stays (the server is unchanged). def _abort_patch(route: Any) -> None: if route.request.method == "PATCH": route.abort() else: route.continue_() page.route("**/api/git-sources/**", _abort_patch) try: _hidden_box(page, stored_path).click() expect(error).to_be_visible(timeout=30_000) expect(error).to_have_text( "Could not reach the server — the setting was not changed." ) expect( _row(page, stored_path).locator(".git-source-hidden-count") ).to_have_count(1) # the server state is unchanged: still on reverted = _hidden_box(page, stored_path) expect(reverted).to_be_checked() # reverted to the server state expect(reverted).to_be_enabled() # re-enabled — never stale finally: page.unroute("**/api/git-sources/**")