browserctl 0.3.0 → 0.4.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +34 -0
- data/README.md +56 -197
- data/bin/browserctl +33 -11
- data/bin/browserd +7 -1
- data/examples/smoke/params_file.rb +35 -0
- data/examples/smoke/store_fetch.rb +39 -0
- data/examples/the_internet/add_remove_elements.rb +3 -3
- data/examples/the_internet/checkboxes.rb +3 -3
- data/examples/the_internet/dropdown.rb +3 -3
- data/examples/the_internet/dynamic_loading.rb +3 -3
- data/examples/the_internet/login.rb +5 -5
- data/lib/browserctl/client.rb +25 -0
- data/lib/browserctl/commands/export_cookies.rb +18 -0
- data/lib/browserctl/commands/import_cookies.rb +23 -0
- data/lib/browserctl/commands/init.rb +11 -0
- data/lib/browserctl/commands/{pause_resume.rb → pause.rb} +2 -12
- data/lib/browserctl/commands/record.rb +2 -0
- data/lib/browserctl/commands/resume.rb +21 -0
- data/lib/browserctl/commands/status.rb +30 -0
- data/lib/browserctl/constants.rb +9 -2
- data/lib/browserctl/logger.rb +40 -5
- data/lib/browserctl/recording.rb +81 -15
- data/lib/browserctl/runner.rb +21 -0
- data/lib/browserctl/server/command_dispatcher.rb +24 -5
- data/lib/browserctl/server.rb +16 -1
- data/lib/browserctl/version.rb +1 -1
- data/lib/browserctl/workflow.rb +24 -0
- metadata +24 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: aa3b71160a355802bb8e1fd6667506af34dc354c97b60eb2e82e0b418c02dbe6
|
|
4
|
+
data.tar.gz: 83da78edf2d59d84be5a6fd617e5b2388928c77b005d7735f466ce4aad19699b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1ff6294fbc0284a342d9d2377077a02f004aec2acfeb3c1908039b09753d203e8a641d1dd2cd8ab2fd1fc607c42af3aa86537fb47361bb6820cf9a04fa8d6159
|
|
7
|
+
data.tar.gz: d029df7a2d280172543cb52dcee92326c97464d3b8d750d2f718710d46db16402b8e1a235c83c5da229a2ae047a4ab92738c3d736f741a4964c1d324f7871c15
|
data/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,44 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
> **Do not edit this file manually.** It is generated automatically by
|
|
4
|
+
> [release-please](https://github.com/googleapis/release-please) on every merge to `main`.
|
|
5
|
+
> To include a change in the next release, write a
|
|
6
|
+
> [Conventional Commit](https://www.conventionalcommits.org/) message (`feat:`, `fix:`, `chore:`, etc.).
|
|
7
|
+
|
|
3
8
|
All notable changes to this project will be documented in this file.
|
|
4
9
|
|
|
5
10
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
11
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
12
|
|
|
13
|
+
## [0.4.0](https://github.com/patrick204nqh/browserctl/compare/v0.3.1...v0.4.0) (2026-04-25)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* add Claude Code plugin support with installation instructions and plugin metadata ([b568ed2](https://github.com/patrick204nqh/browserctl/commit/b568ed29015d77bc521e4484aada1054b73362e6))
|
|
19
|
+
* add PRODUCT and standardization plan documentation; update VISION, README, and command references ([5594b52](https://github.com/patrick204nqh/browserctl/commit/5594b520f65d19f6c0c022e7944f9a560044549f))
|
|
20
|
+
* allow .browserctl/screenshots/ as project-scoped screenshot directory ([b6914f4](https://github.com/patrick204nqh/browserctl/commit/b6914f42ead76865b8c6e8e8c0e0f88516cb53e3))
|
|
21
|
+
* browserctl v0.4 hardening (security, cookie I/O, store/fetch, params file) ([#15](https://github.com/patrick204nqh/browserctl/issues/15)) ([c702661](https://github.com/patrick204nqh/browserctl/commit/c702661196e0784179afdbd824e6064c1db047bc))
|
|
22
|
+
* DX improvements, doc refresh, and hardening ([#16](https://github.com/patrick204nqh/browserctl/issues/16)) ([6e32e00](https://github.com/patrick204nqh/browserctl/commit/6e32e0038abfa28be9e92ec94f9a7642e29803c4))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Bug Fixes
|
|
26
|
+
|
|
27
|
+
* add enabledPlugins section to settings.json for browserctl plugin activation ([924dbee](https://github.com/patrick204nqh/browserctl/commit/924dbeef9c86ea2c2486d4d93118045acc042833))
|
|
28
|
+
* add marketplace.json and correct plugin metadata ([5cfc1d6](https://github.com/patrick204nqh/browserctl/commit/5cfc1d621bc162fb200150c71ad4f5b62b4bdcc7))
|
|
29
|
+
* allow any path within daemon CWD for screenshots, not just .browserctl/screenshots ([817a492](https://github.com/patrick204nqh/browserctl/commit/817a49211e991493b6018b663fc198c4a0ff3742))
|
|
30
|
+
* save login screenshot to ~/.browserctl/screenshots/ instead of docs/ path ([246688e](https://github.com/patrick204nqh/browserctl/commit/246688e5c586d2028b90c178994d0eb7f1e9c626))
|
|
31
|
+
* update marketplace.json to set strict mode to true for browserctl plugin ([99db2ad](https://github.com/patrick204nqh/browserctl/commit/99db2ada2aae08f8bceb78658e15a267246df973))
|
|
32
|
+
* use screenshot_path param in all examples; CI passes docs/screenshots/ explicitly ([64e073d](https://github.com/patrick204nqh/browserctl/commit/64e073d74979d869560d814b9a986c001b400238))
|
|
33
|
+
|
|
34
|
+
## [0.3.1](https://github.com/patrick204nqh/browserctl/compare/v0.3.0...v0.3.1) (2026-04-20)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
### Bug Fixes
|
|
38
|
+
|
|
39
|
+
* allow pre-registered workflows with slash names (e.g. the_internet/login) ([e755a4a](https://github.com/patrick204nqh/browserctl/commit/e755a4aa20608fb11ce425c81ed8c0a43f3b13fe))
|
|
40
|
+
* remove unused client variable in runner spec ([94e1a31](https://github.com/patrick204nqh/browserctl/commit/94e1a310acc1d5ddb5e538860907040d3504f2c9))
|
|
41
|
+
|
|
8
42
|
## [0.3.0](https://github.com/patrick204nqh/browserctl/compare/v0.2.2...v0.3.0) (2026-04-20)
|
|
9
43
|
|
|
10
44
|
|
data/README.md
CHANGED
|
@@ -13,10 +13,10 @@ A persistent browser automation daemon and CLI, purpose-built for AI agents and
|
|
|
13
13
|
Unlike tools that restart the browser on every script run, **browserctl keeps a named browser session alive** — preserving cookies, localStorage, open tabs, and page state across discrete commands.
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
browserd &
|
|
16
|
+
browserd & # start the daemon (headless)
|
|
17
17
|
browserctl open login --url https://example.com/login
|
|
18
|
-
browserctl snap login
|
|
19
|
-
browserctl fill login --ref e1 --value me@example.com # interact by ref
|
|
18
|
+
browserctl snap login # AI-friendly JSON snapshot with ref IDs
|
|
19
|
+
browserctl fill login --ref e1 --value me@example.com # interact by ref, no selectors needed
|
|
20
20
|
browserctl click login --ref e2
|
|
21
21
|
browserctl shutdown
|
|
22
22
|
```
|
|
@@ -35,11 +35,12 @@ Most automation tools are stateless — every script spins up a fresh browser an
|
|
|
35
35
|
| Session persists across commands | ✓ | ✗ (per-script lifecycle) |
|
|
36
36
|
| Named page handles | ✓ | ✗ |
|
|
37
37
|
| AI-friendly DOM snapshot | ✓ | ✗ |
|
|
38
|
+
| Human-in-the-loop pause/resume | ✓ | ✗ |
|
|
38
39
|
| Lightweight CLI interface | ✓ | ✗ |
|
|
39
40
|
| Full browser automation API | — | ✓ |
|
|
40
41
|
| Parallel multi-browser testing | — | ✓ |
|
|
41
42
|
|
|
42
|
-
**Use browserctl when** you need a browser that stays alive and remembers state — for AI agents, iterative dev workflows, or
|
|
43
|
+
**Use browserctl when** you need a browser that stays alive and remembers state — for AI agents, iterative dev workflows, or tasks that mix automation with human judgment.
|
|
43
44
|
|
|
44
45
|
**Use Playwright/Selenium when** you need parallel test suites, multi-browser support, or a full programmatic API.
|
|
45
46
|
|
|
@@ -47,8 +48,8 @@ Most automation tools are stateless — every script spins up a fresh browser an
|
|
|
47
48
|
|
|
48
49
|
## Requirements
|
|
49
50
|
|
|
50
|
-
- Ruby >= 3.
|
|
51
|
-
- Chrome or Chromium
|
|
51
|
+
- Ruby >= 3.3
|
|
52
|
+
- Chrome or Chromium on your `PATH`
|
|
52
53
|
|
|
53
54
|
---
|
|
54
55
|
|
|
@@ -66,223 +67,70 @@ gem "browserctl"
|
|
|
66
67
|
|
|
67
68
|
---
|
|
68
69
|
|
|
69
|
-
##
|
|
70
|
-
|
|
71
|
-
**1. Start the daemon**
|
|
72
|
-
|
|
73
|
-
```bash
|
|
74
|
-
browserd # headless (default)
|
|
75
|
-
browserd --headed # visible browser window
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
**2. Open a named page**
|
|
79
|
-
|
|
80
|
-
```bash
|
|
81
|
-
browserctl open login --url https://app.example.com/login
|
|
82
|
-
```
|
|
70
|
+
## Claude Code Plugin
|
|
83
71
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
```bash
|
|
87
|
-
browserctl snap login # AI-friendly JSON with ref IDs (default)
|
|
88
|
-
browserctl snap login --format html
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
**4. Interact using refs or selectors**
|
|
92
|
-
|
|
93
|
-
```bash
|
|
94
|
-
browserctl fill login --ref e1 --value user@example.com
|
|
95
|
-
browserctl fill login --ref e2 --value s3cr3t
|
|
96
|
-
browserctl click login --ref e3
|
|
97
|
-
|
|
98
|
-
# or using explicit CSS selectors
|
|
99
|
-
browserctl fill login "input[name=email]" user@example.com
|
|
100
|
-
browserctl click login "button[type=submit]"
|
|
101
|
-
```
|
|
72
|
+
browserctl ships as a Claude Code plugin. Install it once and Claude automatically knows how to use the daemon, ref-based interaction, HITL patterns, and workflow authoring.
|
|
102
73
|
|
|
103
|
-
**
|
|
74
|
+
**Install (interactive)**
|
|
104
75
|
|
|
105
|
-
```bash
|
|
106
|
-
browserctl snap login --diff # only changed elements since last snap
|
|
107
|
-
browserctl shot login --out /tmp/after-login.png --full
|
|
108
|
-
browserctl url login
|
|
109
76
|
```
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
```bash
|
|
114
|
-
browserctl pages
|
|
115
|
-
browserctl close login
|
|
116
|
-
browserctl ping
|
|
117
|
-
browserctl shutdown
|
|
77
|
+
/plugin marketplace add patrick204nqh/browserctl
|
|
78
|
+
/plugin install browserctl@browserctl
|
|
118
79
|
```
|
|
119
80
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
## All Commands
|
|
123
|
-
|
|
124
|
-
### Browser commands _(require `browserd` running)_
|
|
125
|
-
|
|
126
|
-
| Command | Description |
|
|
127
|
-
|---|---|
|
|
128
|
-
| `open <page> [--url URL]` | Open or focus a named page |
|
|
129
|
-
| `close <page>` | Close a named page |
|
|
130
|
-
| `pages` | List open pages |
|
|
131
|
-
| `goto <page> <url>` | Navigate a page to a URL |
|
|
132
|
-
| `fill <page> <selector> <value>` | Fill an input field by CSS selector |
|
|
133
|
-
| `fill <page> --ref <id> --value <v>` | Fill an input field by snapshot ref |
|
|
134
|
-
| `click <page> <selector>` | Click an element by CSS selector |
|
|
135
|
-
| `click <page> --ref <id>` | Click an element by snapshot ref |
|
|
136
|
-
| `snap <page> [--format ai\|html] [--diff]` | Snapshot DOM; `--diff` returns only changed elements |
|
|
137
|
-
| `watch <page> <selector> [--timeout N]` | Poll until selector appears (default timeout: 30s) |
|
|
138
|
-
| `shot <page> [--out PATH] [--full]` | Take a screenshot |
|
|
139
|
-
| `url <page>` | Print current URL |
|
|
140
|
-
| `eval <page> <expression>` | Evaluate a JS expression |
|
|
141
|
-
| `pause <page>` | Pause automation — browser stays live for manual interaction |
|
|
142
|
-
| `resume <page>` | Resume automation after manual action |
|
|
143
|
-
| `inspect <page>` | Open Chrome DevTools for a named page |
|
|
144
|
-
| `cookies <page>` | List all cookies as JSON |
|
|
145
|
-
| `set_cookie <page> <name> <value> <domain>` | Set a cookie (path defaults to `/`) |
|
|
146
|
-
| `clear_cookies <page>` | Clear all cookies for a page |
|
|
147
|
-
| `record start <name>` | Begin recording commands as a replayable workflow |
|
|
148
|
-
| `record stop [--out path]` | End recording; saves to `.browserctl/workflows/` or custom path |
|
|
149
|
-
| `record status` | Show whether a recording is active |
|
|
150
|
-
|
|
151
|
-
### Daemon commands
|
|
152
|
-
|
|
153
|
-
| Command | Description |
|
|
154
|
-
|---|---|
|
|
155
|
-
| `ping` | Check if `browserd` is alive |
|
|
156
|
-
| `shutdown` | Stop `browserd` |
|
|
157
|
-
|
|
158
|
-
### Workflow commands
|
|
159
|
-
|
|
160
|
-
| Command | Description |
|
|
161
|
-
|---|---|
|
|
162
|
-
| `run <name\|file.rb> [--key value ...]` | Run a named workflow or workflow file |
|
|
163
|
-
| `workflows` | List available workflows |
|
|
164
|
-
| `describe <name>` | Show workflow params and steps |
|
|
165
|
-
|
|
166
|
-
---
|
|
167
|
-
|
|
168
|
-
## AI Snapshot Format
|
|
169
|
-
|
|
170
|
-
`browserctl snap <page>` returns a compact JSON array of interactable elements — designed to be token-efficient for AI agents:
|
|
81
|
+
**Install (project settings** — commit `.claude/settings.json` to share with your team)
|
|
171
82
|
|
|
172
83
|
```json
|
|
173
|
-
|
|
174
|
-
{
|
|
175
|
-
"
|
|
176
|
-
|
|
177
|
-
"text": "",
|
|
178
|
-
"selector": "form > input[name=email]",
|
|
179
|
-
"attrs": {
|
|
180
|
-
"type": "email",
|
|
181
|
-
"name": "email",
|
|
182
|
-
"placeholder": "Enter email"
|
|
84
|
+
{
|
|
85
|
+
"extraKnownMarketplaces": {
|
|
86
|
+
"browserctl": {
|
|
87
|
+
"source": { "source": "github", "repo": "patrick204nqh/browserctl" }
|
|
183
88
|
}
|
|
184
89
|
},
|
|
185
|
-
{
|
|
186
|
-
"
|
|
187
|
-
"tag": "button",
|
|
188
|
-
"text": "Sign in",
|
|
189
|
-
"selector": "form > button",
|
|
190
|
-
"attrs": {
|
|
191
|
-
"type": "submit"
|
|
192
|
-
}
|
|
90
|
+
"enabledPlugins": {
|
|
91
|
+
"browserctl@browserctl": true
|
|
193
92
|
}
|
|
194
|
-
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
Use `ref` values directly with `--ref` for zero-fragility interactions, or use `selector` values with `fill` and `click`.
|
|
198
|
-
|
|
199
|
-
### Ref-based interaction
|
|
200
|
-
|
|
201
|
-
After a `snap`, use ref IDs instead of CSS selectors — no selector knowledge required:
|
|
202
|
-
|
|
203
|
-
```bash
|
|
204
|
-
browserctl fill login --ref e1 --value user@example.com
|
|
205
|
-
browserctl click login --ref e2
|
|
93
|
+
}
|
|
206
94
|
```
|
|
207
95
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
Track only what changed since the last snapshot — useful for AI agents monitoring async updates:
|
|
211
|
-
|
|
212
|
-
```bash
|
|
213
|
-
browserctl snap login --diff
|
|
214
|
-
```
|
|
96
|
+
Once installed, Claude Code loads the `browserctl` skill automatically — no `/invoke` needed.
|
|
215
97
|
|
|
216
98
|
---
|
|
217
99
|
|
|
218
|
-
##
|
|
219
|
-
|
|
220
|
-
Workflows are Ruby files using the `Browserctl.workflow` DSL. Place them in any of:
|
|
221
|
-
|
|
222
|
-
- `./.browserctl/workflows/`
|
|
223
|
-
- `~/.browserctl/workflows/`
|
|
224
|
-
|
|
225
|
-
### Example
|
|
226
|
-
|
|
227
|
-
```ruby
|
|
228
|
-
# .browserctl/workflows/smoke_login.rb
|
|
229
|
-
Browserctl.workflow "smoke_login" do
|
|
230
|
-
desc "Log in and confirm the dashboard loads"
|
|
231
|
-
|
|
232
|
-
param :email, required: true
|
|
233
|
-
param :password, required: true, secret: true
|
|
234
|
-
param :base_url, default: "https://app.example.com"
|
|
235
|
-
|
|
236
|
-
step "open login page" do
|
|
237
|
-
page(:login).goto("#{base_url}/login")
|
|
238
|
-
end
|
|
239
|
-
|
|
240
|
-
step "submit credentials" do
|
|
241
|
-
page(:login).fill("input[name=email]", email)
|
|
242
|
-
page(:login).fill("input[name=password]", password)
|
|
243
|
-
page(:login).click("button[type=submit]")
|
|
244
|
-
end
|
|
245
|
-
|
|
246
|
-
step "verify dashboard" do
|
|
247
|
-
page(:login).wait_for("[data-test=dashboard]", timeout: 10)
|
|
248
|
-
assert page(:login).url.include?("/dashboard")
|
|
249
|
-
end
|
|
250
|
-
end
|
|
251
|
-
```
|
|
100
|
+
## Quick Start
|
|
252
101
|
|
|
253
102
|
```bash
|
|
254
|
-
|
|
255
|
-
|
|
103
|
+
# 1. Start the daemon
|
|
104
|
+
browserd &
|
|
256
105
|
|
|
257
|
-
|
|
106
|
+
# 2. Open a named page
|
|
107
|
+
browserctl open main --url https://the-internet.herokuapp.com/login
|
|
258
108
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
| `desc "text"` | Human-readable description |
|
|
262
|
-
| `param :name, required:, secret:, default:` | Declare a parameter |
|
|
263
|
-
| `step "label" { }` | Add a step (runs in order, halts on failure) |
|
|
264
|
-
| `step "label", retry_count: N, timeout: S { }` | Step with retry and/or timeout |
|
|
265
|
-
| `page(:name)` | Returns a `PageProxy` for the named page |
|
|
266
|
-
| `invoke "other_workflow", **overrides` | Call another workflow |
|
|
267
|
-
| `assert condition, "message"` | Raise `WorkflowError` if condition is false |
|
|
268
|
-
|
|
269
|
-
### PageProxy methods
|
|
109
|
+
# 3. Snapshot the page — get AI-friendly JSON with ref IDs
|
|
110
|
+
browserctl snap main
|
|
270
111
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
112
|
+
# 4. Interact using refs
|
|
113
|
+
browserctl fill main --ref e1 --value tomsmith
|
|
114
|
+
browserctl fill main --ref e2 --value SuperSecretPassword!
|
|
115
|
+
browserctl click main --ref e3
|
|
274
116
|
|
|
275
|
-
|
|
117
|
+
# 5. Observe
|
|
118
|
+
browserctl url main
|
|
119
|
+
browserctl snap main --diff # only what changed
|
|
276
120
|
|
|
277
|
-
|
|
121
|
+
# 6. Done
|
|
122
|
+
browserctl shutdown
|
|
123
|
+
```
|
|
278
124
|
|
|
279
|
-
|
|
125
|
+
→ [Full Getting Started guide](docs/getting-started.md)
|
|
280
126
|
|
|
281
127
|
---
|
|
282
128
|
|
|
283
129
|
## How it works
|
|
284
130
|
|
|
285
|
-
`browserd` runs as a background process, listening on a Unix socket at `~/.browserctl/browserd.sock`.
|
|
131
|
+
`browserd` runs as a background process, listening on a Unix socket at `~/.browserctl/browserd.sock`. It manages a Ferrum (Chrome DevTools Protocol) browser instance with named page handles. `browserctl` sends JSON-RPC commands over the socket and prints the result.
|
|
132
|
+
|
|
133
|
+
Start multiple named instances for agent isolation:
|
|
286
134
|
|
|
287
135
|
```bash
|
|
288
136
|
browserd --name agent-a &
|
|
@@ -290,11 +138,22 @@ browserd --name agent-b &
|
|
|
290
138
|
browserctl --daemon agent-a open main --url https://app.example.com
|
|
291
139
|
```
|
|
292
140
|
|
|
293
|
-
|
|
141
|
+
The daemon shuts itself down after 30 minutes of inactivity.
|
|
142
|
+
|
|
143
|
+
---
|
|
294
144
|
|
|
295
|
-
|
|
145
|
+
## Documentation
|
|
296
146
|
|
|
297
|
-
|
|
147
|
+
| | |
|
|
148
|
+
|---|---|
|
|
149
|
+
| [Getting Started](docs/getting-started.md) | Install, first session, first snapshot |
|
|
150
|
+
| [Concepts](docs/concepts/) | Sessions, snapshots, human-in-the-loop |
|
|
151
|
+
| [Guides](docs/guides/) | Writing workflows, handling challenges, smoke testing |
|
|
152
|
+
| [Command Reference](docs/reference/commands.md) | Every command and flag |
|
|
153
|
+
| [API Stability](docs/reference/api-stability.md) | Wire protocol contract and stability zones |
|
|
154
|
+
| [Product](docs/product.md) | What browserctl is and who it's for |
|
|
155
|
+
| [Vision & Roadmap](docs/vision.md) | Philosophy and release roadmap |
|
|
156
|
+
| [vs. agent-browser](docs/vs-agent-browser.md) | How browserctl differs from Vercel's agent-browser |
|
|
298
157
|
|
|
299
158
|
---
|
|
300
159
|
|
data/bin/browserctl
CHANGED
|
@@ -21,9 +21,13 @@ require "browserctl/commands/snapshot"
|
|
|
21
21
|
require "browserctl/commands/screenshot"
|
|
22
22
|
require "browserctl/commands/watch"
|
|
23
23
|
require "browserctl/commands/record"
|
|
24
|
-
require "browserctl/commands/
|
|
24
|
+
require "browserctl/commands/pause"
|
|
25
|
+
require "browserctl/commands/resume"
|
|
25
26
|
require "browserctl/commands/init"
|
|
26
27
|
require "browserctl/commands/inspect"
|
|
28
|
+
require "browserctl/commands/export_cookies"
|
|
29
|
+
require "browserctl/commands/import_cookies"
|
|
30
|
+
require "browserctl/commands/status"
|
|
27
31
|
|
|
28
32
|
def print_result(res)
|
|
29
33
|
if res.is_a?(Hash) && res[:error]
|
|
@@ -59,8 +63,10 @@ def usage
|
|
|
59
63
|
resume <page> Resume automation after manual action
|
|
60
64
|
inspect <page> Open Chrome DevTools for a named page
|
|
61
65
|
cookies <page> List all cookies as JSON
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
set-cookie <page> <name> <value> <domain> Set a cookie (path defaults to /)
|
|
67
|
+
clear-cookies <page> Clear all cookies for a page
|
|
68
|
+
export-cookies <page> <path> Export cookies to a JSON file
|
|
69
|
+
import-cookies <page> <path> Import cookies from a JSON file
|
|
64
70
|
|
|
65
71
|
Recording commands:
|
|
66
72
|
record start <name> Start recording browser commands
|
|
@@ -68,12 +74,13 @@ def usage
|
|
|
68
74
|
record status Show active recording name
|
|
69
75
|
|
|
70
76
|
Workflow commands:
|
|
71
|
-
run <name|file> [--key value ...] Run a workflow
|
|
77
|
+
run <name|file> [--params file] [--key value ...] Run a workflow
|
|
72
78
|
workflows List available workflows
|
|
73
79
|
describe <name> Describe a workflow
|
|
74
80
|
|
|
75
81
|
Daemon commands:
|
|
76
82
|
ping Check if browserd is alive
|
|
83
|
+
status Show daemon status, PID, and open pages
|
|
77
84
|
shutdown Stop browserd
|
|
78
85
|
|
|
79
86
|
Options:
|
|
@@ -105,11 +112,23 @@ when "run"
|
|
|
105
112
|
load File.expand_path(name)
|
|
106
113
|
name = (Browserctl::REGISTRY.keys - before).first || File.basename(name, ".rb")
|
|
107
114
|
end
|
|
108
|
-
|
|
115
|
+
params_file_idx = args.index("--params")
|
|
116
|
+
file_params = {}
|
|
117
|
+
if params_file_idx
|
|
118
|
+
params_path = args.delete_at(params_file_idx + 1)
|
|
119
|
+
args.delete_at(params_file_idx)
|
|
120
|
+
begin
|
|
121
|
+
file_params = Browserctl::Runner.load_params_file(params_path)
|
|
122
|
+
rescue StandardError => e
|
|
123
|
+
abort "Error loading params file: #{e.message}"
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
cli_params = {}
|
|
109
127
|
args.each_slice(2) do |flag, val|
|
|
110
128
|
key = flag.sub(/\A--/, "").to_sym
|
|
111
|
-
|
|
129
|
+
cli_params[key] = val
|
|
112
130
|
end
|
|
131
|
+
params = file_params.merge(cli_params)
|
|
113
132
|
success = runner.run_workflow(name, **params)
|
|
114
133
|
exit(success ? 0 : 1)
|
|
115
134
|
|
|
@@ -139,13 +158,16 @@ else
|
|
|
139
158
|
when "url" then print_result(client.url(args[0]))
|
|
140
159
|
when "eval" then print_result(client.evaluate(args[0], args[1]))
|
|
141
160
|
when "watch" then Browserctl::Commands::Watch.run(client, args)
|
|
142
|
-
when "pause" then Browserctl::Commands::
|
|
143
|
-
when "resume" then Browserctl::Commands::
|
|
161
|
+
when "pause" then Browserctl::Commands::Pause.run(client, args)
|
|
162
|
+
when "resume" then Browserctl::Commands::Resume.run(client, args)
|
|
144
163
|
when "inspect" then Browserctl::Commands::Inspect.run(client, args)
|
|
145
|
-
when "cookies"
|
|
146
|
-
when "
|
|
147
|
-
when "
|
|
164
|
+
when "cookies" then print_result(client.cookies(args[0]))
|
|
165
|
+
when "set-cookie" then print_result(client.set_cookie(args[0], args[1], args[2], args[3]))
|
|
166
|
+
when "clear-cookies" then print_result(client.clear_cookies(args[0]))
|
|
167
|
+
when "export-cookies" then Browserctl::Commands::ExportCookies.run(client, args)
|
|
168
|
+
when "import-cookies" then Browserctl::Commands::ImportCookies.run(client, args)
|
|
148
169
|
when "ping" then print_result(client.ping)
|
|
170
|
+
when "status" then Browserctl::Commands::Status.run(client)
|
|
149
171
|
when "shutdown" then print_result(client.shutdown)
|
|
150
172
|
else
|
|
151
173
|
abort "unknown command: #{cmd}\nRun 'browserctl --help' for usage."
|
data/bin/browserd
CHANGED
|
@@ -16,7 +16,13 @@ opts = Optimist.options do
|
|
|
16
16
|
opt :name, "Daemon instance name for multi-agent use", default: nil, short: "-n", type: :string
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
if opts[:name] && opts[:name] !~ /\A[a-zA-Z0-9_-]{1,64}\z/
|
|
20
|
+
abort "Invalid daemon name #{opts[:name].inspect} — use only letters, digits, _ or -"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
log_path = Browserctl.log_path(opts[:name])
|
|
24
|
+
warn "browserd starting — log: #{log_path}"
|
|
25
|
+
Browserctl.logger = Browserctl.build_logger(opts[:log_level], log_path: log_path)
|
|
20
26
|
Browserctl::Server.new(
|
|
21
27
|
headless: !opts[:headed],
|
|
22
28
|
socket_path: Browserctl.socket_path(opts[:name]),
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
# Smoke test for --params file loading (Task 7.5).
|
|
5
|
+
#
|
|
6
|
+
# Run with:
|
|
7
|
+
# browserctl run examples/smoke/params_file.rb --params examples/smoke/params_file.yml
|
|
8
|
+
#
|
|
9
|
+
# The workflow logs in using credentials from the params file and asserts
|
|
10
|
+
# the secure area is reached — proving the params were loaded and available.
|
|
11
|
+
|
|
12
|
+
Browserctl.workflow "smoke/params_file" do
|
|
13
|
+
desc "Smoke: load credentials from a --params file and use them in a workflow"
|
|
14
|
+
|
|
15
|
+
param :username, required: true
|
|
16
|
+
param :password, required: true, secret: true
|
|
17
|
+
param :base_url, default: "https://the-internet.herokuapp.com"
|
|
18
|
+
|
|
19
|
+
step "open login page" do
|
|
20
|
+
client.open_page("main", url: "#{base_url}/login")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
step "fill credentials from params file" do
|
|
24
|
+
puts " [params] username = #{username.inspect}"
|
|
25
|
+
puts " [params] password = (#{password.length} chars, secret)"
|
|
26
|
+
page(:main).fill("input#username", username)
|
|
27
|
+
page(:main).fill("input#password", password)
|
|
28
|
+
page(:main).click("button[type=submit]")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
step "assert login succeeded" do
|
|
32
|
+
assert page(:main).url.include?("/secure"), "expected redirect to /secure — params may not have loaded"
|
|
33
|
+
puts " [ok] reached secure area — params file loaded correctly"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
# Smoke test for WorkflowContext#store / #fetch (Task 7.3).
|
|
5
|
+
#
|
|
6
|
+
# Uses the-internet's dynamic loading example: click Start, wait for "Hello World!",
|
|
7
|
+
# capture the text in step 1, assert it is still accessible in step 2 via fetch.
|
|
8
|
+
|
|
9
|
+
Browserctl.workflow "smoke/store_fetch" do
|
|
10
|
+
desc "Smoke: store a value in one step and retrieve it in a later step"
|
|
11
|
+
|
|
12
|
+
param :base_url, default: "https://the-internet.herokuapp.com"
|
|
13
|
+
|
|
14
|
+
step "open dynamic loading page" do
|
|
15
|
+
client.open_page("main", url: "#{base_url}/dynamic_loading/1")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
step "click start and capture loaded text" do
|
|
19
|
+
page(:main).click("div#start button")
|
|
20
|
+
page(:main).wait_for("div#finish", timeout: 10)
|
|
21
|
+
text = client.evaluate("main", "document.querySelector('div#finish h4')?.innerText?.trim()")[:result]
|
|
22
|
+
assert text && !text.empty?, "expected loaded text, got: #{text.inspect}"
|
|
23
|
+
store(:loaded_text, text)
|
|
24
|
+
puts " [store] loaded_text = #{text.inspect}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
step "fetch value from previous step and assert" do
|
|
28
|
+
text = fetch(:loaded_text)
|
|
29
|
+
puts " [fetch] loaded_text = #{text.inspect}"
|
|
30
|
+
assert text == "Hello World!", "expected 'Hello World!', got: #{text.inspect}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
step "confirm fetch raises for unknown key" do
|
|
34
|
+
fetch(:nonexistent_key)
|
|
35
|
+
assert false, "expected KeyError was not raised"
|
|
36
|
+
rescue KeyError => e
|
|
37
|
+
puts " [ok] KeyError raised as expected: #{e.message}"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
Browserctl.workflow "the_internet/add_remove_elements" do
|
|
4
4
|
desc "Add/Remove Elements: add several elements, remove some, assert final count"
|
|
5
5
|
|
|
6
|
-
param :base_url,
|
|
6
|
+
param :base_url, default: "https://the-internet.herokuapp.com"
|
|
7
|
+
param :screenshot_path, default: File.expand_path(".browserctl/screenshots/the_internet_add_remove_elements.png")
|
|
7
8
|
|
|
8
9
|
step "open add/remove elements page" do
|
|
9
10
|
client.open_page("main", url: "#{base_url}/add_remove_elements/")
|
|
@@ -13,8 +14,7 @@ Browserctl.workflow "the_internet/add_remove_elements" do
|
|
|
13
14
|
3.times { page(:main).click("button[onclick]") }
|
|
14
15
|
count = client.evaluate("main", "document.querySelectorAll('#elements button').length")[:result]
|
|
15
16
|
assert count == 3, "expected 3 elements, got: #{count}"
|
|
16
|
-
|
|
17
|
-
page(:main).screenshot(path: "#{screenshots_dir}/the_internet_add_remove_elements.png")
|
|
17
|
+
page(:main).screenshot(path: screenshot_path)
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
step "remove one element" do
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
Browserctl.workflow "the_internet/checkboxes" do
|
|
4
4
|
desc "Checkboxes: read state, toggle each, verify both checked"
|
|
5
5
|
|
|
6
|
-
param :base_url,
|
|
6
|
+
param :base_url, default: "https://the-internet.herokuapp.com"
|
|
7
|
+
param :screenshot_path, default: File.expand_path(".browserctl/screenshots/the_internet_checkboxes.png")
|
|
7
8
|
|
|
8
9
|
step "open checkboxes page" do
|
|
9
10
|
client.open_page("main", url: "#{base_url}/checkboxes")
|
|
@@ -29,7 +30,6 @@ Browserctl.workflow "the_internet/checkboxes" do
|
|
|
29
30
|
end
|
|
30
31
|
|
|
31
32
|
step "capture screenshot" do
|
|
32
|
-
|
|
33
|
-
page(:main).screenshot(path: "#{screenshots_dir}/the_internet_checkboxes.png")
|
|
33
|
+
page(:main).screenshot(path: screenshot_path)
|
|
34
34
|
end
|
|
35
35
|
end
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
Browserctl.workflow "the_internet/dropdown" do
|
|
4
4
|
desc "Dropdown: select each option via JS, assert selected value"
|
|
5
5
|
|
|
6
|
-
param :base_url,
|
|
6
|
+
param :base_url, default: "https://the-internet.herokuapp.com"
|
|
7
|
+
param :screenshot_path, default: File.expand_path(".browserctl/screenshots/the_internet_dropdown.png")
|
|
7
8
|
|
|
8
9
|
step "open dropdown page" do
|
|
9
10
|
client.open_page("main", url: "#{base_url}/dropdown")
|
|
@@ -27,7 +28,6 @@ Browserctl.workflow "the_internet/dropdown" do
|
|
|
27
28
|
end
|
|
28
29
|
|
|
29
30
|
step "capture screenshot" do
|
|
30
|
-
|
|
31
|
-
page(:main).screenshot(path: "#{screenshots_dir}/the_internet_dropdown.png")
|
|
31
|
+
page(:main).screenshot(path: screenshot_path)
|
|
32
32
|
end
|
|
33
33
|
end
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
Browserctl.workflow "the_internet/dynamic_loading" do
|
|
4
4
|
desc "Dynamic loading: click Start, wait for hidden element to appear"
|
|
5
5
|
|
|
6
|
-
param :base_url,
|
|
6
|
+
param :base_url, default: "https://the-internet.herokuapp.com"
|
|
7
|
+
param :screenshot_path, default: File.expand_path(".browserctl/screenshots/the_internet_dynamic_loading.png")
|
|
7
8
|
|
|
8
9
|
step "open dynamic loading page" do
|
|
9
10
|
client.open_page("main", url: "#{base_url}/dynamic_loading/1")
|
|
@@ -27,7 +28,6 @@ Browserctl.workflow "the_internet/dynamic_loading" do
|
|
|
27
28
|
sleep 0.2 until client.evaluate("main",
|
|
28
29
|
"document.querySelector('#loading')?.style?.display")[:result] == "none" ||
|
|
29
30
|
Time.now > deadline
|
|
30
|
-
|
|
31
|
-
page(:main).screenshot(path: "#{screenshots_dir}/the_internet_dynamic_loading.png")
|
|
31
|
+
page(:main).screenshot(path: screenshot_path)
|
|
32
32
|
end
|
|
33
33
|
end
|