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
@@ -81,32 +81,6 @@ module Browserctl
81
81
  name_safe = page_name.to_s.gsub(/[^a-zA-Z0-9_-]/, "_")
82
82
  File.join(SCREENSHOT_DIR, "browserctl_shot_#{name_safe}_#{Time.now.to_i}.png")
83
83
  end
84
-
85
- def cmd_wait_for(req)
86
- with_page(req[:name]) do |session|
87
- wait_for_selector(session.page, req[:selector], req.fetch(:timeout, 10).to_f)
88
- end
89
- end
90
-
91
- def cmd_watch(req)
92
- with_page(req[:name]) do |session|
93
- result = wait_for_selector(session.page, req[:selector], req.fetch(:timeout, 30).to_f)
94
- result[:error] ? result : { ok: true, selector: req[:selector] }
95
- end
96
- end
97
-
98
- def wait_for_selector(page, selector, timeout)
99
- deadline = Time.now + timeout
100
- loop do
101
- found = page.at_css(selector)
102
- break { ok: true } if found
103
- if Time.now >= deadline
104
- break { error: "wait_for timeout: selector '#{selector}' not found after #{timeout}s" }
105
- end
106
-
107
- sleep 0.2
108
- end
109
- end
110
84
  end
111
85
  end
112
86
  end
@@ -6,7 +6,7 @@ module Browserctl
6
6
  module PageLifecycle
7
7
  private
8
8
 
9
- def cmd_open_page(req)
9
+ def cmd_page_open(req)
10
10
  session = @global_mutex.synchronize do
11
11
  @pages[req[:name]] ||= PageSession.new(@browser.create_page)
12
12
  end
@@ -14,15 +14,26 @@ module Browserctl
14
14
  { ok: true, name: req[:name] }
15
15
  end
16
16
 
17
- def cmd_close_page(req)
17
+ def cmd_page_close(req)
18
18
  session = @global_mutex.synchronize { @pages.delete(req[:name]) }
19
19
  session&.page&.close
20
20
  { ok: true }
21
21
  end
22
22
 
23
- def cmd_list_pages(_req)
23
+ def cmd_page_list(_req)
24
24
  { pages: @global_mutex.synchronize { @pages.keys } }
25
25
  end
26
+
27
+ def cmd_page_focus(req)
28
+ unless @browser.options.headless == false
29
+ return { error: "page focus requires headed mode — start browserd with --headed" }
30
+ end
31
+
32
+ with_page(req[:name]) do |session|
33
+ session.page.activate
34
+ { ok: true }
35
+ end
36
+ end
26
37
  end
27
38
  end
28
39
  end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../session"
