isomorfeus-puppetmaster 0.1.0

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.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +191 -0
  3. data/lib/isomorfeus-puppetmaster.rb +43 -0
  4. data/lib/isomorfeus/puppetmaster.rb +62 -0
  5. data/lib/isomorfeus/puppetmaster/console_message.rb +19 -0
  6. data/lib/isomorfeus/puppetmaster/cookie.rb +46 -0
  7. data/lib/isomorfeus/puppetmaster/document.rb +160 -0
  8. data/lib/isomorfeus/puppetmaster/driver/jsdom.rb +370 -0
  9. data/lib/isomorfeus/puppetmaster/driver/jsdom_document.rb +908 -0
  10. data/lib/isomorfeus/puppetmaster/driver/jsdom_node.rb +836 -0
  11. data/lib/isomorfeus/puppetmaster/driver/puppeteer.rb +401 -0
  12. data/lib/isomorfeus/puppetmaster/driver/puppeteer_document.rb +944 -0
  13. data/lib/isomorfeus/puppetmaster/driver/puppeteer_node.rb +866 -0
  14. data/lib/isomorfeus/puppetmaster/driver_registration.rb +19 -0
  15. data/lib/isomorfeus/puppetmaster/dsl.rb +40 -0
  16. data/lib/isomorfeus/puppetmaster/errors.rb +90 -0
  17. data/lib/isomorfeus/puppetmaster/iframe.rb +17 -0
  18. data/lib/isomorfeus/puppetmaster/node.rb +241 -0
  19. data/lib/isomorfeus/puppetmaster/node/checkbox.rb +17 -0
  20. data/lib/isomorfeus/puppetmaster/node/content_editable.rb +18 -0
  21. data/lib/isomorfeus/puppetmaster/node/filechooser.rb +9 -0
  22. data/lib/isomorfeus/puppetmaster/node/input.rb +21 -0
  23. data/lib/isomorfeus/puppetmaster/node/radiobutton.rb +13 -0
  24. data/lib/isomorfeus/puppetmaster/node/select.rb +36 -0
  25. data/lib/isomorfeus/puppetmaster/node/textarea.rb +7 -0
  26. data/lib/isomorfeus/puppetmaster/request.rb +17 -0
  27. data/lib/isomorfeus/puppetmaster/response.rb +26 -0
  28. data/lib/isomorfeus/puppetmaster/rspec/features.rb +23 -0
  29. data/lib/isomorfeus/puppetmaster/rspec/matcher_proxies.rb +80 -0
  30. data/lib/isomorfeus/puppetmaster/rspec/matchers.rb +164 -0
  31. data/lib/isomorfeus/puppetmaster/rspec/matchers/base.rb +98 -0
  32. data/lib/isomorfeus/puppetmaster/rspec/matchers/become_closed.rb +33 -0
  33. data/lib/isomorfeus/puppetmaster/rspec/matchers/compound.rb +88 -0
  34. data/lib/isomorfeus/puppetmaster/rspec/matchers/have_current_path.rb +29 -0
  35. data/lib/isomorfeus/puppetmaster/rspec/matchers/have_selector.rb +69 -0
  36. data/lib/isomorfeus/puppetmaster/rspec/matchers/have_text.rb +33 -0
  37. data/lib/isomorfeus/puppetmaster/rspec/matchers/have_title.rb +29 -0
  38. data/lib/isomorfeus/puppetmaster/rspec/matchers/match_selector.rb +27 -0
  39. data/lib/isomorfeus/puppetmaster/rspec/matchers/match_style.rb +38 -0
  40. data/lib/isomorfeus/puppetmaster/self_forwardable.rb +31 -0
  41. data/lib/isomorfeus/puppetmaster/server.rb +128 -0
  42. data/lib/isomorfeus/puppetmaster/server/checker.rb +40 -0
  43. data/lib/isomorfeus/puppetmaster/server/middleware.rb +60 -0
  44. data/lib/isomorfeus/puppetmaster/server_registration.rb +37 -0
  45. data/lib/isomorfeus/puppetmaster/version.rb +3 -0
  46. metadata +282 -0
