Compare commits

...
4 Commits
Author SHA1 Message Date
daniel156161 34aaf78c66 chore(deps): update python and node dependencies
Testing / test (push) Successful in 28s
Testing / remote-protocol-compat (0.16.0) (push) Successful in 27s
Testing / remote-protocol-compat (0.15.0) (push) Successful in 23s
- Refresh uv.lock with uv sync --upgrade for all resolved Python deps.
- Bump anyio 4.14.2 to 4.15.1 and coverage 7.15.4 to 7.16.0 in the lock.
- Bump pydantic 2.13.4 to 2.13.5 and pydantic-core 2.46.4 to 2.46.5.
- Bump sse-starlette 3.4.8 to 3.4.11 and wcwidth 0.8.2 to 0.8.3.
- Leave direct Python dependency ranges in pyproject.toml untouched.
- Update extension build tooling to esbuild 0.28.2 and typescript 7.0.2.
- Update @types/chrome to 0.2.9 for current MV3 typings.

- Bump n8n-nodes-browser-cli from 0.3.1 to 0.3.2 after the dep refresh.
- Update the node to @noble/post-quantum 0.7.1 and @types/node 26.4.1.
- Pin n8n-workflow dev dependency to 2.37.4 instead of the floating star range.
- The star range resolved to the 2.38.1 beta while npm latest points at 2.16.0.
- 2.37.4 is the current stable dist-tag and audits clean.
- Keep peerDependencies.n8n-workflow as star so host installs stay flexible.
- Switch the node tsconfig to module/moduleResolution Node16.
- TypeScript 7 removed moduleResolution node10, which broke the old build.

- Verified with uv run pytest -q: 639 passed, 98 skipped.
- Verified with npm run check:extension: typecheck, build and 37 tests pass.
- Verified the node with npm ci, npm run build and npm test: 42 tests pass.
- npm audit --audit-level=moderate reports no vulnerabilities in both packages.
2026-09-07 11:05:17 +02:00
daniel156161 914508e2db feat(read): target any tab for page info and extraction
Testing / test (push) Successful in 40s
Testing / remote-protocol-compat (0.16.0) (push) Successful in 31s
Testing / remote-protocol-compat (0.15.0) (push) Successful in 39s
- Add --tab option to all extract commands and page info
- Thread tab_id through the SDK extract and page namespaces
- Route page.info with a tab_id to tabs.status for cross-tab metadata
- Accept tab_id in the MCP page_info, extract_text, extract_markdown tools
- Forward tabId in the extension page.info and extract.html handlers
- Keep the active tab as default when no tab is given

- Cover tab-scoped reads in API, CLI, and MCP tests
- Bump package and extension version to 0.16.7
- Refresh uv.lock with current dependency versions
2026-08-28 11:19:02 +02:00
daniel156161 6352d9994e Bound extension job and window-alias state
Testing / test (push) Successful in 48s
Testing / remote-protocol-compat (0.16.0) (push) Successful in 39s
Testing / remote-protocol-compat (0.15.0) (push) Successful in 41s
Two service-worker leaks. Running jobs were only bounded by their watchdog, so
a command flood could hold many timers and results for up to JOB_TIMEOUT_MS;
cap concurrent jobs at 32 and reject beyond that, since a clear error beats an
unresponsive worker. Window aliases were never removed, so storage kept an
entry for every window the user had ever renamed.

Prune aliases against the live window set on list, rename, and close rather
than only on close, because windows also disappear without going through
windows.close.

Also extract the repeated job summary in perf.status into a helper.
2026-08-09 20:49:37 +02:00
daniel156161 541b950519 Bound remote pool and rate-limiter memory
Both tables grew one entry per distinct key and never shrank, so a long-running
SDK client or a public serve process accumulated state for every endpoint or
identity it had ever seen. GC cannot reclaim them while the pool and limiter
still reference them.

Cap the pool at 64 endpoint buckets and the limiter at 4096 buckets. Both evict
useless state first: connections past the idle timeout the server has likely
dropped anyway, and buckets that have fully refilled, which carry no throttling
information. Only then fall back to evicting the oldest entry.