4
+
5
+ module Browserctl
6
+ class CommandDispatcher
7
+ module Handlers
8
+ module Session
9
+ private
10
+
11
+ def cmd_session_save(req)
12
+ first_session = @global_mutex.synchronize { @pages.values.first }
13
+ return { error: "no open pages — open a page before saving a session" } unless first_session
14
+
15
+ cookies = first_session.page.cookies.all.values.map(&:to_h)
16
+
17
+ pages_meta = {}
18
+ local_storage = {}
19
+ @global_mutex.synchronize { @pages.dup }.each do |page_name, session|
20
+ session.mutex.synchronize do
21
+ origin = session.page.evaluate("location.origin")
22
+ local_str = session.page.evaluate("JSON.stringify({...localStorage})")
23
+ pages_meta[page_name] = { url: session.page.current_url, title: session.page.title }
24
+ local_storage[origin] = JSON.parse(local_str)
25
+ end
26
+ end
27
+
28
+ now = Time.now.iso8601
29
+ Browserctl::Session.save(
30
+ req[:session_name],
31
+ metadata: { version: 1, name: req[:session_name],
32
+ created_at: now, updated_at: now, pages: pages_meta },
33
+ cookies: cookies,
34
+ local_storage: local_storage,
35
+ session_storage: {}
36
+ )
37
+ { ok: true, path: Browserctl::Session.path(req[:session_name]),
38
+ pages: pages_meta.length, cookies: cookies.length }
39
+ end
40
+
41
+ def cmd_session_load(req)
42
+ data = Browserctl::Session.load(req[:session_name])
43
+
44
+ data[:metadata][:pages].each do |page_name, page_data|
45
+ existing = @global_mutex.synchronize { @pages[page_name.to_s] }
46
+ if existing
47
+ existing.page.go_to(page_data[:url])
48
+ else
49
+ new_page = @browser.create_page
50
+ new_page.go_to(page_data[:url])
51
+ @global_mutex.synchronize { @pages[page_name.to_s] = PageSession.new(new_page) }
52
+ end
53
+ end
54
+
55
+ seed_session = @global_mutex.synchronize { @pages.values.first }
56
+ cookie_count = data[:cookies].length
57
+ data[:cookies].each { |c| seed_session.page.cookies.set(**c.slice(:name, :value, :domain, :path)) }
58
+
59
+ ls_key_count = 0
60
+ data[:local_storage].each do |origin, keys|
61
+ next if keys.empty?
62
+
63
+ tmp_page = @browser.create_page
64
+ begin
65
+ tmp_page.go_to(origin)
66
+ keys.each do |k, v|
67
+ tmp_page.evaluate("localStorage.setItem(#{k.to_json}, #{v.to_json})")
68
+ ls_key_count += 1
69
+ end
70
+ ensure
71
+ tmp_page.close
72
+ end
73
+ end
74
+
75
+ { ok: true, cookies: cookie_count, pages: data[:metadata][:pages].length,
76
+ local_storage_keys: ls_key_count }
77
+ rescue RuntimeError => e
78
+ { error: e.message }
79
+ end
80
+
81
+ def cmd_session_list(_req)
82
+ sessions = Browserctl::Session.all
83
+ { ok: true, sessions: sessions }
84
+ end
85
+
86
+ def cmd_session_delete(req)
87
+ Browserctl::Session.delete(req[:session_name])
88
+ { ok: true }
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Browserctl
4
+ class CommandDispatcher
5
+ module Handlers
6
+ module Storage
7
+ private
8
+
9
+ # Returns { ok: true, value: } or { error: }
10
+ def cmd_storage_get(req)
11
+ with_page(req[:name]) do |session|
12
+ store = req.fetch(:store, "local")
13
+ js = storage_js_get(store, req[:key])
14
+ return { error: "unknown store '#{store}' — use 'local' or 'session'" } unless js
15
+
16
+ value = session.page.evaluate(js)
17
+ { ok: true, value: value }
18
+ end
19
+ end
20
+
21
+ # Returns { ok: true } or { error: }
22
+ def cmd_storage_set(req)
23
+ with_page(req[:name]) do |session|
24
+ store = req.fetch(:store, "local")
25
+ js = storage_js_set(store, req[:key], req[:value])
26
+ return { error: "unknown store '#{store}' — use 'local' or 'session'" } unless js
27
+
28
+ session.page.evaluate(js)
29
+ { ok: true }
30
+ end
31
+ end
32
+
33
+ # Exports localStorage and/or sessionStorage to a JSON file.
34
+ # Returns { ok: true, path:, key_count: } or { error: }
35
+ def cmd_storage_export(req)
36
+ with_page(req[:name]) do |session|
37
+ stores = req.fetch(:stores, "all")
38
+ data = {}
39
+
40
+ origin = session.page.evaluate("location.origin")
41
+ data[origin] = {}
42
+
43
+ if %w[local all].include?(stores)
44
+ local = JSON.parse(session.page.evaluate("JSON.stringify({...localStorage})") || "{}")
45
+ data[origin].merge!(local)
46
+ end
47
+
48
+ if %w[session all].include?(stores)
49
+ sess = JSON.parse(session.page.evaluate("JSON.stringify({...sessionStorage})") || "{}")
50
+ data[origin].merge!(sess)
51
+ end
52
+
53
+ path = File.expand_path(req[:path])
54
+ FileUtils.mkdir_p(File.dirname(path))
55
+ File.write(path, JSON.generate(data))
56
+ { ok: true, path: path, key_count: data[origin].length }
57
+ end
58
+ end
59
+
60
+ # Imports keys from a JSON file into the page's localStorage only.
61
+ # sessionStorage keys in the file are ignored (sessionStorage is tab-scoped and not restorable).
62
+ # Returns { ok: true, origins: N, key_count: M } or { error: }
63
+ def cmd_storage_import(req)
64
+ path = File.expand_path(req[:path])
65
+ return { error: "file not found: #{path}" } unless File.exist?(path)
66
+
67
+ data = JSON.parse(File.read(path))
68
+ return { error: "invalid storage file format" } unless data.is_a?(Hash)
69
+
70
+ with_page(req[:name]) do |session|
71
+ key_count = 0
72
+ data.each_value do |keys|
73
+ keys.each do |k, v|
74
+ session.page.evaluate("localStorage.setItem(#{k.to_json}, #{v.to_json})")
75
+ key_count += 1
76
+ end
77
+ end
78
+ { ok: true, origins: data.length, key_count: key_count }
79
+ end
80
+ end
81
+
82
+ # Clears localStorage and/or sessionStorage for the page.
83
+ # Returns { ok: true } or { error: }
84
+ def cmd_storage_delete(req)
85
+ with_page(req[:name]) do |session|
86
+ stores = req.fetch(:stores, "all")
87
+ session.page.evaluate("localStorage.clear()") if %w[local all].include?(stores)
88
+ session.page.evaluate("sessionStorage.clear()") if %w[session all].include?(stores)
89
+ { ok: true }
90
+ end
91
+ end
92
+
93
+ def storage_js_get(store, key)
94
+ case store
95
+ when "local" then "localStorage.getItem(#{key.to_json})"
96
+ when "session" then "sessionStorage.getItem(#{key.to_json})"
97
+ end
98
+ end
99
+
100
+ def storage_js_set(store, key, value)
101
+ case store
102
+ when "local" then "localStorage.setItem(#{key.to_json}, #{value.to_json})"
103
+ when "session" then "sessionStorage.setItem(#{key.to_json}, #{value.to_json})"
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -76,7 +76,7 @@ module Browserctl
76
76
  return unless pid.positive?
