browserctl 0.4.0 → 0.6.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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +45 -0
  3. data/README.md +97 -55
  4. data/bin/browserctl +117 -108
  5. data/bin/browserd +9 -3
  6. data/bin/setup +7 -3
  7. data/examples/cloudflare_hitl.rb +6 -6
  8. data/examples/smoke/params_file.rb +3 -2
  9. data/examples/smoke/store_fetch.rb +5 -5
  10. data/examples/test_automation_practices/checkboxes.rb +39 -0
  11. data/examples/test_automation_practices/dynamic_elements.rb +40 -0
  12. data/examples/test_automation_practices/key_press.rb +41 -0
  13. data/examples/test_automation_practices/login.rb +34 -0
  14. data/examples/test_automation_practices/login_negative.rb +28 -0
  15. data/examples/test_automation_practices/notifications.rb +57 -0
  16. data/examples/the_internet/add_remove_elements.rb +1 -1
  17. data/examples/the_internet/checkboxes.rb +1 -1
  18. data/examples/the_internet/dropdown.rb +1 -1
  19. data/examples/the_internet/dynamic_loading.rb +2 -2
  20. data/examples/the_internet/login.rb +1 -1
  21. data/lib/browserctl/client.rb +112 -28
  22. data/lib/browserctl/commands/cookie.rb +59 -0
  23. data/lib/browserctl/commands/daemon.rb +77 -0
  24. data/lib/browserctl/commands/page.rb +47 -0
  25. data/lib/browserctl/commands/record.rb +1 -1
  26. data/lib/browserctl/commands/screenshot.rb +2 -2
  27. data/lib/browserctl/commands/session.rb +69 -0
  28. data/lib/browserctl/commands/snapshot.rb +5 -5
  29. data/lib/browserctl/commands/storage.rb +67 -0
  30. data/lib/browserctl/commands/workflow.rb +64 -0
  31. data/lib/browserctl/constants.rb +20 -1
  32. data/lib/browserctl/detectors.rb +23 -0
  33. data/lib/browserctl/errors.rb +25 -0
  34. data/lib/browserctl/logger.rb +4 -4
  35. data/lib/browserctl/policy.rb +36 -0
  36. data/lib/browserctl/recording.rb +4 -4
  37. data/lib/browserctl/runner.rb +4 -4
  38. data/lib/browserctl/server/command_dispatcher.rb +49 -258
  39. data/lib/browserctl/server/handlers/cookies.rb +57 -0
  40. data/lib/browserctl/server/handlers/daemon_control.rb +29 -0
  41. data/lib/browserctl/server/handlers/devtools.rb +22 -0
  42. data/lib/browserctl/server/handlers/hitl.rb +31 -0
  43. data/lib/browserctl/server/handlers/navigation.rb +94 -0
  44. data/lib/browserctl/server/handlers/observation.rb +87 -0
  45. data/lib/browserctl/server/handlers/page_lifecycle.rb +36 -0
  46. data/lib/browserctl/server/handlers/session.rb +93 -0
  47. data/lib/browserctl/server/handlers/storage.rb +109 -0
  48. data/lib/browserctl/server.rb +4 -3
  49. data/lib/browserctl/session.rb +79 -0
  50. data/lib/browserctl/version.rb +1 -1
  51. data/lib/browserctl/workflow.rb +58 -17
  52. data/lib/browserctl.rb +12 -2
  53. metadata +43 -11
  54. data/lib/browserctl/commands/export_cookies.rb +0 -18
  55. data/lib/browserctl/commands/import_cookies.rb +0 -23
  56. data/lib/browserctl/commands/inspect.rb +0 -21
  57. data/lib/browserctl/commands/open_page.rb +0 -21
  58. data/lib/browserctl/commands/pause.rb +0 -22
  59. data/lib/browserctl/commands/status.rb +0 -30
  60. data/lib/browserctl/commands/watch.rb +0 -27
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Browserctl
4
+ class CommandDispatcher
5
+ module Handlers
6
+ module PageLifecycle
7
+ private
8
+
9
+ def cmd_page_open(req)
10
+ session = @global_mutex.synchronize do
11
+ @pages[req[:name]] ||= PageSession.new(@browser.create_page)
12
+ end
13
+ session.page.go_to(req[:url]) if req[:url]
14
+ { ok: true, name: req[:name] }
15
+ end
16
+
17
+ def cmd_page_close(req)
18
+ session = @global_mutex.synchronize { @pages.delete(req[:name]) }
19
+ session&.page&.close
20
+ { ok: true }
21
+ end
22
+
23
+ def cmd_page_list(_req)
24
+ { pages: @global_mutex.synchronize { @pages.keys } }
25
+ end
26
+
27
+ def cmd_page_focus(req)
28
+ with_page(req[:name]) do |session|
29
+ session.page.activate
30
+ { ok: true }
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ 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
@@ -62,8 +62,9 @@ module Browserctl
62
62
 