@@ -0,0 +1,836 @@
1
+ module Isomorfeus
2
+ module Puppetmaster
3
+ module Driver
4
+ module JsdomNode
5
+ def node_all_text(node)
6
+ @context.eval "AllElementHandles[#{node.handle}].textContent"
7
+ end
8
+
9
+ def node_click(node, x: nil, y: nil, modifiers: nil)
10
+ # modifier_keys: :alt, :control, :meta, :shift
11
+ # raise Isomorfeus::Pupppetmaster::InvalidActionError.new(:click) unless visible?
12
+ modifiers = [modifiers] if modifiers.is_a?(Symbol)
13
+ modifiers = [] unless modifiers
14
+ modifiers = modifiers.map {|key| key.to_s.camelize(:lower) }
15
+ @context.exec <<~JAVASCRIPT
16
+ var options = {button: 0, bubbles: true, cancelable: true};
17
+ var window = AllDomHandles[#{node.document.handle}].window;
18
+ var modifiers = #{modifiers};
19
+ if (modifiers.includes('meta')) { options['metaKey'] = true; }
20
+ if (modifiers.includes('control')) { options['ctrlKey'] = true; }
21
+ if (modifiers.includes('shift')) { options['shiftKey'] = true; }
22
+ if (modifiers.includes('alt')) { options['altKey'] = true; }
23
+ var x = #{x ? x : 'null'};
24
+ var y = #{y ? y : 'null'};
25
+ if (x && y) {
26
+ options['clientX'] = x;
27
+ options['clientY'] = y;
28
+ }
29
+ AllElementHandles[#{node.handle}].dispatchEvent(new window.MouseEvent('mousedown', options));
30
+ AllElementHandles[#{node.handle}].dispatchEvent(new window.MouseEvent('mouseup', options));
31
+ AllElementHandles[#{node.handle}].dispatchEvent(new window.MouseEvent('click', options));
32
+ JAVASCRIPT
33
+ end
34
+
35
+ def node_disabled?(node)
36
+ @context.exec <<~JAVASCRIPT
37
+ var window = AllDomHandles[#{node.document.handle}].window;
38
+ var node = AllElementHandles[#{node.handle}];
39
+ const xpath = `parent::optgroup[@disabled] | ancestor::select[@disabled] | parent::fieldset[@disabled] |
40
+ ancestor::*[not(self::legend) or preceding-sibling::legend][parent::fieldset[@disabled]]`;
41
+ return node.disabled || window.document.evaluate(xpath, node, null, window.XPathResult.BOOLEAN_TYPE, null).booleanValue;
42
+ JAVASCRIPT
43
+ end
44
+
45
+ def node_dispatch_event(node, name, event_type = nil, **options)
46
+ raise ArgumentError, 'Unknown event' unless Isomorfeus::Puppetmaster::Driver::Jsdom::EVENTS.key?(name.to_sym) || event_type
47
+ event_type, opts = *Isomorfeus::Puppetmaster::Driver::Jsdom::EVENTS[name.to_sym] if event_type.nil?
48
+ opts.merge!(options)
49
+ final_options = options.map { |k,v| "#{k}: '#{v}'" }
50
+ @context.exec <<~JAVASCRIPT
51
+ var window = AllDomHandles[#{node.document.handle}].window;
52
+ var event = new window.#{event_type}('#{name}', { #{final_options.join(', ')} });
53
+ AllElementHandles[#{node.handle}].dispatchEvent(event);
54
+ JAVASCRIPT
55
+ end
56
+
57
+ def node_double_click(node, x: nil, y: nil, modifiers: nil)
58
+ # modifier_keys: :alt, :control, :meta, :shift
59
+ # offset: { x: int, y: int }
60
+ modifiers = [modifiers] if modifiers.is_a?(Symbol)
61
+ modifiers = [] unless modifiers
62
+ modifiers = modifiers.map {|key| key.to_s.camelize(:lower) }
63
+ @context.exec <<~JAVASCRIPT
64
+ var options = {button: 0, bubbles: true, cancelable: true};
65
+ var window = AllDomHandles[#{node.document.handle}].window;
66
+ var modifiers = #{modifiers};
67
+ if (modifiers.includes('meta')) { options['metaKey'] = true; }
68
+ if (modifiers.includes('control')) { options['ctrlKey'] = true; }
69
+ if (modifiers.includes('shift')) { options['shiftKey'] = true; }
70
+ if (modifiers.includes('alt')) { options['altKey'] = true; }
71
+ var x = #{x ? x : 'null'};
72
+ var y = #{y ? y : 'null'};
73
+ if (x && y) {
74
+ options['clientX'] = x;
75
+ options['clientY'] = y;
76
+ }
77
+ AllElementHandles[#{node.handle}].dispatchEvent(new window.MouseEvent('mousedown', options));
78
+ AllElementHandles[#{node.handle}].dispatchEvent(new window.MouseEvent('mouseup', options));
79
+ AllElementHandles[#{node.handle}].dispatchEvent(new window.MouseEvent('dblclick', options));
80
+ JAVASCRIPT
81
+ end
82
+
83
+ def node_drag_to(node, other_node)
84
+ # TODO
85
+ if node[:draggable]
86
+ await <<~JAVASCRIPT
87
+ var window = AllDomHandles[#{node.document.handle}].window;
88
+ window.document.addEventListener('mousedown', event => {
89
+ window.jsdom_mousedown_prevented = event.defaultPrevented;
90
+ }, { once: true, passive: true });
91
+ JAVASCRIPT
92
+ # TODO use scrollIntoView once chromium bug is fixed
93
+ # https://bugs.chromium.org/p/chromium/issues/detail?id=939740&can=2&start=0&num=100&q=mousemove%20scrollintoview&colspec=ID%20Type%20Status%20Priority%20Milestone%20Owner%20Summary&groupby=&sort=
94
+ await <<~JAVASCRIPT
95
+ var window = AllDomHandles[#{node.document.handle}].window;
96
+ var node = AllElementHandles[#{node.handle}];
97
+ var other_node = AllElementHandles[#{other_node.handle}];
98
+ var n = node;
99
+ var top = n.offsetTop, left = n.offsetLeft, width = n.offsetWidth, height = n.offsetHeight;
100
+ while(n.offsetParent) { n = n.offsetParent; top += n.offsetTop; left += n.offsetLeft; }
101
+ var node_in_view = (top >= window.pageYOffset && left >= window.pageXOffset &&
102
+ (top + height) <= (window.pageYOffset + window.innerHeight) && (left + width) <= (window.pageXOffset + window.innerWidth));
103
+ if (!node_in_view) { node.scrollTo(0,0); };
104
+ setTimeout(function(){
105
+ var client_rect = node.getBoundingClientRect();
106
+ var x = (client_rect.left + (client_rect.width / 2));
107
+ var y = (client_rect.top + (client_rect.height / 2));
108
+ node.dispatchEvent(new window.MouseEvent('mousemove', {clientX: x, clientY: y, bubbles: true, cancelable: true}));
109
+ setTimeout(function(){
110
+ node.dispatchEvent(new window.MouseEvent('mousedown', {clientX: x, clientY: y, bubbles: true, cancelable: true}));
111
+ if (window.jsdom_mousedown_prevented) {
112
+ n = other_node;
113
+ top = n.offsetTop; left = n.offsetLeft; width = n.offsetWidth; height = n.offsetHeight;
114
+ while(n.offsetParent) { n = n.offsetParent; top += n.offsetTop; left += n.offsetLeft; }
115
+ var node_in_view = (top >= window.pageYOffset && left >= window.pageXOffset &&
116
+ (top + height) <= (window.pageYOffset + window.innerHeight) && (left + width) <= (window.pageXOffset + window.innerWidth));
117
+ if (!node_in_view) { other_node.scrollTo(0,0) };
118
+ setTimeout(function(){
119
+ client_rect = other_node.getBoundingClientRect();
120
+ x = (client_rect.left + (client_rect.width / 2));
121
+ y = (client_rect.top + (client_rect.height / 2));
122
+ node.dispatchEvent(new window.MouseEvent('mousemove', {clientX: x, clientY: y, bubbles: true, cancelable: true}));
123
+ setTimeout(function(){
124
+ node.dispatchEvent(new window.MouseEvent('mouseup', {clientX: x, clientY: y, bubbles: true, cancelable: true}));
125
+ }, #{@jsdom_reaction_timeout/2});
126
+ }, #{@jsdom_reaction_timeout});
127
+ } else {
128
+ var dt = new window.DataTransfer();
129
+ if (node.tagName == 'A'){ dt.setData('text/uri-list', node.href); dt.setData('text', node.href); }
130
+ if (node.tagName == 'IMG'){ dt.setData('text/uri-list', node.src); dt.setData('text', node.src); }
131
+ var opts = { cancelable: true, bubbles: true, dataTransfer: dt };
132
+ var dragEvent = new window.DragEvent('dragstart', opts);
133
+ node.dispatchEvent(dragEvent);
134
+ n = other_node;
135
+ top = n.offsetTop; left = n.offsetLeft; width = n.offsetWidth; height = n.offsetHeight;
136
+ while(n.offsetParent) { n = n.offsetParent; top += n.offsetTop; left += n.offsetLeft; }
137
+ var node_in_view = (top >= window.pageYOffset && left >= window.pageXOffset &&
138
+ (top + height) <= (window.pageYOffset + window.innerHeight) && (left + width) <= (window.pageXOffset + window.innerWidth));
139
+ if (!node_in_view) { other_node.scrollTo(0,0); };
140
+ setTimeout(function(){
141
+ var rect = node.getBoundingClientRect()
142
+ var node_center = new window.DOMPoint((rect.left + rect.right)/2, (rect.top + rect.bottom)/2);
143
+ var other_rect = other_node.getBoundingClientRect();
144
+ var other_point = new window.DOMPoint((other_rect.left + other_rect.right)/2, (other_rect.top + other_rect.bottom)/2);
145
+ var entry_point = null;
146
+ var slope = (other_point.y - other_point.y) / (other_point.x - node_center.x);
147
+ if (other_point.x <= other_point.x) { // left side
148
+ var minXy = slope * (other_rect.left - node_center.x) + node_center.y;
149
+ if (other_rect.top <= minXy && minXy <= other_rect.bottom) { entry_point = new window.DOMPoint(other_rect.left, minXy); }
150
+ }
151
+ if (node_center.x >= other_point.x) { // right side
152
+ var maxXy = slope * (other_rect.right - node_center.x) + node_center.y;
153
+ if (other_rect.top <= maxXy && maxXy <= other_rect.bottom) { entry_point = new window.DOMPoint(other_rect.right, maxXy); }
154
+ }
155
+ if (node_center.y <= other_point.y) { // top side
156
+ var minYx = (other_point.top - node_center.y) / slope + node_center.x;
157
+ if (other_rect.left <= minYx && minYx <= other_rect.right) { entry_point = new window.DOMPoint(minYx, other_rect.top); }
158
+ }
159
+ if (node_center.y >= other_point.y) { // bottom side
160
+ var maxYx = (other_rect.bottom - node_center.y) / slope + node_center.x;
161
+ if (other_rect.left <= maxYx && maxYx <= other_rect.right) { entry_point = new window.DOMPoint(maxYx, other_rect.bottom); }
162
+ }
163
+ if (!entry_point) {
164
+ entry_point = new window.DOMPoint(node_center.x, node_center.y);
165
+ }
166
+ var drag_over_event = new window.DragEvent('dragover', {clientX: entry_point.x, clientY: entry_point.y, bubbles: true, cancelable: true});
167
+ other_node.dispatchEvent(drag_over_event);
168
+ var other_center = new window.DOMPoint((other_rect.left + other_rect.right)/2, (other_rect.top + other_rect.bottom)/2);
169
+ drag_over_event = new window.DragEvent('dragover', {clientX: targetCenter.x, clientY: targetCenter.y, bubbles: true, cancelable: true});
170
+ other_node.dispatchEvent(drag_over_event);
171
+ other_node.dispatchEvent(new window.DragEvent('dragleave', {bubbles: true, cancelable: true}));
172
+ if (drag_over_event.defaultPrevented) {
173
+ other_node.dispatchEvent(new window.DragEvent('drop', {bubbles: true, cancelable: true}));
174
+ }
175
+ node.dispatchEvent(new window.DragEvent('dragend', {bubbles: true, cancelable: true}));
176
+ client_rect = other_node.getBoundingClientRect();
177
+ x = (client_rect.left + (client_rect.width / 2));
178
+ y = (client_rect.top + (client_rect.height / 2));
179
+ node.dispatchEvent(new window.MouseEvent('mouseup', {clientX: x, clientY: y, bubbles: true, cancelable: true}));
180
+ }, #{@jsdom_reaction_timeout});
181
+ }
182
+ }, #{@jsdom_reaction_timeout/2});
183
+ }, #{@jsdom_reaction_timeout});
184
+ JAVASCRIPT
185
+ sleep (@reaction_timeout * 3) + 0.2
186
+ else
187
+ await <<~JAVASCRIPT
188
+ var window = AllDomHandles[#{node.document.handle}].window;
189
+ var node = AllElementHandles[#{node.handle}];
190
+ var other_node = AllElementHandles[#{other_node.handle}];
191
+ var n = node;
192
+ var top = n.offsetTop, left = n.offsetLeft, width = n.offsetWidth, height = n.offsetHeight;
193
+ while(n.offsetParent) { n = n.offsetParent; top += n.offsetTop; left += n.offsetLeft; }
194
+ var node_in_view = (top >= window.pageYOffset && left >= window.pageXOffset &&
195
+ (top + height) <= (window.pageYOffset + window.innerHeight) && (left + width) <= (window.pageXOffset + window.innerWidth));
196
+ if (!node_in_view) { res = (n === node); node.scrollTo(0,0); };
197
+ setTimeout(function() {
198
+ var client_rect = node.getBoundingClientRect();
199
+ var x = (client_rect.left + (client_rect.width / 2));
200
+ var y = (client_rect.top + (client_rect.height / 2));
201
+ node.dispatchEvent(new window.MouseEvent('mousemove', {clientX: x, clientY: y, bubbles: true, cancelable: true}));
202
+ setTimeout(function() {
203
+ node.dispatchEvent(new window.MouseEvent('mousedown', {button: 0, buttons: 1, clientX: x, clientY: y, bubbles: true, cancelable: true}));
204
+ var n = other_node;
205
+ var top = n.offsetTop, left = n.offsetLeft, width = n.offsetWidth, height = n.offsetHeight;
206
+ while(n.offsetParent) { n = n.offsetParent; top += n.offsetTop; left += n.offsetLeft; }
207
+ var other_node_in_view = (top >= window.pageYOffset && left >= window.pageXOffset &&
208
+ (top + height) <= (window.pageYOffset + window.innerHeight) && (left + width) <= (window.pageXOffset + window.innerWidth));
209
+ if (!other_node_in_view) { other_node.scrollTo(0,0); };
210
+ setTimeout(function() {
211
+ var other_client_rect = other_node.getBoundingClientRect();
212
+ var x = (other_client_rect.left + (other_client_rect.width / 2));
213
+ var y = (other_client_rect.top + (other_client_rect.height / 2));
214
+ node.dispatchEvent(new window.MouseEvent('mousemove', {button: 0, buttons: 1, clientX: x, clientY: y, bubbles: true, cancelable: true}));
215
+ setTimeout(function() {
216
+ node.dispatchEvent(new window.MouseEvent('mouseup', {button: 0, buttons: 1, clientX: x, clientY: y, bubbles: true, cancelable: true}));
217
+ }, #{@jsdom_reaction_timeout/2});
218
+ }, #{@jsdom_reaction_timeout});
219
+ }, #{@jsdom_reaction_timeout/2});
220
+ }, #{@jsdom_reaction_timeout});
221
+ JAVASCRIPT
222
+ sleep (@reaction_timeout * 3) + 0.2
223
+ end
224
+ end
225
+
226
+ def node_equal(node, other_node)
227
+ @context.eval "AllElementHandles[#{node.handle}] === AllElementHandles[#{other_node.handle}]"
228
+ end
229
+
230
+ def node_execute_script(node, script, *args)
231
+ # TODO
232
+ await <<~JAVASCRIPT
233
+ var node_handle = #{node.handle};
234
+ await AllElementHandles[node_handle].executionContext().evaluateHandle((node, arguments) => {
235
+ arguments.unshift(node);
236
+ #{script}
237
+ }, AllElementHandles[node_handle], #{args[1..-1]});
238
+ JAVASCRIPT
239
+ end
240
+
241
+ def node_evaluate_script(node, script, *args)
242
+ # TODO
243
+ await <<~JAVASCRIPT
244
+ var node_handle = #{node.handle};
245
+ await AllElementHandles[node_handle].executionContext().evaluateHandle((node, arguments) => {
246
+ arguments.unshift(node);
247
+ return #{script};
248
+ }, AllElementHandles[node_handle], #{args[1..-1]});
249
+ LastResult = await handle.jsonValue();
250
+ JAVASCRIPT
251
+ end
252
+
253
+ def node_find(node, selector)
254
+ js_escaped_selector = selector.gsub('\\', '\\\\\\').gsub('"', '\"')
255
+ node_data = @context.exec <<~JAVASCRIPT
256
+ var node = AllElementHandles[#{node.handle}].querySelector("#{js_escaped_selector}");
257
+ if (node) {
258
+ var node_handle = RegisterElementHandle(node);
259
+ var tag = node.tagName.toLowerCase();
260
+ var type = null;
261
+ if (tag === 'input') { type = node.getAttribute('type'); }
262
+ return {handle: node_handle, tag: tag, type: type, content_editable: node.isContentEditable};
263
+ }
264
+ JAVASCRIPT
265
+ if node_data
266
+ node_data[:css_selector] = selector
267
+ Isomorfeus::Puppetmaster::Node.new_by_tag(self, node.document, node_data)
268
+ else
269
+ raise Isomorfeus::Puppetmaster::ElementNotFound.new(selector)
270
+ end
271
+ rescue ExecJS::RuntimeError => e
272
+ raise determine_error(e.message)
273
+ end
274
+
275
+ def node_find_all(node, selector)
276
+ js_escaped_selector = selector.gsub('\\', '\\\\\\').gsub('"', '\"')
277
+ node_data_array = @context.exec <<~JAVASCRIPT
278
+ var node_array = AllElementHandles[#{node.handle}].querySelectorAll("#{js_escaped_selector}");
279
+ var node_data_array = [];
280
+ if (node_array) {
281
+ for (var i=0; i<node_array.length; i++) {
282
+ var node_handle = RegisterElementHandle(node_array[i]);
283
+ var tag = node_array[i].tagName.toLowerCase();
284
+ var type = null;
285
+ if (tag === 'input') { type = node_array[i].getAttribute('type'); }
286
+ node_data_array.push({handle: node_handle, tag: tag, type: type, content_editable: node_array[i].isContentEditable});
287
+ }
288
+ }
289
+ return node_data_array;
290
+ JAVASCRIPT
291
+ node_data_array.map do |node_data|
292
+ node_data[:css_selector] = selector
293
+ Isomorfeus::Puppetmaster::Node.new_by_tag(self, node.document, node_data)
294
+ end
295
+ end
296
+
297
+ def node_find_all_xpath(node, query)
298
+ js_escaped_query = query.gsub('\\', '\\\\\\').gsub('"', '\"')
299
+ node_data_array = @context.exec <<~JAVASCRIPT
300
+ var window = AllDomHandles[#{node.document.handle}].window;
301
+ var document = window.document;
302
+ var xpath_result = document.evaluate("#{js_escaped_query}", AllElementHandles[#{node.handle}], null, window.XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
303
+ var node;
304
+ var node_data_array = [];
305
+ while (node = xpath_result.iterateNext) {
306
+ var node_handle = RegisterElementHandle(node);
307
+ var tag = node.tagName.toLowerCase();
308
+ var type = null;
309
+ if (tag === 'input') { type = node.getAttribute('type'); }
310
+ node_data_array.push({handle: node_handle, tag: tag, type: type, content_editable: node.isContentEditable});
311
+ }
312
+ return node_data_array;
313
+ JAVASCRIPT
314
+ node_data_array.map do |node_data|
315
+ node_data[:xpath_query] = query
316
+ Isomorfeus::Puppetmaster::Node.new_by_tag(self, node.document, node_data)
317
+ end
318
+ end
319
+
320
+ def node_find_xpath(node, query)
321
+ js_escaped_query = query.gsub('\\', '\\\\\\').gsub('"', '\"')
322
+ node_data = @context.exec <<~JAVASCRIPT
323
+ var window = AllDomHandles[#{node.document.handle}].window;
324
+ var document = window.document;
325
+ var xpath_result = document.evaluate("#{js_escaped_query}", AllElementHandles[#{node.handle}], null, window.XPathResult.FIRST_ORDERED_NODE_TYPE, null);
326
+ var node = xpath_result.singleNodeValue;
327
+ if (node) {
328
+ var node_handle = RegisterElementHandle(node);
329
+ var tag = node.tagName.toLowerCase();
330
+ var type = null;
331
+ if (tag === 'input') { type = node.getAttribute('type'); }
332
+ return {handle: node_handle, tag: tag, type: type, content_editable: node.isContentEditable};
333
+ }
334
+ JAVASCRIPT
335
+ if node_data
336
+ node_data[:xpath_query] = query
337
+ Isomorfeus::Puppetmaster::Node.new_by_tag(self, node.document, node_data)
338
+ else
339
+ raise Isomorfeus::Puppetmaster::ElementNotFound.new(query)
340
+ end
341
+ rescue ExecJS::ProgramError => e
342
+ raise determine_error('invalid xpath query')
343
+ end
344
+
345
+ def node_focus(node)
346
+ await "await AllElementHandles[#{node.handle}].focus();"
347
+ end
348
+
349
+ def node_get_attribute(node, attribute)
350
+ @context.eval "AllElementHandles[#{node.handle}].getAttribute('#{attribute}')"
351
+ end
352
+
353
+ def node_get_property(node, property)
354
+ @context.eval "AllElementHandles[#{node.handle}]['#{property}']"
355
+ end
356
+
357
+ def node_hover(node)
358
+ @context.exec "AllElementHandles[#{node.handle}].hover()"
359
+ end
360
+
361
+ def node_in_viewport?(node)
362
+ await <<~JAVASCRIPT
363
+ var node_handle = #{node.handle};
364
+ var handle = await AllElementHandles[node_handle].executionContext().evaluateHandle(function(node) {
365
+ var top = node.offsetTop, left = node.offsetLeft, width = node.offsetWidth, height = node.offsetHeight;
366
+ while(node.offsetParent) { node = node.offsetParent; top += node.offsetTop; left += node.offsetLeft; }
367
+ return (top >= window.pageYOffset && left >= window.pageXOffset &&
368
+ (top + height) <= (window.pageYOffset + window.innerHeight) && (left + width) <= (window.pageXOffset + window.innerWidth));
369
+ }, AllElementHandles[node_handle]);
370
+ LastResult = await handle.jsonValue();
371
+ JAVASCRIPT
372
+ end
373
+
374
+ def node_render_base64(_node, **_options)
375
+ raise Isomorfeus::Puppetmaster::NotSupported
376
+ end
377
+
378
+ def node_right_click(node, x: nil, y: nil, modifiers: nil)
379
+ # modifier_keys: :alt, :control, :meta, :shift
380
+ modifiers = [modifiers] if modifiers.is_a?(Symbol)
381
+ modifiers = [] unless modifiers
382
+ modifiers = modifiers.map {|key| key.to_s.camelize(:lower) }
383
+ await <<~JAVASCRIPT
384
+ var options = {button: 2, bubbles: true, cancelable: true};
385
+ var window = AllDomHandles[#{node.document.handle}].window;
386
+ var modifiers = #{modifiers};
387
+ if (modifiers.includes('meta')) { options['metaKey'] = true; }
388
+ if (modifiers.includes('control')) { options['ctrlKey'] = true; }
389
+ if (modifiers.includes('shift')) { options['shiftKey'] = true; }
390
+ if (modifiers.includes('alt')) { options['altKey'] = true; }
391
+ var x = #{x ? x : 'null'};
392
+ var y = #{y ? y : 'null'};
393
+ if (x && y) {
394
+ options['clientX'] = x;
395
+ options['clientY'] = y;
396
+ }
397
+ AllElementHandles[#{node.handle}].dispatchEvent(new window.MouseEvent('mousedown', options));
398
+ AllElementHandles[#{node.handle}].dispatchEvent(new window.MouseEvent('mouseup', options));
399
+ AllElementHandles[#{node.handle}].dispatchEvent(new window.MouseEvent('contextmenu', options));
400
+ JAVASCRIPT
401
+ end
402
+
403
+ def node_inner_html(node)
404
+ @context.eval("AllElementHandles[#{node.handle}].innerHTML")
405
+ end
406
+
407
+ def node_save_screenshot(_node, _path, **_options)
408
+ raise Isomorfeus::Puppetmaster::NotSupported
409
+ end
410
+
411
+ def node_scroll_by(node, x, y)
412
+ await <<~JAVASCRIPT
413
+ await AllElementHandles[#{node.handle}].executionContext().evaluateHandle(function(node) {
414
+ node.scrollBy(#{x}, #{y});
415
+ }, AllElementHandles[#{node.handle}]);
416
+ JAVASCRIPT
417
+ end
418
+
419
+ def node_scroll_into_view(node)
420
+ await <<~JAVASCRIPT
421
+ await AllElementHandles[#{node.handle}].executionContext().evaluateHandle(function(node) {
422
+ node.scrollIntoView();
423
+ }, AllElementHandles[#{node.handle}]);
424
+ JAVASCRIPT
425
+ end
426
+
427
+ def node_scroll_to(node, x, y)
428
+ await <<~JAVASCRIPT
429
+ await AllElementHandles[#{node.handle}].executionContext().evaluateHandle(function(node) {
430
+ node.scrollTo(#{x}, #{y});
431
+ }, AllElementHandles[#{node.handle}]);
432
+ JAVASCRIPT
433
+ end
434
+
435
+ def node_select(node)
436
+ # In the case of an OPTION tag, the change event should come
437
+ # from the parent SELECT
438
+ await <<~JAVASCRIPT
439
+ await AllElementHandles[#{node.handle}].executionContext().evaluateHandle(function(node){
440
+ var xpath = "parent::optgroup[@disabled] | ancestor::select[@disabled] | parent::fieldset[@disabled] | \
441
+ ancestor::*[not(self::legend) or preceding-sibling::legend][parent::fieldset[@disabled]]";
442
+ if (node.disabled || document.evaluate(xpath, node, null, XPathResult.BOOLEAN_TYPE, null).booleanValue) { return false; }
443
+ else if (node.value == false && !node.parentNode.multiple) { return false; }
444
+ else {
445
+ node.parentNode.dispatchEvent(new FocusEvent('focus',{bubbles: true, cancelable: true}));
446
+ node.selected = true;
447
+ var element;
448
+ if (node.nodeName == "OPTION") {
449
+ element = node.parentNode;
450
+ if (element.nodeName == "OPTGROUP") { element = element.parentNode; }
451
+ } else { element = node; }
452
+ element.dispatchEvent(new Event('change',{bubbles: true, cancelable: false}));
453
+ node.parentNode.dispatchEvent(new FocusEvent('blur',{bubbles: true, cancelable: true}));
454
+ return true;
455
+ }
456
+ }, AllElementHandles[#{node.handle}]);
457
+ JAVASCRIPT
458
+ end
459
+
460
+ def node_style(node, *styles)
461
+ await <<~JAVASCRIPT
462
+ var handle = await AllElementHandles[#{node.handle}].executionContext().evaluateHandle(function(node, styles){
463
+ var style = window.getComputedStyle(node);
464
+ if (styles.length > 0) {
465
+ return styles.reduce(function(res,name) {
466
+ res[name] = style[name];
467
+ return res;
468
+ }, {});
469
+ } else { return style; }
470
+ }, AllElementHandles[#{node.handle}], #{styles});
471
+ LastResult = await handle.jsonValue();
472
+ JAVASCRIPT
473
+ end
474
+
475
+ def node_type_keys(node, *keys)
476
+ cjs = <<~JAVASCRIPT
477
+ AllElementHandles[#{node.handle}].focus();
478
+ var window = AllDomHandles[#{node.document.handle}].window;
479
+ var events = [];
480
+ var chars = '';
481
+ var tag = AllElementHandles[#{node.handle}].tagName;
482
+ JAVASCRIPT
483
+ # new KeyboardEvent("keydown", { bubbles: true, cancelable: true, key: character.charCodeAt(0), char: character, shiftKey: false });
484
+ top_modifiers = []
485
+ keys.each do |key|
486
+ if key.is_a?(String)
487
+ key.each_char do |c|
488
+ shift = !! /[[:upper:]]/.match(c)
489
+ cjs << <<~JAVASCRIPT
490
+ events.push(new window.KeyboardEvent('keydown', { bubbles: true, cancelable: true, key: '#{c}'.charCodeAt(0), char: '#{c}',
491
+ altKey: #{need_alt?(top_modifiers)}, ctrlKey: #{need_control?(top_modifiers)}, metaKey: #{need_meta?(top_modifiers)},
492
+ shiftKey: #{shift || need_shift?(top_modifiers)}}));
493
+ JAVASCRIPT
494
+ cjs << <<~JAVASCRIPT
495
+ events.push(new window.KeyboardEvent('keypress', { bubbles: true, cancelable: true, key: '#{c}'.charCodeAt(0), char: '#{c}',
496
+ altKey: #{need_alt?(top_modifiers)}, ctrlKey: #{need_control?(top_modifiers)}, metaKey: #{need_meta?(top_modifiers)},
497
+ shiftKey: #{shift || need_shift?(top_modifiers)}}));
498
+ JAVASCRIPT
499
+ # hack to make input actually happen, sort of
500
+ cjs << <<~JAVASCRIPT
501
+ chars = chars + '#{(shift || need_shift?(top_modifiers)) ? c.upcase : c}';
502
+ JAVASCRIPT
503
+ cjs << <<~JAVASCRIPT
504
+ events.push(new window.KeyboardEvent('keyup', { bubbles: true, cancelable: true, key: '#{c}'.charCodeAt(0), char: '#{c}',
505
+ altKey: #{need_alt?(top_modifiers)}, ctrlKey: #{need_control?(top_modifiers)}, metaKey: #{need_meta?(top_modifiers)},
506
+ shiftKey: #{shift || need_shift?(top_modifiers)}}));
507
+ JAVASCRIPT
508
+ end
509
+ elsif key.is_a?(Symbol)
510
+ if %i[ctrl Ctrl].include?(key)
511
+ key = :control
512
+ elsif %i[command Command Meta].include?(key)
513
+ key = :meta
514
+ elsif %i[divide Divide].include?(key)
515
+ key = :numpad_divide
516
+ elsif %i[decimal Decimal].include?(key)
517
+ key = :numpad_decimal
518
+ elsif %i[left right up down].include?(key)
519
+ key = "arrow_#{key}".to_sym
520
+ end
521
+ if %i[alt alt_left alt_right control control_left control_rigth meta meta_left meta_right shift shift_left shift_right].include?(key)
522
+ top_modifiers << key
523
+ cjs << <<~JAVASCRIPT
524
+ events.push(new window.KeyboardEvent('keydown', { bubbles: true, cancelable: true, key: '#{key.to_s.camelize}', code: '#{key.to_s.camelize}',
525
+ altKey: #{need_alt?(top_modifiers)}, ctrlKey: #{need_control?(top_modifiers)}, metaKey: #{need_meta?(top_modifiers)},
526
+ shiftKey: #{need_shift?(top_modifiers)}}));
527
+ JAVASCRIPT
528
+ else
529
+ cjs << <<~JAVASCRIPT
530
+ events.push(new window.KeyboardEvent('keydown', { bubbles: true, cancelable: true, key: '#{key.to_s.camelize}', code: '#{key.to_s.camelize}',
531
+ altKey: #{need_alt?(top_modifiers)}, ctrlKey: #{need_control?(top_modifiers)}, metaKey: #{need_meta?(top_modifiers)},
532
+ shiftKey: #{need_shift?(top_modifiers)}}));
533
+ JAVASCRIPT
534
+ cjs << <<~JAVASCRIPT
535
+ events.push(new window.KeyboardEvent('keypress', { bubbles: true, cancelable: true, key: '#{key.to_s.camelize}', code: '#{key.to_s.camelize}',
536
+ altKey: #{need_alt?(top_modifiers)}, ctrlKey: #{need_control?(top_modifiers)}, metaKey: #{need_meta?(top_modifiers)},
537
+ shiftKey: #{need_shift?(top_modifiers)}}));
538
+ JAVASCRIPT
539
+ cjs << <<~JAVASCRIPT
540
+ events.push(new window.KeyboardEvent('keyup', { bubbles: true, cancelable: true, key: '#{key.to_s.camelize}', code: '#{key.to_s.camelize}',
541
+ altKey: #{need_alt?(top_modifiers)}, ctrlKey: #{need_control?(top_modifiers)}, metaKey: #{need_meta?(top_modifiers)},
542
+ shiftKey: #{need_shift?(top_modifiers)}}));
543
+ JAVASCRIPT
544
+ end
545
+ elsif key.is_a?(Array)
546
+ modifiers = []
547
+ key.each do |k|
548
+ if k.is_a?(Symbol)
549
+ if %i[ctrl Ctrl].include?(k)
550
+ k = :control
551
+ elsif %i[command Command Meta].include?(k)
552
+ k = :meta
553
+ elsif %i[divide Divide].include?(k)
554
+ k = :numpad_divide
555
+ elsif %i[decimal Decimal].include?(k)
556
+ k = :numpad_decimal
557
+ elsif %i[left right up down].include?(key)
558
+ k = "arrow_#{key}".to_sym
559
+ end
560
+ if %i[alt alt_left alt_right control control_left control_rigth meta meta_left meta_right shift shift_left shift_right].include?(k)
561
+ modifiers << k
562
+ cjs << <<~JAVASCRIPT
563
+ events.push(new window.KeyboardEvent('keydown', { bubbles: true, cancelable: true, key: '#{k.to_s.camelize}', code: '#{k.to_s.camelize}',
564
+ altKey: #{need_alt?(modifiers)}, ctrlKey: #{need_control?(modifiers)}, metaKey: #{need_meta?(modifiers)},
565
+ shiftKey: #{need_shift?(modifiers)}}));
566
+ JAVASCRIPT
567
+ else
568
+ cjs << <<~JAVASCRIPT
569
+ events.push(new window.KeyboardEvent('keydown', { bubbles: true, cancelable: true, key: '#{k.to_s.camelize}', code: '#{k.to_s.camelize}',
570
+ altKey: #{need_alt?(modifiers)}, ctrlKey: #{need_control?(modifiers)}, metaKey: #{need_meta?(modifiers)},
571
+ shiftKey: #{need_shift?(modifiers)}}));
572
+ JAVASCRIPT
573
+ cjs << <<~JAVASCRIPT
574
+ events.push(new window.KeyboardEvent('keypress', { bubbles: true, cancelable: true, key: '#{k.to_s.camelize}', code: '#{k.to_s.camelize}',
575
+ altKey: #{need_alt?(modifiers)}, ctrlKey: #{need_control?(modifiers)}, metaKey: #{need_meta?(modifiers)},
576
+ shiftKey: #{need_shift?(modifiers)}}));
577
+ JAVASCRIPT
578
+ cjs << <<~JAVASCRIPT
579
+ events.push(new window.KeyboardEvent('keyup', { bubbles: true, cancelable: true, key: '#{k.to_s.camelize}', code: '#{k.to_s.camelize}',
580
+ altKey: #{need_alt?(modifiers)}, ctrlKey: #{need_control?(modifiers)}, metaKey: #{need_meta?(modifiers)},
581
+ shiftKey: #{need_shift?(modifiers)}}));
582
+ JAVASCRIPT
583
+ end
584
+ elsif k.is_a?(String)
585
+ k.each_char do |c|
586
+ shift = !! /[[:upper:]]/.match(c)
587
+ cjs << <<~JAVASCRIPT
588
+ events.push(new window.KeyboardEvent('keydown', { bubbles: true, cancelable: true, key: '#{c}'.charCodeAt(0), char: '#{c}',
589
+ altKey: #{need_alt?(modifiers)}, ctrlKey: #{need_control?(modifiers)}, metaKey: #{need_meta?(modifiers)},
590
+ shiftKey: #{shift || need_shift?(modifiers)}}));
591
+ JAVASCRIPT
592
+ cjs << <<~JAVASCRIPT
593
+ events.push(new window.KeyboardEvent('keypress', { bubbles: true, cancelable: true, key: '#{c}'.charCodeAt(0), char: '#{c}',
594
+ altKey: #{need_alt?(modifiers)}, ctrlKey: #{need_control?(modifiers)}, metaKey: #{need_meta?(modifiers)},
595
+ shiftKey: #{shift || need_shift?(modifiers)}}));
596
+ JAVASCRIPT
597
+ # hack to make input actually happen, sort of
598
+ cjs << <<~JAVASCRIPT
599
+ chars = chars + '#{(shift || need_shift?(modifiers)) ? c.upcase : c}';
600
+ JAVASCRIPT
601
+ cjs << <<~JAVASCRIPT
602
+ events.push(new window.KeyboardEvent('keyup', { bubbles: true, cancelable: true, key: '#{c}'.charCodeAt(0), char: '#{c}',
603
+ altKey: #{need_alt?(modifiers)}, ctrlKey: #{need_control?(modifiers)}, metaKey: #{need_meta?(modifiers)},
604
+ shiftKey: #{shift || need_shift?(modifiers)}}));
605
+ JAVASCRIPT
606
+ end
607
+ end
608
+ end
609
+ modifiers.reverse.each do |k|
610
+ cjs << <<~JAVASCRIPT
611
+ events.push(new window.KeyboardEvent('keyup', { bubbles: true, cancelable: true, key: '#{k.to_s.camelize}', code: '#{k.to_s.camelize}',
612
+ altKey: #{need_alt?(modifiers)}, ctrlKey: #{need_control?(modifiers)}, metaKey: #{need_meta?(modifiers)},
613
+ shiftKey: #{need_shift?(modifiers)}}));
614
+ JAVASCRIPT
615
+ end
616
+ end
617
+ end
618
+ top_modifiers.reverse.each do |key|
619
+ cjs << <<~JAVASCRIPT
620
+ events.push(new window.KeyboardEvent('keyup', { bubbles: true, cancelable: true, key: '#{key.to_s.camelize}', code: '#{key.to_s.camelize}',
621
+ altKey: #{need_alt?(top_modifiers)}, ctrlKey: #{need_control?(top_modifiers)}, metaKey: #{need_meta?(top_modifiers)},
622
+ shiftKey: #{need_shift?(top_modifiers)}}));
623
+ JAVASCRIPT
624
+ end
625
+ cjs << <<~JAVASCRIPT
626
+ for (i=0; i<events.length; i++) {
627
+ AllElementHandles[#{node.handle}].dispatchEvent(events[i]);
628
+ }
629
+ if (tag === 'INPUT' || tag === 'TEXTAREA') {AllElementHandles[#{node.handle}].value = chars }
630
+ JAVASCRIPT
631
+ @context.exec cjs
632
+ end
633
+
634
+ def node_unselect(node)
635
+ # In the case of an OPTION tag, the change event should come
636
+ # from the parent SELECT
637
+ await <<~JAVASCRIPT
638
+ await AllElementHandles[#{node.handle}].executionContext().evaluateHandle(function(node){
639
+ var xpath = "parent::optgroup[@disabled] | ancestor::select[@disabled] | parent::fieldset[@disabled] | \
640
+ ancestor::*[not(self::legend) or preceding-sibling::legend][parent::fieldset[@disabled]]";
641
+ if (node.disabled || document.evaluate(xpath, node, null, XPathResult.BOOLEAN_TYPE, null).booleanValue) { return false; }
642
+ else if (node.value == false && !node.parentNode.multiple) { return false; }
643
+ else {
644
+ node.parentNode.dispatchEvent(new FocusEvent('focus',{bubbles: true, cancelable: true}));
645
+ node.selected = false;
646
+ var element;
647
+ if (node.nodeName == "OPTION") {
648
+ element = node.parentNode;
649
+ if (element.nodeName == "OPTGROUP") { element = element.parentNode; }
650
+ } else { element = node; }
651
+ element.dispatchEvent(new Event('change',{bubbles: true, cancelable: false}));
652
+ node.parentNode.dispatchEvent(new FocusEvent('blur',{bubbles: true, cancelable: true}));
653
+ return true;
654
+ }
655
+ }, AllElementHandles[#{node.handle}]);
656
+ JAVASCRIPT
657
+ end
658
+
659
+ def node_value(node)
660
+ @context.exec <<~JAVASCRIPT
661
+ var node = AllElementHandles[#{node.handle}];
662
+ if (node.tagName == "SELECT" && node.multiple) {
663
+ var result = []
664
+ for (let i = 0, len = node.children.length; i < len; i++) {
665
+ var option = node.children[i];
666
+ if (option.selected) { result.push(option.value); }
667
+ }
668
+ return result;
669
+ } else if (node.isContentEditable) { return node.textContent; }
670
+ else { return node.value; }
671
+ JAVASCRIPT
672
+ end
673
+
674
+ def node_value=(node, value)
675
+ raise Isomorfeus::Puppetmaster::ReadOnlyElementError if node.readonly?
676
+ real_value = "`#{value}`"
677
+ @context.exec <<~JAVASCRIPT
678
+ var window = AllDomHandles[#{node.document.handle}].window;
679
+ var node = AllElementHandles[#{node.handle}];
680
+ var value = #{real_value};
681
+ if (node.maxLength >= 0) { value = value.substr(0, node.maxLength); }
682
+ node.dispatchEvent(new window.FocusEvent("focus",{bubbles: true, cancelable: true}));
683
+ var tag_name = node.tagName.toLowerCase();
684
+ if (tag_name === 'input') {
685
+ node.value = '';
686
+ if (node.type === "number" || node.type === "date") {
687
+ for (var i = 0; i < value.length; i++) {
688
+ node.dispatchEvent(new window.KeyboardEvent("keydown", {key: value[i], code: value.charCodeAt(0), bubbles: true, cancelable: true}));
689
+ node.dispatchEvent(new window.KeyboardEvent("keyup", {key: value[i], code: value.charCodeAt(0), bubbles: true, cancelable: true}));
690
+ node.dispatchEvent(new window.KeyboardEvent("keypress", {key: value[i], code: value.charCodeAt(0), bubbles: true, cancelable: true}));
691
+ }
692
+ node.value = value;
693
+ }
694
+ else if (node.type == "time") { node.value = new Date(value).toTimeString().split(" ")[0]; }
695
+ else if (node.type == "datetime-local") {
696
+ value = new Date(value);
697
+ var year = value.getFullYear();
698
+ var month = ("0" + (value.getMonth() + 1)).slice(-2);
699
+ var date = ("0" + value.getDate()).slice(-2);
700
+ var hour = ("0" + value.getHours()).slice(-2);
701
+ var min = ("0" + value.getMinutes()).slice(-2);
702
+ var sec = ("0" + value.getSeconds()).slice(-2);
703
+ value = `${year}-${month}-${date}T${hour}:${min}:${sec}`;
704
+ for (var i = 0; i < value.length; i++) {
705
+ node.dispatchEvent(new window.KeyboardEvent("keydown", {key: value[i], code: value.charCodeAt(0), bubbles: true, cancelable: true}));
706
+ node.value = node.value + value[i];
707
+ node.dispatchEvent(new window.KeyboardEvent("keyup", {key: value[i], code: value.charCodeAt(0), bubbles: true, cancelable: true}));
708
+ node.dispatchEvent(new window.KeyboardEvent("keypress", {key: value[i], code: value.charCodeAt(0), bubbles: true, cancelable: true}));
709
+ }
710
+ } else if (node.type === 'checkbox' || node.type === 'radio') { node.checked = value; }
711
+ else {
712
+ for (var i = 0; i < value.length; i++) {
713
+ node.dispatchEvent(new window.KeyboardEvent("keydown", {key: value[i], code: value.charCodeAt(0), bubbles: true, cancelable: true}));
714
+ node.value = node.value + value[i];
715
+ node.dispatchEvent(new window.KeyboardEvent("keyup", {key: value[i], code: value.charCodeAt(0), bubbles: true, cancelable: true}));
716
+ node.dispatchEvent(new window.KeyboardEvent("keypress", {key: value[i], code: value.charCodeAt(0), bubbles: true, cancelable: true}));
717
+ }
718
+ }
719
+ } else if (tag_name === 'textarea') {
720
+ for (var i = 0; i < value.length; i++) {
721
+ node.dispatchEvent(new window.KeyboardEvent("keydown", {key: value[i], code: value.charCodeAt(0), bubbles: true, cancelable: true}));
722
+ node.value = node.value + value[i];
723
+ node.dispatchEvent(new window.KeyboardEvent("keyup", {key: value[i], code: value.charCodeAt(0), bubbles: true, cancelable: true}));
724
+ node.dispatchEvent(new window.KeyboardEvent("keypress", {key: value[i], code: value.charCodeAt(0), bubbles: true, cancelable: true}));
725
+ }
726
+ }
727
+ JAVASCRIPT
728
+ real_value
729
+ end
730
+
731
+ def node_visible_text(node)
732
+ # if node is AREA, check visibility of relevant image
733
+ text = @context.exec <<~JAVASCRIPT
734
+ var node = AllElementHandles[#{node.handle}];
735
+ var window = AllDomHandles[#{node.document.handle}].window
736
+ var temp_node = node;
737
+ var mapName, style;
738
+ if (node.tagName === "AREA") {
739
+ mapName = document.evaluate("./ancestor::map/@name", node, null, XPathResult.STRING_TYPE, null).stringValue;
740
+ temp_node = document.querySelector('img[usemap="#${mapName}"]');
741
+ if (temp_node == null) { return ''; }
742
+ } else {
743
+ temp_node = node;
744
+ while (temp_node) {
745
+ style = window.getComputedStyle(node);
746
+ if (style.display === "none" || style.visibility === "hidden" || parseFloat(style.opacity) === 0) { return ''; }
747
+ temp_node = temp_node.parentElement;
748
+ }
749
+ }
750
+ if (node.nodeName == "TEXTAREA" || node instanceof window.SVGElement) { return node.textContent; }
751
+ else { return (node.innerText ? node.innerText : node.textContent); }
752
+ JAVASCRIPT
753
+ text.gsub(/\A[[:space:]&&[^\u00a0]]+/, "").gsub(/[[:space:]&&[^\u00a0]]+\z/, "").gsub(/\n+/, "\n").tr("\u00a0", " ")
754
+ end
755
+
756
+ def node_visible?(node)
757
+ await <<~JAVASCRIPT
758
+ var handle = await AllElementHandles[#{node.handle}].executionContext().evaluateHandle(function(node){
759
+ if (node.tagName == 'AREA'){
760
+ const map_name = document.evaluate('./ancestor::map/@name', node, null, XPathResult.STRING_TYPE, null).stringValue;
761
+ node = document.querySelector(`img[usemap='#${map_name}']`);
762
+ if (!node){ return false; }
763
+ }
764
+ var forced_visible = false;
765
+ while (node) {
766
+ const style = window.getComputedStyle(node);
767
+ if (style.visibility == 'visible') { forced_visible = true; }
768
+ if ((style.display == 'none') || ((style.visibility == 'hidden') && !forced_visible) || (parseFloat(style.opacity) == 0)) {
769
+ return false;
770
+ }
771
+ node = node.parentElement;
772
+ }
773
+ return true;
774
+ }, AllElementHandles[#{node.handle}]);
775
+ LastResult = await handle.jsonValue();
776
+ JAVASCRIPT
777
+ end
778
+
779
+ def node_wait_for(node, selector)
780
+ js_escaped_selector = selector.gsub('\\', '\\\\\\').gsub('"', '\"')
781
+ node_data = await <<~JAVASCRIPT
782
+ var start_time = new Date();
783
+ var resolver = function(resolve) {
784
+ var node = AllElementHandles[#{node.handle}].querySelector("#{js_escaped_selector}");
785
+ if (node) {
786
+ var node_handle = RegisterElementHandle(node);
787
+ var tag = node.tagName.toLowerCase();
788
+ var type = null;
789
+ if (tag === 'input') { type = node.getAttribute('type'); }
790
+ LastResult = {handle: node_handle, tag: tag, type: type, content_editable: node.isContentEditable};
791
+ resolve(true);
792
+ }
793
+ else if ((new Date() - start_time) > #{@jsdom_timeout}) { resolve(true); }
794
+ else { setTimeout(resolver, #{@jsdom_reaction_timeout}, resolve) }
795
+ };
796
+ var promise = new Promise(function(resolve, reject){ resolver(resolve); });
797
+ await promise;
798
+ JAVASCRIPT
799
+ if node_data
800
+ node_data[:css_selector] = selector
801
+ Isomorfeus::Puppetmaster::Node.new_by_tag(self, document, node_data)
802
+ end
803
+ end
804
+
805
+ def node_wait_for_xpath(node, query)
806
+ js_escaped_query = query.gsub('\\', '\\\\\\').gsub('"', '\"')
807
+ node_data = await <<~JAVASCRIPT
808
+ var start_time = new Date();
809
+ var resolver = function(resolve) {
810
+ var window = AllDomHandles[#{document.handle}].window;
811
+ var document = window.document;
812
+ var xpath_result = document.evaluate("#{js_escaped_query}", AllElementHandles[#{node.handle}], null, window.XPathResult.FIRST_ORDERED_NODE_TYPE, null);
813
+ var node = xpath_result.singleNodeValue;
814
+ if (node) {
815
+ var node_handle = RegisterElementHandle(node);
816
+ var tag = node.tagName.toLowerCase();
817
+ var type = null;
818
+ if (tag === 'input') { type = node.getAttribute('type'); }
819
+ LastResult = {handle: node_handle, tag: tag, type: type, content_editable: node.isContentEditable};
820
+ resolve(true);
821
+ }
822
+ else if ((new Date() - start_time) > #{@jsdom_timeout}) { resolve(true); }
823
+ else { setTimeout(resolver, #{@jsdom_reaction_timeout}, resolve) }
824
+ };
825
+ var promise = new Promise(function(resolve, reject){ resolver(resolve); });
826
+ await promise;
827
+ JAVASCRIPT
828
+ if node_data
829
+ node_data[:xpath_query] = query
830
+ Isomorfeus::Puppetmaster::Node.new_by_tag(self, document, node_data)
831
+ end
832
+ end
833
+ end
834
+ end
835
+ end
836
+ end