Files
browser-cli/tests/test_extension_error_page_handling.py
T
daniel156161 fd5447cbb9
Testing / remote-protocol-compat (0.9.3) (push) Successful in 42s
Testing / remote-protocol-compat (0.9.5) (push) Successful in 44s
Package Extension / package-extension (push) Successful in 43s
Build & Publish Package / publish (push) Successful in 43s
Testing / test (push) Successful in 45s
refactor(api): namespaced SDK + dedicated transport layer
Restructure the Python API and internals around composable namespaces and
a standalone transport/endpoint layer. Bump to 0.12.0.

Python API:
- Replace flat methods (b.tabs_list(), b.group_list()) with namespaces:
  b.nav, b.tabs, b.groups, b.windows, b.dom, b.extract, b.page, b.storage,
  b.cookies, b.session, b.perf, b.extension.
- Shrink browser_cli/__init__.py to a thin composition root; move all
  behaviour into browser_cli/sdk/ (one module per namespace + factories,
  base, routing).

Internals:
- Add browser_cli/transport.py and remote_transport.py to isolate IPC from
  command logic; client.py now delegates instead of owning transport.
- Add browser_cli/endpoints.py for endpoint resolution and
  browser_cli/errors.py for shared error types.
- Extract markdown rendering into browser_cli/markdown.py (out of extract).
- Add USER_AGENT to version_manager.

Tooling & tests:
- Add justfile with common dev tasks.
- Update CLI commands and demo to the namespaced API.
- Rework tests for the new layout; add test_transport.py and
  test_refactor_boundaries.py to lock in module boundaries.

BREAKING CHANGE: flat API methods are removed in favour of namespaces
(e.g. b.tabs_list() -> b.tabs.list(), b.group_list() -> b.groups.list()).
2026-06-11 13:58:41 +02:00

174 lines
8.6 KiB
Python