63
63
  def setup_socket
64
64
  FileUtils.rm_f(@socket_path)
65
+ old_umask = File.umask(0o177)
65
66
  server = UNIXServer.new(@socket_path)
66
- File.chmod(0o600, @socket_path)
67
+ File.umask(old_umask)
67
68
  Browserctl.logger.info "daemon ready — listening on #{@socket_path}"
68
69
  server
69
70
  end
@@ -75,7 +76,7 @@ module Browserctl
75
76
  return unless pid.positive?
76
77
 
77
78
  Process.kill(0, pid)
78
- abort "browserd already running (PID #{pid}). Use 'browserctl shutdown' first."
79
+ abort "browserd already running (PID #{pid}). Use 'browserctl daemon stop' first."
79
80
  rescue Errno::ESRCH
80
81
  # Dead process — stale PID file, safe to continue
81
82
  rescue Errno::EPERM
@@ -124,7 +125,7 @@ module Browserctl
124
125
 
125
126
  def quietly
126
127
  yield
127
- rescue Exception # rubocop:disable Lint/RescueException
128
+ rescue Exception
128
129
  nil
129
130
  end
130
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.4.0"
4
+ VERSION = "0.6.0"
5
5
  end
@@ -16,15 +16,20 @@ module Browserctl
16
16
  def initialize(params, client)
17
17
  @params = params
18
18
  @client = client
19
- @_store = {}
20
19
  end
21
20
 
22
21
  def store(key, value)
23
- @_store[key] = value
22
+ res = @client.store(key.to_s, value)
23
+ raise WorkflowError, res[:error] if res[:error]
24
+
25
+ value
24
26
  end
25
27
 
26
28
  def fetch(key)
27
- @_store.fetch(key) { raise KeyError, "no value stored for key #{key.inspect}" }
29
+ res = @client.fetch(key.to_s)
30
+ raise WorkflowError, res[:error] if res[:error]
31
+
32
+ res[:value]
28
33
  end
29
34
 
30
35
  def method_missing(name, *args)
@@ -42,19 +47,37 @@ module Browserctl
42
47
  end
43
48
 
44
49
  def open_page(page_name, url: nil)
45
- res = @client.open_page(page_name.to_s, url: url)
50
+ res = @client.page_open(page_name.to_s, url: url)
46
51
  raise WorkflowError, res[:error] if res[:error]
47
52
 
48
53
  res
49
54
  end
50
55
 
51
56
  def close_page(page_name)
52
- res = @client.close_page(page_name.to_s)
57
+ res = @client.page_close(page_name.to_s)
58
+ raise WorkflowError, res[:error] if res[:error]
59
+
60
+ res
61
+ end
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)
53
72
  raise WorkflowError, res[:error] if res[:error]
54
73
 
55
74
  res
56
75
  end
57
76
 
77
+ def list_sessions
78
+ @client.session_list[:sessions]
79
+ end
80
+
58
81
  def invoke(workflow_name, **override_params)
59
82
  name = workflow_name.to_s
60
83
  guard_circular!(name)
