fix(extension): detect browser error pages earlier
Testing / test (push) Successful in 26s
Testing / remote-protocol-compat (0.9.3) (push) Successful in 27s
Testing / remote-protocol-compat (0.9.5) (push) Successful in 20s
Package Extension / package-extension (push) Successful in 28s
Build & Publish Package / publish (push) Successful in 31s
Testing / test (push) Successful in 26s
Testing / remote-protocol-compat (0.9.3) (push) Successful in 27s
Testing / remote-protocol-compat (0.9.5) (push) Successful in 20s
Package Extension / package-extension (push) Successful in 28s
Build & Publish Package / publish (push) Successful in 31s
- Add shared browser error URL detection for Chrome, Edge, Brave, and Firefox-style about:error pages. - Short-circuit read-only DOM and HTML commands with safe fallbacks when tabs are already on browser error pages. - Fail navigation waits, DOM waits, polling, and URL watches with clearer error-page messages. - Bump package and extension version to 0.9.8 and extend regression coverage for cross-browser error-page handling.
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "browser-cli",
|
"name": "browser-cli",
|
||||||
"version": "0.9.6",
|
"version": "0.9.8",
|
||||||
"description": "Control your browser from the terminal via browser-cli",
|
"description": "Control your browser from the terminal via browser-cli",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"tabs",
|
"tabs",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { executeScript, getActiveTab, isErrorPageScriptError, isScriptableUrl } from '../core';
|
import { executeScript, getActiveTab, isBrowserErrorUrl, isErrorPageScriptError, isScriptableUrl } from '../core';
|
||||||
import { contentDispatch } from './injected';
|
import { contentDispatch } from './injected';
|
||||||
|
|
||||||
function fallbackForErrorPageDomOp(funcName, tab) {
|
function fallbackForErrorPageDomOp(funcName, tab) {
|
||||||
@@ -31,6 +31,10 @@ function fallbackForErrorPageDomOp(funcName, tab) {
|
|||||||
export async function domOp(funcName, args = {}) {
|
export async function domOp(funcName, args = {}) {
|
||||||
const tab = args?.tabId ? await chrome.tabs.get(args.tabId) : await getActiveTab();
|
const tab = args?.tabId ? await chrome.tabs.get(args.tabId) : await getActiveTab();
|
||||||
const tabUrl = tab.url || tab.pendingUrl || "";
|
const tabUrl = tab.url || tab.pendingUrl || "";
|
||||||
|
if (isBrowserErrorUrl(tabUrl)) {
|
||||||
|
const fallback = fallbackForErrorPageDomOp(funcName, tab);
|
||||||
|
if (fallback !== undefined) return fallback;
|
||||||
|
}
|
||||||
if (!isScriptableUrl(tabUrl)) {
|
if (!isScriptableUrl(tabUrl)) {
|
||||||
throw new Error(`Cannot run DOM commands on ${tabUrl} — navigate to a regular web page first`);
|
throw new Error(`Cannot run DOM commands on ${tabUrl} — navigate to a regular web page first`);
|
||||||
}
|
}
|
||||||
@@ -53,7 +57,7 @@ export async function domOp(funcName, args = {}) {
|
|||||||
export async function domEval({ code, tabId } = {}) {
|
export async function domEval({ code, tabId } = {}) {
|
||||||
const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab();
|
const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab();
|
||||||
const tabUrl = tab.url || tab.pendingUrl || "";
|
const tabUrl = tab.url || tab.pendingUrl || "";
|
||||||
if (!isScriptableUrl(tabUrl)) {
|
if (!isScriptableUrl(tabUrl) || isBrowserErrorUrl(tabUrl)) {
|
||||||
throw new Error(`Cannot run DOM commands on ${tabUrl} — navigate to a regular web page first`);
|
throw new Error(`Cannot run DOM commands on ${tabUrl} — navigate to a regular web page first`);
|
||||||
}
|
}
|
||||||
const results = await executeScript({
|
const results = await executeScript({
|
||||||
@@ -68,6 +72,10 @@ export async function domEval({ code, tabId } = {}) {
|
|||||||
export async function domWaitFor({ selector, timeout = 10000, visible = false, hidden = false, tabId } = {}) {
|
export async function domWaitFor({ selector, timeout = 10000, visible = false, hidden = false, tabId } = {}) {
|
||||||
const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab();
|
const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab();
|
||||||
const tabUrl = tab.url || tab.pendingUrl || "";
|
const tabUrl = tab.url || tab.pendingUrl || "";
|
||||||
|
if (isBrowserErrorUrl(tabUrl)) {
|
||||||
|
if (hidden) return { selector, found: false };
|
||||||
|
throw new Error(`Cannot wait for DOM on browser error page ${tabUrl}`);
|
||||||
|
}
|
||||||
if (!isScriptableUrl(tabUrl)) {
|
if (!isScriptableUrl(tabUrl)) {
|
||||||
throw new Error(`Cannot run DOM commands on ${tabUrl} — navigate to a regular web page first`);
|
throw new Error(`Cannot run DOM commands on ${tabUrl} — navigate to a regular web page first`);
|
||||||
}
|
}
|
||||||
@@ -101,6 +109,9 @@ export async function domWaitFor({ selector, timeout = 10000, visible = false, h
|
|||||||
export async function domPoll({ selector, pattern, attr, timeout = 30000, interval = 500, tabId } = {}) {
|
export async function domPoll({ selector, pattern, attr, timeout = 30000, interval = 500, tabId } = {}) {
|
||||||
const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab();
|
const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab();
|
||||||
const tabUrl = tab.url || tab.pendingUrl || "";
|
const tabUrl = tab.url || tab.pendingUrl || "";
|
||||||
|
if (isBrowserErrorUrl(tabUrl)) {
|
||||||
|
throw new Error(`Cannot poll DOM on browser error page ${tabUrl}`);
|
||||||
|
}
|
||||||
if (!isScriptableUrl(tabUrl)) {
|
if (!isScriptableUrl(tabUrl)) {
|
||||||
throw new Error(`Cannot run DOM commands on ${tabUrl} — navigate to a regular web page first`);
|
throw new Error(`Cannot run DOM commands on ${tabUrl} — navigate to a regular web page first`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { getActiveTab, getAliases, resolveGroupId, tabInfo } from '../core';
|
import { getActiveTab, getAliases, isBrowserErrorUrl, resolveGroupId, tabInfo } from '../core';
|
||||||
export async function navOpen({ url, background, window: windowName, windowId: explicitWindowId, group: groupNameOrId }) {
|
export async function navOpen({ url, background, window: windowName, windowId: explicitWindowId, group: groupNameOrId }) {
|
||||||
let windowId;
|
let windowId;
|
||||||
if (explicitWindowId != null) {
|
if (explicitWindowId != null) {
|
||||||
@@ -77,6 +77,10 @@ export async function navWait({ tabId, timeout = 30000, readyState = "complete"
|
|||||||
const interval = 200;
|
const interval = 200;
|
||||||
while (Date.now() < deadline) {
|
while (Date.now() < deadline) {
|
||||||
const t = await chrome.tabs.get(tab.id);
|
const t = await chrome.tabs.get(tab.id);
|
||||||
|
const currentUrl = t.url || t.pendingUrl || "";
|
||||||
|
if (isBrowserErrorUrl(currentUrl)) {
|
||||||
|
throw new Error(`Tab ${tab.id} is showing an error page while waiting for load (${currentUrl})`);
|
||||||
|
}
|
||||||
if (readyState === "complete" ? t.status === "complete" : t.status !== "loading") {
|
if (readyState === "complete" ? t.status === "complete" : t.status !== "loading") {
|
||||||
return tabInfo(t);
|
return tabInfo(t);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
import { executeScript, getActiveTab, getAliases, isScriptableUrl, resolveTabForDirectAction, tabInfo } from '../core';
|
import { executeScript, getActiveTab, getAliases, isBrowserErrorUrl, isErrorPageScriptError, isScriptableUrl, resolveTabForDirectAction, tabInfo } from '../core';
|
||||||
export async function tabsList() {
|
export async function tabsList() {
|
||||||
const windows = await chrome.windows.getAll({ populate: true });
|
const windows = await chrome.windows.getAll({ populate: true });
|
||||||
const aliases = await getAliases();
|
const aliases = await getAliases();
|
||||||
@@ -102,8 +102,12 @@ export async function tabsQuery({ search }) {
|
|||||||
export async function tabsHtml({ tabId }) {
|
export async function tabsHtml({ tabId }) {
|
||||||
for (let i = 0; i < 3; i++) {
|
for (let i = 0; i < 3; i++) {
|
||||||
const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab();
|
const tab = tabId ? await chrome.tabs.get(tabId) : await getActiveTab();
|
||||||
if (!isScriptableUrl(tab.url || tab.pendingUrl || "")) {
|
const tabUrl = tab.url || tab.pendingUrl || "";
|
||||||
throw new Error(`Cannot get HTML of ${tab.url || tab.pendingUrl} — navigate to a regular web page first`);
|
if (isBrowserErrorUrl(tabUrl)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (!isScriptableUrl(tabUrl)) {
|
||||||
|
throw new Error(`Cannot get HTML of ${tabUrl} — navigate to a regular web page first`);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const results = await executeScript({
|
const results = await executeScript({
|
||||||
@@ -112,7 +116,8 @@ export async function tabsHtml({ tabId }) {
|
|||||||
});
|
});
|
||||||
return results[0]?.result || "";
|
return results[0]?.result || "";
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const transient = e.message && (e.message.includes("Frame with ID") || e.message.includes("No tab with id")) && !e.message.includes("error page");
|
if (isErrorPageScriptError(e)) return "";
|
||||||
|
const transient = e.message && (e.message.includes("Frame with ID") || e.message.includes("No tab with id"));
|
||||||
if (i < 2 && transient) {
|
if (i < 2 && transient) {
|
||||||
await new Promise(r => setTimeout(r, 300));
|
await new Promise(r => setTimeout(r, 300));
|
||||||
continue;
|
continue;
|
||||||
@@ -205,7 +210,7 @@ export async function tabsWatchUrl({ pattern, timeout = 30000, tabId } = {}) {
|
|||||||
lastUrl = t.url || t.pendingUrl || "";
|
lastUrl = t.url || t.pendingUrl || "";
|
||||||
lastStatus = t.status || "unknown";
|
lastStatus = t.status || "unknown";
|
||||||
if (matches(t.pendingUrl || "") || matches(t.url || "")) return tabInfo(t);
|
if (matches(t.pendingUrl || "") || matches(t.url || "")) return tabInfo(t);
|
||||||
if ((t.url || "").startsWith("chrome-error://")) {
|
if (isBrowserErrorUrl(t.url || "")) {
|
||||||
throw new Error(`Tab ${tab.id} is showing an error page while waiting for URL to match '${pattern}'`);
|
throw new Error(`Tab ${tab.id} is showing an error page while waiting for URL to match '${pattern}'`);
|
||||||
}
|
}
|
||||||
await new Promise(r => setTimeout(r, 200));
|
await new Promise(r => setTimeout(r, 200));
|
||||||
|
|||||||
+19
-2
@@ -5,9 +5,26 @@ export async function getProfileAlias() {
|
|||||||
return profileAlias || "default";
|
return profileAlias || "default";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isBrowserErrorUrl(url) {
|
||||||
|
const value = String(url || "").toLowerCase();
|
||||||
|
return value.startsWith("chrome-error://") ||
|
||||||
|
value.startsWith("edge-error://") ||
|
||||||
|
value.startsWith("brave-error://") ||
|
||||||
|
value.startsWith("about:neterror") ||
|
||||||
|
value.startsWith("about:certerror") ||
|
||||||
|
value.startsWith("about:blocked") ||
|
||||||
|
value.startsWith("about:tabcrashed");
|
||||||
|
}
|
||||||
|
|
||||||
export function isErrorPageScriptError(error) {
|
export function isErrorPageScriptError(error) {
|
||||||
const message = String(error?.message || error || "");
|
const message = String(error?.message || error || "").toLowerCase();
|
||||||
return message.includes("error page") || message.includes("chrome-error://chromewebdata");
|
return message.includes("error page") ||
|
||||||
|
message.includes("chrome-error://") ||
|
||||||
|
message.includes("edge-error://") ||
|
||||||
|
message.includes("brave-error://") ||
|
||||||
|
message.includes("about:neterror") ||
|
||||||
|
message.includes("about:certerror") ||
|
||||||
|
message.includes("about:tabcrashed");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isTransientScriptError(error) {
|
export function isTransientScriptError(error) {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "browser-cli"
|
name = "browser-cli"
|
||||||
version = "0.9.6"
|
version = "0.9.8"
|
||||||
description = "Control your real running browser from the terminal via a browser extension"
|
description = "Control your real running browser from the terminal via a browser extension"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -5,8 +5,13 @@ ROOT = Path(__file__).resolve().parents[1]
|
|||||||
def test_extension_retries_error_page_script_injection_before_failing():
|
def test_extension_retries_error_page_script_injection_before_failing():
|
||||||
core = (ROOT / "extension" / "src" / "core.ts").read_text()
|
core = (ROOT / "extension" / "src" / "core.ts").read_text()
|
||||||
|
|
||||||
|
assert "isBrowserErrorUrl" in core
|
||||||
assert "isErrorPageScriptError" in core
|
assert "isErrorPageScriptError" in core
|
||||||
assert "chrome-error://chromewebdata" in core
|
assert "chrome-error://" in core
|
||||||
|
assert "edge-error://" in core
|
||||||
|
assert "brave-error://" in core
|
||||||
|
assert "about:neterror" in core
|
||||||
|
assert "about:certerror" in core
|
||||||
assert "isTransientScriptError(e)" in core
|
assert "isTransientScriptError(e)" in core
|
||||||
|
|
||||||
def test_read_only_dom_commands_have_error_page_fallbacks():
|
def test_read_only_dom_commands_have_error_page_fallbacks():
|
||||||
@@ -17,12 +22,16 @@ def test_read_only_dom_commands_have_error_page_fallbacks():
|
|||||||
assert "return false;" in dom
|
assert "return false;" in dom
|
||||||
assert 'case "domQuery":' in dom
|
assert 'case "domQuery":' in dom
|
||||||
assert 'case "extractText":' in dom
|
assert 'case "extractText":' in dom
|
||||||
|
assert "isBrowserErrorUrl(tabUrl)" in dom
|
||||||
assert "isErrorPageScriptError(e)" in dom
|
assert "isErrorPageScriptError(e)" in dom
|
||||||
|
|
||||||
def test_tabs_watch_url_reports_last_seen_url_on_timeout():
|
def test_navigation_and_tabs_report_browser_error_pages():
|
||||||
tabs = (ROOT / "extension" / "src" / "commands" / "tabs.ts").read_text()
|
tabs = (ROOT / "extension" / "src" / "commands" / "tabs.ts").read_text()
|
||||||
|
navigation = (ROOT / "extension" / "src" / "commands" / "navigation.ts").read_text()
|
||||||
|
|
||||||
assert "lastUrl" in tabs
|
assert "lastUrl" in tabs
|
||||||
assert "lastStatus" in tabs
|
assert "lastStatus" in tabs
|
||||||
assert "showing an error page" in tabs
|
assert "showing an error page" in tabs
|
||||||
assert "last URL:" in tabs
|
assert "last URL:" in tabs
|
||||||
|
assert "isBrowserErrorUrl" in navigation
|
||||||
|
assert "showing an error page while waiting for load" in navigation
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ requires-python = ">=3.10"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "browser-cli"
|
name = "browser-cli"
|
||||||
version = "0.9.6"
|
version = "0.9.8"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "click" },
|
{ name = "click" },
|
||||||
|
|||||||
Reference in New Issue
Block a user