77
77
 
78
78
  Process.kill(0, pid)
79
- abort "browserd already running (PID #{pid}). Use 'browserctl shutdown' first."
79
+ abort "browserd already running (PID #{pid}). Use 'browserctl daemon stop' first."
80
80
  rescue Errno::ESRCH
81
81
  # Dead process — stale PID file, safe to continue
82
82
  rescue Errno::EPERM
@@ -125,7 +125,7 @@ module Browserctl
125
125
 
126
126
  def quietly
127
127
  yield
128
- rescue Exception # rubocop:disable Lint/RescueException
128
+ rescue Exception
129
129
  nil
130
130
  end
131
131
  end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "json"
5
+ require_relative "constants"
6
+
7
+ module Browserctl
8
+ class Session
9
+ BASE_DIR = File.join(BROWSERCTL_DIR, "sessions")
10
+
11
+ SAFE_NAME = /\A[a-zA-Z0-9_-]{1,64}\z/
12
+
13
+ def self.path(name) = File.join(BASE_DIR, name)
14
+ def self.exist?(name) = Dir.exist?(path(name))
15
+
16
+ def self.delete(name)
17
+ validate_name!(name)
18
+ FileUtils.rm_rf(path(name))
19
+ end
20
+
21
+ def self.all
22
+ Dir[File.join(BASE_DIR, "*/metadata.json")].filter_map { |f| load_meta(f) }
23
+ end
24
+
25
+ def self.save(session_name, metadata:, cookies:, local_storage:, session_storage:)
26
+ validate_name!(session_name)
27
+ dir = path(session_name)
28
+ FileUtils.mkdir_p(dir)
29
+ write_json(File.join(dir, "metadata.json"), metadata)
30
+ write_secret(File.join(dir, "cookies.json"), cookies)
31
+ write_secret(File.join(dir, "local_storage.json"), local_storage)
32
+ return if session_storage.empty?
33
+
34
+ write_secret(File.join(dir, "session_storage.json"), session_storage)
35
+ end
36
+
37
+ def self.load(session_name)
38
+ validate_name!(session_name)
39
+ dir = path(session_name)
40
+ raise "session '#{session_name}' not found" unless Dir.exist?(dir)
41
+
42
+ {
43
+ metadata: JSON.parse(File.read(File.join(dir, "metadata.json")), symbolize_names: true),
44
+ cookies: JSON.parse(File.read(File.join(dir, "cookies.json")), symbolize_names: true),
45
+ local_storage: JSON.parse(File.read(File.join(dir, "local_storage.json")), symbolize_names: false),
46
+ session_storage: load_session_storage(dir)
47
+ }
48
+ end
49
+
50
+ def self.load_meta(path)
51
+ JSON.parse(File.read(path), symbolize_names: true)
52
+ rescue JSON::ParserError
53
+ nil
54
+ end
55
+
56
+ def self.validate_name!(name)
57
+ return if SAFE_NAME.match?(name.to_s)
58
+
59
+ raise ArgumentError, "invalid session name #{name.inspect} — use letters, digits, _ or - (max 64 chars)"
60
+ end
61
+
62
+ def self.load_session_storage(dir)
63
+ ss_path = File.join(dir, "session_storage.json")
64
+ File.exist?(ss_path) ? JSON.parse(File.read(ss_path), symbolize_names: false) : {}
65
+ end
66
+ private_class_method :load_session_storage
67
+
68
+ def self.write_json(path, data)
69
+ File.write(path, JSON.generate(data))
70
+ end
71
+ private_class_method :write_json
72
+
73
+ # Cookies and storage contain secrets — restrict to owner read/write only.
74
+ def self.write_secret(path, data)
75
+ File.open(path, "w", 0o600) { |f| f.write(JSON.generate(data)) }
76
+ end
77
+ private_class_method :write_secret
78
+ end
79
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Browserctl
4
- VERSION = "0.5.0"
4
+ VERSION = "0.7.0"
5
5
  end