@@ -95,15 +118,24 @@ module Browserctl
95
118
  @client = client
96
119
  end
97
120
 
98
- def goto(url) = unwrap @client.goto(@name, url)
99
- def fill(sel, val) = unwrap @client.fill(@name, sel, val)
100
- def click(sel) = unwrap @client.click(@name, sel)
101
- def snapshot(**) = unwrap @client.snapshot(@name, **)
102
- def screenshot(**) = unwrap @client.screenshot(@name, **)
103
- def watch(sel, timeout: 30) = unwrap @client.watch(@name, sel, timeout: timeout)
104
- def wait_for(sel, timeout: 10) = unwrap @client.wait_for(@name, sel, timeout: timeout)
105
- def url = @client.url(@name)[:url]
106
- def evaluate(expr) = @client.evaluate(@name, expr)[:result]
121
+ def navigate(url) = unwrap @client.navigate(@name, url)
122
+ def fill(sel, val) = unwrap @client.fill(@name, sel, val)
123
+ def click(sel) = unwrap @client.click(@name, sel)
124
+ def snapshot(**) = unwrap @client.snapshot(@name, **)
125
+ def screenshot(**) = unwrap @client.screenshot(@name, **)
126
+ def wait(sel, timeout: 30) = unwrap @client.wait(@name, sel, timeout: timeout)
127
+ def delete_cookies = unwrap @client.delete_cookies(@name)
128
+ def devtools = @client.devtools(@name)[:devtools_url]
129
+ def url = @client.url(@name)[:url]
130
+ def evaluate(expr) = @client.evaluate(@name, expr)[:result]
131
+
132
+ def storage_get(key, store: "local")
133
+ @client.storage_get(@name, key, store: store)[:value]
134
+ end
135
+
136
+ def storage_set(key, value, store: "local")
137
+ unwrap @client.storage_set(@name, key, value, store: store)
138
+ end
107
139
 
108
140
  private
109
141
 
@@ -137,7 +169,7 @@ module Browserctl
137
169
  end
138
170
 
139
171
  def compose(workflow_name)
140
- source = REGISTRY[workflow_name.to_s]
172
+ source = Browserctl.lookup_workflow(workflow_name.to_s)
141
173
  raise WorkflowError, "workflow '#{workflow_name}' not found for composition" unless source
142
174
 
143
175
  @steps.concat(source.steps)
@@ -187,11 +219,20 @@ module Browserctl
187
219
  end
188
220
  end
189
221
 
190
- REGISTRY = {} # rubocop:disable Style/MutableConstant
222
+ @registry_mutex = Mutex.new
223
+ @registry = {}
191
224
 
192
225
  def self.workflow(name, &)
193
226
  defn = WorkflowDefinition.new(name.to_s)
194
227
  defn.instance_exec(&)
195
- REGISTRY[name.to_s] = defn
228
+ @registry_mutex.synchronize { @registry[name.to_s] = defn }
229
+ end
230
+
231
+ def self.lookup_workflow(name)
232
+ @registry_mutex.synchronize { @registry[name.to_s] }
233
+ end
234
+
235
+ def self.registry_snapshot
236
+ @registry_mutex.synchronize { @registry.dup }
196
237
  end
197
238
  end
data/lib/browserctl.rb CHANGED
@@ -2,14 +2,24 @@
2
2
 
3
3
  require_relative "browserctl/version"
4
4
  require_relative "browserctl/constants"
5
+ require_relative "browserctl/errors"
5
6
  require_relative "browserctl/workflow"
6
7
  require_relative "browserctl/runner"
7
8
  require_relative "browserctl/client"
8
9
 
9
10
  module Browserctl
10
- PLUGIN_COMMANDS = {} # rubocop:disable Style/MutableConstant
11
+ @plugin_commands_mutex = Mutex.new
12
+ @plugin_commands = {}
11
13
 
12
14
  def self.register_command(name, &block)