from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def test_extension_retries_error_page_script_injection_before_failing():
# core.ts was split into a core/ subfolder during the structure refactor:
# the URL/error classifiers live in core/errors.ts and the executeScript
# retry wrapper (which calls isTransientScriptError) lives in core/scripting.ts.
errors = (ROOT / "extension" / "src" / "core" / "errors.ts").read_text()
scripting = (ROOT / "extension" / "src" / "core" / "scripting.ts").read_text()
assert "isBrowserErrorUrl" in errors
assert "isErrorPageScriptError" in errors
assert "chrome-error://" in errors
assert "edge-error://" in errors
assert "brave-error://" in errors
assert "about:neterror" in errors
assert "about:certerror" in errors
assert "isTransientScriptError(e)" in scripting
def test_read_only_dom_commands_have_error_page_fallbacks():
dom = (ROOT / "extension" / "src" / "commands" / "dom.ts").read_text()
assert "fallbackForErrorPageDomOp" in dom
assert 'case "domExists":' in dom
assert "return false;" in dom
assert 'case "domQuery":' in dom
assert 'case "extractText":' in dom
assert "isBrowserErrorUrl(tabUrl)" in dom
assert "isErrorPageScriptError(e)" in dom
def test_navigation_and_tabs_report_browser_error_pages():
# tabs.watch_url (which reports tab error pages) moved into the read-only
# TabsQueryCommands class in tabs-query.ts during the structure refactor.
tabs = (ROOT / "extension" / "src" / "commands" / "tabs-query.ts").read_text()
navigation = (ROOT / "extension" / "src" / "commands" / "navigation.ts").read_text()
assert "lastUrl" in tabs
assert "lastStatus" in tabs
assert "showing an error page" in tabs
assert "last URL:" in tabs
assert "isBrowserErrorUrl" in navigation
assert "showing an error page while waiting for load" in navigation
def test_large_extension_operations_yield_between_batches():
# The large-operation throttling/queue helpers moved from core.ts into
# core/throttle.ts when core was split into a subfolder. The slice-based
# batch loop (cancel-check -> batch call -> progress -> yield) was then
# centralized into the processInBatches() helper in core/throttle.ts, so the
# command modules call that instead of inlining the loop.
core = (ROOT / "extension" / "src" / "core" / "throttle.ts").read_text()
tabs = (ROOT / "extension" / "src" / "commands" / "tabs.ts").read_text()
groups = (ROOT / "extension" / "src" / "commands" / "groups.ts").read_text()
session = (ROOT / "extension" / "src" / "commands" / "session.ts").read_text()
assert "yieldForLargeOperation" in core
assert "getLargeOperationThrottle" in core
assert "hasAudibleTabs" in core
assert "runLargeOperation" in core
assert "largeOperationQueue" in core
assert "updateJobProgress" in core
assert "throwIfJobCancelled" in core
assert "getPerformanceProfile" in core
assert "setPerformanceProfile" in core
assert "GENTLE_OPERATION_BATCH_SIZE" in core
assert "GENTLE_OPERATION_PAUSE_MS" in core
assert "itemCount >= 300" in core
assert "itemCount >= 100" in core
assert "chrome.tabs.query({ audible: true })" in core
# The centralized batch loop drives cancellation + progress + throttled yield.
assert "processInBatches" in core
assert "throwIfJobCancelled(progress.job)" in core
assert "yieldForLargeOperation(done" in core
# tabs.close / tabs.merge_windows now batch via processInBatches; tabs.sort
# still runs its own per-item move loop with yieldForLargeOperation.
assert "processInBatches(toClose" in tabs
assert "processInBatches(ids" in tabs
assert "yieldForLargeOperation" in tabs
assert "w.tabs.every" in tabs
assert "getLargeOperationThrottle" in tabs
assert "runLargeOperation(\"tabs.sort\"" in tabs
assert "processInBatches(tabIds" in groups
assert "getLargeOperationThrottle" in groups
assert "runLargeOperation(\"group.close\"" in groups
assert "yieldForLargeOperation(createdTabs.length" in session
assert "getLargeOperationThrottle" in session
assert "runLargeOperation(\"session.load\"" in session
assert "chrome.tabs.discard" in session
assert "lazyPlaceholderUrl" in session
assert "activateLazyTab" in session
assert "lazySessionTabs" in session
assert "throwIfJobCancelled" in session
assert "updateJobProgress" in session
# The background-job machinery moved out of index.ts into the JobManager
# (classes/JobManager.ts), the message router (classes/NativeConnection.ts)
# and the command registry/groups during the class-based refactor. The
# behavior is identical; assert the defining patterns still exist in their
# new homes.
jobs_module = (ROOT / "extension" / "src" / "classes" / "JobManager.ts").read_text()
connection = (ROOT / "extension" / "src" / "classes" / "NativeConnection.ts").read_text()
perf = (ROOT / "extension" / "src" / "commands" / "perf.ts").read_text()
tabs_module = (ROOT / "extension" / "src" / "commands" / "tabs.ts").read_text()
assert "background: true" in tabs_module # background command flag (was BACKGROUND_COMMANDS)
assert "persistJobs" in jobs_module
assert "recentJobs" in jobs_module
assert "jobs.status" in perf
assert "jobs.cancel" in perf
assert "perf.status" in perf
assert "perf.set_profile" in perf
assert "__background" in connection
def test_session_autosave_is_debounced_and_non_overlapping():
# The autosave lifecycle moved out of session.ts into a dedicated
# AutoSaveManager (autosave.ts) during the structure refactor; the shared
# snapshot/signature helpers live in session-snapshot.ts. Behavior is
# identical — the defining patterns just live in their new homes.
autosave = (ROOT / "extension" / "src" / "commands" / "autosave.ts").read_text()
snapshot = (ROOT / "extension" / "src" / "commands" / "session-snapshot.ts").read_text()
assert "autoSaveTimer" in autosave
assert "autoSaveInFlight" in autosave
assert "autoSavePending" in autosave
assert "scheduleAutoSave" in autosave
assert "autoSaveUpdatedHandler" in autosave
assert "saveAutoSessionIfChanged" in autosave
assert "sessionSignature" in snapshot
assert "autoSaveSignature" in autosave
# AutoSaveManager binds the handlers as instance fields (this.*), so the
# add/removeListener references stay identity-stable across enable/disable.
assert "chrome.tabs.onUpdated.addListener(this.autoSaveUpdatedHandler)" in autosave
assert "chrome.tabs.onCreated.addListener(this.autoSaveHandler)" in autosave
assert "chrome.tabs.onMoved.addListener(this.autoSaveHandler)" in autosave
assert "if (!(\"url\" in changeInfo)) return;" in autosave
assert "setTimeout(() => this.runAutoSave(), delayMs)" in autosave
assert "clearTimeout(this.autoSaveTimer)" in autosave
def test_cli_and_sdk_expose_gentle_restore_controls():
session_cli = (ROOT / "browser_cli" / "commands" / "session.py").read_text()
tabs_cli = (ROOT / "browser_cli" / "commands" / "tabs.py").read_text()
groups_cli = (ROOT / "browser_cli" / "commands" / "groups.py").read_text()
commands_init = (ROOT / "browser_cli" / "commands" / "__init__.py").read_text()
sdk_session = (ROOT / "browser_cli" / "sdk" / "session.py").read_text()
sdk_perf = (ROOT / "browser_cli" / "sdk" / "perf.py").read_text()
# The --gentle-mode flag is defined once via the shared gentle_mode_option helper.
assert '"--gentle-mode"' in commands_init
assert "gentle_mode_option" in session_cli
assert "--discard-background-tabs" in session_cli
assert "--background" in session_cli
assert "--lazy" in session_cli
assert "--eager-tabs" in session_cli
assert "job-status" in session_cli
assert "job-cancel" in session_cli
assert "discard_background_tabs" in session_cli
assert "gentle_mode_option" in tabs_cli
assert "gentle_mode" in tabs_cli
assert "gentle_mode_option" in groups_cli
assert "discard_background_tabs" in sdk_session
assert "discardBackgroundTabs" in sdk_session
assert "def load_background" in sdk_session
assert "def job_status" in sdk_perf
assert "def job_cancel" in sdk_perf
assert "def status" in sdk_perf
assert "def set_profile" in sdk_perf
perf_cli = (ROOT / "browser_cli" / "commands" / "perf.py").read_text()
root_cli = (ROOT / "browser_cli" / "cli.py").read_text()
assert "perf_group" in perf_cli
assert "perf.status" in perf_cli
assert "perf.set_profile" in perf_cli
assert "main.add_command(perf_group)" in root_cli