apparition 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 yield_with_keys(keys = [])
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
- keys = Array(keys)
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
- if sequence.is_a? Array
78
- type_with_modifiers(sequence)
79
- elsif sequence.is_a? String
80
- sequence.each_char { |char| press char }
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 & 8).nonzero?
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 & ~8).nonzero?
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
- case key
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
- DEFAULT_OPTIONS = {
14
- 'disable-background-networking' => nil,
15
- 'disable-background-timer-throttling' => nil,
16
- 'disable-breakpad' => nil,
17
- 'disable-client-side-phishing-detection' => nil,
18
- 'disable-default-apps' => nil,
19
- 'disable-dev-shm-usage' => nil,
20
- 'disable-extensions' => nil,
21
- 'disable-features=site-per-process' => nil,
22
- 'disable-hang-monitor' => nil,
23
- 'disable-popup-blocking' => nil,
24
- 'disable-prompt-on-repost' => nil,
25
- 'disable-sync' => nil,
26
- 'disable-translate' => nil,
27
- 'metrics-recording-only' => nil,
28
- 'no-first-run' => nil,
29
- 'safebrowsing-disable-auto-update' => nil,
30
- 'enable-automation' => nil,
31
- 'password-store=basic' => nil,
32
- 'use-mock-keychain' => nil,
33
- 'keep-alive-for-test' => nil,
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
- # Note: --no-sandbox is not needed if you properly setup a user in the container.
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
- start = Time.now
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 (Time.now - start) > KILL_TIMEOUT
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(x: x, y: y)
12
- @keyboard.yield_with_keys(modifiers) do
13
- down(x: x, y: y, button: button, count: count)
14
- up(x: x, y: y, button: button, count: count)
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:, button: 'none')
19
- @page.command('Input.dispatchMouseEvent',
20
- type: 'mouseMoved',
21
- button: button,
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(x:, y:, button: 'left', count: 1)
28
- @page.command('Input.dispatchMouseEvent',
29
- type: 'mousePressed',
30
- button: button,
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(x:, y:, button: 'left', count: 1)
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: 'mouseReleased',
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
- results = if method == :css
28
- evaluate_on <<~JS, value: selector
29
- function(selector){
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 <<~JS
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
- if ((tag_name == 'img') && (name == 'src')) || ((tag_name == 'a') && (name == 'href'))
106
- # if attribute exists get the property
107
- return attribute(name) && property(name)
108
- end
109
-
110
- value = property(name)
111
- value = attribute(name) if value.nil? || value.is_a?(Hash)
112
-
113
- value
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 <<~JS
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 <<~JS
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 <<~JS
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
- res = evaluate_on <<~JS
198
- function(){
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
- # if an area element, check visibility of relevant image
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 <<~JS
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 = if options[:x] && options[:y]
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
- puts 'Waiting to see if click triggered page load' if ENV['DEBUG']
282
- sleep 0.1
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 <<~JS, { value: name }, value: opts.merge(options)
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
- selected = evaluate_on <<~JS
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 <<~JS
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 visible_top_left
407
- evaluate_on('function(){ this.scrollIntoViewIfNeeded() }')
408
- # result = @page.command('DOM.getBoxModel', objectId: id)
409
- result = evaluate_on <<~JS
410
- function(){
411
- var rect = this.getBoundingClientRect();
412
- return rect.toJSON();
413
- }
414
- JS
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
- return nil if result.nil?
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 (result['width'].zero? || result['height'].zero?) && (tag_name == 'area')
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: result['left'] + frame_offset[: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
- evaluate_on('function(){ this.scrollIntoViewIfNeeded() }')
450
- # result = @page.command('DOM.getBoxModel', objectId: id)
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 (result['width'].zero? || result['height'].zero?) && (tag_name == 'area')
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
- # quad = result["border"]
488
- # xs,ys = quad.partition.with_index { |_, idx| idx.even? }
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 <<~JS
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
- # { x: result["model"]["content"][0],
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
- driver.execute_script <<~JS, self, x, y
519
- var el = arguments[0];
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
- private
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
- driver.execute_script <<~JS, element
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
- 'arguments[0].scrollHeight'
544
+ 'this.scrollHeight'
730
545
  when :center
731
- '(arguments[0].scrollHeight - arguments[0].clientHeight)/2'
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
- driver.execute_script <<~JS, self, x, y
741
- arguments[0].scrollTo(arguments[1], arguments[2]);
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 <<~JS
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