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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/README.md +27 -32
  4. data/bin/browserctl +146 -108
  5. data/bin/browserd +9 -3
  6. data/examples/cloudflare_hitl.rb +5 -5
  7. data/examples/smoke/params_file.rb +3 -2
  8. data/examples/smoke/store_fetch.rb +5 -5
  9. data/examples/test_automation_practices/advanced/ab_testing.rb +38 -0
  10. data/examples/test_automation_practices/advanced/broken_images.rb +25 -0
  11. data/examples/test_automation_practices/advanced/file_download.rb +40 -0
  12. data/examples/test_automation_practices/advanced/iframes.rb +37 -0
  13. data/examples/test_automation_practices/advanced/shadow_dom.rb +35 -0
  14. data/examples/test_automation_practices/auth/login.rb +34 -0
  15. data/examples/test_automation_practices/auth/login_negative.rb +28 -0
  16. data/examples/test_automation_practices/dialogs/alerts.rb +45 -0
  17. data/examples/test_automation_practices/dialogs/notifications.rb +57 -0
  18. data/examples/test_automation_practices/dynamic/dynamic_elements.rb +41 -0
  19. data/examples/test_automation_practices/dynamic/tables.rb +47 -0
  20. data/examples/test_automation_practices/forms/checkboxes.rb +39 -0
  21. data/examples/test_automation_practices/forms/file_upload.rb +30 -0
  22. data/examples/test_automation_practices/forms/forms.rb +47 -0
  23. data/examples/test_automation_practices/forms/slider.rb +51 -0
  24. data/examples/test_automation_practices/interactions/context_menu.rb +54 -0
  25. data/examples/test_automation_practices/interactions/drag_drop.rb +41 -0
  26. data/examples/test_automation_practices/interactions/exit_intent.rb +47 -0
  27. data/examples/test_automation_practices/interactions/hover.rb +30 -0
  28. data/examples/test_automation_practices/interactions/key_press.rb +38 -0
  29. data/examples/the_internet/add_remove_elements.rb +1 -1
  30. data/examples/the_internet/checkboxes.rb +1 -1
  31. data/examples/the_internet/dropdown.rb +1 -1
  32. data/examples/the_internet/dynamic_loading.rb +2 -2
  33. data/examples/the_internet/login.rb +1 -1
  34. data/lib/browserctl/client.rb +143 -28
  35. data/lib/browserctl/commands/ask.rb +20 -0
  36. data/lib/browserctl/commands/cookie.rb +59 -0
  37. data/lib/browserctl/commands/daemon.rb +77 -0
  38. data/lib/browserctl/commands/dialog.rb +33 -0
  39. data/lib/browserctl/commands/page.rb +47 -0
  40. data/lib/browserctl/commands/record.rb +1 -1
  41. data/lib/browserctl/commands/screenshot.rb +2 -2
  42. data/lib/browserctl/commands/session.rb +69 -0
  43. data/lib/browserctl/commands/snapshot.rb +2 -2
  44. data/lib/browserctl/commands/storage.rb +67 -0
  45. data/lib/browserctl/commands/workflow.rb +64 -0
  46. data/lib/browserctl/constants.rb +20 -1
  47. data/lib/browserctl/logger.rb +4 -4
  48. data/lib/browserctl/recording.rb +4 -4
  49. data/lib/browserctl/server/command_dispatcher.rb +30 -9
  50. data/lib/browserctl/server/handlers/cookies.rb +1 -1
  51. data/lib/browserctl/server/handlers/devtools.rb +1 -1
  52. data/lib/browserctl/server/handlers/hitl.rb +2 -1
  53. data/lib/browserctl/server/handlers/interaction.rb +87 -0
  54. data/lib/browserctl/server/handlers/navigation.rb +24 -2
  55. data/lib/browserctl/server/handlers/observation.rb +0 -26
  56. data/lib/browserctl/server/handlers/page_lifecycle.rb +14 -3
  57. data/lib/browserctl/server/handlers/session.rb +93 -0
  58. data/lib/browserctl/server/handlers/storage.rb +109 -0
  59. data/lib/browserctl/server.rb +2 -2
  60. data/lib/browserctl/session.rb +79 -0
  61. data/lib/browserctl/version.rb +1 -1
  62. data/lib/browserctl/workflow.rb +50 -11
  63. metadata +36 -11
  64. data/lib/browserctl/commands/export_cookies.rb +0 -18
  65. data/lib/browserctl/commands/import_cookies.rb +0 -23
  66. data/lib/browserctl/commands/inspect.rb +0 -21
  67. data/lib/browserctl/commands/open_page.rb +0 -21
  68. data/lib/browserctl/commands/pause.rb +0 -22
  69. data/lib/browserctl/commands/status.rb +0 -30
  70. data/lib/browserctl/commands/watch.rb +0 -27
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ec75744264ce56f8c3f94ab95518a106c8225ec1dc11dd35a97f43186b590502
4
- data.tar.gz: 973c3b270b5d1bba3dfa900623790e3c5cb9d5bd8ebe8faa50c28d09e48fbfff
3
+ metadata.gz: a2662c63f1ab585b29689e8e09ac2c8f7c3214539c440c51da82efc954fc0493
4
+ data.tar.gz: 3e8f6790cdb3a3a79382f68c3a1fb6235805e6acc96658270d06c4d91ee1098b
5
5
  SHA512:
6
- metadata.gz: 16cd84c58a070de0b9d340ed2bb4e3a951216526dd77a9f18e0f9ea7292311eb51eddf1989c93c11d488cf4bf29163a60a13510f5949599d6027142cf808c1a0
7
- data.tar.gz: 615213ac9ba1e694c9fb4597a348abebab9d8f2ba04801dd7d59c1dc00829111f0cce9f49391a7883e7502422ed0b246e86d8c553c62b9be406e0f1e23946e8c
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 persistent browser daemon for AI agents and iterative dev workflows — the session stays alive between commands.
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 login --url https://example.com/login
24
- browserctl snap login # AI-friendly JSON snapshot with ref IDs
25
- browserctl fill login --ref e1 --value me@example.com # interact by ref, no selectors needed
26
- browserctl click login --ref e2
27
- browserctl shutdown
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.webp" alt="browserctl terminal demo"/>
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://the-internet.herokuapp.com/login
65
+ browserctl page open main --url https://moatazeldebsy.github.io/test-automation-practices/#/auth
75
66
 
76
- # 4. Snapshot the page get AI-friendly JSON with ref IDs
77
- browserctl snap main
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 refs
80
- browserctl fill main --ref e1 --value tomsmith
81
- browserctl fill main --ref e2 --value SuperSecretPassword!
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 main
86
- browserctl snap main --diff # only what changed
77
+ browserctl url main
78
+ browserctl snapshot main --diff # only what changed
87
79
 
88
80
  # 7. Done
89
- browserctl shutdown
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 on your `PATH`
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, Claude Code loads the `browserctl` skill automatically — no `/invoke` needed.
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 # regenerate screenshots + terminal GIF
214
- rake demo:screenshots # screenshots only (no VHS required)
215
- rake demo:terminal # terminal GIF only
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/inspect"
28
- require "browserctl/commands/export_cookies"
29
- require "browserctl/commands/import_cookies"
30
- require "browserctl/commands/status"
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 Scaffold .browserctl/ in this project
47
-
48
- Browser commands (require browserd running):
49
- open <page> [--url URL] Open or focus a named page
50
- close <page> Close a named page
51
- pages List open pages
52
- goto <page> <url> Navigate a page
53
- fill <page> <selector> <value> Fill an input
54
- <page> --ref <ref> --value <value> Fill via snapshot ref
55
- click <page> <selector> Click an element
56
- <page> --ref <ref> Click via snapshot ref
57
- shot <page> [--out PATH] [--full] Take a screenshot
58
- snap <page> [--format ai|html] [--diff] Snapshot DOM (default: ai)
59
- url <page> Print current URL
60
- eval <page> <expression> Evaluate JS expression
61
- watch <page> <selector> [--timeout N] Wait for a selector to appear
62
- pause <page> Pause automation — browser stays live
63
- resume <page> Resume automation after manual action
64
- inspect <page> Open Chrome DevTools for a named page
65
- cookies <page> List all cookies as JSON
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
70
-
71
- Recording commands:
72
- record start <name> Start recording browser commands
73
- record stop [--out PATH] Stop recording and save workflow
74
- record status Show active recording name
75
-
76
- Workflow commands:
77
- run <name|file> [--params file] [--key value ...] Run a workflow
78
- workflows List available workflows
79
- describe <name> Describe a workflow
80
-
81
- Daemon commands:
82
- ping Check if browserd is alive
83
- status Show daemon status, PID, and open pages
84
- shutdown Stop browserd
85
-
86
- Options:
87
- --daemon <name> Connect to a named daemon instance
88
- --version, -v Print version and exit
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
- name = args.shift or abort "usage: browserctl run <workflow_name|file.rb> [--key value ...]"
110
- if File.exist?(name)
111
- before = Browserctl.registry_snapshot.keys
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 "open" then Browserctl::Commands::OpenPage.run(client, args)
151
- when "close" then print_result(client.close_page(args[0]))
152
- when "pages" then print_result(client.list_pages)
153
- when "goto" then print_result(client.goto(args[0], args[1]))
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 "shot" then Browserctl::Commands::Screenshot.run(client, args)
157
- when "snap" then Browserctl::Commands::Snapshot.run(client, args)
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 "eval" then print_result(client.evaluate(args[0], args[1]))
160
- when "watch" then Browserctl::Commands::Watch.run(client, args)
161
- when "pause" then Browserctl::Commands::Pause.run(client, args)
162
- when "resume" then Browserctl::Commands::Resume.run(client, args)
163
- when "inspect" then Browserctl::Commands::Inspect.run(client, args)
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)
169
- when "ping" then print_result(client.ping)
170
- when "status" then Browserctl::Commands::Status.run(client)
171
- when "shutdown" then print_result(client.shutdown)
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
- log_path = Browserctl.log_path(opts[:name])
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(opts[:name]),
29
- pid_path: Browserctl.pid_path(opts[:name])
34
+ socket_path: Browserctl.socket_path(assigned_name),
35
+ pid_path: Browserctl.pid_path(assigned_name)
30
36
  ).run
@@ -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
- client.open_page("main")
25
+ open_page(:main)
26
26
  end
27
27
 
28
28
  step "navigate to target URL" do
29
- res = client.goto("main", url)
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).wait_for(selector, timeout: 15)
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
- client.close_page("main")
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
- client.open_page("main", url: "#{base_url}/login")
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
- client.open_page("main", url: "#{base_url}/dynamic_loading/1")
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).wait_for("div#finish", timeout: 10)
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 KeyError was not raised"
36
- rescue KeyError => e
37
- puts " [ok] KeyError raised as expected: #{e.message}"
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