apparition 0.0.1 → 0.0.2
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/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
|