Evicting a limiter bucket resets throttling for that identity, which is the
deliberate trade: an attacker cycling identities faster than they go idle can
regain tokens, but unbounded growth would take the process down instead.
2026-08-09 20:49:35 +02:00
25 changed files with 2685 additions and 892 deletions
+25 -19
View File
@@ -1,7 +1,7 @@
import json
import click
from browser_cli.commands import client_from_ctx, handle_errors
from browser_cli.commands import client_from_ctx, handle_errors, tab_option
from rich.console import Console
from rich.table import Table
@@ -12,10 +12,11 @@ def extract_group():
"""Extract content from the active tab."""
@extract_group.command("links")
@tab_option
@handle_errors
def extract_links():
"""Extract all links from the active tab."""
links = client_from_ctx().extract.links()
def extract_links(tab_id):
"""Extract all links from the active tab or --tab."""
links = client_from_ctx().extract.links(tab_id)
if not links:
console.print("[yellow]No links found[/yellow]")
return
@@ -27,10 +28,11 @@ def extract_links():
console.print(table)
@extract_group.command("images")
@tab_option
@handle_errors
def extract_images():
"""Extract all images from the active tab."""
images = client_from_ctx().extract.images()
def extract_images(tab_id):
"""Extract all images from the active tab or --tab."""
images = client_from_ctx().extract.images(tab_id)
if not images:
console.print("[yellow]No images found[/yellow]")
return
@@ -42,29 +44,33 @@ def extract_images():
console.print(table)
@extract_group.command("text")
@tab_option
@handle_errors
def extract_text():
"""Extract all visible text from the active tab."""
console.print(client_from_ctx().extract.text())
def extract_text(tab_id):
"""Extract all visible text from the active tab or --tab."""
console.print(client_from_ctx().extract.text(tab_id))
@extract_group.command("json")
@click.argument("selector")
@tab_option
@handle_errors
def extract_json(selector):
"""Parse and pretty-print JSON content inside SELECTOR."""
data = client_from_ctx().extract.json(selector)
def extract_json(selector, tab_id):
"""Parse and pretty-print JSON content inside SELECTOR in the active tab or --tab."""
data = client_from_ctx().extract.json(selector, tab_id)
console.print_json(json.dumps(data))
@extract_group.command("html")
@tab_option
@handle_errors
def extract_html():
"""Print the full HTML of the active tab to stdout."""
click.echo(client_from_ctx().extract.html())
def extract_html(tab_id):
"""Print the full HTML of the active tab or --tab to stdout."""
click.echo(client_from_ctx().extract.html(tab_id))
@extract_group.command("markdown")
@click.option("--selector", help="Extract only the DOM subtree matching this CSS selector.")
@tab_option
@handle_errors
def extract_markdown(selector):
"""Extract the page's main content as Markdown."""
markdown = client_from_ctx().extract.markdown(selector)
def extract_markdown(selector, tab_id):
"""Extract the page's main content as Markdown from the active tab or --tab."""
markdown = client_from_ctx().extract.markdown(selector, tab_id)
click.echo(markdown or "", nl=not (markdown or "").endswith("\n"))
+5 -4
View File
@@ -1,5 +1,5 @@
import click
from browser_cli.commands import client_from_ctx, handle_errors
from browser_cli.commands import client_from_ctx, handle_errors, tab_option
from rich.console import Console
from rich.table import Table
@@ -10,10 +10,11 @@ def page_group():
"""Inspect current page metadata."""
@page_group.command("info")
@tab_option
@handle_errors
def page_info():
"""Show title, URL, readyState, language, and meta tags of the active tab."""
info = client_from_ctx().page.info()
def page_info(tab_id):
"""Show title, URL, readyState, language, and meta tags of the active tab or --tab."""
info = client_from_ctx().page.info(tab_id)
table = Table(show_header=False)
table.add_column("Field", style="bold cyan", no_wrap=True)
table.add_column("Value")
+12 -6
View File
@@ -120,31 +120,37 @@ def create_server(*, client_factory: ClientFactory = BrowserCLI, tool_prefix: st
@mcp.tool(name=tool_name("page_info", prefix))
def page_info(
tab_id: int | None = None,
browser: str | None = None,
remote: str | None = None,
key: str | None = None,
) -> dict[str, Any]:
"""Return title, URL, readiness, language, and metadata for the active page."""
return structured(_client(client_factory, browser, remote, key).page.info())
"""Return title, URL, readiness, language, and metadata for the active page or tab_id."""
client = _client(client_factory, browser, remote, key)
if tab_id is not None:
return structured(client.tabs.status(tab_id))
return structured(client.page.info())
@mcp.tool(name=tool_name("extract_text", prefix))
def extract_text(
tab_id: int | None = None,
browser: str | None = None,
remote: str | None = None,
key: str | None = None,
) -> str:
"""Extract plain text from the active page."""
return _client(client_factory, browser, remote, key).extract.text()
"""Extract plain text from the active page or tab_id."""
return _client(client_factory, browser, remote, key).extract.text(tab_id)
@mcp.tool(name=tool_name("extract_markdown", prefix))
def extract_markdown(
selector: str | None = None,
tab_id: int | None = None,
browser: str | None = None,
remote: str | None = None,
key: str | None = None,
) -> str:
"""Extract clean Markdown from the active page or an optional CSS selector."""
return _client(client_factory, browser, remote, key).extract.markdown(selector)
"""Extract clean Markdown from the active page or tab_id, optionally scoped by CSS selector."""
return _client(client_factory, browser, remote, key).extract.markdown(selector, tab_id)
@mcp.tool(name=tool_name("dom_query", prefix))
def dom_query(
+23
View File
@@ -26,6 +26,7 @@ from browser_cli.framing import frame
# hand back one the server has just timed out and closed.
_MAX_IDLE_SECONDS = max(5, REMOTE_SESSION_IDLE_TIMEOUT - 5)
_MAX_PER_ENDPOINT = 8
_MAX_ENDPOINTS = 64
class PooledConnection:
__slots__ = ("sock", "secret", "last_used")
@@ -56,10 +57,32 @@ def checkout(endpoint: str) -> PooledConnection | None:
_close(conn.sock) # too old — assume the server has dropped it
return None
def _prune_endpoints_locked(now: float) -> None:
"""Keep the number of endpoint buckets bounded for long-running SDK users."""
for endpoint, bucket in list(_POOL.items()):
fresh = [conn for conn in bucket if now - conn.last_used <= _MAX_IDLE_SECONDS]
if fresh:
_POOL[endpoint] = fresh
else:
for conn in bucket:
_close(conn.sock)
_POOL.pop(endpoint, None)
while len(_POOL) >= _MAX_ENDPOINTS:
oldest_endpoint, bucket = min(
_POOL.items(),
key=lambda item: min(conn.last_used for conn in item[1]) if item[1] else 0.0,
)
for conn in bucket:
_close(conn.sock)
_POOL.pop(oldest_endpoint, None)
def checkin(endpoint: str, conn: PooledConnection) -> None:
"""Return a still-healthy connection to the pool for reuse."""
conn.last_used = time.monotonic()
with _LOCK:
if endpoint not in _POOL and len(_POOL) >= _MAX_ENDPOINTS:
_prune_endpoints_locked(conn.last_used)
bucket = _POOL.setdefault(endpoint, [])
if len(bucket) >= _MAX_PER_ENDPOINT:
_close(conn.sock)
+25 -23
View File
@@ -131,39 +131,41 @@ class DomNS(Namespace):
})
class ExtractNS(Namespace):
"""Extract structured content from the active tab."""
"""Extract structured content from the active (or specified) tab."""
@sdk_command("extract.links", default=[])
def links(self) -> list[dict]:
"""Return links from the active tab."""
@sdk_command("extract.links", lambda self, tab_id=None: {"tabId": tab_id}, default=[])
def links(self, tab_id: int | None = None) -> list[dict]:
"""Return links from the active tab or *tab_id*."""
@sdk_command("extract.images", default=[])
def images(self) -> list[dict]:
"""Return images from the active tab."""
@sdk_command("extract.images", lambda self, tab_id=None: {"tabId": tab_id}, default=[])
def images(self, tab_id: int | None = None) -> list[dict]:
"""Return images from the active tab or *tab_id*."""
@sdk_command("extract.text", default="")
def text(self) -> str:
"""Return plain text from the active tab."""
@sdk_command("extract.text", lambda self, tab_id=None: {"tabId": tab_id}, default="")
def text(self, tab_id: int | None = None) -> str:
"""Return plain text from the active tab or *tab_id*."""
@sdk_command("extract.json", lambda self, selector: {"selector": selector})
def json(self, selector: str):
"""Extract JSON-like structured data from a selector."""
@sdk_command("extract.json", lambda self, selector, tab_id=None: {"selector": selector, "tabId": tab_id})
def json(self, selector: str, tab_id: int | None = None):
"""Extract JSON-like structured data from a selector in the active tab or *tab_id*."""
@sdk_command("extract.html", default="")
def html(self) -> str:
"""Return the full HTML source of the active tab."""
@sdk_command("extract.html", lambda self, tab_id=None: {"tabId": tab_id}, default="")
def html(self, tab_id: int | None = None) -> str:
"""Return the full HTML source of the active tab or *tab_id*."""
@sdk_command("extract.markdown", lambda self, selector=None: {"selector": selector}, mapper=_extract_markdown)
def markdown(self, selector: str | None = None) -> str:
"""Extract the page's main content as clean Markdown.
@sdk_command("extract.markdown", lambda self, selector=None, tab_id=None: {"selector": selector, "tabId": tab_id}, mapper=_extract_markdown)
def markdown(self, selector: str | None = None, tab_id: int | None = None) -> str:
"""Extract the page's main content as clean Markdown from the active tab or *tab_id*.
The extractor may return either Markdown or raw HTML; both are
normalized to Markdown here so SDK and CLI callers get identical output.
"""
class PageNS(Namespace):
"""Inspect the active page."""
"""Inspect the active page or a specified tab."""
@sdk_command("page.info", default={})
def info(self) -> dict:
"""Return title, URL, readyState, lang, and meta tags of the active tab."""
def info(self, tab_id: int | None = None) -> dict:
"""Return metadata for the active page, or tab status for *tab_id*."""
if tab_id is not None:
return self.command("tabs.status", {"tabId": tab_id}) or {}
return self.command("page.info", {}) or {}
+30 -1
View File
@@ -70,19 +70,48 @@ class RateLimiter:
``rate`` is the sustained refill in tokens/second; ``burst`` is the bucket
capacity (defaults to ``rate``). ``rate <= 0`` disables limiting entirely.
Thread-safe so it can be shared across all connections of one serve process.
The bucket table is capped. Without that bound, a long-running public server
could retain one entry per ever-seen identity/IP forever; GC cannot reclaim
those entries because the limiter still references them.
"""
def __init__(self, rate: float, burst: float | None = None) -> None:
def __init__(self, rate: float, burst: float | None = None, max_buckets: int = 4096) -> None:
self.rate = float(rate)
self.capacity = float(burst) if burst is not None else max(float(rate), 1.0)
self.max_buckets = max(1, int(max_buckets))
self._buckets: dict[str, tuple[float, float]] = {}
self._lock = threading.Lock()
def _prune_locked(self, now: float) -> None:
"""Drop idle/full buckets, then oldest buckets, until the table is bounded."""
if len(self._buckets) < self.max_buckets or self.rate <= 0:
return
# Once a bucket has fully refilled, keeping it around carries no useful
# throttling state. Use at least 60s so normal active identities are not
# churned out aggressively on high-rate configs.
idle_seconds = max(60.0, (self.capacity / self.rate) * 2)
full_epsilon = 1e-9
for bucket_key, (tokens, last) in list(self._buckets.items()):
refilled = min(self.capacity, tokens + (now - last) * self.rate)
if refilled >= self.capacity - full_epsilon and now - last >= idle_seconds:
self._buckets.pop(bucket_key, None)
# If an attacker keeps creating fresh identities faster than they go idle,
# still keep memory bounded. Evict the oldest identity state; that may reset
# throttling for that identity, but bounded memory is more important here.
while len(self._buckets) >= self.max_buckets:
oldest_key = min(self._buckets, key=lambda k: self._buckets[k][1])
self._buckets.pop(oldest_key, None)
def allow(self, key: str) -> bool:
if self.rate <= 0:
return True
now = time.monotonic()
with self._lock:
if key not in self._buckets and len(self._buckets) >= self.max_buckets:
self._prune_locked(now)
tokens, last = self._buckets.get(key, (self.capacity, now))
tokens = min(self.capacity, tokens + (now - last) * self.rate)
if tokens < 1.0:
+1 -1
View File
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "browser-cli",
"version": "0.16.6",
"version": "0.16.7",
"description": "Control your browser from the terminal or Python SDK",
"browser_specific_settings": {
"gecko": {
+10
View File
@@ -15,6 +15,11 @@ import type { Job, Serializable, ErrorLike, DispatchArgs } from '../types';
// jobs only need to survive long enough for the CLI to poll their result.
export const MAX_FINISHED_JOBS = 20;
// Cap simultaneously running background jobs. A hung job has a watchdog, but a
// command flood could still pin many timers/results for up to JOB_TIMEOUT_MS.
// Rejecting above this bound keeps service-worker memory predictable.
export const MAX_RUNNING_JOBS = 32;
// Watchdog: if a runner never resolves/rejects (e.g. executeScript against a
// dead tab), finalize the job as an error so its persist interval stops instead
// of writing to api.storage.local every second forever.
@@ -77,6 +82,11 @@ export class JobManager {
}
async start(command: string, args: DispatchArgs, runner: JobRunner) {
const runningCount = [...this.jobs.values()].filter(job => job.status === "running").length;
if (runningCount >= MAX_RUNNING_JOBS) {
throw new Error(`too many background jobs running (${runningCount}); wait for jobs to finish or cancel one`);
}
const jobId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
const job: Job = {
id: jobId,
+2 -2
View File
@@ -56,7 +56,7 @@ export class DomCommands extends CommandGroup {
"dom.poll": (a: DomPollArgs) => this.domPoll(a),
// ── Page ─────────────────────────────────────────────────────────────
"page.info": () => this.domOp("pageInfo", {}),
"page.info": (a: DomArgs) => this.domOp("pageInfo", a),
// ── Extract ──────────────────────────────────────────────────────────
"extract.links": (a: DomArgs) => this.domOp("extractLinks", a),
@@ -64,7 +64,7 @@ export class DomCommands extends CommandGroup {
"extract.text": (a: DomArgs) => this.domOp("extractText", a),
"extract.json": (a: DomArgs) => this.domOp("extractJson", a),
"extract.markdown": (a: DomArgs) => this.domOp("extractMarkdown", a),
"extract.html": () => fetchTabHtml(undefined),
"extract.html": (a: DomArgs = {}) => fetchTabHtml(a.tabId),
};
private async domOp(funcName: string, args: DomArgs = {}) {
+14 -10
View File
@@ -1,7 +1,7 @@
import { getLargeOperationThrottle, getPerformanceProfile, hasAudibleTabs, setPerformanceProfile } from '../core';
import { CommandGroup } from '../classes/CommandGroup';
import type { CommandEntry } from '../classes/CommandGroup';
import type { PerfSetProfileArgs, JobIdArgs } from '../types';
import type { Job, PerfSetProfileArgs, JobIdArgs } from '../types';
// PerfCommands also owns the jobs.* status/cancel queries: they read the same
// JobManager (ctx.jobs) that perf.status reports, and there is no dedicated
@@ -15,15 +15,8 @@ export class PerfCommands extends CommandGroup {
"jobs.cancel": (a: JobIdArgs) => this.ctx.jobs.cancel(a),
};
private async perfStatus() {
const profile = await getPerformanceProfile();
const audible = await hasAudibleTabs();
const throttle = await getLargeOperationThrottle(0, "auto");
private jobSummary(job: Job) {
return {
performanceProfile: profile,
audible,
throttle,
jobs: this.ctx.jobs.list().map(job => ({
id: job.id,
command: job.command,
status: job.status,
@@ -32,7 +25,18 @@ export class PerfCommands extends CommandGroup {
total: job.total,
percent: job.percent,
cancelRequested: job.cancelRequested,
})),
};
}
private async perfStatus() {
const profile = await getPerformanceProfile();
const audible = await hasAudibleTabs();
const throttle = await getLargeOperationThrottle(0, "auto");
return {
performanceProfile: profile,
audible,
throttle,
jobs: this.ctx.jobs.list().map(job => this.jobSummary(job)),
};
}
}
+29 -2
View File
@@ -14,9 +14,31 @@ export class WindowsCommands extends CommandGroup {
"windows.open": (a: WindowsOpenArgs) => this.windowsOpen(a),
};
private async activeWindowIds(): Promise<Set<number>> {
const windows = await api.windows.getAll({});
return new Set(windows.map(w => w.id).filter(id => typeof id === "number"));
}
private async pruneAliases(activeIds?: Set<number>): Promise<Record<string, string>> {
const aliases = await getAliases();
const liveIds = activeIds || await this.activeWindowIds();
const pruned: Record<string, string> = {};
let changed = false;
for (const [id, alias] of Object.entries(aliases)) {
if (liveIds.has(Number(id))) {
pruned[id] = alias;
} else {
changed = true;
}
}
if (changed) await api.storage.local.set({ windowAliases: pruned });
return pruned;
}
private async windowsList() {
const windows = await api.windows.getAll({ populate: true });
const aliases = await getAliases();
const activeIds = new Set(windows.map(w => w.id).filter(id => typeof id === "number"));
const aliases = await this.pruneAliases(activeIds);
return windows.map(w => ({
id: w.id,
alias: aliases[w.id] || null,
@@ -27,7 +49,7 @@ export class WindowsCommands extends CommandGroup {
}
private async windowsRename({ windowId, name }: WindowsRenameArgs) {
const aliases = await getAliases();
const aliases = await this.pruneAliases();
aliases[windowId] = name;
await api.storage.local.set({ windowAliases: aliases });
return { windowId, name };
@@ -35,6 +57,11 @@ export class WindowsCommands extends CommandGroup {
private async windowsClose({ windowId }: WindowsCloseArgs) {
await api.windows.remove(windowId);
const aliases = await this.pruneAliases();
if (windowId in aliases) {
delete aliases[windowId];
await api.storage.local.set({ windowAliases: aliases });
}
return { windowId };
}
+16 -1
View File
@@ -1,7 +1,7 @@
// @ts-nocheck
import { test, mock } from 'node:test';
import assert from 'node:assert/strict';
import { JobManager, JOB_TIMEOUT_MS, MAX_FINISHED_JOBS, pruneFinishedJobs } from '../src/classes/JobManager';
import { JobManager, JOB_TIMEOUT_MS, MAX_FINISHED_JOBS, MAX_RUNNING_JOBS, pruneFinishedJobs } from '../src/classes/JobManager';
import { makeChromeMock } from './chrome-mock';
// Drain pending microtasks (finalize() chains several awaits). setImmediate is
@@ -129,6 +129,21 @@ test('JobManager: a runner that settles after the watchdog cannot resurrect the
mock.timers.reset();
});
test('JobManager: rejects new background jobs above the running-job cap', async () => {
mock.timers.enable({ apis: ['setInterval', 'setTimeout'] });
globalThis.chrome = makeChromeMock();
const mgr = new JobManager();
for (let i = 0; i < MAX_RUNNING_JOBS; i++) {
await mgr.start(`running${i}`, {}, () => new Promise(() => {}));
}
await assert.rejects(
() => mgr.start('overflow', {}, async () => 'nope'),
/too many background jobs running/,
);
assert.equal(mgr.list().filter(job => job.status === 'running').length, MAX_RUNNING_JOBS);
mock.timers.reset();
});
test('JobManager: persisted set keeps running jobs even past the finished cap', async () => {
mock.timers.enable({ apis: ['setInterval', 'setTimeout'] });
globalThis.chrome = makeChromeMock();
+69
View File
@@ -0,0 +1,69 @@
// @ts-nocheck
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { WindowsCommands } from '../src/commands/windows';
import { makeChromeMock } from './chrome-mock';
function makeWindowsChromeMock(windows) {
const chrome = makeChromeMock();
chrome.windows = {
getAll: async () => windows,
remove: async () => {},
create: async () => ({ id: 99 }),
};
return chrome;
}
test('windows.list prunes aliases for closed windows', async () => {
globalThis.chrome = makeWindowsChromeMock([
{ id: 1, focused: true, state: 'normal', tabs: [{ id: 10 }] },
{ id: 2, focused: false, state: 'minimized', tabs: [] },
]);
globalThis.chrome.storage.local._store.windowAliases = {
1: 'main',
2: 'side',
999: 'closed',
};
const commands = new WindowsCommands({ jobs: {} });
const result = await commands.commands['windows.list']({});
assert.deepEqual(result.map(w => [w.id, w.alias]), [[1, 'main'], [2, 'side']]);
assert.deepEqual(globalThis.chrome.storage.local._store.windowAliases, { 1: 'main', 2: 'side' });
});
test('windows.rename prunes stale aliases before saving the new name', async () => {
globalThis.chrome = makeWindowsChromeMock([
{ id: 1, focused: true, state: 'normal', tabs: [] },
{ id: 2, focused: false, state: 'normal', tabs: [] },
]);
globalThis.chrome.storage.local._store.windowAliases = {
1: 'main',
999: 'closed',
};
const commands = new WindowsCommands({ jobs: {} });
await commands.commands['windows.rename']({ windowId: 2, name: 'work' });
assert.deepEqual(globalThis.chrome.storage.local._store.windowAliases, { 1: 'main', 2: 'work' });
});
test('windows.close removes the closed window alias immediately', async () => {
let removed = null;
globalThis.chrome = makeWindowsChromeMock([
{ id: 1, focused: true, state: 'normal', tabs: [] },
{ id: 2, focused: false, state: 'normal', tabs: [] },
]);
globalThis.chrome.windows.remove = async id => { removed = id; };
globalThis.chrome.storage.local._store.windowAliases = {
1: 'main',
2: 'side',
999: 'closed',
};
const commands = new WindowsCommands({ jobs: {} });
await commands.commands['windows.close']({ windowId: 2 });
assert.equal(removed, 2);
assert.deepEqual(globalThis.chrome.storage.local._store.windowAliases, { 1: 'main' });
});
+1309 -216
View File
File diff suppressed because it is too large Load Diff
+6 -6
View File
@@ -1,6 +1,6 @@
{
"name": "n8n-nodes-browser-cli",
"version": "0.3.1",
"version": "0.3.2",
"description": "n8n community node that controls a remote browser by talking directly to a browser-cli serve endpoint (Ed25519 + post-quantum encrypted)",
"keywords": [
"n8n-community-node-package",
@@ -31,15 +31,15 @@
]
},
"devDependencies": {
"@types/node": "^22.0.0",
"esbuild": "^0.28.0",
"n8n-workflow": "*",
"typescript": "^5.6.0"
"@types/node": "^26.4.1",
"esbuild": "^0.28.2",
"n8n-workflow": "2.37.4",
"typescript": "^7.0.2"
},
"peerDependencies": {
"n8n-workflow": "*"
},
"dependencies": {
"@noble/post-quantum": "^0.6.1"
"@noble/post-quantum": "^0.7.1"
}
}
+2 -2
View File
@@ -1,8 +1,8 @@
{
"compilerOptions": {
"target": "ES2021",
"module": "CommonJS",
"moduleResolution": "Node",
"module": "Node16",
"moduleResolution": "Node16",
"lib": ["ES2021"],
"outDir": "dist",
"rootDir": ".",
+480 -119
View File
@@ -6,16 +6,16 @@
"": {
"name": "browser-cli-extension-build",
"devDependencies": {
"@types/chrome": "^0.1.40",
"@types/chrome": "^0.2.9",
"@types/firefox-webext-browser": "^143.0.0",
"esbuild": "^0.28.0",
"typescript": "^6.0.3"
"esbuild": "^0.28.2",
"typescript": "^7.0.2"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
"integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
"integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
"cpu": [
"ppc64"
],
@@ -30,9 +30,9 @@
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
"integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
"integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
"cpu": [
"arm"
],
@@ -47,9 +47,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
"integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
"integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
"cpu": [
"arm64"
],
@@ -64,9 +64,9 @@
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
"integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
"integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
"cpu": [
"x64"
],
@@ -81,9 +81,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
"integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
"integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
"cpu": [
"arm64"
],
@@ -98,9 +98,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
"integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
"integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
"cpu": [
"x64"
],
@@ -115,9 +115,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
"integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
"integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
"cpu": [
"arm64"
],
@@ -132,9 +132,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
"integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
"integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
"cpu": [
"x64"
],
@@ -149,9 +149,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
"integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
"integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
"cpu": [
"arm"
],
@@ -166,9 +166,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
"integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
"integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
"cpu": [
"arm64"
],
@@ -183,9 +183,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
"integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
"integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
"cpu": [
"ia32"
],
@@ -200,9 +200,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
"integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
"integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
"cpu": [
"loong64"
],
@@ -217,9 +217,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
"integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
"integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
"cpu": [
"mips64el"
],
@@ -234,9 +234,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
"integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
"integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
"cpu": [
"ppc64"
],
@@ -251,9 +251,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
"integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
"integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
"cpu": [
"riscv64"
],
@@ -268,9 +268,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
"integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
"integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
"cpu": [
"s390x"
],
@@ -285,9 +285,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
"integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
"integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
"cpu": [
"x64"
],
@@ -302,9 +302,9 @@
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
"integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
"integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
"cpu": [
"arm64"
],
@@ -319,9 +319,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
"integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
"integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
"cpu": [
"x64"
],
@@ -336,9 +336,9 @@
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
"integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
"integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
"cpu": [
"arm64"
],
@@ -353,9 +353,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
"integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
"integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
"cpu": [
"x64"
],
@@ -370,9 +370,9 @@
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
"integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
"integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
"cpu": [
"arm64"
],
@@ -387,9 +387,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
"integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
"integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
"cpu": [
"x64"
],
@@ -404,9 +404,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
"integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
"integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
"cpu": [
"arm64"
],
@@ -421,9 +421,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
"integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
"integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
"cpu": [
"ia32"
],
@@ -438,9 +438,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
"integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
"integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
"cpu": [
"x64"
],
@@ -455,9 +455,9 @@
}
},
"node_modules/@types/chrome": {
"version": "0.1.43",
"resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.1.43.tgz",
"integrity": "sha512-ukH/HhmR6ht+UTX3PLUWJxgJ/RQcK2Foj4lBzsF24SIWsXgqhGuXqjd8FFuwioPP7d/JUKLM4g8GZxw3F4HTcA==",
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.2.9.tgz",
"integrity": "sha512-6rXrDPkkWv4D2Q23HqT+iED4voODU5CpOSc2VFgKyecSFItuEsIBvz9CY6jUQ3dd+Q7zo1bnCE9pKRMtkYut5g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -496,10 +496,350 @@
"dev": true,
"license": "MIT"
},
"node_modules/@typescript/typescript-aix-ppc64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz",
"integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-darwin-arm64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz",
"integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-darwin-x64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz",
"integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-freebsd-arm64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz",
"integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-freebsd-x64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz",
"integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-linux-arm": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz",
"integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-linux-arm64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz",
"integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-linux-loong64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz",
"integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==",
"cpu": [
"loong64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-linux-mips64el": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz",
"integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-linux-ppc64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz",
"integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-linux-riscv64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz",
"integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-linux-s390x": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz",
"integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==",
"cpu": [
"s390x"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-linux-x64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz",
"integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-netbsd-arm64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz",
"integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-netbsd-x64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz",
"integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-openbsd-arm64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz",
"integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-openbsd-x64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz",
"integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-sunos-x64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz",
"integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-win32-arm64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz",
"integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/@typescript/typescript-win32-x64": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz",
"integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=16.20.0"
}
},
"node_modules/esbuild": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
"integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
"integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -510,46 +850,67 @@
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.28.1",
"@esbuild/android-arm": "0.28.1",
"@esbuild/android-arm64": "0.28.1",
"@esbuild/android-x64": "0.28.1",
"@esbuild/darwin-arm64": "0.28.1",
"@esbuild/darwin-x64": "0.28.1",
"@esbuild/freebsd-arm64": "0.28.1",
"@esbuild/freebsd-x64": "0.28.1",
"@esbuild/linux-arm": "0.28.1",
"@esbuild/linux-arm64": "0.28.1",
"@esbuild/linux-ia32": "0.28.1",
"@esbuild/linux-loong64": "0.28.1",
"@esbuild/linux-mips64el": "0.28.1",
"@esbuild/linux-ppc64": "0.28.1",
"@esbuild/linux-riscv64": "0.28.1",
"@esbuild/linux-s390x": "0.28.1",
"@esbuild/linux-x64": "0.28.1",
"@esbuild/netbsd-arm64": "0.28.1",
"@esbuild/netbsd-x64": "0.28.1",
"@esbuild/openbsd-arm64": "0.28.1",
"@esbuild/openbsd-x64": "0.28.1",
"@esbuild/openharmony-arm64": "0.28.1",
"@esbuild/sunos-x64": "0.28.1",
"@esbuild/win32-arm64": "0.28.1",
"@esbuild/win32-ia32": "0.28.1",
"@esbuild/win32-x64": "0.28.1"
"@esbuild/aix-ppc64": "0.28.2",
"@esbuild/android-arm": "0.28.2",
"@esbuild/android-arm64": "0.28.2",
"@esbuild/android-x64": "0.28.2",
"@esbuild/darwin-arm64": "0.28.2",
"@esbuild/darwin-x64": "0.28.2",
"@esbuild/freebsd-arm64": "0.28.2",
"@esbuild/freebsd-x64": "0.28.2",
"@esbuild/linux-arm": "0.28.2",
"@esbuild/linux-arm64": "0.28.2",
"@esbuild/linux-ia32": "0.28.2",
"@esbuild/linux-loong64": "0.28.2",
"@esbuild/linux-mips64el": "0.28.2",
"@esbuild/linux-ppc64": "0.28.2",
"@esbuild/linux-riscv64": "0.28.2",
"@esbuild/linux-s390x": "0.28.2",
"@esbuild/linux-x64": "0.28.2",
"@esbuild/netbsd-arm64": "0.28.2",
"@esbuild/netbsd-x64": "0.28.2",
"@esbuild/openbsd-arm64": "0.28.2",
"@esbuild/openbsd-x64": "0.28.2",
"@esbuild/openharmony-arm64": "0.28.2",
"@esbuild/sunos-x64": "0.28.2",
"@esbuild/win32-arm64": "0.28.2",
"@esbuild/win32-ia32": "0.28.2",
"@esbuild/win32-x64": "0.28.2"
}
},
"node_modules/typescript": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz",
"integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
"tsc": "bin/tsc"
},
"engines": {
"node": ">=14.17"
"node": ">=16.20.0"
},
"optionalDependencies": {
"@typescript/typescript-aix-ppc64": "7.0.2",
"@typescript/typescript-darwin-arm64": "7.0.2",
"@typescript/typescript-darwin-x64": "7.0.2",
"@typescript/typescript-freebsd-arm64": "7.0.2",
"@typescript/typescript-freebsd-x64": "7.0.2",
"@typescript/typescript-linux-arm": "7.0.2",
"@typescript/typescript-linux-arm64": "7.0.2",
"@typescript/typescript-linux-loong64": "7.0.2",
"@typescript/typescript-linux-mips64el": "7.0.2",
"@typescript/typescript-linux-ppc64": "7.0.2",
"@typescript/typescript-linux-riscv64": "7.0.2",
"@typescript/typescript-linux-s390x": "7.0.2",
"@typescript/typescript-linux-x64": "7.0.2",
"@typescript/typescript-netbsd-arm64": "7.0.2",
"@typescript/typescript-netbsd-x64": "7.0.2",
"@typescript/typescript-openbsd-arm64": "7.0.2",
"@typescript/typescript-openbsd-x64": "7.0.2",
"@typescript/typescript-sunos-x64": "7.0.2",
"@typescript/typescript-win32-arm64": "7.0.2",
"@typescript/typescript-win32-x64": "7.0.2"
}
}
}
+3 -3
View File
@@ -13,9 +13,9 @@
"package:extension:firefox": "npm run build:extension && python scripts/package_extension.py --firefox"
},
"devDependencies": {
"@types/chrome": "^0.1.40",
"@types/chrome": "^0.2.9",
"@types/firefox-webext-browser": "^143.0.0",
"esbuild": "^0.28.0",
"typescript": "^6.0.3"
"esbuild": "^0.28.2",
"typescript": "^7.0.2"
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "real-browser-cli"
version = "0.16.6"
version = "0.16.7"
description = "Control your real running browser from the terminal or Python SDK"
readme = "README.md"
license = { file = "LICENSE" }
+14 -3
View File
@@ -299,22 +299,28 @@ class TestExtract:
result = b.extract.markdown()
assert result == "# Title"
mock_send.assert_called_once_with("extract.markdown", {"selector": None}, profile=None, remote=None, key=None)
mock_send.assert_called_once_with("extract.markdown", {"selector": None, "tabId": None}, profile=None, remote=None, key=None)
def test_extract_markdown_selector(self, b, mock_send):
mock_send.return_value = "## Post"
assert b.extract.markdown("article") == "## Post"
mock_send.assert_called_once_with("extract.markdown", {"selector": "article"}, profile=None, remote=None, key=None)
mock_send.assert_called_once_with("extract.markdown", {"selector": "article", "tabId": None}, profile=None, remote=None, key=None)
def test_extract_links(self, b, mock_send):
mock_send.return_value = [{"href": "https://x"}]
assert b.extract.links() == [{"href": "https://x"}]
mock_send.assert_called_once_with("extract.links", {}, profile=None, remote=None, key=None)
mock_send.assert_called_once_with("extract.links", {"tabId": None}, profile=None, remote=None, key=None)
def test_extract_text_none(self, b, mock_send):
mock_send.return_value = None
assert b.extract.text() == ""
mock_send.assert_called_once_with("extract.text", {"tabId": None}, profile=None, remote=None, key=None)
def test_extract_markdown_tab_id(self, b, mock_send):
mock_send.return_value = "# Specific"
assert b.extract.markdown(tab_id=42) == "# Specific"
mock_send.assert_called_once_with("extract.markdown", {"selector": None, "tabId": 42}, profile=None, remote=None, key=None)
# ── Tabs ──────────────────────────────────────────────────────────────────────
@@ -869,6 +875,11 @@ class TestPageStorageCookies:
assert b.page.info() == {"title": "X"}
mock_send.assert_called_once_with("page.info", {}, profile=None, remote=None, key=None)
def test_page_info_tab_id(self, b, mock_send):
mock_send.return_value = {"title": "X"}
assert b.page.info(tab_id=42) == {"title": "X"}
mock_send.assert_called_once_with("tabs.status", {"tabId": 42}, profile=None, remote=None, key=None)
def test_storage_get(self, b, mock_send):
mock_send.return_value = "v"
assert b.storage.get("k") == "v"
+18 -2
View File
@@ -796,13 +796,29 @@ def test_windows_open_passes_url():
assert "https://example.com" in result.output
send_command.assert_called_once_with("windows.open", {"url": "https://example.com"}, profile=None, remote=None, key=None)
def test_page_info_command_with_tab():
with patch("browser_cli.send_command", return_value={"title": "Example", "url": "https://example.com"}) as send_command:
result = CliRunner().invoke(main, ["page", "info", "--tab", "42"])
assert result.exit_code == 0
assert "Example" in result.output
send_command.assert_called_once_with("tabs.status", {"tabId": 42}, profile=None, remote=None, key=None)
def test_extract_markdown_command():
with patch("browser_cli.send_command", return_value="# Title") as send_command:
result = CliRunner().invoke(main, ["extract", "markdown"])
assert result.exit_code == 0
assert result.output == "# Title\n"
send_command.assert_called_once_with("extract.markdown", {"selector": None}, profile=None, remote=None, key=None)
send_command.assert_called_once_with("extract.markdown", {"selector": None, "tabId": None}, profile=None, remote=None, key=None)
def test_extract_markdown_command_with_tab():
with patch("browser_cli.send_command", return_value="# Title") as send_command:
result = CliRunner().invoke(main, ["extract", "markdown", "--tab", "42"])
assert result.exit_code == 0
assert result.output == "# Title\n"
send_command.assert_called_once_with("extract.markdown", {"selector": None, "tabId": 42}, profile=None, remote=None, key=None)
def test_extract_markdown_command_with_selector():
with patch("browser_cli.send_command", return_value="## Post") as send_command:
@@ -810,7 +826,7 @@ def test_extract_markdown_command_with_selector():
assert result.exit_code == 0
assert result.output == "## Post\n"
send_command.assert_called_once_with("extract.markdown", {"selector": "article"}, profile=None, remote=None, key=None)
send_command.assert_called_once_with("extract.markdown", {"selector": "article", "tabId": None}, profile=None, remote=None, key=None)
def test_clean_markdown_output_removes_escaped_underscores_and_dashes():
assert _clean_markdown_output(r"hello\_world \- item") == "hello_world - item"
+26 -5
View File
@@ -62,14 +62,17 @@ class FakeClient:
def navigate_to(self, tab_id, url):
self.calls.append(("navigate", tab_id, url))
def page_info(self):
return {"title": "Example", "url": "https://example.com"}
def page_info(self, tab_id=None):
self.calls.append(("page_info", tab_id))
return {"title": "Example", "url": "https://example.com", "tab_id": tab_id}
def extract_text(self):
def extract_text(self, tab_id=None):
self.calls.append(("extract_text", tab_id))
return "Page text"
def extract_markdown(self, selector=None):
return f"# Page {selector or ''}".rstrip()
def extract_markdown(self, selector=None, tab_id=None):
self.calls.append(("extract_markdown", selector, tab_id))
return f"# Page {selector or ''} {tab_id or ''}".rstrip()
def dom_query(self, selector):
return [{"tag": "button", "selector": selector}]
@@ -112,6 +115,7 @@ async def test_each_tool_call_constructs_a_fresh_targeted_client(client, monkeyp
assert second.structured_content == {
"title": "Example",
"url": "https://example.com",
"tab_id": None,
}
assert len(FakeClient.instances) == 2
assert FakeClient.instances[0].target == ("work", "browser-host.example:443", "agent")
@@ -195,6 +199,23 @@ async def test_tab_tools_default_to_the_active_tab(client):
]
assert FakeClient.instances[1].calls == [("active",), ("close", 7)]
@pytest.mark.anyio
async def test_read_tools_accept_explicit_tab_id(client):
info = await client.call_tool("browser_page_info", {"tab_id": 42})
text = await client.call_tool("browser_extract_text", {"tab_id": 42})
markdown = await client.call_tool("browser_extract_markdown", {"selector": "main", "tab_id": 42})
assert info.structured_content == {
"id": 42,
"title": "Navigated",
"url": "https://example.com/next",
}
assert text.content[0].text == "Page text"
assert markdown.content[0].text == "# Page main 42"
assert FakeClient.instances[0].calls == [("status", 42)]
assert FakeClient.instances[1].calls == [("extract_text", 42)]
assert FakeClient.instances[2].calls == [("extract_markdown", "main", 42)]
@pytest.mark.anyio
async def test_screenshot_returns_image_content(client):
result = await client.call_tool("browser_screenshot", {"tab_id": 7, "format": "png"})
+33
View File
@@ -43,6 +43,39 @@ def test_checkin_caps_pool_size():
b.close()
pool.close_all()
def test_checkin_caps_endpoint_buckets():
pool.close_all()
peers = []
try:
for i in range(pool._MAX_ENDPOINTS + 5):
a, b = _socketpair()
peers.append(b)
pool.checkin(f"host-{i}:443", pool.PooledConnection(a, b"secret"))
assert len(pool._POOL) <= pool._MAX_ENDPOINTS
finally:
for peer in peers:
peer.close()
pool.close_all()
def test_checkin_prunes_stale_endpoint_buckets():
pool.close_all()
old_a, old_b = _socketpair()
old = pool.PooledConnection(old_a, b"secret")
pool.checkin("old:443", old)
old.last_used -= pool._MAX_IDLE_SECONDS + 1
peers = [old_b]
try:
for i in range(pool._MAX_ENDPOINTS):
a, b = _socketpair()
peers.append(b)
pool.checkin(f"new-{i}:443", pool.PooledConnection(a, b"secret"))
assert "old:443" not in pool._POOL
assert len(pool._POOL) <= pool._MAX_ENDPOINTS
finally:
for peer in peers:
peer.close()
pool.close_all()
def test_session_inner_message_strips_auth_fields():
msg = {
"id": "1", "command": "tabs.list", "args": {}, "user_agent": "browser-cli/1",
+17
View File
@@ -181,6 +181,23 @@ def test_rate_limiter_is_per_key():
assert limiter.allow("a") is False
assert limiter.allow("b") is False
def test_rate_limiter_caps_identity_buckets():
limiter = RateLimiter(rate=0.0001, burst=1, max_buckets=3)
for i in range(10):
assert limiter.allow(f"key-{i}") is True
assert len(limiter._buckets) <= 3
def test_rate_limiter_prunes_refilled_idle_buckets(monkeypatch):
current = 1000.0
monkeypatch.setattr("browser_cli.serve.security.time.monotonic", lambda: current)
limiter = RateLimiter(rate=1, burst=2, max_buckets=2)
assert limiter.allow("old") is True
current += 120.0
assert limiter.allow("a") is True
assert limiter.allow("b") is True
assert "old" not in limiter._buckets
assert len(limiter._buckets) <= 2
# ── ServeSecurity ────────────────────────────────────────────────────────────────
def test_effective_policy_prefers_per_key_override():
Generated
+513 -464
View File
File diff suppressed because it is too large Load Diff