Files
browser-cli/tests/test_extension_error_page_handling.py
T
daniel156161 509f1387de
Testing / remote-protocol-compat (0.9.5) (push) Successful in 57s
Testing / remote-protocol-compat (0.9.3) (push) Successful in 1m1s
Testing / test (push) Successful in 1m7s
fix: prevent browser target and focus surprises
- Respect the globally selected browser when renaming client aliases.
- Pass the resolved local profile into sync and async local transports so
  BROWSER_CLI_PROFILE is honored consistently.
- Stop tabs.active from explicitly focusing the OS browser window, avoiding
  virtual-desktop jumps during tab activation.
- Make window merging skip audible, unmuted windows so video playback windows
  are not selected as merge targets.
- Bump the Python package and extension manifest versions to 0.12.2.
- Add regression coverage for browser selection and focus-stealing behavior.
2026-06-14 13:00:33 +02:00

183 lines
9.0 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_tab_activation_and_merge_do_not_steal_audible_video_window():
tabs = (ROOT / "extension" / "src" / "commands" / "tabs.ts").read_text()
assert "await chrome.windows.update(tab.windowId, { focused: true });" not in tabs
assert "windowHasAudibleTabs" in tabs
assert "!this.windowHasAudibleTabs(w)" in tabs
assert "skippedAudibleWindows" in tabs
assert "const target = movableWindows.find(w => w.focused) || movableWindows[0];" in tabs
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