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
|
@@ -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
|
|
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
|
|
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
|
|
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
|
data/lib/browserctl/server.rb
CHANGED
|
@@ -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
|
|
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
|
|
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
|
data/lib/browserctl/version.rb
CHANGED
data/lib/browserctl/workflow.rb
CHANGED
|
@@ -47,19 +47,42 @@ module Browserctl
|
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
def open_page(page_name, url: nil)
|
|
50
|
-
res = @client.
|
|
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.
|
|
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
|
|
104
|
-
def fill(sel, val)
|
|
105
|
-
def click(sel)
|
|
106
|
-
def snapshot(**)
|
|
107
|
-
def screenshot(**)
|
|
108
|
-
def
|
|
109
|
-
def
|
|
110
|
-
def
|
|
111
|
-
def
|
|
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.
|
|
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-
|
|
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
|
|
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
|
|
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/
|
|
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/
|
|
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/
|
|
165
|
-
- lib/browserctl/commands/
|
|
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
|