chromate-rb 0.0.1.pre → 0.0.3.pre
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/.rubocop.yml +2 -0
- data/CHANGELOG.md +72 -3
- data/README.md +33 -6
- data/Rakefile +48 -16
- data/docker_root/Gemfile +4 -0
- data/docker_root/Gemfile.lock +28 -0
- data/docker_root/TestInDocker.gif +0 -0
- data/docker_root/app.rb +87 -0
- data/dockerfiles/Dockerfile +21 -7
- data/dockerfiles/README.md +49 -0
- data/docs/BOT_BROWSER.md +74 -0
- data/docs/README.md +74 -0
- data/docs/browser.md +124 -102
- data/docs/client.md +126 -0
- data/docs/element.md +365 -0
- data/docs/elements/checkbox.md +69 -0
- data/docs/elements/radio.md +57 -0
- data/lib/bot_browser/downloader.rb +64 -0
- data/lib/bot_browser/installer.rb +99 -0
- data/lib/bot_browser.rb +43 -0
- data/lib/chromate/actions/dom.rb +35 -27
- data/lib/chromate/actions/navigate.rb +7 -5
- data/lib/chromate/actions/screenshot.rb +71 -14
- data/lib/chromate/actions/stealth.rb +62 -0
- data/lib/chromate/binary.rb +83 -0
- data/lib/chromate/browser.rb +120 -24
- data/lib/chromate/c_logger.rb +8 -0
- data/lib/chromate/client.rb +65 -26
- data/lib/chromate/configuration.rb +31 -14
- data/lib/chromate/element.rb +119 -16
- data/lib/chromate/elements/checkbox.rb +40 -0
- data/lib/chromate/elements/option.rb +43 -0
- data/lib/chromate/elements/radio.rb +37 -0
- data/lib/chromate/elements/select.rb +50 -6
- data/lib/chromate/elements/tags.rb +29 -0
- data/lib/chromate/exceptions.rb +2 -0
- data/lib/chromate/files/agents.json +11 -0
- data/lib/chromate/files/stealth.js +199 -0
- data/lib/chromate/hardwares/keyboard_controller.rb +45 -0
- data/lib/chromate/hardwares/keyboards/virtual_controller.rb +65 -0
- data/lib/chromate/hardwares/mouse_controller.rb +55 -11
- data/lib/chromate/hardwares/mouses/linux_controller.rb +124 -21
- data/lib/chromate/hardwares/mouses/mac_os_controller.rb +6 -6
- data/lib/chromate/hardwares/mouses/virtual_controller.rb +95 -7
- data/lib/chromate/hardwares/mouses/x11.rb +36 -0
- data/lib/chromate/hardwares.rb +19 -3
- data/lib/chromate/helpers.rb +22 -15
- data/lib/chromate/user_agent.rb +41 -15
- data/lib/chromate/version.rb +1 -1
- data/lib/chromate.rb +2 -0
- data/logo.png +0 -0
- data/results/bot.png +0 -0
- data/results/brotector.png +0 -0
- data/results/cloudflare.png +0 -0
- data/results/headers.png +0 -0
- data/results/pixelscan.png +0 -0
- metadata +45 -2
data/lib/chromate/client.rb
CHANGED
@@ -2,23 +2,27 @@
|
|
2
2
|
|
3
3
|
require 'websocket-client-simple'
|
4
4
|
require 'chromate/helpers'
|
5
|
+
require 'chromate/exceptions'
|
5
6
|
|
6
7
|
module Chromate
|
7
8
|
class Client
|
8
9
|
include Helpers
|
9
10
|
|
11
|
+
# @return [Array<Proc>]
|
10
12
|
def self.listeners
|
11
13
|
@@listeners ||= [] # rubocop:disable Style/ClassVars
|
12
14
|
end
|
13
15
|
|
14
16
|
attr_reader :port, :ws, :browser
|
15
17
|
|
18
|
+
# @param [Chromate::Browser] browser
|
16
19
|
def initialize(browser)
|
17
|
-
@browser
|
18
|
-
options
|
19
|
-
@port
|
20
|
+
@browser = browser
|
21
|
+
options = browser.options
|
22
|
+
@port = options[:port] || find_available_port
|
20
23
|
end
|
21
24
|
|
25
|
+
# @return [self]
|
22
26
|
def start
|
23
27
|
@ws_url = fetch_websocket_debug_url
|
24
28
|
@ws = WebSocket::Client::Simple.connect(@ws_url)
|
@@ -29,7 +33,7 @@ module Chromate
|
|
29
33
|
|
30
34
|
@ws.on :message do |msg|
|
31
35
|
message = JSON.parse(msg.data)
|
32
|
-
client_self.handle_message
|
36
|
+
client_self.send(:handle_message, message)
|
33
37
|
|
34
38
|
Client.listeners.each do |listener|
|
35
39
|
listener.call(message)
|
@@ -37,79 +41,114 @@ module Chromate
|
|
37
41
|
end
|
38
42
|
|
39
43
|
@ws.on :open do
|
40
|
-
|
44
|
+
Chromate::CLogger.log('Successfully connected to WebSocket', level: :debug)
|
41
45
|
end
|
42
46
|
|
43
47
|
@ws.on :error do |e|
|
44
|
-
|
48
|
+
Chromate::CLogger.log("WebSocket error: #{e.message}", level: :error)
|
45
49
|
end
|
46
50
|
|
47
51
|
@ws.on :close do |_e|
|
48
|
-
|
52
|
+
Chromate::CLogger.log('WebSocket connection closed', level: :debug)
|
49
53
|
end
|
50
54
|
|
51
|
-
sleep 0.2
|
55
|
+
sleep 0.2 # Wait for the connection to be established
|
52
56
|
client_self.send_message('Target.setDiscoverTargets', { discover: true })
|
57
|
+
|
53
58
|
client_self
|
54
59
|
end
|
55
60
|
|
61
|
+
# @return [self]
|
56
62
|
def stop
|
57
63
|
@ws&.close
|
64
|
+
|
65
|
+
self
|
58
66
|
end
|
59
67
|
|
68
|
+
# @param [String] method
|
69
|
+
# @param [Hash] params
|
70
|
+
# @return [Hash]
|
60
71
|
def send_message(method, params = {})
|
61
72
|
@id += 1
|
62
73
|
message = { id: @id, method: method, params: params }
|
63
|
-
|
74
|
+
Chromate::CLogger.log("Sending WebSocket message: #{message}", level: :debug)
|
64
75
|
|
65
76
|
begin
|
66
77
|
@ws.send(message.to_json)
|
67
78
|
@callbacks[@id] = Queue.new
|
68
79
|
result = @callbacks[@id].pop
|
69
|
-
|
80
|
+
Chromate::CLogger.log("Response received for message #{message[:id]}: #{result}", level: :debug)
|
70
81
|
result
|
71
82
|
rescue StandardError => e
|
72
|
-
|
83
|
+
Chromate::CLogger.log("Error sending WebSocket message: #{e.message}", level: :error)
|
73
84
|
reconnect
|
74
85
|
retry
|
75
86
|
end
|
76
87
|
end
|
77
88
|
|
89
|
+
# @return [self]
|
78
90
|
def reconnect
|
79
91
|
@ws_url = fetch_websocket_debug_url
|
80
92
|
@ws = WebSocket::Client::Simple.connect(@ws_url)
|
81
|
-
|
93
|
+
Chromate::CLogger.log('Successfully reconnected to WebSocket')
|
94
|
+
|
95
|
+
self
|
82
96
|
end
|
83
97
|
|
98
|
+
# Allowing different parts to subscribe to WebSocket messages
|
99
|
+
# @yieldparam [Hash] message
|
100
|
+
# @return [void]
|
101
|
+
def on_message(&block)
|
102
|
+
Client.listeners << block
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
# @param [Hash] message
|
108
|
+
# @return [self]
|
84
109
|
def handle_message(message)
|
85
|
-
|
110
|
+
Chromate::CLogger.log("Message received: #{message}", level: :debug)
|
86
111
|
return unless message['id'] && @callbacks[message['id']]
|
87
112
|
|
88
113
|
@callbacks[message['id']].push(message['result'])
|
89
114
|
@callbacks.delete(message['id'])
|
90
|
-
end
|
91
115
|
|
92
|
-
|
93
|
-
def on_message(&block)
|
94
|
-
Client.listeners << block
|
116
|
+
self
|
95
117
|
end
|
96
118
|
|
119
|
+
# @return [String]
|
97
120
|
def fetch_websocket_debug_url
|
98
|
-
|
99
|
-
|
100
|
-
|
121
|
+
retries = 0
|
122
|
+
max_retries = 5
|
123
|
+
base_delay = 0.5
|
101
124
|
|
102
|
-
|
125
|
+
begin
|
126
|
+
uri = URI("http://localhost:#{@port}/json/list")
|
127
|
+
response = Net::HTTP.get(uri)
|
128
|
+
targets = JSON.parse(response)
|
129
|
+
|
130
|
+
page_target = targets.find { |target| target['type'] == 'page' }
|
131
|
+
websocket_url = if page_target
|
132
|
+
page_target['webSocketDebuggerUrl']
|
133
|
+
else
|
134
|
+
create_new_page_target
|
135
|
+
end
|
136
|
+
raise Exceptions::DebugURLError, 'Can\'t get WebSocket URL' if websocket_url.nil?
|
137
|
+
|
138
|
+
websocket_url
|
139
|
+
rescue StandardError => e
|
140
|
+
retries += 1
|
141
|
+
raise Exceptions::ConnectionTimeoutError, "Can't get WebSocket URL after #{max_retries} retries" if retries >= max_retries
|
103
142
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
143
|
+
delay = base_delay * (2**retries) # Exponential delay: 0.5s, 1s, 2s, 4s, 8s
|
144
|
+
Chromate::CLogger.log("Attempting to reconnect in #{delay} seconds, #{e.message}", level: :debug)
|
145
|
+
sleep delay
|
146
|
+
retry
|
108
147
|
end
|
109
148
|
end
|
110
149
|
|
150
|
+
# @return [String]
|
111
151
|
def create_new_page_target
|
112
|
-
# Créer une nouvelle page
|
113
152
|
uri = URI("http://localhost:#{@port}/json/new")
|
114
153
|
response = Net::HTTP.get(uri)
|
115
154
|
new_target = JSON.parse(response)
|
@@ -9,25 +9,26 @@ module Chromate
|
|
9
9
|
include Helpers
|
10
10
|
include Exceptions
|
11
11
|
DEFAULT_ARGS = [
|
12
|
-
'--no-first-run',
|
13
|
-
'--no-default-browser-check',
|
14
|
-
'--disable-blink-features=AutomationControlled',
|
15
|
-
'--disable-extensions',
|
16
|
-
'--disable-infobars',
|
17
|
-
'--no-sandbox',
|
18
|
-
'--
|
19
|
-
'--
|
20
|
-
'--disable-
|
21
|
-
'--
|
12
|
+
'--no-first-run', # Skip the first run wizard
|
13
|
+
'--no-default-browser-check', # Disable the default browser check
|
14
|
+
'--disable-blink-features=AutomationControlled', # Disable the AutomationControlled feature
|
15
|
+
'--disable-extensions', # Disable extensions
|
16
|
+
'--disable-infobars', # Disable the infobar that asks if you want to install Chrome
|
17
|
+
'--no-sandbox', # Required for chrome devtools to work
|
18
|
+
'--test-type', # Remove the not allowed message for --no-sandbox flag
|
19
|
+
'--disable-dev-shm-usage', # Disable /dev/shm usage
|
20
|
+
'--disable-popup-blocking', # Disable popup blocking
|
21
|
+
'--ignore-certificate-errors', # Ignore certificate errors
|
22
22
|
'--window-size=1920,1080', # TODO: Make this automatic
|
23
|
-
'--hide-crash-restore-bubble'
|
23
|
+
'--hide-crash-restore-bubble' # Hide the crash restore bubble
|
24
24
|
].freeze
|
25
25
|
HEADLESS_ARGS = [
|
26
26
|
'--headless=new',
|
27
27
|
'--window-position=2400,2400'
|
28
28
|
].freeze
|
29
29
|
XVFB_ARGS = [
|
30
|
-
'--window-position=0,0'
|
30
|
+
'--window-position=0,0',
|
31
|
+
'--start-fullscreen'
|
31
32
|
].freeze
|
32
33
|
DISABLED_FEATURES = %w[
|
33
34
|
Translate
|
@@ -45,14 +46,16 @@ module Chromate
|
|
45
46
|
enable-automation
|
46
47
|
].freeze
|
47
48
|
|
48
|
-
attr_accessor :user_data_dir, :headless, :xfvb, :native_control, :
|
49
|
-
:disable_features
|
49
|
+
attr_accessor :user_data_dir, :headless, :xfvb, :native_control, :startup_patch,
|
50
|
+
:args, :headless_args, :xfvb_args, :exclude_switches, :proxy, :disable_features,
|
51
|
+
:mouse_controller, :keyboard_controller
|
50
52
|
|
51
53
|
def initialize
|
52
54
|
@user_data_dir = File.expand_path('~/.config/google-chrome/Default')
|
53
55
|
@headless = true
|
54
56
|
@xfvb = false
|
55
57
|
@native_control = false
|
58
|
+
@startup_patch = true
|
56
59
|
@proxy = nil
|
57
60
|
@args = [] + DEFAULT_ARGS
|
58
61
|
@headless_args = [] + HEADLESS_ARGS
|
@@ -63,18 +66,27 @@ module Chromate
|
|
63
66
|
@args << '--use-angle=metal' if mac?
|
64
67
|
end
|
65
68
|
|
69
|
+
# @return [Chromate::Configuration]
|
66
70
|
def self.config
|
67
71
|
@config ||= Configuration.new
|
68
72
|
end
|
69
73
|
|
74
|
+
# @yield [Chromate::Configuration]
|
70
75
|
def self.configure
|
71
76
|
yield(config)
|
72
77
|
end
|
73
78
|
|
79
|
+
# @return [Chromate::Configuration]
|
74
80
|
def config
|
75
81
|
self.class.config
|
76
82
|
end
|
77
83
|
|
84
|
+
# @return [Boolean]
|
85
|
+
def patch?
|
86
|
+
@startup_patch
|
87
|
+
end
|
88
|
+
|
89
|
+
# @return [String]
|
78
90
|
def chrome_path
|
79
91
|
return ENV['CHROME_BIN'] if ENV['CHROME_BIN']
|
80
92
|
|
@@ -89,6 +101,10 @@ module Chromate
|
|
89
101
|
end
|
90
102
|
end
|
91
103
|
|
104
|
+
# @option [Boolean] headless
|
105
|
+
# @option [Boolean] xfvb
|
106
|
+
# @option [Hash] proxy
|
107
|
+
# @option [Array<String>] disable_features
|
92
108
|
def generate_arguments(headless: @headless, xfvb: @xfvb, proxy: @proxy, disable_features: @disable_features, **_args)
|
93
109
|
dynamic_args = []
|
94
110
|
|
@@ -100,6 +116,7 @@ module Chromate
|
|
100
116
|
@args + dynamic_args
|
101
117
|
end
|
102
118
|
|
119
|
+
# @return [Hash]
|
103
120
|
def options
|
104
121
|
{
|
105
122
|
chrome_path: chrome_path,
|
data/lib/chromate/element.rb
CHANGED
@@ -1,7 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'chromate/elements/tags'
|
4
|
+
|
3
5
|
module Chromate
|
4
6
|
class Element
|
7
|
+
include Elements::Tags
|
8
|
+
|
5
9
|
class NotFoundError < StandardError
|
6
10
|
def initialize(selector, root_id)
|
7
11
|
super("Element not found with selector: #{selector} under root_id: #{root_id}")
|
@@ -13,7 +17,7 @@ module Chromate
|
|
13
17
|
super("Unable to resolve element with selector: #{selector}")
|
14
18
|
end
|
15
19
|
end
|
16
|
-
attr_reader :selector, :client, :
|
20
|
+
attr_reader :selector, :client, :root_id, :object_id, :node_id
|
17
21
|
|
18
22
|
# @param [String] selector
|
19
23
|
# @param [Chromate::Client] client
|
@@ -25,37 +29,51 @@ module Chromate
|
|
25
29
|
@client = client
|
26
30
|
@object_id = object_id
|
27
31
|
@node_id = node_id
|
28
|
-
@object_id, @node_id =
|
29
|
-
@root_id
|
30
|
-
|
32
|
+
@object_id, @node_id = magick_find(selector, root_id) unless @object_id && @node_id
|
33
|
+
@root_id = root_id || document['root']['nodeId']
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [Chromate::Hardwares::MouseController]
|
37
|
+
def mouse
|
38
|
+
Chromate.configuration.mouse_controller.set_element(self)
|
39
|
+
end
|
40
|
+
|
41
|
+
# @return [Chromate::Hardwares::KeyboardController]
|
42
|
+
def keyboard
|
43
|
+
Chromate.configuration.keyboard_controller.set_element(self)
|
31
44
|
end
|
32
45
|
|
46
|
+
# @return [String]
|
33
47
|
def inspect
|
34
48
|
value = selector.length > 20 ? "#{selector[0..20]}..." : selector
|
35
49
|
"#<Chromate::Element:#{value}>"
|
36
50
|
end
|
37
51
|
|
52
|
+
# @return [String]
|
38
53
|
def text
|
39
|
-
return
|
54
|
+
evaluate_script('function() { return this.innerText; }')
|
55
|
+
end
|
40
56
|
|
41
|
-
|
42
|
-
|
57
|
+
# @return [String]
|
58
|
+
def value
|
59
|
+
evaluate_script('function() { return this.value; }')
|
43
60
|
end
|
44
61
|
|
45
62
|
# @return [String]
|
46
63
|
def html
|
47
|
-
|
48
|
-
|
49
|
-
@html = client.send_message('DOM.getOuterHTML', objectId: @object_id)
|
50
|
-
@html = @html['outerHTML']
|
64
|
+
html = client.send_message('DOM.getOuterHTML', objectId: @object_id)
|
65
|
+
html['outerHTML']
|
51
66
|
end
|
52
67
|
|
53
68
|
# @return [Hash]
|
54
69
|
def attributes
|
55
|
-
return @attributes if @attributes
|
56
|
-
|
57
70
|
result = client.send_message('DOM.getAttributes', nodeId: @node_id)
|
58
|
-
|
71
|
+
Hash[*result['attributes']]
|
72
|
+
end
|
73
|
+
|
74
|
+
# @return [String]
|
75
|
+
def tag_name
|
76
|
+
evaluate_script('function() { return this.tagName.toLowerCase(); }')
|
59
77
|
end
|
60
78
|
|
61
79
|
# @param [String] name
|
@@ -94,6 +112,13 @@ module Chromate
|
|
94
112
|
bounding_box['height']
|
95
113
|
end
|
96
114
|
|
115
|
+
# @return [self]
|
116
|
+
def focus
|
117
|
+
client.send_message('DOM.focus', nodeId: @node_id)
|
118
|
+
|
119
|
+
self
|
120
|
+
end
|
121
|
+
|
97
122
|
# @return [self]
|
98
123
|
def click
|
99
124
|
mouse.click
|
@@ -109,9 +134,24 @@ module Chromate
|
|
109
134
|
end
|
110
135
|
|
111
136
|
# @param [String] text
|
137
|
+
# @return [self]
|
112
138
|
def type(text)
|
113
|
-
|
114
|
-
|
139
|
+
focus
|
140
|
+
keyboard.type(text)
|
141
|
+
|
142
|
+
self
|
143
|
+
end
|
144
|
+
|
145
|
+
# @return [self]
|
146
|
+
def press_enter
|
147
|
+
keyboard.press_key('Enter')
|
148
|
+
submit_parent_form
|
149
|
+
|
150
|
+
self
|
151
|
+
end
|
152
|
+
|
153
|
+
def drop_to(element)
|
154
|
+
mouse.drag_and_drop_to(element)
|
115
155
|
|
116
156
|
self
|
117
157
|
end
|
@@ -169,8 +209,23 @@ module Chromate
|
|
169
209
|
end
|
170
210
|
end
|
171
211
|
|
212
|
+
# @param [String] script
|
213
|
+
# @return [String]
|
214
|
+
def evaluate_script(script, options = {})
|
215
|
+
result = client.send_message(
|
216
|
+
'Runtime.callFunctionOn',
|
217
|
+
functionDeclaration: script,
|
218
|
+
objectId: @object_id,
|
219
|
+
returnByValue: true,
|
220
|
+
**options
|
221
|
+
)
|
222
|
+
result['result']['value']
|
223
|
+
end
|
224
|
+
|
172
225
|
private
|
173
226
|
|
227
|
+
# @param [String] event
|
228
|
+
# @return [void]
|
174
229
|
def dispatch_event(event)
|
175
230
|
client.send_message('DOM.dispatchEvent', nodeId: @node_id, type: event)
|
176
231
|
end
|
@@ -187,8 +242,56 @@ module Chromate
|
|
187
242
|
[node_info['object']['objectId'], result['nodeId']]
|
188
243
|
end
|
189
244
|
|
245
|
+
# @param [String] selector
|
246
|
+
# @option [Integer] root_id
|
247
|
+
# @return [Chromate::Element, nil]
|
248
|
+
def magick_find(selector, root_id = nil)
|
249
|
+
find(selector, root_id)
|
250
|
+
rescue NotFoundError, InvalidSelectorError
|
251
|
+
el = find_in_shadow_recursively(selector)
|
252
|
+
raise NotFoundError.new(selector, @root_id) unless el
|
253
|
+
|
254
|
+
el
|
255
|
+
end
|
256
|
+
|
257
|
+
# @param [String] selector
|
258
|
+
# @return [Chromate::Element, nil]
|
259
|
+
def find_in_shadow_recursively(selector)
|
260
|
+
shadow_children = find_shadow_children('*')
|
261
|
+
shadow_children.each do |child|
|
262
|
+
found_element = child.find_element(selector) || child.find_in_shadow_recursively(selector)
|
263
|
+
return found_element if found_element
|
264
|
+
end
|
265
|
+
|
266
|
+
nil
|
267
|
+
end
|
268
|
+
|
269
|
+
# @return [Hash]
|
190
270
|
def document
|
191
271
|
@document ||= client.send_message('DOM.getDocument')
|
192
272
|
end
|
273
|
+
|
274
|
+
# Allows to submit the parent form of the element
|
275
|
+
# can be used to submit a form
|
276
|
+
#
|
277
|
+
# @return [void]
|
278
|
+
def submit_parent_form
|
279
|
+
script = <<~JAVASCRIPT
|
280
|
+
function() {
|
281
|
+
const form = this.closest('form');
|
282
|
+
if (form) {
|
283
|
+
const submitEvent = new Event('submit', {
|
284
|
+
bubbles: true,
|
285
|
+
cancelable: true
|
286
|
+
});
|
287
|
+
if (form.dispatchEvent(submitEvent)) {
|
288
|
+
form.submit();
|
289
|
+
}
|
290
|
+
}
|
291
|
+
}
|
292
|
+
JAVASCRIPT
|
293
|
+
|
294
|
+
evaluate_script(script)
|
295
|
+
end
|
193
296
|
end
|
194
297
|
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'chromate/element'
|
4
|
+
|
5
|
+
module Chromate
|
6
|
+
module Elements
|
7
|
+
class Checkbox < Element
|
8
|
+
def initialize(selector, client, **options)
|
9
|
+
super
|
10
|
+
raise InvalidSelectorError, selector unless checkbox?
|
11
|
+
end
|
12
|
+
|
13
|
+
# @return [Boolean]
|
14
|
+
def checked?
|
15
|
+
attributes['checked'] == 'true'
|
16
|
+
end
|
17
|
+
|
18
|
+
# @return [self]
|
19
|
+
def check
|
20
|
+
click unless checked?
|
21
|
+
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
# @return [self]
|
26
|
+
def uncheck
|
27
|
+
click if checked?
|
28
|
+
|
29
|
+
self
|
30
|
+
end
|
31
|
+
|
32
|
+
# @return [self]
|
33
|
+
def toggle
|
34
|
+
click
|
35
|
+
|
36
|
+
self
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'chromate/element'
|
4
|
+
|
5
|
+
module Chromate
|
6
|
+
module Elements
|
7
|
+
class Option < Element
|
8
|
+
attr_reader :value
|
9
|
+
|
10
|
+
# @param [String] value
|
11
|
+
def initialize(value, client, node_id: nil, object_id: nil, root_id: nil)
|
12
|
+
super("option[value='#{value}']", client, node_id: node_id, object_id: object_id, root_id: root_id)
|
13
|
+
|
14
|
+
@value = value
|
15
|
+
end
|
16
|
+
|
17
|
+
def bounding_box
|
18
|
+
script = <<~JAVASCRIPT
|
19
|
+
function() {
|
20
|
+
const select = this.closest('select');
|
21
|
+
const rect = select.getBoundingClientRect();
|
22
|
+
return {
|
23
|
+
x: rect.x,
|
24
|
+
y: rect.y,
|
25
|
+
width: rect.width,
|
26
|
+
height: rect.height
|
27
|
+
};
|
28
|
+
}
|
29
|
+
JAVASCRIPT
|
30
|
+
|
31
|
+
result = evaluate_script(script)
|
32
|
+
# TODO: fix this
|
33
|
+
# The offset is due to the fact that the option return the wrong coordinates
|
34
|
+
# can be fixed by mesuring an option and use the offset multiply by the index of the option
|
35
|
+
{
|
36
|
+
'content' => [result['x'] + 100, result['y'] + 100],
|
37
|
+
'width' => result['width'],
|
38
|
+
'height' => result['height']
|
39
|
+
}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'chromate/element'
|
4
|
+
|
5
|
+
module Chromate
|
6
|
+
module Elements
|
7
|
+
class Radio < Element
|
8
|
+
def initialize(selector = nil, client = nil, **options)
|
9
|
+
if selector
|
10
|
+
super
|
11
|
+
raise InvalidSelectorError, selector unless radio?
|
12
|
+
else
|
13
|
+
super(**options)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# @return [Boolean]
|
18
|
+
def checked?
|
19
|
+
attributes['checked'] == 'true'
|
20
|
+
end
|
21
|
+
|
22
|
+
# @return [self]
|
23
|
+
def check
|
24
|
+
click unless checked?
|
25
|
+
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
# @return [self]
|
30
|
+
def uncheck
|
31
|
+
click if checked?
|
32
|
+
|
33
|
+
self
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -1,18 +1,62 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'chromate/element'
|
4
|
+
require 'chromate/elements/option'
|
4
5
|
|
5
6
|
module Chromate
|
6
7
|
module Elements
|
7
8
|
class Select < Element
|
8
|
-
# @param [String]
|
9
|
+
# @param [String] value
|
10
|
+
# @return [self]
|
9
11
|
def select_option(value)
|
10
12
|
click
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
|
14
|
+
evaluate_script(javascript, arguments: [{ value: value }]) unless Chromate.configuration.native_control
|
15
|
+
|
16
|
+
Option.new(value, client).click
|
17
|
+
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
# @return [String|nil]
|
22
|
+
def selected_value
|
23
|
+
evaluate_script('function() { return this.value; }')
|
24
|
+
end
|
25
|
+
|
26
|
+
# @return [String|nil]
|
27
|
+
def selected_text
|
28
|
+
evaluate_script('function() {
|
29
|
+
const option = this.options[this.selectedIndex];
|
30
|
+
return option ? option.textContent.trim() : null;
|
31
|
+
}')
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# @return [String]
|
37
|
+
def javascript
|
38
|
+
<<~JAVASCRIPT
|
39
|
+
function() {
|
40
|
+
this.focus();
|
41
|
+
this.dispatchEvent(new MouseEvent('mousedown'));
|
42
|
+
|
43
|
+
const options = Array.from(this.options);
|
44
|
+
const option = options.find(opt =>#{" "}
|
45
|
+
opt.value === arguments[0] || opt.textContent.trim() === arguments[0]
|
46
|
+
);
|
47
|
+
|
48
|
+
if (!option) {
|
49
|
+
throw new Error(`Option '${arguments[0]}' not found in select`);
|
50
|
+
}
|
51
|
+
|
52
|
+
this.value = option.value;
|
53
|
+
|
54
|
+
this.dispatchEvent(new Event('change', { bubbles: true }));
|
55
|
+
this.dispatchEvent(new Event('input', { bubbles: true }));
|
56
|
+
|
57
|
+
this.blur();
|
58
|
+
}
|
59
|
+
JAVASCRIPT
|
16
60
|
end
|
17
61
|
end
|
18
62
|
end
|