@@ -47,19 +47,42 @@ module Browserctl
47
47
  end
48
48
 
49
49
  def open_page(page_name, url: nil)
50
- res = @client.open_page(page_name.to_s, url: url)
50
+ res = @client.page_open(page_name.to_s, url: url)
51
51
  raise WorkflowError, res[:error] if res[:error]
52
52
 
53
53
  res
54
54
  end
55
55
 
56
56
  def close_page(page_name)
57
- res = @client.close_page(page_name.to_s)
57
+ res = @client.page_close(page_name.to_s)
58
58
  raise WorkflowError, res[:error] if res[:error]
59
59
 
60
60
  res
61
61
  end
62
62
 
63
+ def save_session(session_name)
64
+ res = @client.session_save(session_name)
65
+ raise WorkflowError, res[:error] if res[:error]
66
+
67
+ res
68
+ end
69
+
70
+ def load_session(session_name)
71
+ res = @client.session_load(session_name)
72
+ raise WorkflowError, res[:error] if res[:error]
73
+
74
+ res
75
+ end
76
+
77
+ def list_sessions
78
+ @client.session_list[:sessions]
79
+ end
80
+
81
+ def ask(prompt)
82
+ $stderr.print("[browserctl] #{prompt} ")
83
+ $stdin.gets.chomp
84
+ end
85
+
63
86
  def invoke(workflow_name, **override_params)
64
87
  name = workflow_name.to_s
65
88
  guard_circular!(name)
@@ -100,15 +123,31 @@ module Browserctl
100
123
  @client = client
101
124
  end
102
125
 
103
- def goto(url) = unwrap @client.goto(@name, url)
104
- def fill(sel, val) = unwrap @client.fill(@name, sel, val)
105
- def click(sel) = unwrap @client.click(@name, sel)
106
- def snapshot(**) = unwrap @client.snapshot(@name, **)
107
- def screenshot(**) = unwrap @client.screenshot(@name, **)
108
- def watch(sel, timeout: 30) = unwrap @client.watch(@name, sel, timeout: timeout)
109
- def wait_for(sel, timeout: 10) = unwrap @client.wait_for(@name, sel, timeout: timeout)
110
- def url = @client.url(@name)[:url]
111
- def evaluate(expr) = @client.evaluate(@name, expr)[:result]
126
+ def navigate(url) = unwrap @client.navigate(@name, url)
127
+ def fill(sel, val) = unwrap @client.fill(@name, sel, val)
128
+ def click(sel) = unwrap @client.click(@name, sel)
129
+ def snapshot(**) = unwrap @client.snapshot(@name, **)
130
+ def screenshot(**) = unwrap @client.screenshot(@name, **)
131
+ def wait(sel, timeout: 30) = unwrap @client.wait(@name, sel, timeout: timeout)
132
+ def delete_cookies = unwrap @client.delete_cookies(@name)
133
+ def devtools = @client.devtools(@name)[:devtools_url]
134
+ def url = @client.url(@name)[:url]
135
+ def evaluate(expr) = @client.evaluate(@name, expr)[:result]
136
+
137
+ def storage_get(key, store: "local")
138
+ @client.storage_get(@name, key, store: store)[:value]
139
+ end
140
+
141
+ def storage_set(key, value, store: "local")
142
+ unwrap @client.storage_set(@name, key, value, store: store)
143
+ end
144
+
145
+ def press(key) = unwrap @client.press(@name, key)
146
+ def hover(selector) = unwrap @client.hover(@name, selector)
147
+ def upload(selector, path) = unwrap @client.upload(@name, selector, path)
148
+ def select(selector, value) = unwrap @client.select(@name, selector, value)
149
+ def dialog_accept(text: nil) = unwrap @client.dialog_accept(@name, text: text)
150
+ def dialog_dismiss = unwrap @client.dialog_dismiss(@name)
112
151
 
