ai-browser-toolkit 0.2.0__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/PKG-INFO +2 -2
- ai_browser_toolkit-0.2.2/guidelines/toolkit-workflow.md +390 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/packaging/aur/PKGBUILD.template +1 -1
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/packaging/homebrew/formula.rb.template +1 -1
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/packaging/nfpm.yaml +1 -1
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/packaging/scoop/manifest.json.template +1 -1
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/pyproject.toml +3 -3
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/browser.py +41 -4
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/cli.py +137 -8
- ai_browser_toolkit-0.2.2/src/abt/errors.py +99 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/guidelines.py +10 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/ops/navigate.py +6 -1
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/server.py +14 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_guidelines.py +106 -0
- ai_browser_toolkit-0.2.0/guidelines/toolkit-workflow.md +0 -433
- ai_browser_toolkit-0.2.0/src/abt/errors.py +0 -34
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/.gitattributes +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/.github/workflows/check-secrets.yml +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/.github/workflows/ci.yml +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/.github/workflows/release.yml +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/.gitignore +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/.sheet-url +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/AGENTS.md +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/CLAUDE.md +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/GEMINI.md +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/LICENSE +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/README.md +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/docs/TODO.md +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/docs/engine-choice-for-profile-sessions.md +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/docs/known-issues.md +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/docs/packaging.md +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/docs/playwright-spike-2026-08-19.md +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/docs/session-review-20260806-linkedin.md +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/docs/superpowers/plans/2026-08-19-browser-server-decoupling.md +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/docs/superpowers/plans/2026-08-20-packaging-and-distribution.md +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/docs/superpowers/specs/2026-08-03-selenium-browser-api-design.md +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/docs/superpowers/specs/2026-08-19-browser-server-decoupling-design.md +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/docs/superpowers/specs/2026-08-19-profile-sessions-design.md +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/docs/superpowers/specs/2026-08-20-packaging-and-distribution-design.md +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/guidelines/README.md +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/guidelines/docs.google.com/docs.md +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/guidelines/docs.google.com/meta.json +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/guidelines/docs.google.com/sheets.md +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/guidelines/index.json +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/guidelines/messenger.com/messenger.md +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/guidelines/messenger.com/meta.json +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/guidelines/script.google.com/forms-apps-script.md +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/guidelines/script.google.com/meta.json +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/packaging/aur/aibrowsertoolkit-bin.install +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/packaging/build_index.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/packaging/bundle.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/packaging/postinstall.sh +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/packaging/preremove.sh +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/packaging/smoke.sh +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/packaging/windows/abt.iss +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/__init__.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/__main__.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/autostart.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/diff.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/doctor.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/engine.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/frames.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/launch.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/mcp.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/messenger.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/ops/__init__.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/ops/control.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/ops/inspect.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/ops/interact.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/ops/read.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/ops/tabs.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/paths.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/proc.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/pwdriver.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/recorder.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/refs.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/schema.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/shadow.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/shots.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/targeting.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/viewer.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/start-server.bat +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/start-server.sh +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/conftest.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/actionable.html +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/canvas.html +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/cards.html +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/chainfetch.html +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/controlled.html +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/controlled_trusted.html +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/delayed.html +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/form.html +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/framed_button.html +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/framed_preload.html +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/frames.html +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/frames_cross.html +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/frames_order.html +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/frames_swap.html +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/inspect.html +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/late.html +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/links.html +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/messenger.html +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/nav.html +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/overlay.html +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/repeated.html +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/shadow.html +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/slowfetch.html +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_actionable.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_autostart.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_bundle.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_channel_names.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_click_at.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_diff.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_doctor.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_engine.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_frames.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_hit_test.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_inspect.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_launch.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_lifecycle.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_lifecycle_live.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_mcp.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_messenger.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_ops_click_options.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_ops_interact.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_ops_navigate.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_ops_read.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_ops_tabs.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_paths.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_proc.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_recorder.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_schema.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_server.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_server_logs.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_settle.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_shadow.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_shots.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_shots_live.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_targeting.py +0 -0
- {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_workflows.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ai-browser-toolkit
|
|
3
|
-
Version: 0.2.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary: Agentic browser automation CLI for AI agents
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
License-File: LICENSE
|
|
7
7
|
Requires-Python: >=3.11
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
# Toolkit workflow
|
|
2
|
+
|
|
3
|
+
Concepts for driving the AI Browser Toolkit well. Read this before touching any
|
|
4
|
+
site. These are the lessons that took the longest to learn.
|
|
5
|
+
|
|
6
|
+
## The mental model
|
|
7
|
+
|
|
8
|
+
A long-running server owns a real Chrome window, and **`abt` drives it**. You
|
|
9
|
+
do not write WebDriver code; you run small commands and read JSON back. The
|
|
10
|
+
browser stays up between commands, so tabs, logins and focus persist.
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
abt status # a server up? usually yes. URL, tabs, refs
|
|
14
|
+
abt up # start one if not; returns immediately
|
|
15
|
+
abt goto https://example.com
|
|
16
|
+
abt find --text "Sign in"
|
|
17
|
+
abt click --ref el_3
|
|
18
|
+
abt input --css "#email" --value "someone@example.com"
|
|
19
|
+
abt ops # every op and its exact parameters
|
|
20
|
+
abt exec '{"op":"scroll","to":"bottom"}' # any op, raw
|
|
21
|
+
abt exec-batch '[{"op":"click","text":"Edit"},{"op":"get_text","css":"h1"}]'
|
|
22
|
+
abt guidelines show toolkit-workflow # this document
|
|
23
|
+
abt guidelines search <domain> # a playbook for the site you are on?
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`abt exec` reaches **every** op, including those with no named subcommand — so
|
|
27
|
+
the op tables below are the real vocabulary. `abt --help` lists subcommands;
|
|
28
|
+
`abt <command> --help` lists flags. On PowerShell, pipe JSON rather than
|
|
29
|
+
quoting it inline: `'{"op":"press","key":"Enter"}' | abt exec -`.
|
|
30
|
+
|
|
31
|
+
**Never run `abt serve` from a tool call.** That is the command loop itself: it
|
|
32
|
+
never returns, so whatever launched it hangs until killed. `abt up` is the one
|
|
33
|
+
you want — safe at any time, no-ops when a server already answers, and spawns
|
|
34
|
+
the server outside your job object so your call returns while it keeps running.
|
|
35
|
+
The server usually *is* already running, holding tabs and logins that must not
|
|
36
|
+
be thrown away, so check before starting anything.
|
|
37
|
+
|
|
38
|
+
**Batch what you already know.** `abt exec-batch` runs a list in order and stops
|
|
39
|
+
at the first error. Two round trips become one, and you stop guessing between
|
|
40
|
+
them.
|
|
41
|
+
|
|
42
|
+
**The viewer.** `/viewer` in a browser tab replays every command and response as
|
|
43
|
+
it happens — the best debugging tool here. Open it beside your work.
|
|
44
|
+
|
|
45
|
+
**Underneath is HTTP**, and that surface is supported: `POST /command`,
|
|
46
|
+
`POST /commands`, `GET /status`, `GET /ops`. Every `abt` subcommand except
|
|
47
|
+
`serve` is one of those requests. Use it directly when you are already making
|
|
48
|
+
HTTP calls and want to avoid a process launch per command; otherwise prefer
|
|
49
|
+
`abt`.
|
|
50
|
+
|
|
51
|
+
## The op groups
|
|
52
|
+
|
|
53
|
+
| Group | Ops |
|
|
54
|
+
|---|---|
|
|
55
|
+
| Navigate | `goto` `back` `forward` `reload` `current_url` |
|
|
56
|
+
| Read | `find` `find_full` `get_text` `get_html` `run_js` `screenshot` |
|
|
57
|
+
| Inspect | `read_console` `read_network` |
|
|
58
|
+
| Interact | `click` `input` `press` `select` `hover` `scroll` `wait_for` |
|
|
59
|
+
| Tabs | `tab_new` `tab_switch` `tab_close` `tab_list` |
|
|
60
|
+
| Control | `diff` `status` `shutdown` |
|
|
61
|
+
|
|
62
|
+
A few sites have sequences that always run together, and those are packaged as
|
|
63
|
+
shortcuts of their own. They are shortcuts over these same ops, never a
|
|
64
|
+
replacement: when one does not fit what you need, drive the page with the ops
|
|
65
|
+
directly. Which sites have them, and what they do, belongs in that site's
|
|
66
|
+
playbook — `abt guidelines search <domain>`.
|
|
67
|
+
|
|
68
|
+
## Targeting and refs
|
|
69
|
+
|
|
70
|
+
- **`near` qualifies a selector that matches too much.** A table whose every
|
|
71
|
+
row has an `Edit` button is the case: `text` alone matches all of them, and
|
|
72
|
+
an `index` is a guess that breaks the moment a row is added.
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{"op": "click", "text": "Edit", "near": "Medication"}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
It picks the match sharing the *smallest* container with that text — the row
|
|
79
|
+
on a table, the section on a card. **Do not reach for `run_js` to stamp an
|
|
80
|
+
attribute on the right row and click that.** An agent doing exactly that
|
|
81
|
+
spent 14 of its 33 `run_js` calls on it, and when one stamp failed it left a
|
|
82
|
+
selector matching nothing and retried the same click ten times.
|
|
83
|
+
|
|
84
|
+
A `near` that matches nothing lists the qualifiers that do exist, so a miss
|
|
85
|
+
tells you what to ask for instead.
|
|
86
|
+
|
|
87
|
+
- Ops that touch an element take exactly **one** of `css`, `xpath`, `text`
|
|
88
|
+
(exact visible text), or `ref`. An `index` picks the Nth match.
|
|
89
|
+
- `find` returns compact element **shells** (tag + attributes, no children) —
|
|
90
|
+
perfect for surveying a page cheaply. `find_full` returns inner content.
|
|
91
|
+
- Every match carries a `ref`. Act on it directly:
|
|
92
|
+
`find` then `click {"ref": "el_0"}` — no re-selecting.
|
|
93
|
+
- Refs die on navigation or when the element leaves the DOM. A dead ref returns
|
|
94
|
+
`stale_ref`, never silently hits a different element.
|
|
95
|
+
- Numbering never restarts, so ref numbers climb through a session. That is
|
|
96
|
+
deliberate: a reused name would let a new page's `el_0` answer to a handle you
|
|
97
|
+
were holding for the old one. Do not read meaning into the numbers.
|
|
98
|
+
- The `actionable` track on a diff also hands out refs — see below. Reaching for
|
|
99
|
+
`find` right after a click is usually a wasted round trip.
|
|
100
|
+
|
|
101
|
+
## Diffs: your primary feedback loop
|
|
102
|
+
|
|
103
|
+
> **Trust the diff. Do not re-read the page to check what a command did.**
|
|
104
|
+
>
|
|
105
|
+
> This is the most common and most expensive mistake made with this toolkit. A
|
|
106
|
+
> click succeeds, returns a `dom_diff` saying exactly what changed — and the
|
|
107
|
+
> agent fires `get_text {"css": "body"}` to "see what happened". You already
|
|
108
|
+
> have what happened. That follow-up costs a round trip and can cost tens of
|
|
109
|
+
> thousands of tokens, to learn what was in the response you just received.
|
|
110
|
+
>
|
|
111
|
+
> The diff is snapshotted from the live DOM either side of the command. It is
|
|
112
|
+
> not a hint to be confirmed. **Read it and act.** (It was once worth
|
|
113
|
+
> distrusting — navigations snapshotted before an SPA had rendered. Navigation
|
|
114
|
+
> now waits for network idle and a still DOM. A `Loading…` in a diff today is a
|
|
115
|
+
> bug to report, not a reason to re-read.)
|
|
116
|
+
|
|
117
|
+
Interactive ops (`click input press select hover scroll wait_for run_js`) and
|
|
118
|
+
navigation ops (`goto back forward reload`) snapshot before/after and return
|
|
119
|
+
`dom_diff`. Three tracks:
|
|
120
|
+
|
|
121
|
+
**`text` — always on, no budget.** Strings that appeared on screen, one entry
|
|
122
|
+
per element, plus form-control values. Read this first; on most pages it is the
|
|
123
|
+
whole answer.
|
|
124
|
+
|
|
125
|
+
```json
|
|
126
|
+
"text": {"added": ["Widgets", "Gadgets"], "removed_count": 1, "truncated": false}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Only *rendered* text counts, so a hover that reveals a menu reads as its items
|
|
130
|
+
appearing, and an attribute-only change (`aria-expanded`, a class flip) shows as
|
|
131
|
+
an empty diff. That is the trade: text stays clean because it drops that churn.
|
|
132
|
+
Removals are counted, not listed — on a page that rewrites itself they are the
|
|
133
|
+
whole old document. Add `"include_removed": true` when the count says it matters.
|
|
134
|
+
|
|
135
|
+
**After a navigation, `added` is the whole destination page.** `goto` `back`
|
|
136
|
+
`forward` `reload`, and any click that redirected, return the text of the page
|
|
137
|
+
they landed on. So you never need a `find` or `get_text` merely to see what is
|
|
138
|
+
on a page you just opened. The element track is skipped there.
|
|
139
|
+
|
|
140
|
+
**`actionable` — on by default.** The controls among those additions, each with
|
|
141
|
+
a **ref you can act on immediately**. This is the shortest path in the toolkit:
|
|
142
|
+
click, read what appeared, click what appeared — no `find` in between.
|
|
143
|
+
|
|
144
|
+
```json
|
|
145
|
+
{"op": "click", "css": "#insert-menu"}
|
|
146
|
+
→ "text": {"added": ["Chart", "Pivot table", "Macro"]},
|
|
147
|
+
"actionable": {"added": [
|
|
148
|
+
{"ref": "el_7", "role": "menuitem", "name": "Chart"},
|
|
149
|
+
{"ref": "el_9", "role": "button", "name": "Macro", "disabled": true}]}
|
|
150
|
+
|
|
151
|
+
{"op": "click", "ref": "el_7"}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Every `name` is also a string in `text.added`, so the two line up: read the text
|
|
155
|
+
to decide, use the ref to act. `role` disambiguates, `disabled` warns you off.
|
|
156
|
+
|
|
157
|
+
When several controls share a name each carries `near` — the nearest text that
|
|
158
|
+
is not its own label, so a row of identical `Edit` buttons tells you which row
|
|
159
|
+
it belongs to: `{"ref": "el_9", "name": "Edit", "near": "Medication"}`. **This
|
|
160
|
+
exists so you do not reach for `run_js` to match a button to its row.** That
|
|
161
|
+
DOM-walking is expensive and wrong more often than it looks — it has opened the
|
|
162
|
+
wrong row's dialog and returned `ok: true`.
|
|
163
|
+
|
|
164
|
+
It skips navigations (on a new page every control is "new") and drops controls
|
|
165
|
+
with no accessible name, so you never get a ref you cannot tie to something you
|
|
166
|
+
read. **Exception: file inputs are reported even when invisible**, because sites
|
|
167
|
+
hide the real `<input type=file>` behind a custom uploader. Look for
|
|
168
|
+
`role: "file"` and write the path straight to it — `input` handles the hiding:
|
|
169
|
+
|
|
170
|
+
```json
|
|
171
|
+
{"op": "input", "ref": "el_4", "value": "C:/shots/page.png"}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Unlike the text track this one is not free — roughly a quarter again on a diffed
|
|
175
|
+
op. Pass `"actionable": false` on batch steps whose new controls you will never
|
|
176
|
+
click.
|
|
177
|
+
|
|
178
|
+
**`elements` — pass `element_diff: true`.** A line-per-element unified diff with
|
|
179
|
+
tags, ids, classes and attributes. For a change with no visible text, or when
|
|
180
|
+
you need a selector for something the text track reported. Budget it with
|
|
181
|
+
`diff_max_tokens`; passing a budget implies `element_diff: true`.
|
|
182
|
+
|
|
183
|
+
- Navigation and tab ops reset the baseline automatically.
|
|
184
|
+
- Suppress noise on one command with `"diff": false`; disable entirely with
|
|
185
|
+
`--no-diff`.
|
|
186
|
+
- Set a manual baseline with `{"op": "diff", "reset": true}`, then re-check with
|
|
187
|
+
`{"op": "diff"}` to catch async SPA updates. The manual `diff` is explicit, so
|
|
188
|
+
it returns everything: both tracks, removals listed.
|
|
189
|
+
|
|
190
|
+
### When the diff looks empty
|
|
191
|
+
|
|
192
|
+
The one case that tempts you back into re-reading the page. Don't — there are
|
|
193
|
+
three causes and each has a cheaper answer:
|
|
194
|
+
|
|
195
|
+
1. **No visible text changed** — a class flip, `aria-expanded`, a `data-`
|
|
196
|
+
attribute. Ask for the element track:
|
|
197
|
+
`{"op": "click", "css": "…", "element_diff": true}`.
|
|
198
|
+
2. **It has not landed yet** — an SPA updating after the command returned. Take
|
|
199
|
+
a second look with `{"op": "diff"}`, which compares against the state the
|
|
200
|
+
last command left. That is what it is for.
|
|
201
|
+
3. **Nothing happened** — a real outcome, and worth knowing. `click` hit-tests
|
|
202
|
+
first and raises `not_interactable` naming what covered it, so `ok: true`
|
|
203
|
+
with an empty diff genuinely means the click landed and the page did not
|
|
204
|
+
react.
|
|
205
|
+
|
|
206
|
+
`get_text {"css": "body"}` answers none of these better, and costs more than all
|
|
207
|
+
three together. There used to be a fourth cause — a change inside an iframe,
|
|
208
|
+
invisible to every selector. All three tracks now read into frames and refs from
|
|
209
|
+
inside them act normally; you never switch frames yourself.
|
|
210
|
+
|
|
211
|
+
**Verify effects with the diff, not with screenshots or downloads.** Downloads
|
|
212
|
+
and exports lag; the diff is live.
|
|
213
|
+
|
|
214
|
+
**The loop, in one line:** act, read `dom_diff.text.added` to see what appeared,
|
|
215
|
+
act on `dom_diff.actionable.added[].ref` to use it. A `find` between those steps
|
|
216
|
+
is usually a round trip you did not need.
|
|
217
|
+
|
|
218
|
+
## Searching for something that should be there
|
|
219
|
+
|
|
220
|
+
The rule above is about *verifying what a command did*. This one is about
|
|
221
|
+
*looking for something*, and its failure mode is the opposite: a search comes
|
|
222
|
+
back empty, the agent reads that as "my selector was wrong", and starts
|
|
223
|
+
guessing. On a live LinkedIn profile an agent searched for `input[type=file]`,
|
|
224
|
+
got `count: 0`, and spent **fifteen commands and six minutes** widening
|
|
225
|
+
`run_js` scans. There was no file input — the page creates it when you engage
|
|
226
|
+
the drop zone.
|
|
227
|
+
|
|
228
|
+
**A search that finds nothing is an answer.** Climb this ladder once, then
|
|
229
|
+
believe it:
|
|
230
|
+
|
|
231
|
+
| | step | what it covers |
|
|
232
|
+
|---|---|---|
|
|
233
|
+
| 1 | **Re-read the response you have** | A navigation already returned the whole page in `text.added` |
|
|
234
|
+
| 2 | **`find`** — `css`, then `text` | The document and every frame |
|
|
235
|
+
| 3 | **`get_text`** | Rendered text, *including open shadow roots* |
|
|
236
|
+
| 4 | **`find` with `"shadow": true`** | Turns shadow content into a ref you can act on |
|
|
237
|
+
| 5 | **Stop** | It is not there |
|
|
238
|
+
|
|
239
|
+
Step 3 follows the composed tree, so a component's internals are in it with no
|
|
240
|
+
flag — discovery is free. Step 4 exists because reading a label is not the same
|
|
241
|
+
as being able to click it.
|
|
242
|
+
|
|
243
|
+
**You are told when step 4 is worth taking.** Shadow roots are counted on every
|
|
244
|
+
snapshot but never walked:
|
|
245
|
+
|
|
246
|
+
```json
|
|
247
|
+
{"op": "find", "css": "#resume"}
|
|
248
|
+
→ {"count": 0, "shadow_hosts": 2,
|
|
249
|
+
"note": "nothing matched, but this page has 2 shadow root(s) … retry with \"shadow\": true"}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
No hosts, no note — so on most pages this costs and says nothing.
|
|
253
|
+
|
|
254
|
+
**Do not `run_js` a `querySelectorAll` to "look harder".** Steps 2–4 already
|
|
255
|
+
searched the document, its frames and its open shadow roots. JavaScript finds
|
|
256
|
+
the same nothing, a round trip and a few thousand tokens later.
|
|
257
|
+
|
|
258
|
+
**The usual real cause:** the page has not created the control yet. Hidden file
|
|
259
|
+
inputs are the classic — mounted when you click the visible upload button or
|
|
260
|
+
drop zone. Act on what the page is showing you, then look again.
|
|
261
|
+
|
|
262
|
+
**The one honest limit:** a `mode: "closed"` shadow root returns `null` from
|
|
263
|
+
`.shadowRoot`, so no JavaScript can read it or prove it exists, and neither can
|
|
264
|
+
this. "Not there" means *nothing reachable has it*. Closed roots are rare
|
|
265
|
+
outside browser internals like `<video>` controls.
|
|
266
|
+
|
|
267
|
+
## Types of work
|
|
268
|
+
|
|
269
|
+
- **Reading a page:** `find` shells → `find_full` the interesting ones →
|
|
270
|
+
`get_text`/`get_html` for specifics. `run_js` for anything the ops don't cover.
|
|
271
|
+
- **Typing:** `input` clears then types into a **visible** target (use
|
|
272
|
+
`"clear": false` to append). The one exception is `<input type=file>`, which
|
|
273
|
+
sites hide on purpose — `input` writes a path to it anyway. `press` sends keystrokes to whatever has focus —
|
|
274
|
+
use it when there is no visible input element (see google-docs.md).
|
|
275
|
+
- **Press chords:** `press` accepts `ctrl+v`, `ctrl+alt+1`, `shift+enter`, …
|
|
276
|
+
modifiers are `ctrl/control`, `shift`, `alt/option`, `meta/command/cmd/windows`.
|
|
277
|
+
- **Waiting:** `wait_for` with `state` = `present|visible|clickable|absent` and
|
|
278
|
+
a `timeout`. Use it before acting on slow-loading content.
|
|
279
|
+
|
|
280
|
+
## Clicking what the DOM cannot address
|
|
281
|
+
|
|
282
|
+
`click` takes `at: [x, y]` for a canvas, a closed shadow root, an image map —
|
|
283
|
+
anything a selector cannot reach. It is a real synthesized pointer sequence, so
|
|
284
|
+
`event.isTrusted` is true and a page cannot tell it from a person.
|
|
285
|
+
|
|
286
|
+
```json
|
|
287
|
+
{"op": "click", "css": "canvas#pad", "at": [120, 80]} // inside that element
|
|
288
|
+
{"op": "click", "at": [640, 400]} // in the viewport
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
**Prefer the element-relative form.** With a target, `at` is an offset from the
|
|
292
|
+
element's top-left and the element is scrolled into view first, so the numbers
|
|
293
|
+
mean the same thing however the page is scrolled. A bare viewport point is
|
|
294
|
+
whole-pixel while layout is fractional, so it is only good to about a pixel.
|
|
295
|
+
|
|
296
|
+
The response reports `hit` — what `elementFromPoint` found there. A coordinate
|
|
297
|
+
click is blind by nature; check `hit` rather than assume. A point off screen
|
|
298
|
+
raises `not_interactable` instead of clicking nothing.
|
|
299
|
+
|
|
300
|
+
Reach for this only when targeting genuinely cannot work. A `css` selector
|
|
301
|
+
survives a redesign; a coordinate does not.
|
|
302
|
+
|
|
303
|
+
## When the page won't say what went wrong
|
|
304
|
+
|
|
305
|
+
The DOM tells you what a page *is*. `read_console` and `read_network` tell you
|
|
306
|
+
what it *did* — and a failed request usually leaves nothing in the DOM at all.
|
|
307
|
+
|
|
308
|
+
- `read_console` — everything the page logged, plus uncaught errors and
|
|
309
|
+
unhandled rejections. Captured from **document start**, so a reload gives you
|
|
310
|
+
what a page said while loading, which is where the useful errors are. Filter
|
|
311
|
+
with `pattern` (regex on the text) and `levels`.
|
|
312
|
+
- `read_network` — every request with its status, duration, and size.
|
|
313
|
+
`failures_only: true` is the one you want: it keeps 4xx/5xx and anything the
|
|
314
|
+
browser refused to disclose. `pattern` filters on URL.
|
|
315
|
+
|
|
316
|
+
A real case: a site showed "Failed to load PDF" and nothing else. The DOM had no
|
|
317
|
+
more to give. `read_network` with `failures_only` showed a 404 on a storage key
|
|
318
|
+
— the whole diagnosis in one call.
|
|
319
|
+
|
|
320
|
+
Statuses and URLs, not bodies. A cross-origin response without
|
|
321
|
+
`Timing-Allow-Origin` reports `status: null` and `opaque: true`; the browser
|
|
322
|
+
genuinely will not say, so neither does this.
|
|
323
|
+
|
|
324
|
+
## Typing into dates
|
|
325
|
+
|
|
326
|
+
`input` on `<input type="date|time|month|week|datetime-local">` sets the value
|
|
327
|
+
directly rather than typing, and fires the events a framework listens for.
|
|
328
|
+
Typing into these is a trap: the browser feeds keystrokes to *locale* segment
|
|
329
|
+
boxes, so `2026-08-03` on an en-US date input lands as `60803-02-20` and the
|
|
330
|
+
form submits silently wrong.
|
|
331
|
+
|
|
332
|
+
Use the field's own format — `YYYY-MM-DD`, `HH:MM`, `YYYY-MM`, `YYYY-Www`,
|
|
333
|
+
`YYYY-MM-DDTHH:MM` — and anything the field rejects raises `not_interactable`
|
|
334
|
+
instead of quietly emptying itself.
|
|
335
|
+
|
|
336
|
+
## Errors
|
|
337
|
+
|
|
338
|
+
A closed set of `error.type` values to branch on: `invalid_op`,
|
|
339
|
+
`element_not_found`, `stale_ref`, `not_interactable`, `not_a_select`, `timeout`,
|
|
340
|
+
`navigation_failed`, `js_error`, `last_tab`, `tab_not_found`, `browser_dead`,
|
|
341
|
+
`bad_browser`.
|
|
342
|
+
|
|
343
|
+
Failed ops never quietly continue: batches stop unless `continue_on_error`.
|
|
344
|
+
|
|
345
|
+
### A tab that closes itself takes the session with it
|
|
346
|
+
|
|
347
|
+
OAuth popups, payment frames and "you may close this window" tabs close
|
|
348
|
+
themselves when they finish. **If such a tab was the active one, the session
|
|
349
|
+
dies** — every later call returns `browser_dead` with `no such window: target
|
|
350
|
+
window already closed`, and neither `tab_switch` nor `tab_list` can recover it,
|
|
351
|
+
even though the other windows are still on screen.
|
|
352
|
+
|
|
353
|
+
Avoid it by not standing there: `tab_switch` back to the tab you came from
|
|
354
|
+
*before* clicking the control that completes the flow, when the flow allows it.
|
|
355
|
+
|
|
356
|
+
Once it has happened, send `{"op": "browser_restart"}`. Like `status` and
|
|
357
|
+
`shutdown` it skips the health check, so it works precisely when everything else
|
|
358
|
+
returns `browser_dead`. The server stays up, the session log continues, and you
|
|
359
|
+
get a fresh browser on the same profile — so you are still logged in, but
|
|
360
|
+
**every tab and every ref is gone** and you must navigate back.
|
|
361
|
+
|
|
362
|
+
You no longer have to check by hand that nothing else holds the profile: `stop`
|
|
363
|
+
waits for the old browser to release it, and `start` probes the new session and
|
|
364
|
+
fails loudly rather than handing you one that dies on first use.
|
|
365
|
+
|
|
366
|
+
Still worth fixing properly: falling back to a surviving window handle on
|
|
367
|
+
`NoSuchWindowException` would keep the tabs, which `browser_restart` cannot.
|
|
368
|
+
|
|
369
|
+
## Site traps to remember
|
|
370
|
+
|
|
371
|
+
- **Canvas-rendered editors** (Google Docs, Figma, Excalidraw) put the text on
|
|
372
|
+
a `<canvas>`, not in the DOM. `find`/`get_text`/diff cannot see the typed
|
|
373
|
+
text — the diff still catches structural and save-indicator changes.
|
|
374
|
+
- **Offscreen focus targets** (`input` on a hidden editor iframe) fail with
|
|
375
|
+
"not visible within Ns" even though the element exists and has focus.
|
|
376
|
+
Drive them with `press` instead.
|
|
377
|
+
- **Exports/downloads can be stale** — a doc's export endpoint may serve old
|
|
378
|
+
content while the live editor is ahead. Cache-bust the URL and wait for save.
|
|
379
|
+
|
|
380
|
+
## Hygiene
|
|
381
|
+
|
|
382
|
+
- Keep bulk operations on `"diff": false` and do one verification pass at the
|
|
383
|
+
end — huge diffs are noise. **Except across a form fill:** the text track
|
|
384
|
+
captures input values, so a diff after typing is the cheapest way to catch a
|
|
385
|
+
field that took something other than what you sent. A whole form filled on
|
|
386
|
+
`"diff": false` once hid a mangled date until the server rejected it twice.
|
|
387
|
+
- Tabs you create for downloads/export are often closed by the human operator;
|
|
388
|
+
never assume a tab id survives. Create a fresh tab for each export check and
|
|
389
|
+
read the newest file in the Downloads folder.
|
|
390
|
+
- Log everything. If a run goes wrong, `/viewer` shows exactly what happened.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
pkgname=aibrowsertoolkit-bin
|
|
3
3
|
pkgver=@VERSION@
|
|
4
4
|
pkgrel=1
|
|
5
|
-
pkgdesc="
|
|
5
|
+
pkgdesc="Agentic browser automation CLI for AI agents"
|
|
6
6
|
arch=('x86_64' 'aarch64')
|
|
7
7
|
url="https://github.com/skssmd/Ai-Browser-Toolkit"
|
|
8
8
|
license=('Apache-2.0')
|
|
@@ -5,7 +5,7 @@ version: ${PKG_VERSION}
|
|
|
5
5
|
section: utils
|
|
6
6
|
priority: optional
|
|
7
7
|
maintainer: skssmd <skssmd78475@gmail.com>
|
|
8
|
-
description:
|
|
8
|
+
description: Agentic browser automation CLI for AI agents
|
|
9
9
|
vendor: skssmd
|
|
10
10
|
homepage: https://github.com/skssmd/Ai-Browser-Toolkit
|
|
11
11
|
license: Apache-2.0
|
|
@@ -4,11 +4,11 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ai-browser-toolkit"
|
|
7
|
-
version = "0.2.
|
|
8
|
-
description = "
|
|
7
|
+
version = "0.2.2"
|
|
8
|
+
description = "Agentic browser automation CLI for AI agents"
|
|
9
9
|
# PyPI renders this as the whole project page. Without it the page is a blank
|
|
10
10
|
# stub carrying only the one-line description above -- which is what every
|
|
11
|
-
# `pip install
|
|
11
|
+
# `pip install ai-browser-toolkit` sends people to look at.
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
license = "Apache-2.0"
|
|
14
14
|
requires-python = ">=3.11"
|
|
@@ -571,13 +571,33 @@ class BrowserSession:
|
|
|
571
571
|
except WebDriverException:
|
|
572
572
|
return None
|
|
573
573
|
|
|
574
|
-
def goto(self, url: str) ->
|
|
574
|
+
def goto(self, url: str) -> bool:
|
|
575
|
+
"""Navigate. Returns False when it landed but overran the budget.
|
|
576
|
+
|
|
577
|
+
A redirect chain can outrun the navigation timeout while the
|
|
578
|
+
navigation itself succeeds -- `https://sheets.new` is the canonical
|
|
579
|
+
case: it 302s to a freshly created document, the wait expires, and the
|
|
580
|
+
page is nevertheless there. Reporting that as `navigation_failed` sends
|
|
581
|
+
a caller off to retry something that already worked.
|
|
582
|
+
|
|
583
|
+
So a timeout is only a failure if the browser did not actually move.
|
|
584
|
+
"""
|
|
585
|
+
before = None
|
|
586
|
+
try:
|
|
587
|
+
before = self.driver.current_url
|
|
588
|
+
except WebDriverException:
|
|
589
|
+
pass
|
|
590
|
+
|
|
591
|
+
overran = False
|
|
575
592
|
try:
|
|
576
593
|
self.driver.get(url)
|
|
577
594
|
except WebDriverException as exc:
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
595
|
+
if not self._moved_from(before):
|
|
596
|
+
raise OpError(
|
|
597
|
+
"navigation_failed", f"could not load {url!r}: {exc.msg or exc}"
|
|
598
|
+
) from exc
|
|
599
|
+
overran = True
|
|
600
|
+
|
|
581
601
|
self.refs.invalidate(self.active_tab)
|
|
582
602
|
code = self.error_page_code()
|
|
583
603
|
if code:
|
|
@@ -585,6 +605,23 @@ class BrowserSession:
|
|
|
585
605
|
"navigation_failed", f"could not load {url!r}: chrome reported {code}"
|
|
586
606
|
)
|
|
587
607
|
self.settle()
|
|
608
|
+
return not overran
|
|
609
|
+
|
|
610
|
+
def _moved_from(self, before: str | None) -> bool:
|
|
611
|
+
"""Did the browser actually end up somewhere new and usable?
|
|
612
|
+
|
|
613
|
+
`about:blank` and a chrome error page both count as not having moved:
|
|
614
|
+
one means nothing happened, the other means something did and failed.
|
|
615
|
+
"""
|
|
616
|
+
try:
|
|
617
|
+
after = self.driver.current_url
|
|
618
|
+
except WebDriverException:
|
|
619
|
+
return False
|
|
620
|
+
if not after or after.startswith("about:"):
|
|
621
|
+
return False
|
|
622
|
+
if after == before:
|
|
623
|
+
return False
|
|
624
|
+
return not self.error_page_code()
|
|
588
625
|
|
|
589
626
|
def settle(self, timeout: float | None = None) -> bool:
|
|
590
627
|
"""Wait for the DOM to stop changing. Returns whether it did.
|