apparition 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +1 -0
- data/lib/capybara/apparition/browser.rb +41 -48
- data/lib/capybara/apparition/chrome_client.rb +52 -55
- data/lib/capybara/apparition/dev_tools_protocol/session.rb +2 -2
- data/lib/capybara/apparition/dev_tools_protocol/target.rb +1 -1
- data/lib/capybara/apparition/driver.rb +29 -135
- data/lib/capybara/apparition/keyboard.rb +44 -43
- data/lib/capybara/apparition/launcher.rb +35 -36
- data/lib/capybara/apparition/mouse.rb +25 -21
- data/lib/capybara/apparition/node.rb +279 -309
- data/lib/capybara/apparition/node/drag.rb +148 -0
- data/lib/capybara/apparition/page.rb +64 -30
- data/lib/capybara/apparition/response.rb +41 -0
- data/lib/capybara/apparition/version.rb +1 -1
- metadata +5 -4
- data/lib/capybara/apparition/command.rb +0 -21
@@ -16,8 +16,7 @@ module Capybara::Apparition
|
|
16
16
|
|
17
17
|
def press(key)
|
18
18
|
if key.is_a? Symbol
|
19
|
-
orig_key = key
|
20
|
-
key = key.to_s.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase.to_sym
|
19
|
+
orig_key, key = key, key.to_s.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase.to_sym
|
21
20
|
warn "The use of :#{orig_key} is deprecated, please use :#{key} instead" unless key == orig_key
|
22
21
|
end
|
23
22
|
description = key_description(key)
|
@@ -55,9 +54,8 @@ module Capybara::Apparition
|
|
55
54
|
location: description.location)
|
56
55
|
end
|
57
56
|
|
58
|
-
def
|
59
|
-
old_pressed_keys = @pressed_keys
|
60
|
-
@pressed_keys = {}
|
57
|
+
def with_keys(keys = [])
|
58
|
+
old_pressed_keys, @pressed_keys = @pressed_keys, {}
|
61
59
|
keys.each do |key|
|
62
60
|
press key
|
63
61
|
end
|
@@ -69,17 +67,13 @@ module Capybara::Apparition
|
|
69
67
|
private
|
70
68
|
|
71
69
|
def type_with_modifiers(keys)
|
72
|
-
|
73
|
-
old_pressed_keys = @pressed_keys
|
74
|
-
@pressed_keys = {}
|
70
|
+
old_pressed_keys, @pressed_keys = @pressed_keys, {}
|
75
71
|
|
76
|
-
keys.each do |sequence|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
else
|
82
|
-
press sequence
|
72
|
+
Array(keys).each do |sequence|
|
73
|
+
case sequence
|
74
|
+
when Array then type_with_modifiers(sequence)
|
75
|
+
when String then sequence.each_char { |char| press char }
|
76
|
+
else press sequence
|
83
77
|
end
|
84
78
|
end
|
85
79
|
|
@@ -94,7 +88,7 @@ module Capybara::Apparition
|
|
94
88
|
end
|
95
89
|
|
96
90
|
def key_description(key)
|
97
|
-
shift = (@modifiers &
|
91
|
+
shift = (@modifiers & modifier_bit('Shift')).nonzero?
|
98
92
|
description = OpenStruct.new(
|
99
93
|
key: '',
|
100
94
|
keyCode: 0,
|
@@ -123,20 +117,14 @@ module Capybara::Apparition
|
|
123
117
|
description.text = definition.shiftText if shift && definition.shiftText
|
124
118
|
|
125
119
|
# if any modifiers besides shift are pressed, no text should be sent
|
126
|
-
description.text = '' if (@modifiers & ~
|
120
|
+
description.text = '' if (@modifiers & ~modifier_bit('Shift')).nonzero?
|
127
121
|
|
128
122
|
description
|
129
123
|
end
|
130
124
|
|
125
|
+
MODIFIERS = { 'Alt' => 1, 'Control' => 2, 'Meta' => 4, 'Shift' => 8 }.tap { |h| h.default = 0 }
|
131
126
|
def modifier_bit(key)
|
132
|
-
|
133
|
-
when 'Alt' then 1
|
134
|
-
when 'Control' then 2
|
135
|
-
when 'Meta' then 4
|
136
|
-
when 'Shift' then 8
|
137
|
-
else
|
138
|
-
0
|
139
|
-
end
|
127
|
+
MODIFIERS[key]
|
140
128
|
end
|
141
129
|
|
142
130
|
# /**
|
@@ -151,7 +139,6 @@ module Capybara::Apparition
|
|
151
139
|
# * @property {number=} location
|
152
140
|
# */
|
153
141
|
|
154
|
-
# rubocop:disable Metrics/LineLength
|
155
142
|
KEY_DEFINITIONS = {
|
156
143
|
'0': { 'keyCode': 48, 'key': '0', 'code': 'Digit0' },
|
157
144
|
'1': { 'keyCode': 49, 'key': '1', 'code': 'Digit1' },
|
@@ -163,14 +150,43 @@ module Capybara::Apparition
|
|
163
150
|
'7': { 'keyCode': 55, 'key': '7', 'code': 'Digit7' },
|
164
151
|
'8': { 'keyCode': 56, 'key': '8', 'code': 'Digit8' },
|
165
152
|
'9': { 'keyCode': 57, 'key': '9', 'code': 'Digit9' },
|
153
|
+
# 'numpad0': { 'keyCode': 45, 'shiftKeyCode': 96, 'key': 'Insert', 'code': 'Numpad0', 'shiftKey': '0', 'location': 3 },
|
154
|
+
# 'numpad1': { 'keyCode': 35, 'shiftKeyCode': 97, 'key': 'End', 'code': 'Numpad1', 'shiftKey': '1', 'location': 3 },
|
155
|
+
# 'numpad2': { 'keyCode': 40, 'shiftKeyCode': 98, 'key': 'ArrowDown', 'code': 'Numpad2', 'shiftKey': '2', 'location': 3 },
|
156
|
+
# 'numpad3': { 'keyCode': 34, 'shiftKeyCode': 99, 'key': 'PageDown', 'code': 'Numpad3', 'shiftKey': '3', 'location': 3 },
|
157
|
+
# 'numpad4': { 'keyCode': 37, 'shiftKeyCode': 100, 'key': 'ArrowLeft', 'code': 'Numpad4', 'shiftKey': '4', 'location': 3 },
|
158
|
+
# 'numpad5': { 'keyCode': 12, 'shiftKeyCode': 101, 'key': 'Clear', 'code': 'Numpad5', 'shiftKey': '5', 'location': 3 },
|
159
|
+
# 'numpad6': { 'keyCode': 39, 'shiftKeyCode': 102, 'key': 'ArrowRight', 'code': 'Numpad6', 'shiftKey': '6', 'location': 3 },
|
160
|
+
# 'numpad7': { 'keyCode': 36, 'shiftKeyCode': 103, 'key': 'Home', 'code': 'Numpad7', 'shiftKey': '7', 'location': 3 },
|
161
|
+
# 'numpad8': { 'keyCode': 38, 'shiftKeyCode': 104, 'key': 'ArrowUp', 'code': 'Numpad8', 'shiftKey': '8', 'location': 3 },
|
162
|
+
# 'numpad9': { 'keyCode': 33, 'shiftKeyCode': 105, 'key': 'PageUp', 'code': 'Numpad9', 'shiftKey': '9', 'location': 3 },
|
163
|
+
# 'multiply': { 'keyCode': 106, 'code': 'NumpadMultiply', 'key': '*', 'location': 3 },
|
164
|
+
# 'add': { 'keyCode': 107, 'code': 'NumpadAdd', 'key': '+', 'location': 3 },
|
165
|
+
# 'subtract': { 'keyCode': 109, 'code': 'NumpadSubtract', 'key': '-', 'location': 3 },
|
166
|
+
# 'divide': { 'keyCode': 111, 'code': 'NumpadDivide', 'key': '/', 'location': 3 },
|
167
|
+
# 'decimal': { 'keyCode': 46, 'shiftKeyCode': 110, 'code': 'NumpadDecimal', 'key': "\u0000", 'shiftKey': '.', 'location': 3 },
|
168
|
+
'numpad0': { 'keyCode': 96, 'code': 'Numpad0', 'key': '0', 'location': 3 },
|
169
|
+
'numpad1': { 'keyCode': 97, 'code': 'Numpad1', 'key': '1', 'location': 3 },
|
170
|
+
'numpad2': { 'keyCode': 98, 'code': 'Numpad2', 'key': '2', 'location': 3 },
|
171
|
+
'numpad3': { 'keyCode': 99, 'code': 'Numpad3', 'key': '3', 'location': 3 },
|
172
|
+
'numpad4': { 'keyCode': 100, 'code': 'Numpad4', 'key': '4', 'location': 3 },
|
173
|
+
'numpad5': { 'keyCode': 101, 'code': 'Numpad5', 'key': '5', 'location': 3 },
|
174
|
+
'numpad6': { 'keyCode': 102, 'code': 'Numpad6', 'key': '6', 'location': 3 },
|
175
|
+
'numpad7': { 'keyCode': 103, 'code': 'Numpad7', 'key': '7', 'location': 3 },
|
176
|
+
'numpad8': { 'keyCode': 104, 'code': 'Numpad8', 'key': '8', 'location': 3 },
|
177
|
+
'numpad9': { 'keyCode': 104, 'code': 'Numpad9', 'key': '9', 'location': 3 },
|
178
|
+
'multiply': { 'keyCode': 106, 'code': 'NumpadMultiply', 'key': '*', 'location': 3 },
|
179
|
+
'add': { 'keyCode': 107, 'code': 'NumpadAdd', 'key': '+', 'location': 3 },
|
180
|
+
'subtract': { 'keyCode': 109, 'code': 'NumpadSubtract', 'key': '-', 'location': 3 },
|
181
|
+
'decimal': { 'keyCode': 110, 'code': 'NumpadDecimal', 'key': '.', 'location': 3 },
|
182
|
+
'divide': { 'keyCode': 111, 'code': 'NumpadDivide', 'key': '/', 'location': 3 },
|
183
|
+
'numpad_enter': { 'keyCode': 13, 'code': 'NumpadEnter', 'key': 'Enter', 'text': "\r", 'location': 3 },
|
166
184
|
'power': { 'key': 'Power', 'code': 'Power' },
|
167
185
|
'eject': { 'key': 'Eject', 'code': 'Eject' },
|
168
186
|
'abort': { 'keyCode': 3, 'code': 'Abort', 'key': 'Cancel' },
|
169
187
|
'help': { 'keyCode': 6, 'code': 'Help', 'key': 'Help' },
|
170
188
|
'backspace': { 'keyCode': 8, 'code': 'Backspace', 'key': 'Backspace' },
|
171
189
|
'tab': { 'keyCode': 9, 'code': 'Tab', 'key': 'Tab' },
|
172
|
-
'numpad5': { 'keyCode': 12, 'shiftKeyCode': 101, 'key': 'Clear', 'code': 'Numpad5', 'shiftKey': '5', 'location': 3 },
|
173
|
-
'numpad_enter': { 'keyCode': 13, 'code': 'NumpadEnter', 'key': 'Enter', 'text': "\r", 'location': 3 },
|
174
190
|
'enter': { 'keyCode': 13, 'code': 'Enter', 'key': 'Enter', 'text': "\r" },
|
175
191
|
"\r": { 'keyCode': 13, 'code': 'Enter', 'key': 'Enter', 'text': "\r" },
|
176
192
|
"\n": { 'keyCode': 13, 'code': 'Enter', 'key': 'Enter', 'text': "\r" },
|
@@ -186,29 +202,19 @@ module Capybara::Apparition
|
|
186
202
|
'convert': { 'keyCode': 28, 'code': 'Convert', 'key': 'Convert' },
|
187
203
|
'non_convert': { 'keyCode': 29, 'code': 'NonConvert', 'key': 'NonConvert' },
|
188
204
|
'space': { 'keyCode': 32, 'code': 'Space', 'key': ' ' },
|
189
|
-
'numpad9': { 'keyCode': 33, 'shiftKeyCode': 105, 'key': 'PageUp', 'code': 'Numpad9', 'shiftKey': '9', 'location': 3 },
|
190
205
|
'page_up': { 'keyCode': 33, 'code': 'PageUp', 'key': 'PageUp' },
|
191
|
-
'numpad3': { 'keyCode': 34, 'shiftKeyCode': 99, 'key': 'PageDown', 'code': 'Numpad3', 'shiftKey': '3', 'location': 3 },
|
192
206
|
'page_down': { 'keyCode': 34, 'code': 'PageDown', 'key': 'PageDown' },
|
193
207
|
'end': { 'keyCode': 35, 'code': 'End', 'key': 'End' },
|
194
|
-
'numpad1': { 'keyCode': 35, 'shiftKeyCode': 97, 'key': 'End', 'code': 'Numpad1', 'shiftKey': '1', 'location': 3 },
|
195
208
|
'home': { 'keyCode': 36, 'code': 'Home', 'key': 'Home' },
|
196
|
-
'numpad7': { 'keyCode': 36, 'shiftKeyCode': 103, 'key': 'Home', 'code': 'Numpad7', 'shiftKey': '7', 'location': 3 },
|
197
209
|
'left': { 'keyCode': 37, 'code': 'ArrowLeft', 'key': 'ArrowLeft' },
|
198
|
-
'numpad4': { 'keyCode': 37, 'shiftKeyCode': 100, 'key': 'ArrowLeft', 'code': 'Numpad4', 'shiftKey': '4', 'location': 3 },
|
199
|
-
'numpad8': { 'keyCode': 38, 'shiftKeyCode': 104, 'key': 'ArrowUp', 'code': 'Numpad8', 'shiftKey': '8', 'location': 3 },
|
200
210
|
'up': { 'keyCode': 38, 'code': 'ArrowUp', 'key': 'ArrowUp' },
|
201
211
|
'right': { 'keyCode': 39, 'code': 'ArrowRight', 'key': 'ArrowRight' },
|
202
|
-
'numpad6': { 'keyCode': 39, 'shiftKeyCode': 102, 'key': 'ArrowRight', 'code': 'Numpad6', 'shiftKey': '6', 'location': 3 },
|
203
|
-
'numpad2': { 'keyCode': 40, 'shiftKeyCode': 98, 'key': 'ArrowDown', 'code': 'Numpad2', 'shiftKey': '2', 'location': 3 },
|
204
212
|
'down': { 'keyCode': 40, 'code': 'ArrowDown', 'key': 'ArrowDown' },
|
205
213
|
'select': { 'keyCode': 41, 'code': 'Select', 'key': 'Select' },
|
206
214
|
'open': { 'keyCode': 43, 'code': 'Open', 'key': 'Execute' },
|
207
215
|
'print_screen': { 'keyCode': 44, 'code': 'PrintScreen', 'key': 'PrintScreen' },
|
208
216
|
'insert': { 'keyCode': 45, 'code': 'Insert', 'key': 'Insert' },
|
209
|
-
'numpad0': { 'keyCode': 45, 'shiftKeyCode': 96, 'key': 'Insert', 'code': 'Numpad0', 'shiftKey': '0', 'location': 3 },
|
210
217
|
'delete': { 'keyCode': 46, 'code': 'Delete', 'key': 'Delete' },
|
211
|
-
'decimal': { 'keyCode': 46, 'shiftKeyCode': 110, 'code': 'NumpadDecimal', 'key': "\u0000", 'shiftKey': '.', 'location': 3 },
|
212
218
|
'digit0': { 'keyCode': 48, 'code': 'Digit0', 'shiftKey': ')', 'key': '0' },
|
213
219
|
'digit1': { 'keyCode': 49, 'code': 'Digit1', 'shiftKey': '!', 'key': '1' },
|
214
220
|
'digit2': { 'keyCode': 50, 'code': 'Digit2', 'shiftKey': '@', 'key': '2' },
|
@@ -222,10 +228,6 @@ module Capybara::Apparition
|
|
222
228
|
'meta_left': { 'keyCode': 91, 'code': 'MetaLeft', 'key': 'Meta' },
|
223
229
|
'meta_right': { 'keyCode': 92, 'code': 'MetaRight', 'key': 'Meta' },
|
224
230
|
'context_menu': { 'keyCode': 93, 'code': 'ContextMenu', 'key': 'ContextMenu' },
|
225
|
-
'multiply': { 'keyCode': 106, 'code': 'NumpadMultiply', 'key': '*', 'location': 3 },
|
226
|
-
'add': { 'keyCode': 107, 'code': 'NumpadAdd', 'key': '+', 'location': 3 },
|
227
|
-
'subtract': { 'keyCode': 109, 'code': 'NumpadSubtract', 'key': '-', 'location': 3 },
|
228
|
-
'divide': { 'keyCode': 111, 'code': 'NumpadDivide', 'key': '/', 'location': 3 },
|
229
231
|
'F1': { 'keyCode': 112, 'code': 'F1', 'key': 'F1' },
|
230
232
|
'f2': { 'keyCode': 113, 'code': 'F2', 'key': 'F2' },
|
231
233
|
'f3': { 'keyCode': 114, 'code': 'F3', 'key': 'F3' },
|
@@ -378,6 +380,5 @@ module Capybara::Apparition
|
|
378
380
|
'}': { 'keyCode': 221, 'key': '}', 'code': 'BracketRight' },
|
379
381
|
'"': { 'keyCode': 222, 'key': '"', 'code': 'Quote' }
|
380
382
|
}.freeze
|
381
|
-
# rubocop:enable Metrics/LineLength
|
382
383
|
end
|
383
384
|
end
|
@@ -5,47 +5,48 @@ module Capybara::Apparition
|
|
5
5
|
class Launcher
|
6
6
|
KILL_TIMEOUT = 2
|
7
7
|
|
8
|
-
BROWSER_HOST = '127.0.0.1'
|
9
|
-
BROWSER_PORT = '0'
|
10
|
-
|
11
8
|
# Chromium command line options
|
12
9
|
# https://peter.sh/experiments/chromium-command-line-switches/
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
10
|
+
DEFAULT_BOOLEAN_OPTIONS = %w[
|
11
|
+
disable-background-networking
|
12
|
+
disable-background-timer-throttling
|
13
|
+
disable-breakpad
|
14
|
+
disable-client-side-phishing-detection
|
15
|
+
disable-default-apps
|
16
|
+
disable-dev-shm-usage
|
17
|
+
disable-extensions
|
18
|
+
disable-features=site-per-process
|
19
|
+
disable-hang-monitor
|
20
|
+
disable-infobars
|
21
|
+
disable-popup-blocking
|
22
|
+
disable-prompt-on-repost
|
23
|
+
disable-sync
|
24
|
+
disable-translate
|
25
|
+
metrics-recording-only
|
26
|
+
no-first-run
|
27
|
+
safebrowsing-disable-auto-update
|
28
|
+
enable-automation
|
29
|
+
password-store=basic
|
30
|
+
use-mock-keychain
|
31
|
+
keep-alive-for-test
|
32
|
+
].freeze
|
33
|
+
# Note: --no-sandbox is not needed if you properly setup a user in the container.
|
34
|
+
# https://github.com/ebidel/lighthouse-ci/blob/master/builder/Dockerfile#L35-L40
|
35
|
+
# no-sandbox
|
36
|
+
# disable-web-security
|
37
|
+
DEFAULT_VALUE_OPTIONS = {
|
34
38
|
'window-size' => '1024,768',
|
35
39
|
'homepage' => 'about:blank',
|
36
|
-
|
37
|
-
# https://github.com/ebidel/lighthouse-ci/blob/master/builder/Dockerfile#L35-L40
|
38
|
-
# "no-sandbox" => nil,
|
39
|
-
# "disable-web-security" => nil,
|
40
|
-
'remote-debugging-port' => BROWSER_PORT,
|
41
|
-
'remote-debugging-address' => BROWSER_HOST
|
40
|
+
'remote-debugging-address' => '127.0.0.1'
|
42
41
|
}.freeze
|
43
|
-
|
42
|
+
DEFAULT_OPTIONS = DEFAULT_BOOLEAN_OPTIONS.each_with_object({}) { |opt, hsh| hsh[opt] = nil }
|
43
|
+
.merge(DEFAULT_VALUE_OPTIONS)
|
44
|
+
.freeze
|
44
45
|
HEADLESS_OPTIONS = {
|
45
46
|
'headless' => nil,
|
46
47
|
'hide-scrollbars' => nil,
|
47
48
|
'mute-audio' => nil
|
48
|
-
}
|
49
|
+
}.freeze
|
49
50
|
|
50
51
|
def self.start(*args)
|
51
52
|
new(*args).tap(&:start)
|
@@ -58,10 +59,10 @@ module Capybara::Apparition
|
|
58
59
|
::Process.kill('KILL', pid)
|
59
60
|
else
|
60
61
|
::Process.kill('TERM', pid)
|
61
|
-
|
62
|
+
timer = Capybara::Helpers.timer(expire_in: KILL_TIMEOUT)
|
62
63
|
while ::Process.wait(pid, ::Process::WNOHANG).nil?
|
63
64
|
sleep 0.05
|
64
|
-
next unless
|
65
|
+
next unless timer.expired?
|
65
66
|
|
66
67
|
::Process.kill('KILL', pid)
|
67
68
|
::Process.wait(pid)
|
@@ -102,8 +103,6 @@ module Capybara::Apparition
|
|
102
103
|
@pid = ::Process.spawn(*cmd, process_options)
|
103
104
|
ObjectSpace.define_finalizer(self, self.class.process_killer(@pid))
|
104
105
|
end
|
105
|
-
|
106
|
-
sleep 3
|
107
106
|
end
|
108
107
|
|
109
108
|
def stop
|
@@ -5,38 +5,42 @@ module Capybara::Apparition
|
|
5
5
|
def initialize(page, keyboard)
|
6
6
|
@page = page
|
7
7
|
@keyboard = keyboard
|
8
|
+
@current_pos = { x: 0, y: 0 }
|
8
9
|
end
|
9
10
|
|
10
11
|
def click_at(x:, y:, button: 'left', count: 1, modifiers: [])
|
11
|
-
move_to
|
12
|
-
@keyboard.
|
13
|
-
|
14
|
-
|
12
|
+
move_to x: x, y: y
|
13
|
+
@keyboard.with_keys(modifiers) do
|
14
|
+
mouse_params = { x: x, y: y, button: button, count: count }
|
15
|
+
down mouse_params
|
16
|
+
up mouse_params
|
15
17
|
end
|
18
|
+
self
|
16
19
|
end
|
17
20
|
|
18
|
-
def move_to(x:, y:,
|
19
|
-
@
|
20
|
-
|
21
|
-
|
22
|
-
x: x,
|
23
|
-
y: y,
|
24
|
-
modifiers: @keyboard.modifiers)
|
21
|
+
def move_to(x:, y:, **options)
|
22
|
+
@current_pos = { x: x, y: y }
|
23
|
+
mouse_event('mouseMoved', x: x, y: y, **options)
|
24
|
+
self
|
25
25
|
end
|
26
26
|
|
27
|
-
def down(
|
28
|
-
@
|
29
|
-
|
30
|
-
|
31
|
-
x: x,
|
32
|
-
y: y,
|
33
|
-
modifiers: @keyboard.modifiers,
|
34
|
-
clickCount: count)
|
27
|
+
def down(**options)
|
28
|
+
options = @current_pos.merge(options)
|
29
|
+
mouse_event('mousePressed', options)
|
30
|
+
self
|
35
31
|
end
|
36
32
|
|
37
|
-
def up(
|
33
|
+
def up(**options)
|
34
|
+
options = @current_pos.merge(options)
|
35
|
+
mouse_event('mouseReleased', options)
|
36
|
+
self
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def mouse_event(type, x:, y:, button: 'left', count: 1)
|
38
42
|
@page.command('Input.dispatchMouseEvent',
|
39
|
-
type:
|
43
|
+
type: type,
|
40
44
|
button: button,
|
41
45
|
x: x,
|
42
46
|
y: y,
|
@@ -1,8 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'ostruct'
|
4
|
+
require 'capybara/apparition/node/drag'
|
5
|
+
|
4
6
|
module Capybara::Apparition
|
5
7
|
class Node < Capybara::Driver::Node
|
8
|
+
include Drag
|
9
|
+
extend Forwardable
|
10
|
+
|
6
11
|
attr_reader :page_id
|
7
12
|
|
8
13
|
def initialize(driver, page, remote_object)
|
@@ -11,39 +16,21 @@ module Capybara::Apparition
|
|
11
16
|
@remote_object = remote_object
|
12
17
|
end
|
13
18
|
|
19
|
+
delegate browser: :driver
|
20
|
+
|
14
21
|
def id
|
15
22
|
@remote_object
|
16
23
|
end
|
17
24
|
|
18
|
-
def browser
|
19
|
-
driver.browser
|
20
|
-
end
|
21
|
-
|
22
25
|
def parents
|
23
26
|
find('xpath', 'ancestor::*').reverse
|
24
27
|
end
|
25
28
|
|
26
29
|
def find(method, selector)
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
return Array.from(this.querySelectorAll(selector));
|
31
|
-
}
|
32
|
-
JS
|
33
|
-
else
|
34
|
-
evaluate_on <<~JS, value: selector
|
35
|
-
function(selector){
|
36
|
-
const xpath = document.evaluate(selector, this, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
|
37
|
-
let results = [];
|
38
|
-
for (let i=0; i < xpath.snapshotLength; i++){
|
39
|
-
results.push(xpath.snapshotItem(i));
|
40
|
-
}
|
41
|
-
return results;
|
42
|
-
}
|
43
|
-
JS
|
30
|
+
js = method == :css ? FIND_CSS_JS : FIND_XPATH_JS
|
31
|
+
evaluate_on(js, value: selector).map do |r_o|
|
32
|
+
Capybara::Apparition::Node.new(driver, @page, r_o['objectId'])
|
44
33
|
end
|
45
|
-
|
46
|
-
results.map { |r_o| Capybara::Apparition::Node.new(driver, @page, r_o['objectId']) }
|
47
34
|
rescue ::Capybara::Apparition::BrowserError => e
|
48
35
|
raise unless e.name =~ /is not a valid (XPath expression|selector)/
|
49
36
|
|
@@ -70,17 +57,7 @@ module Capybara::Apparition
|
|
70
57
|
def visible_text
|
71
58
|
return '' unless visible?
|
72
59
|
|
73
|
-
text = evaluate_on
|
74
|
-
function(){
|
75
|
-
if (this.nodeName == 'TEXTAREA'){
|
76
|
-
return this.textContent;
|
77
|
-
} else if (this instanceof SVGElement) {
|
78
|
-
return this.textContent;
|
79
|
-
} else {
|
80
|
-
return this.innerText;
|
81
|
-
}
|
82
|
-
}
|
83
|
-
JS
|
60
|
+
text = evaluate_on ELEMENT_VISIBLE_TEXT_JS
|
84
61
|
text.to_s.gsub(/\A[[:space:]&&[^\u00a0]]+/, '')
|
85
62
|
.gsub(/[[:space:]&&[^\u00a0]]+\z/, '')
|
86
63
|
.gsub(/\n+/, "\n")
|
@@ -102,44 +79,24 @@ module Capybara::Apparition
|
|
102
79
|
def [](name)
|
103
80
|
# Although the attribute matters, the property is consistent. Return that in
|
104
81
|
# preference to the attribute for links and images.
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
value =
|
112
|
-
|
113
|
-
|
82
|
+
return evaluate_on ELEMENT_PROP_OR_ATTR_JS, value: name
|
83
|
+
# if ((tag_name == 'img') && (name == 'src')) || ((tag_name == 'a') && (name == 'href'))
|
84
|
+
# # if attribute exists get the property
|
85
|
+
# return attribute(name) && property(name)
|
86
|
+
# end
|
87
|
+
#
|
88
|
+
# value = property(name)
|
89
|
+
# value = attribute(name) if value.nil? || value.is_a?(Hash)
|
90
|
+
#
|
91
|
+
# value
|
114
92
|
end
|
115
93
|
|
116
94
|
def attributes
|
117
|
-
evaluate_on
|
118
|
-
function(){
|
119
|
-
let attrs = {};
|
120
|
-
for (let attr of this.attributes)
|
121
|
-
attrs[attr.name] = attr.value.replace("\\n","\\\\n");
|
122
|
-
return attrs;
|
123
|
-
}
|
124
|
-
JS
|
95
|
+
evaluate_on GET_ATTRIBUTES_JS
|
125
96
|
end
|
126
97
|
|
127
98
|
def value
|
128
|
-
evaluate_on
|
129
|
-
function(){
|
130
|
-
if ((this.tagName == 'SELECT') && this.multiple){
|
131
|
-
let selected = [];
|
132
|
-
for (let option of this.children) {
|
133
|
-
if (option.selected) {
|
134
|
-
selected.push(option.value);
|
135
|
-
}
|
136
|
-
}
|
137
|
-
return selected;
|
138
|
-
} else {
|
139
|
-
return this.value;
|
140
|
-
}
|
141
|
-
}
|
142
|
-
JS
|
99
|
+
evaluate_on GET_VALUE_JS
|
143
100
|
end
|
144
101
|
|
145
102
|
def set(value, **_options)
|
@@ -172,44 +129,15 @@ module Capybara::Apparition
|
|
172
129
|
def select_option
|
173
130
|
return false if disabled?
|
174
131
|
|
175
|
-
evaluate_on
|
176
|
-
function(){
|
177
|
-
let sel = this.parentNode;
|
178
|
-
if (sel.tagName == 'OPTGROUP'){
|
179
|
-
sel = sel.parentNode;
|
180
|
-
}
|
181
|
-
let event_options = { bubbles: true, cancelable: true };
|
182
|
-
sel.dispatchEvent(new FocusEvent('focus', event_options));
|
183
|
-
|
184
|
-
this.selected = true
|
185
|
-
|
186
|
-
sel.dispatchEvent(new Event('change', event_options));
|
187
|
-
sel.dispatchEvent(new FocusEvent('blur', event_options));
|
188
|
-
|
189
|
-
}
|
190
|
-
JS
|
132
|
+
evaluate_on SELECT_OPTION_JS
|
191
133
|
true
|
192
134
|
end
|
193
135
|
|
194
136
|
def unselect_option
|
195
137
|
return false if disabled?
|
196
138
|
|
197
|
-
|
198
|
-
|
199
|
-
let sel = this.parentNode;
|
200
|
-
if (sel.tagName == 'OPTGROUP') {
|
201
|
-
sel = sel.parentNode;
|
202
|
-
}
|
203
|
-
|
204
|
-
if (!sel.multiple){
|
205
|
-
return false;
|
206
|
-
}
|
207
|
-
|
208
|
-
this.selected = false;
|
209
|
-
return true;
|
210
|
-
}
|
211
|
-
JS
|
212
|
-
res || raise(Capybara::UnselectNotAllowed, 'Cannot unselect option from single select box.')
|
139
|
+
evaluate_on(UNSELECT_OPTION_JS) ||
|
140
|
+
raise(Capybara::UnselectNotAllowed, 'Cannot unselect option from single select box.')
|
213
141
|
end
|
214
142
|
|
215
143
|
def tag_name
|
@@ -217,30 +145,7 @@ module Capybara::Apparition
|
|
217
145
|
end
|
218
146
|
|
219
147
|
def visible?
|
220
|
-
|
221
|
-
evaluate_on <<~JS
|
222
|
-
function(){
|
223
|
-
el = this;
|
224
|
-
if (el.tagName == 'AREA'){
|
225
|
-
const map_name = document.evaluate('./ancestor::map/@name', el, null, XPathResult.STRING_TYPE, null).stringValue;
|
226
|
-
el = document.querySelector(`img[usemap='#${map_name}']`);
|
227
|
-
if (!el){
|
228
|
-
return false;
|
229
|
-
}
|
230
|
-
}
|
231
|
-
|
232
|
-
while (el) {
|
233
|
-
const style = window.getComputedStyle(el);
|
234
|
-
if ((style.display == 'none') ||
|
235
|
-
(style.visibility == 'hidden') ||
|
236
|
-
(parseFloat(style.opacity) == 0)) {
|
237
|
-
return false;
|
238
|
-
}
|
239
|
-
el = el.parentElement;
|
240
|
-
}
|
241
|
-
return true;
|
242
|
-
}
|
243
|
-
JS
|
148
|
+
evaluate_on VISIBLE_JS
|
244
149
|
end
|
245
150
|
|
246
151
|
def checked?
|
@@ -252,34 +157,23 @@ module Capybara::Apparition
|
|
252
157
|
end
|
253
158
|
|
254
159
|
def disabled?
|
255
|
-
evaluate_on
|
256
|
-
function() {
|
257
|
-
const xpath = 'parent::optgroup[@disabled] | \
|
258
|
-
ancestor::select[@disabled] | \
|
259
|
-
parent::fieldset[@disabled] | \
|
260
|
-
ancestor::*[not(self::legend) or preceding-sibling::legend][parent::fieldset[@disabled]]';
|
261
|
-
return this.disabled || document.evaluate(xpath, this, null, XPathResult.BOOLEAN_TYPE, null).booleanValue
|
262
|
-
}
|
263
|
-
JS
|
160
|
+
evaluate_on ELEMENT_DISABLED_JS
|
264
161
|
end
|
265
162
|
|
266
163
|
def click(keys = [], button: 'left', count: 1, **options)
|
267
|
-
pos =
|
268
|
-
visible_top_left.tap do |p|
|
269
|
-
p[:x] += options[:x]
|
270
|
-
p[:y] += options[:y]
|
271
|
-
end
|
272
|
-
else
|
273
|
-
visible_center
|
274
|
-
end
|
164
|
+
pos = element_click_pos(options)
|
275
165
|
raise ::Capybara::Apparition::MouseEventImpossible.new(self, 'args' => ['click']) if pos.nil?
|
276
166
|
|
277
167
|
test = mouse_event_test(pos)
|
278
168
|
raise ::Capybara::Apparition::MouseEventFailed.new(self, 'args' => ['click', test.selector, pos]) unless test.success
|
279
169
|
|
280
170
|
@page.mouse.click_at pos.merge(button: button, count: count, modifiers: keys)
|
281
|
-
|
282
|
-
|
171
|
+
if ENV['DEBUG']
|
172
|
+
new_pos = element_click_pos(options)
|
173
|
+
puts "Element moved from #{pos} to #{new_pos}" unless pos == new_pos
|
174
|
+
end
|
175
|
+
# Wait a short time to see if click triggers page load
|
176
|
+
sleep 0.05
|
283
177
|
@page.wait_for_loaded(allow_obsolete: true)
|
284
178
|
end
|
285
179
|
|
@@ -298,43 +192,6 @@ module Capybara::Apparition
|
|
298
192
|
@page.mouse.move_to(pos)
|
299
193
|
end
|
300
194
|
|
301
|
-
def drag_to(other, delay: 0.1)
|
302
|
-
pos = visible_center
|
303
|
-
raise ::Capybara::Apparition::MouseEventImpossible.new(self, 'args' => ['drag_to']) if pos.nil?
|
304
|
-
|
305
|
-
test = mouse_event_test(pos)
|
306
|
-
raise ::Capybara::Apparition::MouseEventFailed.new(self, 'args' => ['drag', test.selector, pos]) unless test.success
|
307
|
-
|
308
|
-
begin
|
309
|
-
@page.mouse.move_to(pos)
|
310
|
-
@page.mouse.down(pos)
|
311
|
-
sleep delay
|
312
|
-
|
313
|
-
other_pos = other.visible_center
|
314
|
-
raise ::Capybara::Apparition::MouseEventImpossible.new(self, 'args' => ['drag_to']) if other_pos.nil?
|
315
|
-
|
316
|
-
@page.mouse.move_to(other_pos.merge(button: 'left'))
|
317
|
-
sleep delay
|
318
|
-
ensure
|
319
|
-
@page.mouse.up(other_pos)
|
320
|
-
end
|
321
|
-
end
|
322
|
-
|
323
|
-
def drag_by(x, y, delay: 0.1)
|
324
|
-
pos = visible_center
|
325
|
-
raise ::Capybara::Apparition::MouseEventImpossible.new(self, 'args' => ['hover']) if pos.nil?
|
326
|
-
|
327
|
-
other_pos = { x: pos[:x] + x, y: pos[:y] + y }
|
328
|
-
raise ::Capybara::Apparition::MouseEventFailed.new(self, 'args' => ['drag', test['selector'], pos]) unless mouse_event_test?(pos)
|
329
|
-
|
330
|
-
@page.mouse.move_to(pos)
|
331
|
-
@page.mouse.down(pos)
|
332
|
-
sleep delay
|
333
|
-
@page.mouse.move_to(other_pos.merge(button: 'left'))
|
334
|
-
sleep delay
|
335
|
-
@page.mouse.up(other_pos)
|
336
|
-
end
|
337
|
-
|
338
195
|
EVENTS = {
|
339
196
|
blur: ['FocusEvent'],
|
340
197
|
focus: ['FocusEvent'],
|
@@ -353,15 +210,9 @@ module Capybara::Apparition
|
|
353
210
|
def trigger(name, **options)
|
354
211
|
raise ArgumentError, 'Unknown event' unless EVENTS.key?(name.to_sym)
|
355
212
|
|
356
|
-
event_type, opts = EVENTS[name.to_sym]
|
357
|
-
opts ||= {}
|
213
|
+
event_type, opts = *EVENTS[name.to_sym], {}
|
358
214
|
|
359
|
-
evaluate_on
|
360
|
-
function(name, options){
|
361
|
-
var event = new #{event_type}(name, options);
|
362
|
-
this.dispatchEvent(event);
|
363
|
-
}
|
364
|
-
JS
|
215
|
+
evaluate_on DISPATCH_EVENT_JS, { value: event_type }, { value: name }, value: opts.merge(options)
|
365
216
|
end
|
366
217
|
|
367
218
|
def ==(other)
|
@@ -371,54 +222,33 @@ module Capybara::Apparition
|
|
371
222
|
end
|
372
223
|
|
373
224
|
def send_keys(*keys)
|
374
|
-
|
375
|
-
function(){
|
376
|
-
let selectedNode = document.getSelection().focusNode;
|
377
|
-
if (!selectedNode)
|
378
|
-
return false;
|
379
|
-
if (selectedNode.nodeType == 3)
|
380
|
-
selectedNode = selectedNode.parentNode;
|
381
|
-
return this.contains(selectedNode);
|
382
|
-
}
|
383
|
-
JS
|
384
|
-
click unless selected
|
225
|
+
click unless evaluate_on CURRENT_NODE_SELECTED_JS
|
385
226
|
@page.keyboard.type(keys)
|
386
227
|
end
|
387
228
|
alias_method :send_key, :send_keys
|
388
229
|
|
389
230
|
def path
|
390
|
-
evaluate_on
|
391
|
-
function(){
|
392
|
-
const xpath = document.evaluate('ancestor-or-self::node()', this, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
|
393
|
-
let elements = [];
|
394
|
-
for (let i=1; i<xpath.snapshotLength; i++){
|
395
|
-
elements.push(xpath.snapshotItem(i));
|
396
|
-
}
|
397
|
-
let selectors = elements.map( el => {
|
398
|
-
prev_siblings = document.evaluate(`./preceding-sibling::${el.tagName}`, el, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
|
399
|
-
return `${el.tagName}[${prev_siblings.snapshotLength + 1}]`;
|
400
|
-
})
|
401
|
-
return '//' + selectors.join('/');
|
402
|
-
}
|
403
|
-
JS
|
231
|
+
evaluate_on GET_PATH_JS
|
404
232
|
end
|
405
233
|
|
406
|
-
def
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
234
|
+
def element_click_pos(x: nil, y: nil, **)
|
235
|
+
if x && y
|
236
|
+
visible_top_left.tap do |p|
|
237
|
+
p[:x] += x
|
238
|
+
p[:y] += y
|
239
|
+
end
|
240
|
+
else
|
241
|
+
visible_center
|
242
|
+
end
|
243
|
+
end
|
415
244
|
|
416
|
-
|
245
|
+
def visible_top_left
|
246
|
+
rect = in_view_bounding_rect
|
247
|
+
return nil if rect.nil?
|
417
248
|
|
418
|
-
result = result['model'] if result['model']
|
419
249
|
frame_offset = @page.current_frame_offset
|
420
250
|
|
421
|
-
if (
|
251
|
+
if (rect['width'].zero? || rect['height'].zero?) && (tag_name == 'area')
|
422
252
|
map = find('xpath', 'ancestor::map').first
|
423
253
|
img = find('xpath', "//img[@usemap='##{map[:name]}']").first
|
424
254
|
return nil unless img.visible?
|
@@ -440,27 +270,17 @@ module Capybara::Apparition
|
|
440
270
|
{ x: img_pos[:x] + offset_pos[:x] + frame_offset[:x],
|
441
271
|
y: img_pos[:y] + offset_pos[:y] + frame_offset[:y] }
|
442
272
|
else
|
443
|
-
{ x:
|
444
|
-
y: result['top'] + frame_offset[:y] }
|
273
|
+
{ x: rect['left'] + frame_offset[:x], y: rect['top'] + frame_offset[:y] }
|
445
274
|
end
|
446
275
|
end
|
447
276
|
|
448
277
|
def visible_center
|
449
|
-
|
450
|
-
|
451
|
-
result = evaluate_on <<~JS
|
452
|
-
function(){
|
453
|
-
var rect = this.getBoundingClientRect();
|
454
|
-
return rect.toJSON();
|
455
|
-
}
|
456
|
-
JS
|
278
|
+
rect = in_view_bounding_rect
|
279
|
+
return nil if rect.nil?
|
457
280
|
|
458
|
-
return nil if result.nil?
|
459
|
-
|
460
|
-
result = result['model'] if result['model']
|
461
281
|
frame_offset = @page.current_frame_offset
|
462
282
|
|
463
|
-
if (
|
283
|
+
if (rect['width'].zero? || rect['height'].zero?) && (tag_name == 'area')
|
464
284
|
map = find('xpath', 'ancestor::map').first
|
465
285
|
img = find('xpath', "//img[@usemap='##{map[:name]}']").first
|
466
286
|
return nil unless img.visible?
|
@@ -484,11 +304,8 @@ module Capybara::Apparition
|
|
484
304
|
y: img_pos[:y] + offset_pos[:y] + frame_offset[:y] }
|
485
305
|
else
|
486
306
|
lm = @page.command('Page.getLayoutMetrics')
|
487
|
-
|
488
|
-
|
489
|
-
xs = [result['left'], result['right']]
|
490
|
-
ys = [result['top'], result['bottom']]
|
491
|
-
x_extents, y_extents = xs.minmax, ys.minmax
|
307
|
+
x_extents = [rect['left'], rect['right']].minmax
|
308
|
+
y_extents = [rect['top'], rect['bottom']].minmax
|
492
309
|
|
493
310
|
x_extents[1] = [x_extents[1], lm['layoutViewport']['clientWidth']].min
|
494
311
|
y_extents[1] = [y_extents[1], lm['layoutViewport']['clientHeight']].min
|
@@ -499,30 +316,15 @@ module Capybara::Apparition
|
|
499
316
|
end
|
500
317
|
|
501
318
|
def top_left
|
502
|
-
result = evaluate_on
|
503
|
-
function(){
|
504
|
-
rect = this.getBoundingClientRect();
|
505
|
-
return rect.toJSON();
|
506
|
-
}
|
507
|
-
JS
|
508
|
-
# @page.command('DOM.getBoxModel', objectId: id)
|
319
|
+
result = evaluate_on GET_BOUNDING_CLIENT_RECT_JS
|
509
320
|
return nil if result.nil?
|
510
321
|
|
511
|
-
|
512
|
-
# y: result["model"]["content"][1] }
|
513
|
-
{ x: result['x'],
|
514
|
-
y: result['y'] }
|
322
|
+
{ x: result['x'], y: result['y'] }
|
515
323
|
end
|
516
324
|
|
517
325
|
def scroll_by(x, y)
|
518
|
-
|
519
|
-
|
520
|
-
if (el.scrollBy){
|
521
|
-
el.scrollBy(arguments[1], arguments[2]);
|
522
|
-
} else {
|
523
|
-
el.scrollTop = el.scrollTop + arguments[2];
|
524
|
-
el.scrollLeft = el.scrollLeft + arguments[1];
|
525
|
-
}
|
326
|
+
evaluate_on <<~JS, { value: x }, value: y
|
327
|
+
function(x, y){ this.scrollBy(x,y); }
|
526
328
|
JS
|
527
329
|
end
|
528
330
|
|
@@ -538,11 +340,7 @@ module Capybara::Apparition
|
|
538
340
|
self
|
539
341
|
end
|
540
342
|
|
541
|
-
|
542
|
-
|
543
|
-
def filter_text(text)
|
544
|
-
text.to_s.gsub(/[[:space:]]+/, ' ').strip
|
545
|
-
end
|
343
|
+
protected
|
546
344
|
|
547
345
|
def evaluate_on(page_function, *args)
|
548
346
|
obsolete_checked_function = <<~JS
|
@@ -560,6 +358,25 @@ module Capybara::Apparition
|
|
560
358
|
process_response(response)
|
561
359
|
end
|
562
360
|
|
361
|
+
def scroll_if_needed
|
362
|
+
driver.execute_script <<~JS, self
|
363
|
+
arguments[0].scrollIntoViewIfNeeded({behavior: 'instant', block: 'center', inline: 'center'})
|
364
|
+
JS
|
365
|
+
end
|
366
|
+
|
367
|
+
private
|
368
|
+
|
369
|
+
def in_view_bounding_rect
|
370
|
+
evaluate_on('function(){ this.scrollIntoViewIfNeeded() }')
|
371
|
+
result = evaluate_on GET_BOUNDING_CLIENT_RECT_JS
|
372
|
+
result = result['model'] if result && result['model']
|
373
|
+
result
|
374
|
+
end
|
375
|
+
|
376
|
+
def filter_text(text)
|
377
|
+
text.to_s.gsub(/[[:space:]]+/, ' ').strip
|
378
|
+
end
|
379
|
+
|
563
380
|
def process_response(response)
|
564
381
|
exception_details = response['exceptionDetails']
|
565
382
|
if exception_details && (exception = exception_details['exception'])
|
@@ -716,9 +533,7 @@ module Capybara::Apparition
|
|
716
533
|
else
|
717
534
|
raise ArgumentError, "Invalid scroll_to location: #{location}"
|
718
535
|
end
|
719
|
-
|
720
|
-
arguments[0].scrollIntoView(#{scroll_opts})
|
721
|
-
JS
|
536
|
+
element.evaluate_on "function(){ this.scrollIntoView(#{scroll_opts}) }"
|
722
537
|
end
|
723
538
|
|
724
539
|
def scroll_to_location(location)
|
@@ -726,55 +541,21 @@ module Capybara::Apparition
|
|
726
541
|
when :top
|
727
542
|
'0'
|
728
543
|
when :bottom
|
729
|
-
'
|
544
|
+
'this.scrollHeight'
|
730
545
|
when :center
|
731
|
-
'(
|
546
|
+
'(this.scrollHeight - this.clientHeight)/2'
|
732
547
|
end
|
733
|
-
|
734
|
-
driver.execute_script <<~JS, self
|
735
|
-
arguments[0].scrollTo(0, #{scroll_y});
|
736
|
-
JS
|
548
|
+
evaluate_on "function(){ this.scrollTo(0, #{scroll_y}) }"
|
737
549
|
end
|
738
550
|
|
739
551
|
def scroll_to_coords(x, y)
|
740
|
-
|
741
|
-
|
552
|
+
evaluate_on <<~JS, { value: x }, value: y
|
553
|
+
function(x,y){ this.scrollTo(x,y) }
|
742
554
|
JS
|
743
555
|
end
|
744
556
|
|
745
|
-
# evaluate_on("function(hit_node){
|
746
|
-
# if ((this == hit_node) || (this.contains(hit_node)))
|
747
|
-
# return { status: 'success' };
|
748
|
-
#
|
749
|
-
# const getSelector = function(element){
|
750
|
-
# let selector = '';
|
751
|
-
# if (element.tagName != 'HTML')
|
752
|
-
# selector = getSelector(element.parentNode) + ' ';
|
753
|
-
# selector += element.tagName.toLowerCase();
|
754
|
-
# if (element.id)
|
755
|
-
# selector += `#${element.id}`;
|
756
|
-
#
|
757
|
-
# for (let className of element.classList){
|
758
|
-
# if (className != '')
|
759
|
-
# selector += `.${className}`;
|
760
|
-
# }
|
761
|
-
# return selector;
|
762
|
-
# }
|
763
|
-
#
|
764
|
-
# return { status: 'failure', selector: getSelector(hit_node)};
|
765
|
-
# }", objectId: hit_node_id)
|
766
|
-
|
767
557
|
def delete_text
|
768
|
-
evaluate_on
|
769
|
-
function(){
|
770
|
-
range = document.createRange();
|
771
|
-
range.selectNodeContents(this);
|
772
|
-
window.getSelection().removeAllRanges();
|
773
|
-
window.getSelection().addRange(range);
|
774
|
-
window.getSelection().deleteFromDocument();
|
775
|
-
window.getSelection().removeAllRanges();
|
776
|
-
}
|
777
|
-
JS
|
558
|
+
evaluate_on DELETE_TEXT_JS
|
778
559
|
end
|
779
560
|
|
780
561
|
# SettableValue encapsulates time/date field formatting
|
@@ -840,5 +621,194 @@ module Capybara::Apparition
|
|
840
621
|
# releasePromises = [helper.releaseObject(@element._client, remote_object)]
|
841
622
|
end
|
842
623
|
end
|
624
|
+
|
625
|
+
####################
|
626
|
+
# JS snippets
|
627
|
+
####################
|
628
|
+
|
629
|
+
GET_PATH_JS = <<~JS
|
630
|
+
function() {
|
631
|
+
const xpath = document.evaluate('ancestor-or-self::node()', this, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
|
632
|
+
let elements = [];
|
633
|
+
for (let i=1; i<xpath.snapshotLength; i++){
|
634
|
+
elements.push(xpath.snapshotItem(i));
|
635
|
+
}
|
636
|
+
let selectors = elements.map( el => {
|
637
|
+
prev_siblings = document.evaluate(`./preceding-sibling::${el.tagName}`, el, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
|
638
|
+
return `${el.tagName}[${prev_siblings.snapshotLength + 1}]`;
|
639
|
+
})
|
640
|
+
return '//' + selectors.join('/');
|
641
|
+
}
|
642
|
+
JS
|
643
|
+
|
644
|
+
CURRENT_NODE_SELECTED_JS = <<~JS
|
645
|
+
function() {
|
646
|
+
let selectedNode = document.getSelection().focusNode;
|
647
|
+
if (!selectedNode)
|
648
|
+
return false;
|
649
|
+
if (selectedNode.nodeType == 3)
|
650
|
+
selectedNode = selectedNode.parentNode;
|
651
|
+
return this.contains(selectedNode);
|
652
|
+
}
|
653
|
+
JS
|
654
|
+
|
655
|
+
FIND_CSS_JS = <<~JS
|
656
|
+
function(selector){
|
657
|
+
return Array.from(this.querySelectorAll(selector));
|
658
|
+
}
|
659
|
+
JS
|
660
|
+
|
661
|
+
FIND_XPATH_JS = <<~JS
|
662
|
+
function(selector){
|
663
|
+
const xpath = document.evaluate(selector, this, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
|
664
|
+
let results = [];
|
665
|
+
for (let i=0; i < xpath.snapshotLength; i++){
|
666
|
+
results.push(xpath.snapshotItem(i));
|
667
|
+
}
|
668
|
+
return results;
|
669
|
+
}
|
670
|
+
JS
|
671
|
+
|
672
|
+
ELEMENT_VISIBLE_TEXT_JS = <<~JS
|
673
|
+
function(){
|
674
|
+
if (this.nodeName == 'TEXTAREA'){
|
675
|
+
return this.textContent;
|
676
|
+
} else if (this instanceof SVGElement) {
|
677
|
+
return this.textContent;
|
678
|
+
} else {
|
679
|
+
return this.innerText;
|
680
|
+
}
|
681
|
+
}
|
682
|
+
JS
|
683
|
+
|
684
|
+
GET_ATTRIBUTES_JS = <<~JS
|
685
|
+
function(){
|
686
|
+
let attrs = {};
|
687
|
+
for (let attr of this.attributes)
|
688
|
+
attrs[attr.name] = attr.value.replace("\\n","\\\\n");
|
689
|
+
return attrs;
|
690
|
+
}
|
691
|
+
JS
|
692
|
+
|
693
|
+
GET_VALUE_JS = <<~JS
|
694
|
+
function(){
|
695
|
+
if ((this.tagName == 'SELECT') && this.multiple){
|
696
|
+
let selected = [];
|
697
|
+
for (let option of this.children) {
|
698
|
+
if (option.selected) {
|
699
|
+
selected.push(option.value);
|
700
|
+
}
|
701
|
+
}
|
702
|
+
return selected;
|
703
|
+
} else {
|
704
|
+
return this.value;
|
705
|
+
}
|
706
|
+
}
|
707
|
+
JS
|
708
|
+
|
709
|
+
SELECT_OPTION_JS = <<~JS
|
710
|
+
function(){
|
711
|
+
let sel = this.parentNode;
|
712
|
+
if (sel.tagName == 'OPTGROUP'){
|
713
|
+
sel = sel.parentNode;
|
714
|
+
}
|
715
|
+
let event_options = { bubbles: true, cancelable: true };
|
716
|
+
sel.dispatchEvent(new FocusEvent('focus', event_options));
|
717
|
+
|
718
|
+
this.selected = true
|
719
|
+
|
720
|
+
sel.dispatchEvent(new Event('change', event_options));
|
721
|
+
sel.dispatchEvent(new FocusEvent('blur', event_options));
|
722
|
+
}
|
723
|
+
JS
|
724
|
+
|
725
|
+
UNSELECT_OPTION_JS = <<~JS
|
726
|
+
function(){
|
727
|
+
let sel = this.parentNode;
|
728
|
+
if (sel.tagName == 'OPTGROUP') {
|
729
|
+
sel = sel.parentNode;
|
730
|
+
}
|
731
|
+
|
732
|
+
if (!sel.multiple){
|
733
|
+
return false;
|
734
|
+
}
|
735
|
+
|
736
|
+
this.selected = false;
|
737
|
+
return true;
|
738
|
+
}
|
739
|
+
JS
|
740
|
+
|
741
|
+
# if an area element, check visibility of relevant image
|
742
|
+
VISIBLE_JS = <<~JS
|
743
|
+
function(){
|
744
|
+
el = this;
|
745
|
+
if (el.tagName == 'AREA'){
|
746
|
+
const map_name = document.evaluate('./ancestor::map/@name', el, null, XPathResult.STRING_TYPE, null).stringValue;
|
747
|
+
el = document.querySelector(`img[usemap='#${map_name}']`);
|
748
|
+
if (!el){
|
749
|
+
return false;
|
750
|
+
}
|
751
|
+
}
|
752
|
+
|
753
|
+
while (el) {
|
754
|
+
const style = window.getComputedStyle(el);
|
755
|
+
if ((style.display == 'none') ||
|
756
|
+
(style.visibility == 'hidden') ||
|
757
|
+
(parseFloat(style.opacity) == 0)) {
|
758
|
+
return false;
|
759
|
+
}
|
760
|
+
el = el.parentElement;
|
761
|
+
}
|
762
|
+
return true;
|
763
|
+
}
|
764
|
+
JS
|
765
|
+
|
766
|
+
DELETE_TEXT_JS = <<~JS
|
767
|
+
function(){
|
768
|
+
range = document.createRange();
|
769
|
+
range.selectNodeContents(this);
|
770
|
+
window.getSelection().removeAllRanges();
|
771
|
+
window.getSelection().addRange(range);
|
772
|
+
window.getSelection().deleteFromDocument();
|
773
|
+
window.getSelection().removeAllRanges();
|
774
|
+
}
|
775
|
+
JS
|
776
|
+
|
777
|
+
GET_BOUNDING_CLIENT_RECT_JS = <<~JS
|
778
|
+
function(){
|
779
|
+
rect = this.getBoundingClientRect();
|
780
|
+
return rect.toJSON();
|
781
|
+
}
|
782
|
+
JS
|
783
|
+
|
784
|
+
ELEMENT_DISABLED_JS = <<~JS
|
785
|
+
function() {
|
786
|
+
const xpath = 'parent::optgroup[@disabled] | \
|
787
|
+
ancestor::select[@disabled] | \
|
788
|
+
parent::fieldset[@disabled] | \
|
789
|
+
ancestor::*[not(self::legend) or preceding-sibling::legend][parent::fieldset[@disabled]]';
|
790
|
+
return this.disabled || document.evaluate(xpath, this, null, XPathResult.BOOLEAN_TYPE, null).booleanValue
|
791
|
+
}
|
792
|
+
JS
|
793
|
+
|
794
|
+
ELEMENT_PROP_OR_ATTR_JS = <<~JS
|
795
|
+
function(name){
|
796
|
+
if (((this.tagName == 'img') && (name == 'src')) ||
|
797
|
+
((this.tagName == 'a') && (name == 'href')))
|
798
|
+
return this.getAttribute(name) && this[name];
|
799
|
+
|
800
|
+
let value = this[name];
|
801
|
+
if ((value == null) || ['object', 'function'].includes(typeof value))
|
802
|
+
value = this.getAttribute(name);
|
803
|
+
return value
|
804
|
+
}
|
805
|
+
JS
|
806
|
+
|
807
|
+
DISPATCH_EVENT_JS = <<~JS
|
808
|
+
function(type, name, options){
|
809
|
+
var event = new window[type](name, options);
|
810
|
+
this.dispatchEvent(event);
|
811
|
+
}
|
812
|
+
JS
|
843
813
|
end
|
844
814
|
end
|