113
152
  private
114
153
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: browserctl
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrick
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-25 00:00:00.000000000 Z
11
+ date: 2026-04-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ferrum
@@ -58,14 +58,14 @@ dependencies:
58
58
  requirements:
59
59
  - - "~>"
60
60
  - !ruby/object:Gem::Version
61
- version: '1.11'
61
+ version: '2.1'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
- version: '1.11'
68
+ version: '2.1'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: rake
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -141,6 +141,26 @@ files:
141
141
  - examples/cloudflare_hitl.rb
142
142
  - examples/smoke/params_file.rb
143
143
  - examples/smoke/store_fetch.rb
144
+ - examples/test_automation_practices/advanced/ab_testing.rb
145
+ - examples/test_automation_practices/advanced/broken_images.rb
146
+ - examples/test_automation_practices/advanced/file_download.rb
147
+ - examples/test_automation_practices/advanced/iframes.rb
148
+ - examples/test_automation_practices/advanced/shadow_dom.rb
149
+ - examples/test_automation_practices/auth/login.rb
150
+ - examples/test_automation_practices/auth/login_negative.rb
151
+ - examples/test_automation_practices/dialogs/alerts.rb
152
+ - examples/test_automation_practices/dialogs/notifications.rb
153
+ - examples/test_automation_practices/dynamic/dynamic_elements.rb
154
+ - examples/test_automation_practices/dynamic/tables.rb
155
+ - examples/test_automation_practices/forms/checkboxes.rb
156
+ - examples/test_automation_practices/forms/file_upload.rb
157
+ - examples/test_automation_practices/forms/forms.rb
158
+ - examples/test_automation_practices/forms/slider.rb
159
+ - examples/test_automation_practices/interactions/context_menu.rb
160
+ - examples/test_automation_practices/interactions/drag_drop.rb
161
+ - examples/test_automation_practices/interactions/exit_intent.rb
162
+ - examples/test_automation_practices/interactions/hover.rb
163
+ - examples/test_automation_practices/interactions/key_press.rb
144
164
  - examples/the_internet/add_remove_elements.rb
145
165
  - examples/the_internet/checkboxes.rb
146
166
  - examples/the_internet/dropdown.rb
@@ -148,21 +168,22 @@ files:
148
168
  - examples/the_internet/login.rb
149
169
  - lib/browserctl.rb
150
170
  - lib/browserctl/client.rb
171
+ - lib/browserctl/commands/ask.rb
151
172
  - lib/browserctl/commands/cli_output.rb
152
173
  - lib/browserctl/commands/click.rb
153
- - lib/browserctl/commands/export_cookies.rb
174
+ - lib/browserctl/commands/cookie.rb
175
+ - lib/browserctl/commands/daemon.rb
176
+ - lib/browserctl/commands/dialog.rb
154
177
  - lib/browserctl/commands/fill.rb
155
- - lib/browserctl/commands/import_cookies.rb
156
178
  - lib/browserctl/commands/init.rb
157
- - lib/browserctl/commands/inspect.rb
158
- - lib/browserctl/commands/open_page.rb
159
- - lib/browserctl/commands/pause.rb
179
+ - lib/browserctl/commands/page.rb
160
180
  - lib/browserctl/commands/record.rb
161
181
  - lib/browserctl/commands/resume.rb
162
182
  - lib/browserctl/commands/screenshot.rb
183
+ - lib/browserctl/commands/session.rb
163
184
  - lib/browserctl/commands/snapshot.rb