13
- PLUGIN_COMMANDS[name.to_s] = block
15
+ @plugin_commands_mutex.synchronize { @plugin_commands[name.to_s] = block }
16
+ end
17
+
18
+ def self.lookup_plugin_command(name)
19
+ @plugin_commands_mutex.synchronize { @plugin_commands[name.to_s] }
20
+ end
21
+
22
+ def self.plugin_commands_snapshot
23
+ @plugin_commands_mutex.synchronize { @plugin_commands.dup }
14
24
  end
15
25
  end
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.4.0
4
+ version: 0.6.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,28 @@ 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
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '13.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '13.0'
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: rspec
71
85
  requirement: !ruby/object:Gem::Requirement
@@ -127,6 +141,12 @@ files:
127
141
  - examples/cloudflare_hitl.rb
128
142
  - examples/smoke/params_file.rb
129
143
  - examples/smoke/store_fetch.rb
144
+ - examples/test_automation_practices/checkboxes.rb
145
+ - examples/test_automation_practices/dynamic_elements.rb
146
+ - examples/test_automation_practices/key_press.rb
147
+ - examples/test_automation_practices/login.rb
148
+ - examples/test_automation_practices/login_negative.rb
149
+ - examples/test_automation_practices/notifications.rb
130
150
  - examples/the_internet/add_remove_elements.rb
131
151
  - examples/the_internet/checkboxes.rb
132
152
  - examples/the_internet/dropdown.rb
@@ -136,28 +156,40 @@ files:
136
156
  - lib/browserctl/client.rb
137
157
  - lib/browserctl/commands/cli_output.rb
138
158
  - lib/browserctl/commands/click.rb
139
- - lib/browserctl/commands/export_cookies.rb
159
+ - lib/browserctl/commands/cookie.rb
160
+ - lib/browserctl/commands/daemon.rb
140
161
  - lib/browserctl/commands/fill.rb
141
- - lib/browserctl/commands/import_cookies.rb
142
162
  - lib/browserctl/commands/init.rb
143
- - lib/browserctl/commands/inspect.rb
144
- - lib/browserctl/commands/open_page.rb
145
- - lib/browserctl/commands/pause.rb
163
+ - lib/browserctl/commands/page.rb
146
164
  - lib/browserctl/commands/record.rb
147
165
  - lib/browserctl/commands/resume.rb
148
166
  - lib/browserctl/commands/screenshot.rb
167
+ - lib/browserctl/commands/session.rb
149
168
  - lib/browserctl/commands/snapshot.rb
150
- - lib/browserctl/commands/status.rb
151
- - lib/browserctl/commands/watch.rb
169
+ - lib/browserctl/commands/storage.rb
170
+ - lib/browserctl/commands/workflow.rb
152
171
  - lib/browserctl/constants.rb
172
+ - lib/browserctl/detectors.rb
173
+ - lib/browserctl/errors.rb
153
174
  - lib/browserctl/logger.rb
175
+ - lib/browserctl/policy.rb
154
176
  - lib/browserctl/recording.rb
155
177
  - lib/browserctl/runner.rb
156
178
  - lib/browserctl/server.rb
157
179
  - lib/browserctl/server/command_dispatcher.rb
180
+ - lib/browserctl/server/handlers/cookies.rb
181
+ - lib/browserctl/server/handlers/daemon_control.rb
182
+ - lib/browserctl/server/handlers/devtools.rb
183
+ - lib/browserctl/server/handlers/hitl.rb
184
+ - lib/browserctl/server/handlers/navigation.rb
185
+ - lib/browserctl/server/handlers/observation.rb
186
+ - lib/browserctl/server/handlers/page_lifecycle.rb
187
+ - lib/browserctl/server/handlers/session.rb
188
+ - lib/browserctl/server/handlers/storage.rb
158
189
  - lib/browserctl/server/idle_watcher.rb
159
190
  - lib/browserctl/server/page_session.rb
160
191
  - lib/browserctl/server/snapshot_builder.rb
192
+ - lib/browserctl/session.rb
161
193
  - lib/browserctl/version.rb
162
194
  - lib/browserctl/workflow.rb
163
195
  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