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.
@@ -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