164
- - lib/browserctl/commands/status.rb
165
- - lib/browserctl/commands/watch.rb
185
+ - lib/browserctl/commands/storage.rb
186
+ - lib/browserctl/commands/workflow.rb
166
187
  - lib/browserctl/constants.rb
167
188
  - lib/browserctl/detectors.rb
168
189
  - lib/browserctl/errors.rb
@@ -176,12 +197,16 @@ files:
176
197
  - lib/browserctl/server/handlers/daemon_control.rb
177
198
  - lib/browserctl/server/handlers/devtools.rb
178
199
  - lib/browserctl/server/handlers/hitl.rb
200
+ - lib/browserctl/server/handlers/interaction.rb
179
201
  - lib/browserctl/server/handlers/navigation.rb
180
202
  - lib/browserctl/server/handlers/observation.rb
181
203
  - lib/browserctl/server/handlers/page_lifecycle.rb
204
+ - lib/browserctl/server/handlers/session.rb
205
+ - lib/browserctl/server/handlers/storage.rb
182
206
  - lib/browserctl/server/idle_watcher.rb
183
207
  - lib/browserctl/server/page_session.rb
184
208
  - lib/browserctl/server/snapshot_builder.rb
209
+ - lib/browserctl/session.rb
185
210
  - lib/browserctl/version.rb
186
211
  - lib/browserctl/workflow.rb
187
212
  homepage: https://github.com/patrick204nqh/browserctl
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Browserctl
4
- module Commands
5
- class ExportCookies
6
- def self.run(client, args)
7
- page = args.shift or abort "usage: browserctl export-cookies <page> <path>"
8
- path = args.shift or abort "usage: browserctl export-cookies <page> <path>"
9
- result = client.export_cookies(page, path)
10
- if result[:error]
11
- warn "Error: #{result[:error]}"
12
- exit 1
13
- end
14
- puts result.to_json
15
- end
16
- end
17
- end
18
- end
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Browserctl
4
- module Commands
5
- class ImportCookies
6
- def self.run(client, args)
7
- page = args.shift or abort "usage: browserctl import-cookies <page> <path>"
8
- path = args.shift or abort "usage: browserctl import-cookies <page> <path>"
9
- begin
10
- result = client.import_cookies(page, path)
11
- rescue StandardError => e
12
- warn "Error: #{e.message}"
13
- exit 1
14
- end
15
- if result[:error]
16
- warn "Error: #{result[:error]}"
17
- exit 1
18
- end
19
- puts result.to_json
20
- end
21
- end
22
- end
23
- end
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Browserctl
4
- module Commands
5
- class Inspect
6
- def self.run(client, args)
7
- name = args.shift or abort "usage: browserctl inspect <page>"
8
- res = client.inspect_page(name)
9
- if res[:error]
10
- warn "Error: #{res[:error]}"
11
- exit 1
12
- end
13
- url = res[:devtools_url]
14
- puts "Opening DevTools for '#{name}':"
15
- puts " #{url}"
16
- opener = RUBY_PLATFORM =~ /darwin/ ? "open" : "xdg-open"
17
- system(opener, url)
18
- end
19
- end
20
- end
21
- end
@@ -1,21 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "optimist"
4
- require_relative "cli_output"
5
-
6
- module Browserctl
7
- module Commands
8
- class OpenPage
9
- extend CliOutput
10
-
11
- def self.run(client, args)
12
- opts = Optimist.options(args) do
13
- banner "Usage: browserctl open <page> [--url URL]"
14
- opt :url, "URL to navigate to", type: :string, short: "-u"
15
- end
16
- name = args.shift or abort "usage: browserctl open <page> [--url URL]"
17
- print_result(client.open_page(name, url: opts[:url]))
18
- end
19
- end
20
- end
21
- end
@@ -1,22 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "cli_output"
4
-
5
- module Browserctl
6
- module Commands
7
- module Pause
8
- extend CliOutput
9
-
10
- def self.run(client, args)
11
- name = args.shift or abort "usage: browserctl pause <page>"
12
- res = client.pause(name)
13
- if res[:error]
14
- warn "Error: #{res[:error]}"
15
- exit 1
16
- end
17
- puts "Page '#{name}' paused. Browser is live — interact freely."
18
- puts "When done: browserctl resume #{name}"
19
- end
20
- end
21
- end
22
- end