browserctl 0.5.0 → 0.7.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 +31 -0
- data/README.md +27 -32
- data/bin/browserctl +146 -108
- data/bin/browserd +9 -3
- data/examples/cloudflare_hitl.rb +5 -5
- data/examples/smoke/params_file.rb +3 -2
- data/examples/smoke/store_fetch.rb +5 -5
- data/examples/test_automation_practices/advanced/ab_testing.rb +38 -0
- data/examples/test_automation_practices/advanced/broken_images.rb +25 -0
- data/examples/test_automation_practices/advanced/file_download.rb +40 -0
- data/examples/test_automation_practices/advanced/iframes.rb +37 -0
- data/examples/test_automation_practices/advanced/shadow_dom.rb +35 -0
- data/examples/test_automation_practices/auth/login.rb +34 -0
- data/examples/test_automation_practices/auth/login_negative.rb +28 -0
- data/examples/test_automation_practices/dialogs/alerts.rb +45 -0
- data/examples/test_automation_practices/dialogs/notifications.rb +57 -0
- data/examples/test_automation_practices/dynamic/dynamic_elements.rb +41 -0
- data/examples/test_automation_practices/dynamic/tables.rb +47 -0
- data/examples/test_automation_practices/forms/checkboxes.rb +39 -0
- data/examples/test_automation_practices/forms/file_upload.rb +30 -0
- data/examples/test_automation_practices/forms/forms.rb +47 -0
- data/examples/test_automation_practices/forms/slider.rb +51 -0
- data/examples/test_automation_practices/interactions/context_menu.rb +54 -0
- data/examples/test_automation_practices/interactions/drag_drop.rb +41 -0
- data/examples/test_automation_practices/interactions/exit_intent.rb +47 -0
- data/examples/test_automation_practices/interactions/hover.rb +30 -0
- data/examples/test_automation_practices/interactions/key_press.rb +38 -0
- data/examples/the_internet/add_remove_elements.rb +1 -1
- data/examples/the_internet/checkboxes.rb +1 -1
- data/examples/the_internet/dropdown.rb +1 -1
- data/examples/the_internet/dynamic_loading.rb +2 -2
- data/examples/the_internet/login.rb +1 -1
- data/lib/browserctl/client.rb +143 -28
- data/lib/browserctl/commands/ask.rb +20 -0
- data/lib/browserctl/commands/cookie.rb +59 -0
- data/lib/browserctl/commands/daemon.rb +77 -0
- data/lib/browserctl/commands/dialog.rb +33 -0
- data/lib/browserctl/commands/page.rb +47 -0
- data/lib/browserctl/commands/record.rb +1 -1
- data/lib/browserctl/commands/screenshot.rb +2 -2
- data/lib/browserctl/commands/session.rb +69 -0
- data/lib/browserctl/commands/snapshot.rb +2 -2
- data/lib/browserctl/commands/storage.rb +67 -0
- data/lib/browserctl/commands/workflow.rb +64 -0
- data/lib/browserctl/constants.rb +20 -1
- data/lib/browserctl/logger.rb +4 -4
- data/lib/browserctl/recording.rb +4 -4
- data/lib/browserctl/server/command_dispatcher.rb +30 -9
- data/lib/browserctl/server/handlers/cookies.rb +1 -1
- data/lib/browserctl/server/handlers/devtools.rb +1 -1
- data/lib/browserctl/server/handlers/hitl.rb +2 -1
- data/lib/browserctl/server/handlers/interaction.rb +87 -0
- data/lib/browserctl/server/handlers/navigation.rb +24 -2
- data/lib/browserctl/server/handlers/observation.rb +0 -26
- data/lib/browserctl/server/handlers/page_lifecycle.rb +14 -3
- data/lib/browserctl/server/handlers/session.rb +93 -0
- data/lib/browserctl/server/handlers/storage.rb +109 -0
- data/lib/browserctl/server.rb +2 -2
- data/lib/browserctl/session.rb +79 -0
- data/lib/browserctl/version.rb +1 -1
- data/lib/browserctl/workflow.rb +50 -11
- metadata +36 -11
- data/lib/browserctl/commands/export_cookies.rb +0 -18
- data/lib/browserctl/commands/import_cookies.rb +0 -23
- data/lib/browserctl/commands/inspect.rb +0 -21
- data/lib/browserctl/commands/open_page.rb +0 -21
- data/lib/browserctl/commands/pause.rb +0 -22
- data/lib/browserctl/commands/status.rb +0 -30
- data/lib/browserctl/commands/watch.rb +0 -27
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a2662c63f1ab585b29689e8e09ac2c8f7c3214539c440c51da82efc954fc0493
|
|
4
|
+
data.tar.gz: 3e8f6790cdb3a3a79382f68c3a1fb6235805e6acc96658270d06c4d91ee1098b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d729193bb86f061227588d46981d7fc5aaf98ef45956a4f4c5e59ca30c95f02f2a06d0d2e9227e671e0f6fb8a3ffcdd2ec4347cc20bd30bb40b2bef54f8b25c4
|
|
7
|
+
data.tar.gz: 48170f604f692e980da8956c41f74255e0f16c6a43a13f3ae090377550bc14967a632a8ac01ebe85c7cabde1ff4226638e92fd40329cde0bcb2539c0c4a5c69a
|
data/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,37 @@ All notable changes to this project will be documented in this file.
|
|
|
10
10
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
11
11
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
12
12
|
|
|
13
|
+
## [0.7.0](https://github.com/patrick204nqh/browserctl/compare/v0.6.0...v0.7.0) (2026-04-28)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* v0.7 interaction primitives (press/hover/upload/select/dialog/ask) + page_focus fix ([#47](https://github.com/patrick204nqh/browserctl/issues/47)) ([63daadc](https://github.com/patrick204nqh/browserctl/commit/63daadcfbd4e967fc539b714393d163fabfc86b2))
|
|
19
|
+
|
|
20
|
+
## [0.6.0](https://github.com/patrick204nqh/browserctl/compare/v0.5.0...v0.6.0) (2026-04-28)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Features
|
|
24
|
+
|
|
25
|
+
* add feedback capture guidelines and update .gitignore to include feedback directory ([03a436d](https://github.com/patrick204nqh/browserctl/commit/03a436d69d37826a6c24662b70d6852eac78aa54))
|
|
26
|
+
* **v0.6:** CLI redesign — noun-verb commands, storage/session, daemon auto-index ([#41](https://github.com/patrick204nqh/browserctl/issues/41)) ([b40040f](https://github.com/patrick204nqh/browserctl/commit/b40040f6fba7b78791e845e55af156be46bde3c3))
|
|
27
|
+
* **v0.6:** page_focus command, integration specs, and doc polish ([#42](https://github.com/patrick204nqh/browserctl/issues/42)) ([8cf006d](https://github.com/patrick204nqh/browserctl/commit/8cf006d0e3698bb0698c59d7af5b87f72a3cd154))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
### Bug Fixes
|
|
31
|
+
|
|
32
|
+
* correct initial checkbox state assertion to [false, true, false] ([#33](https://github.com/patrick204nqh/browserctl/issues/33)) ([5ab0fe4](https://github.com/patrick204nqh/browserctl/commit/5ab0fe4fbe5d5f244c795235f0531e0c970d75c7))
|
|
33
|
+
* dismiss on-load notifications before asserting counts ([#35](https://github.com/patrick204nqh/browserctl/issues/35)) ([b508ba6](https://github.com/patrick204nqh/browserctl/commit/b508ba6748d245526f6321a731e6a7bf4ad5cc86))
|
|
34
|
+
* fill replaces existing input value instead of appending ([#29](https://github.com/patrick204nqh/browserctl/issues/29)) ([c7abb48](https://github.com/patrick204nqh/browserctl/commit/c7abb48153e1c64fcd73e22a5bf376a98555f68c))
|
|
35
|
+
* notifications workflow — exclude container from counts, use evaluate for dismiss ([#34](https://github.com/patrick204nqh/browserctl/issues/34)) ([de43301](https://github.com/patrick204nqh/browserctl/commit/de43301942eb7a6335bc9fc7130c22ecf3dd8b38))
|
|
36
|
+
* README & CONTRIBUTING polish + Rakefile v0.6 CI fix ([#43](https://github.com/patrick204nqh/browserctl/issues/43)) ([422c614](https://github.com/patrick204nqh/browserctl/commit/422c6141035d47bb3fd4bc86f732029b44e35206))
|
|
37
|
+
* replace all remaining stale v0.5 CLI commands with v0.6 equivalents ([#45](https://github.com/patrick204nqh/browserctl/issues/45)) ([8d00923](https://github.com/patrick204nqh/browserctl/commit/8d0092380bbc7890761e6d8f5da7864358ce88f7))
|
|
38
|
+
* update demo assets installation and improve login tape requirements ([cf02e3a](https://github.com/patrick204nqh/browserctl/commit/cf02e3ad11fc99f1483904e396160d4345363e17))
|
|
39
|
+
* update login demo tape to use data-test selectors for input fields and buttons ([016fcda](https://github.com/patrick204nqh/browserctl/commit/016fcdaf77ce542d06efe898312063581fc3fffe))
|
|
40
|
+
* update stale `browserctl run` references to `browserctl workflow run` ([#44](https://github.com/patrick204nqh/browserctl/issues/44)) ([7f7005a](https://github.com/patrick204nqh/browserctl/commit/7f7005ac6fd6517c8751cf808378233343e9ace8))
|
|
41
|
+
* use baseline delta assertions instead of absolute notification counts ([#37](https://github.com/patrick204nqh/browserctl/issues/37)) ([33ac119](https://github.com/patrick204nqh/browserctl/commit/33ac119bbb43c0290fe03da06ca3f8545095f7a2))
|
|
42
|
+
* wait for notification-container before dismissing on-load toasts ([#36](https://github.com/patrick204nqh/browserctl/issues/36)) ([883cdf8](https://github.com/patrick204nqh/browserctl/commit/883cdf839bcef2d57bcb4764ff89862aa1969486))
|
|
43
|
+
|
|
13
44
|
## [0.5.0](https://github.com/patrick204nqh/browserctl/compare/v0.4.0...v0.5.0) (2026-04-25)
|
|
14
45
|
|
|
15
46
|
|
data/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<h1 align="center">browserctl</h1>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
A
|
|
8
|
+
A browser daemon that keeps sessions alive between commands — for AI agents and iterative dev workflows.
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
@@ -20,11 +20,11 @@ Every browser automation tool restarts the browser when your script ends. That m
|
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
22
|
browserd & # start the daemon (headless)
|
|
23
|
-
browserctl open
|
|
24
|
-
browserctl
|
|
25
|
-
browserctl fill
|
|
26
|
-
browserctl click
|
|
27
|
-
browserctl
|
|
23
|
+
browserctl page open main --url https://example.com/login
|
|
24
|
+
browserctl snapshot main # AI-friendly JSON snapshot with ref IDs
|
|
25
|
+
browserctl fill main --ref e1 --value me@example.com # interact by ref, no selectors needed
|
|
26
|
+
browserctl click main --ref e2
|
|
27
|
+
browserctl daemon stop
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
---
|
|
@@ -37,7 +37,7 @@ browserctl shutdown
|
|
|
37
37
|
**Terminal**<br/>
|
|
38
38
|
<sub>CLI commands, live output, session persistence proof</sub>
|
|
39
39
|
|
|
40
|
-
<img src="docs/assets/terminal.
|
|
40
|
+
<img src="docs/assets/terminal.gif" alt="browserctl terminal demo"/>
|
|
41
41
|
|
|
42
42
|
</td>
|
|
43
43
|
<td align="center" width="50%">
|
|
@@ -50,15 +50,6 @@ browserctl shutdown
|
|
|
50
50
|
</td>
|
|
51
51
|
</tr></table>
|
|
52
52
|
|
|
53
|
-
> Demo assets are regenerated automatically on every push to `main` that touches `demo/` or the login example. To regenerate locally:
|
|
54
|
-
>
|
|
55
|
-
> ```bash
|
|
56
|
-
> rake demo # full pipeline: screenshots + browser GIF + terminal GIF
|
|
57
|
-
> rake demo:screenshots # smoke test screenshots only
|
|
58
|
-
> rake demo:browser_gif # browser animation only (requires: ffmpeg)
|
|
59
|
-
> rake demo:terminal # terminal GIF only (requires: vhs)
|
|
60
|
-
> ```
|
|
61
|
-
|
|
62
53
|
---
|
|
63
54
|
|
|
64
55
|
## Quick Start
|
|
@@ -71,22 +62,23 @@ gem install browserctl
|
|
|
71
62
|
browserd &
|
|
72
63
|
|
|
73
64
|
# 3. Open a named page
|
|
74
|
-
browserctl open main --url https://
|
|
65
|
+
browserctl page open main --url https://moatazeldebsy.github.io/test-automation-practices/#/auth
|
|
75
66
|
|
|
76
|
-
# 4. Snapshot
|
|
77
|
-
browserctl
|
|
67
|
+
# 4. Snapshot — returns JSON with a ref ID per interactable element
|
|
68
|
+
browserctl snapshot main
|
|
69
|
+
# → [{"ref":"e1","tag":"input","attrs":{"data-test":"username-input"}}, {"ref":"e2",...}, {"ref":"e3","tag":"button","text":"Login",...}]
|
|
78
70
|
|
|
79
|
-
# 5. Interact using
|
|
80
|
-
browserctl fill
|
|
81
|
-
browserctl fill
|
|
71
|
+
# 5. Interact using the ref IDs from the snapshot
|
|
72
|
+
browserctl fill main --ref e1 --value admin
|
|
73
|
+
browserctl fill main --ref e2 --value admin
|
|
82
74
|
browserctl click main --ref e3
|
|
83
75
|
|
|
84
76
|
# 6. Observe
|
|
85
|
-
browserctl url
|
|
86
|
-
browserctl
|
|
77
|
+
browserctl url main
|
|
78
|
+
browserctl snapshot main --diff # only what changed
|
|
87
79
|
|
|
88
80
|
# 7. Done
|
|
89
|
-
browserctl
|
|
81
|
+
browserctl daemon stop
|
|
90
82
|
```
|
|
91
83
|
|
|
92
84
|
→ [Full Getting Started guide](docs/getting-started.md)
|
|
@@ -107,7 +99,7 @@ browserctl shutdown
|
|
|
107
99
|
|
|
108
100
|
Most automation tools are stateless — every script spins up a fresh browser and tears it down. browserctl doesn't.
|
|
109
101
|
|
|
110
|
-
| | browserctl | Playwright / Selenium |
|
|
102
|
+
| Capability | browserctl | Playwright / Selenium |
|
|
111
103
|
|---|---|---|
|
|
112
104
|
| Session persists across commands | ✓ | ✗ (per-script lifecycle) |
|
|
113
105
|
| Named page handles | ✓ | ✗ |
|
|
@@ -125,7 +117,7 @@ Most automation tools are stateless — every script spins up a fresh browser an
|
|
|
125
117
|
|
|
126
118
|
## Installation
|
|
127
119
|
|
|
128
|
-
**Requirements:** Ruby >= 3.3 · Chrome or Chromium
|
|
120
|
+
**Requirements:** Ruby >= 3.3 · Chrome or Chromium installed
|
|
129
121
|
|
|
130
122
|
```bash
|
|
131
123
|
gem install browserctl
|
|
@@ -165,7 +157,7 @@ browserctl ships as a Claude Code plugin. Install it once and Claude automatical
|
|
|
165
157
|
}
|
|
166
158
|
```
|
|
167
159
|
|
|
168
|
-
Once installed,
|
|
160
|
+
Once installed, the `browserctl` skill loads automatically.
|
|
169
161
|
|
|
170
162
|
---
|
|
171
163
|
|
|
@@ -178,7 +170,7 @@ Start multiple named instances for agent isolation:
|
|
|
178
170
|
```bash
|
|
179
171
|
browserd --name agent-a &
|
|
180
172
|
browserd --name agent-b &
|
|
181
|
-
browserctl --daemon agent-a open main --url https://app.example.com
|
|
173
|
+
browserctl --daemon agent-a page open main --url https://app.example.com
|
|
182
174
|
```
|
|
183
175
|
|
|
184
176
|
The daemon shuts itself down after 30 minutes of inactivity.
|
|
@@ -210,11 +202,14 @@ bin/setup # brew bundle (macOS) + bundle install + Chrome check
|
|
|
210
202
|
bundle exec rspec # run tests
|
|
211
203
|
bundle exec rubocop # lint
|
|
212
204
|
|
|
213
|
-
rake demo
|
|
214
|
-
rake demo:screenshots
|
|
215
|
-
rake demo:
|
|
205
|
+
rake demo # full pipeline: screenshots + browser GIF + terminal GIF
|
|
206
|
+
rake demo:screenshots # smoke test screenshots only
|
|
207
|
+
rake demo:browser_gif # browser animation only (requires: ffmpeg)
|
|
208
|
+
rake demo:terminal # terminal GIF only (requires: vhs)
|
|
216
209
|
```
|
|
217
210
|
|
|
211
|
+
> Demo assets are regenerated automatically on every push to `main` that touches `demo/` or the login example.
|
|
212
|
+
|
|
218
213
|
---
|
|
219
214
|
|
|
220
215
|
## Contributing
|
data/bin/browserctl
CHANGED
|
@@ -14,20 +14,21 @@ require "json"
|
|
|
14
14
|
require "optimist"
|
|
15
15
|
require "browserctl"
|
|
16
16
|
require "browserctl/commands/cli_output"
|
|
17
|
-
require "browserctl/commands/open_page"
|
|
18
17
|
require "browserctl/commands/fill"
|
|
19
18
|
require "browserctl/commands/click"
|
|
20
19
|
require "browserctl/commands/snapshot"
|
|
21
20
|
require "browserctl/commands/screenshot"
|
|
22
|
-
require "browserctl/commands/watch"
|
|
23
21
|
require "browserctl/commands/record"
|
|
24
|
-
require "browserctl/commands/pause"
|
|
25
22
|
require "browserctl/commands/resume"
|
|
26
23
|
require "browserctl/commands/init"
|
|
27
|
-
require "browserctl/commands/
|
|
28
|
-
require "browserctl/commands/
|
|
29
|
-
require "browserctl/commands/
|
|
30
|
-
require "browserctl/commands/
|
|
24
|
+
require "browserctl/commands/page"
|
|
25
|
+
require "browserctl/commands/cookie"
|
|
26
|
+
require "browserctl/commands/storage"
|
|
27
|
+
require "browserctl/commands/session"
|
|
28
|
+
require "browserctl/commands/daemon"
|
|
29
|
+
require "browserctl/commands/workflow"
|
|
30
|
+
require "browserctl/commands/dialog"
|
|
31
|
+
require "browserctl/commands/ask"
|
|
31
32
|
|
|
32
33
|
def print_result(res)
|
|
33
34
|
if res.is_a?(Hash) && res[:error]
|
|
@@ -37,59 +38,86 @@ def print_result(res)
|
|
|
37
38
|
puts res.to_json
|
|
38
39
|
end
|
|
39
40
|
|
|
40
|
-
# rubocop:disable Metrics/MethodLength
|
|
41
41
|
def usage
|
|
42
42
|
puts <<~USAGE
|
|
43
43
|
Usage: browserctl <command> [args]
|
|
44
44
|
|
|
45
45
|
Setup:
|
|
46
|
-
init
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
open
|
|
50
|
-
close
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
46
|
+
init
|
|
47
|
+
|
|
48
|
+
Page:
|
|
49
|
+
page open <name> [--url URL]
|
|
50
|
+
page close <name>
|
|
51
|
+
page list
|
|
52
|
+
page focus <name>
|
|
53
|
+
|
|
54
|
+
Interaction (page is always first arg after verb):
|
|
55
|
+
navigate <page> <url>
|
|
56
|
+
fill <page> <selector> <value>
|
|
57
|
+
<page> --ref <ref> --value <value>
|
|
58
|
+
click <page> <selector>
|
|
59
|
+
<page> --ref <ref>
|
|
60
|
+
snapshot <page> [--format elements|html] [--diff]
|
|
61
|
+
screenshot <page> [--out PATH] [--full]
|
|
62
|
+
evaluate <page> <expression>
|
|
63
|
+
url <page>
|
|
64
|
+
wait <page> <selector> [--timeout N]
|
|
65
|
+
pause <page> [--message MSG]
|
|
66
|
+
resume <page>
|
|
67
|
+
ask <prompt>
|
|
68
|
+
devtools <page>
|
|
69
|
+
press <page> <key>
|
|
70
|
+
hover <page> <selector>
|
|
71
|
+
upload <page> <selector> <file>
|
|
72
|
+
select <page> <selector> <value>
|
|
73
|
+
dialog accept <page> [text]
|
|
74
|
+
dialog dismiss <page>
|
|
75
|
+
|
|
76
|
+
Cookie:
|
|
77
|
+
cookie list <page>
|
|
78
|
+
cookie set <page> <name> <value> --domain DOMAIN [--path /]
|
|
79
|
+
cookie delete <page>
|
|
80
|
+
cookie export <page> <path>
|
|
81
|
+
cookie import <page> <path>
|
|
82
|
+
|
|
83
|
+
Storage:
|
|
84
|
+
storage get <page> <key> [--store local|session]
|
|
85
|
+
storage set <page> <key> <value> [--store local|session]
|
|
86
|
+
storage export <page> <path> [--store local|session|all]
|
|
87
|
+
storage import <page> <path>
|
|
88
|
+
storage delete <page> [--store local|session|all]
|
|
89
|
+
|
|
90
|
+
Session:
|
|
91
|
+
session save <name>
|
|
92
|
+
session load <name>
|
|
93
|
+
session list
|
|
94
|
+
session delete <name>
|
|
95
|
+
session export <name> <path>
|
|
96
|
+
session import <path>
|
|
97
|
+
|
|
98
|
+
Recording:
|
|
99
|
+
record start <name>
|
|
100
|
+
record stop [--out PATH]
|
|
101
|
+
record status
|
|
102
|
+
|
|
103
|
+
Workflow:
|
|
104
|
+
workflow run <name|file> [--params file] [--key value ...]
|
|
105
|
+
workflow list
|
|
106
|
+
workflow describe <name>
|
|
107
|
+
|
|
108
|
+
Daemon:
|
|
109
|
+
daemon start [--headed] [--name NAME]
|
|
110
|
+
daemon stop
|
|
111
|
+
daemon status
|
|
112
|
+
daemon ping
|
|
113
|
+
daemon list
|
|
114
|
+
|
|
115
|
+
Global options:
|
|
116
|
+
--daemon <name> Connect to named or auto-indexed daemon (d1, d2, work, ...)
|
|
117
|
+
--version, -v
|
|
89
118
|
USAGE
|
|
90
119
|
exit 0
|
|
91
120
|
end
|
|
92
|
-
# rubocop:enable Metrics/MethodLength
|
|
93
121
|
|
|
94
122
|
daemon_idx = ARGV.index("--daemon")
|
|
95
123
|
daemon_name = if daemon_idx
|
|
@@ -105,70 +133,80 @@ usage if cmd.nil? || %w[-h --help help].include?(cmd)
|
|
|
105
133
|
runner = Browserctl::Runner.new
|
|
106
134
|
|
|
107
135
|
case cmd
|
|
108
|
-
when "run
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
load File.expand_path(name)
|
|
113
|
-
name = (Browserctl.registry_snapshot.keys - before).first || File.basename(name, ".rb")
|
|
114
|
-
end
|
|
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 = {}
|
|
127
|
-
args.each_slice(2) do |flag, val|
|
|
128
|
-
key = flag.sub(/\A--/, "").to_sym
|
|
129
|
-
cli_params[key] = val
|
|
130
|
-
end
|
|
131
|
-
params = file_params.merge(cli_params)
|
|
132
|
-
success = runner.run_workflow(name, **params)
|
|
133
|
-
exit(success ? 0 : 1)
|
|
134
|
-
|
|
135
|
-
when "workflows"
|
|
136
|
-
list = runner.list_workflows
|
|
137
|
-
list.each { |w| puts "#{w[:name].ljust(24)} #{w[:desc]}" }
|
|
138
|
-
|
|
139
|
-
when "describe"
|
|
140
|
-
name = args.shift or abort "usage: browserctl describe <workflow_name>"
|
|
141
|
-
puts JSON.pretty_generate(runner.describe_workflow(name))
|
|
142
|
-
|
|
143
|
-
when "record" then Browserctl::Commands::Record.run(args)
|
|
144
|
-
when "init" then Browserctl::Commands::Init.run(args)
|
|
136
|
+
when "workflow" then Browserctl::Commands::Workflow.run(runner, args)
|
|
137
|
+
when "record" then Browserctl::Commands::Record.run(args)
|
|
138
|
+
when "init" then Browserctl::Commands::Init.run(args)
|
|
139
|
+
when "ask" then Browserctl::Commands::Ask.run(args)
|
|
145
140
|
|
|
146
141
|
else
|
|
147
142
|
client = Browserctl::Client.new(Browserctl.socket_path(daemon_name))
|
|
148
143
|
|
|
149
144
|
case cmd
|
|
150
|
-
when "
|
|
151
|
-
when "
|
|
152
|
-
when "
|
|
153
|
-
when "
|
|
145
|
+
when "page" then Browserctl::Commands::Page.run(client, args)
|
|
146
|
+
when "cookie" then Browserctl::Commands::Cookie.run(client, args)
|
|
147
|
+
when "storage" then Browserctl::Commands::Storage.run(client, args)
|
|
148
|
+
when "session" then Browserctl::Commands::Session.run(client, args)
|
|
149
|
+
when "daemon" then Browserctl::Commands::Daemon.run(client, args)
|
|
150
|
+
when "navigate" then print_result(client.navigate(args[0], args[1]))
|
|
154
151
|
when "fill" then Browserctl::Commands::Fill.run(client, args)
|
|
155
152
|
when "click" then Browserctl::Commands::Click.run(client, args)
|
|
156
|
-
when "
|
|
157
|
-
when "
|
|
153
|
+
when "snapshot" then Browserctl::Commands::Snapshot.run(client, args)
|
|
154
|
+
when "screenshot" then Browserctl::Commands::Screenshot.run(client, args)
|
|
155
|
+
when "evaluate" then print_result(client.evaluate(args[0], args[1]))
|
|
158
156
|
when "url" then print_result(client.url(args[0]))
|
|
159
|
-
when "
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
when "
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
157
|
+
when "wait"
|
|
158
|
+
opts = Optimist.options(args) do
|
|
159
|
+
opt :timeout, "Seconds to wait (default: 30)", default: 30.0, short: "-t"
|
|
160
|
+
end
|
|
161
|
+
name = args.shift
|
|
162
|
+
selector = args.shift
|
|
163
|
+
abort "usage: browserctl wait <page> <selector> [--timeout N]" unless name && selector
|
|
164
|
+
print_result(client.wait(name, selector, timeout: opts[:timeout]))
|
|
165
|
+
when "pause"
|
|
166
|
+
opts = Optimist.options(args) do
|
|
167
|
+
opt :message, "Message shown to human", type: :string, short: "-m"
|
|
168
|
+
end
|
|
169
|
+
name = args.shift or abort "usage: browserctl pause <page> [--message MSG]"
|
|
170
|
+
res = client.pause(name, message: opts[:message])
|
|
171
|
+
if res[:error]
|
|
172
|
+
warn "Error: #{res[:error]}"
|
|
173
|
+
exit 1
|
|
174
|
+
end
|
|
175
|
+
puts "Page '#{name}' paused. Browser is live — interact freely."
|
|
176
|
+
puts "(#{opts[:message]})" if opts[:message]
|
|
177
|
+
puts "When done: browserctl resume #{name}"
|
|
178
|
+
when "resume" then Browserctl::Commands::Resume.run(client, args)
|
|
179
|
+
when "press"
|
|
180
|
+
name = args.shift or abort "usage: browserctl press <page> <key>"
|
|
181
|
+
key = args.shift or abort "usage: browserctl press <page> <key>"
|
|
182
|
+
print_result(client.press(name, key))
|
|
183
|
+
when "hover"
|
|
184
|
+
name = args.shift or abort "usage: browserctl hover <page> <selector>"
|
|
185
|
+
selector = args.shift or abort "usage: browserctl hover <page> <selector>"
|
|
186
|
+
print_result(client.hover(name, selector))
|
|
187
|
+
when "upload"
|
|
188
|
+
name = args.shift or abort "usage: browserctl upload <page> <selector> <file>"
|
|
189
|
+
selector = args.shift or abort "usage: browserctl upload <page> <selector> <file>"
|
|
190
|
+
path = args.shift or abort "usage: browserctl upload <page> <selector> <file>"
|
|
191
|
+
print_result(client.upload(name, selector, path))
|
|
192
|
+
when "select"
|
|
193
|
+
name = args.shift or abort "usage: browserctl select <page> <selector> <value>"
|
|
194
|
+
selector = args.shift or abort "usage: browserctl select <page> <selector> <value>"
|
|
195
|
+
value = args.shift or abort "usage: browserctl select <page> <selector> <value>"
|
|
196
|
+
print_result(client.select(name, selector, value))
|
|
197
|
+
when "dialog" then Browserctl::Commands::Dialog.run(client, args)
|
|
198
|
+
when "devtools"
|
|
199
|
+
name = args.shift or abort "usage: browserctl devtools <page>"
|
|
200
|
+
res = client.devtools(name)
|
|
201
|
+
if res[:error]
|
|
202
|
+
warn "Error: #{res[:error]}"
|
|
203
|
+
exit 1
|
|
204
|
+
end
|
|
205
|
+
url = res[:devtools_url]
|
|
206
|
+
puts "Opening DevTools for '#{name}':"
|
|
207
|
+
puts " #{url}"
|
|
208
|
+
opener = RUBY_PLATFORM =~ /darwin/ ? "open" : "xdg-open"
|
|
209
|
+
system(opener, url)
|
|
172
210
|
else
|
|
173
211
|
abort "unknown command: #{cmd}\nRun 'browserctl --help' for usage."
|
|
174
212
|
end
|
data/bin/browserd
CHANGED
|
@@ -20,11 +20,17 @@ if opts[:name] && opts[:name] !~ /\A[a-zA-Z0-9_-]{1,64}\z/
|
|
|
20
20
|
abort "Invalid daemon name #{opts[:name].inspect} — use only letters, digits, _ or -"
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
assigned_name = opts[:name] || Browserctl.next_daemon_name
|
|
24
|
+
if assigned_name && !opts[:name]
|
|
25
|
+
warn "browserd: default slot taken — starting as '#{assigned_name}'"
|
|
26
|
+
warn " to connect: browserctl --daemon #{assigned_name} <command>"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
log_path = Browserctl.log_path(assigned_name)
|
|
24
30
|
warn "browserd starting — log: #{log_path}"
|
|
25
31
|
Browserctl.logger = Browserctl.build_logger(opts[:log_level], log_path: log_path)
|
|
26
32
|
Browserctl::Server.new(
|
|
27
33
|
headless: !opts[:headed],
|
|
28
|
-
socket_path: Browserctl.socket_path(
|
|
29
|
-
pid_path: Browserctl.pid_path(
|
|
34
|
+
socket_path: Browserctl.socket_path(assigned_name),
|
|
35
|
+
pid_path: Browserctl.pid_path(assigned_name)
|
|
30
36
|
).run
|
data/examples/cloudflare_hitl.rb
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
# pauses automation so a human can solve it in the live browser, then resumes.
|
|
8
8
|
#
|
|
9
9
|
# Run:
|
|
10
|
-
# browserctl run examples/cloudflare_hitl.rb --url https://example.com
|
|
10
|
+
# browserctl workflow run examples/cloudflare_hitl.rb --url https://example.com
|
|
11
11
|
#
|
|
12
12
|
# Note: modern Cloudflare often passes a real headed Chrome without challenge.
|
|
13
13
|
# The pause/resume branch fires only when the challenge page is actually served
|
|
@@ -22,11 +22,11 @@ Browserctl.workflow "cloudflare_hitl" do
|
|
|
22
22
|
param :selector, default: "body"
|
|
23
23
|
|
|
24
24
|
step "open page" do
|
|
25
|
-
|
|
25
|
+
open_page(:main)
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
step "navigate to target URL" do
|
|
29
|
-
res = client.
|
|
29
|
+
res = client.navigate("main", url)
|
|
30
30
|
|
|
31
31
|
if res[:challenge]
|
|
32
32
|
$stdout.puts ""
|
|
@@ -54,12 +54,12 @@ Browserctl.workflow "cloudflare_hitl" do
|
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
step "wait for content and snapshot" do
|
|
57
|
-
page(:main).
|
|
57
|
+
page(:main).wait(selector, timeout: 15)
|
|
58
58
|
result = page(:main).snapshot(format: "elements")
|
|
59
59
|
$stdout.puts " Snapshot: #{result[:snapshot]&.length || 0} elements captured"
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
step "close page" do
|
|
63
|
-
|
|
63
|
+
close_page(:main)
|
|
64
64
|
end
|
|
65
65
|
end
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# Smoke test for --params file loading (Task 7.5).
|
|
5
5
|
#
|
|
6
6
|
# Run with:
|
|
7
|
-
# browserctl run examples/smoke/params_file.rb --params examples/smoke/params_file.yml
|
|
7
|
+
# browserctl workflow run examples/smoke/params_file.rb --params examples/smoke/params_file.yml
|
|
8
8
|
#
|
|
9
9
|
# The workflow logs in using credentials from the params file and asserts
|
|
10
10
|
# the secure area is reached — proving the params were loaded and available.
|
|
@@ -17,7 +17,7 @@ Browserctl.workflow "smoke/params_file" do
|
|
|
17
17
|
param :base_url, default: "https://the-internet.herokuapp.com"
|
|
18
18
|
|
|
19
19
|
step "open login page" do
|
|
20
|
-
|
|
20
|
+
open_page(:main, url: "#{base_url}/login")
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
step "fill credentials from params file" do
|
|
@@ -29,6 +29,7 @@ Browserctl.workflow "smoke/params_file" do
|
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
step "assert login succeeded" do
|
|
32
|
+
page(:main).wait(".flash.success", timeout: 10)
|
|
32
33
|
assert page(:main).url.include?("/secure"), "expected redirect to /secure — params may not have loaded"
|
|
33
34
|
puts " [ok] reached secure area — params file loaded correctly"
|
|
34
35
|
end
|
|
@@ -12,12 +12,12 @@ Browserctl.workflow "smoke/store_fetch" do
|
|
|
12
12
|
param :base_url, default: "https://the-internet.herokuapp.com"
|
|
13
13
|
|
|
14
14
|
step "open dynamic loading page" do
|
|
15
|
-
|
|
15
|
+
open_page(:main, url: "#{base_url}/dynamic_loading/1")
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
step "click start and capture loaded text" do
|
|
19
19
|
page(:main).click("div#start button")
|
|
20
|
-
page(:main).
|
|
20
|
+
page(:main).wait("div#finish", timeout: 10)
|
|
21
21
|
text = client.evaluate("main", "document.querySelector('div#finish h4')?.innerText?.trim()")[:result]
|
|
22
22
|
assert text && !text.empty?, "expected loaded text, got: #{text.inspect}"
|
|
23
23
|
store(:loaded_text, text)
|
|
@@ -32,8 +32,8 @@ Browserctl.workflow "smoke/store_fetch" do
|
|
|
32
32
|
|
|
33
33
|
step "confirm fetch raises for unknown key" do
|
|
34
34
|
fetch(:nonexistent_key)
|
|
35
|
-
assert false, "expected
|
|
36
|
-
rescue
|
|
37
|
-
puts " [ok]
|
|
35
|
+
assert false, "expected WorkflowError was not raised"
|
|
36
|
+
rescue Browserctl::WorkflowError => e
|
|
37
|
+
puts " [ok] WorkflowError raised as expected: #{e.message}"
|
|
38
38
|
end
|
|
39
39
|
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Browserctl.workflow "test_automation_practices/advanced/ab_testing" do
|
|
4
|
+
desc "A/B testing page: verify one variant renders, click counter increments"
|
|
5
|
+
|
|
6
|
+
param :base_url, default: "https://moatazeldebsy.github.io/test-automation-practices"
|
|
7
|
+
param :screenshot_path, default: File.expand_path(".browserctl/screenshots/tap_advanced_ab_testing.png")
|
|
8
|
+
|
|
9
|
+
step "open A/B testing page and detect variant" do
|
|
10
|
+
open_page(:main, url: "#{base_url}/#/ab-testing")
|
|
11
|
+
page(:main).wait("[data-test='ab-testing-page']", timeout: 10)
|
|
12
|
+
title = page(:main).evaluate(
|
|
13
|
+
"document.querySelector('[data-test=\"variant-title\"]')?.textContent?.trim()"
|
|
14
|
+
)
|
|
15
|
+
assert title&.length&.positive?, "expected variant title to be present, got: #{title.inspect}"
|
|
16
|
+
store(:variant_title, title)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
step "variant title contains A or B" do
|
|
20
|
+
title = fetch(:variant_title)
|
|
21
|
+
assert title.include?("A") || title.include?("B"),
|
|
22
|
+
"expected variant A or B in title, got: #{title.inspect}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
step "click button — counter increments" do
|
|
26
|
+
initial_text = page(:main).evaluate(
|
|
27
|
+
"document.querySelector('[data-test=\"variant-button\"]')?.textContent?.trim()"
|
|
28
|
+
)
|
|
29
|
+
page(:main).click("[data-test='variant-button']")
|
|
30
|
+
sleep 0.5
|
|
31
|
+
updated_text = page(:main).evaluate(
|
|
32
|
+
"document.querySelector('[data-test=\"variant-button\"]')?.textContent?.trim()"
|
|
33
|
+
)
|
|
34
|
+
assert initial_text != updated_text,
|
|
35
|
+
"expected button text to change after click (counter), still: #{updated_text.inspect}"
|
|
36
|
+
page(:main).screenshot(path: screenshot_path)
|
|
37
|
+
end
|
|
38
|
+
end
|