isomorfeus-puppetmaster 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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,370 @@
1
+ module Isomorfeus
2
+ module Puppetmaster
3
+ module Driver
4
+ class Jsdom
5
+ include Isomorfeus::Puppetmaster::Driver::JsdomDocument
6
+ include Isomorfeus::Puppetmaster::Driver::JsdomNode
7
+
8
+ VIEWPORT_DEFAULT_WIDTH = 1024
9
+ VIEWPORT_DEFAULT_HEIGHT = 768
10
+ VIEWPORT_MAX_WIDTH = 1366
11
+ VIEWPORT_MAX_HEIGHT = 768
12
+ TIMEOUT = 30 # seconds
13
+ REACTION_TIMEOUT = 0.5
14
+ EVENTS = {
15
+ blur: ['FocusEvent', {}],
16
+ focus: ['FocusEvent', {}],
17
+ focusin: ['FocusEvent', { bubbles: true }],
18
+ focusout: ['FocusEvent', { bubbles: true }],
19
+ click: ['MouseEvent', { bubbles: true, cancelable: true }],
20
+ dblckick: ['MouseEvent', { bubbles: true, cancelable: true }],
21
+ mousedown: ['MouseEvent', { bubbles: true, cancelable: true }],
22
+ mouseup: ['MouseEvent', { bubbles: true, cancelable: true }],
23
+ mouseenter: ['MouseEvent', {}],
24
+ mouseleave: ['MouseEvent', {}],
25
+ mousemove: ['MouseEvent', { bubbles: true, cancelable: true }],
26
+ mouseover: ['MouseEvent', { bubbles: true, cancelable: true }],
27
+ mouseout: ['MouseEvent', { bubbles: true, cancelable: true }],
28
+ context_menu: ['MouseEvent', { bubble: true, cancelable: true }],
29
+ submit: ['Event', { bubbles: true, cancelable: true }],
30
+ change: ['Event', { bubbles: true, cacnelable: false }],
31
+ input: ['InputEvent', { bubbles: true, cacnelable: false }],
32
+ wheel: ['WheelEvent', { bubbles: true, cancelable: true }]
33
+ }.freeze
34
+
35
+ attr_accessor :default_document
36
+
37
+ def initialize(options = {})
38
+ @app = options.delete(:app)
39
+ @options = options.dup
40
+ @ignore_https_errors = !!@options.delete(:ignore_https_errors)
41
+ @max_width = @options.delete(:max_width) { VIEWPORT_MAX_WIDTH }
42
+ @max_height = @options.delete(:max_height) { VIEWPORT_MAX_HEIGHT }
43
+ @width = @options.delete(:width) { VIEWPORT_DEFAULT_WIDTH > @max_width ? @max_width : VIEWPORT_DEFAULT_WIDTH }
44
+ @height = @options.delete(:height) { VIEWPORT_DEFAULT_HEIGHT > @max_height ? @max_height : VIEWPORT_DEFAULT_HEIGHT }
45
+ @timeout = @options.delete(:timeout) { TIMEOUT }
46
+ @max_wait = @options.delete(:max_wait) { @timeout + 1 }
47
+ @reaction_timeout = @options.delete(:reaction_timeout) { REACTION_TIMEOUT }
48
+ @jsdom_timeout = @timeout * 1000
49
+ @jsdom_reaction_timeout = @reaction_timeout * 1000
50
+ @url_blacklist = @options.delete(:url_blacklist) { [] }
51
+ @context = ExecJS.permissive_compile(jsdom_launch)
52
+ page_handle, @browser = await_result
53
+ @default_document = Isomorfeus::Puppetmaster::Document.new(self, page_handle, Isomorfeus::Puppetmaster::Response.new('status' => 200))
54
+ end
55
+
56
+ def self.document_handle_disposer(driver, handle)
57
+ cjs = <<~JAVASCRIPT
58
+ delete AllDomHandles[#{handle}];
59
+ delete ConsoleMessages[#{handle}];
60
+ JAVASCRIPT
61
+ proc { driver.execute_script(cjs) }
62
+ end
63
+
64
+ def self.node_handle_disposer(driver, handle)
65
+ cjs = <<~JAVASCRIPT
66
+ if (AllElementHandles[#{handle}]) { AllElementHandles[#{handle}].dispose(); }
67
+ delete AllElementHandles[#{handle}];
68
+ JAVASCRIPT
69
+ proc { driver.execute_script(cjs) }
70
+ end
71
+
72
+ def browser
73
+ @browser
74
+ end
75
+
76
+ def document_handles
77
+ @context.eval 'Object.keys(AllDomHandles)'
78
+ end
79
+
80
+ ##### frame, all todo
81
+
82
+ def frame_all_text(frame)
83
+ await <<~JAVASCRIPT
84
+ LastResult = await AllElementHandles[#{frame.handle}].executionContext().evaluate((frame) => {
85
+ return frame.contentDocument.documentElement.textContent;
86
+ }, AllElementHandles[#{frame.handle}]);
87
+ JAVASCRIPT
88
+ end
89
+
90
+ def frame_body(frame)
91
+ node_data = await <<~JAVASCRIPT
92
+ var tt = await AllElementHandles[#{frame.handle}].executionContext().evaluate((frame) => {
93
+ node = frame.contentDocument.body;
94
+ var tag = node.tagName.toLowerCase();
95
+ var type = null;
96
+ if (tag === 'input') { type = node.getAttribute('type'); }
97
+ return [tag, type];
98
+ }, AllElementHandles[#{frame.handle}]);
99
+ LastResult = {handle: node_handle, tag: tt[0], type: tt[1]};
100
+ JAVASCRIPT
101
+ if node_data
102
+ node_data[:css_selector] = 'body'
103
+ Isomorfeus::Puppetmaster::Node.new_by_tag(self, document, node_data)
104
+ end
105
+ end
106
+
107
+ def frame_focus(frame)
108
+ await <<~JAVASCRIPT
109
+ await AllElementHandles[#{frame.handle}].executionContext().evaluate((frame) => {
110
+ frame.contentDocument.documentElement.focus();
111
+ }, AllElementHandles[#{frame.handle}]);
112
+ JAVASCRIPT
113
+ end
114
+
115
+ def frame_head(frame)
116
+ node_data = await <<~JAVASCRIPT
117
+ var tt = await AllElementHandles[#{frame.handle}].executionContext().evaluate((frame) => {
118
+ node = frame.contentDocument.head;
119
+ var tag = node.tagName.toLowerCase();
120
+ var type = null;
121
+ if (tag === 'input') { type = node.getAttribute('type'); }
122
+ return [tag, type];
123
+ }, AllElementHandles[#{frame.handle}]);
124
+ LastResult = {handle: node_handle, tag: tt[0], type: tt[1]};
125
+ JAVASCRIPT
126
+ if node_data
127
+ node_data[:css_selector] = 'body'
128
+ Isomorfeus::Puppetmaster::Node.new_by_tag(self, document, node_data)
129
+ end
130
+ end
131
+
132
+ def frame_html(frame)
133
+ await <<~JAVASCRIPT
134
+ LastResult = await AllElementHandles[#{frame.handle}].executionContext().evaluate((frame) => {
135
+ return frame.contentDocument.documentElement.outerHTML;
136
+ }, AllElementHandles[#{frame.handle}]);
137
+ JAVASCRIPT
138
+ end
139
+
140
+ def frame_title(frame)
141
+ await <<~JAVASCRIPT
142
+ LastResult = await AllElementHandles[#{frame.handle}].executionContext().evaluate((frame) => {
143
+ return frame.contentDocument.title;
144
+ }, AllElementHandles[#{frame.handle}]);
145
+ JAVASCRIPT
146
+ end
147
+
148
+ def frame_url(frame)
149
+ await <<~JAVASCRIPT
150
+ LastResult = await AllElementHandles[#{frame.handle}].executionContext().evaluate((frame) => {
151
+ return frame.contentDocument.location.href;
152
+ }, AllElementHandles[#{frame.handle}]);
153
+ JAVASCRIPT
154
+ end
155
+
156
+ def frame_visible_text(frame)
157
+ # if node is AREA, check visibility of relevant image
158
+ text = await <<~JAVASCRIPT
159
+ LastResult = await AllElementHandles[#{frame.handle}].executionContext().evaluate((frame) => {
160
+ var node = frame.contentDocument.body;
161
+ var temp_node = node;
162
+ while (temp_node) {
163
+ style = window.getComputedStyle(node);
164
+ if (style.display === "none" || style.visibility === "hidden" || parseFloat(style.opacity) === 0) { return ''; }
165
+ temp_node = temp_node.parentElement;
166
+ }
167
+ if (node.nodeName == "TEXTAREA" || node instanceof SVGElement) { return node.textContent; }
168
+ else { return node.innerText; }
169
+ }, AllElementHandles[#{frame.handle}]);
170
+ JAVASCRIPT
171
+ text.gsub(/\A[[:space:]&&[^\u00a0]]+/, "").gsub(/[[:space:]&&[^\u00a0]]+\z/, "").gsub(/\n+/, "\n").tr("\u00a0", " ")
172
+ end
173
+
174
+ private
175
+
176
+ def need_alt?(modifiers)
177
+ (modifiers & %i[alt alt_left alt_right]).size > 0
178
+ end
179
+
180
+ def need_control?(modifiers)
181
+ (modifiers & %i[control control_left control_rigth]).size > 0
182
+ end
183
+
184
+ def need_meta?(modifiers)
185
+ (modifiers & %i[meta meta_left meta_right]).size > 0
186
+ end
187
+
188
+ def need_shift?(modifiers)
189
+ (modifiers & %i[shift shift_left shift_right]).size > 0
190
+ end
191
+
192
+ def await(script)
193
+ @context.eval <<~JAVASCRIPT
194
+ (async () => {
195
+ try {
196
+ LastExecutionFinished = false;
197
+ LastResult = null;
198
+ LastErr = null;
199
+ #{script}
200
+ LastExecutionFinished = true;
201
+ } catch(err) {
202
+ LastResult = null;
203
+ LastErr = err;
204
+ LastExecutionFinished = true;
205
+ }
206
+ })()
207
+ JAVASCRIPT
208
+ await_result
209
+ end
210
+
211
+ def await_result
212
+ start_time = Time.now
213
+ while !execution_finished? && !timed_out?(start_time)
214
+ sleep 0.01
215
+ end
216
+ get_result
217
+ end
218
+
219
+ def determine_error(message)
220
+ if message.include?('Error: certificate has expired')
221
+ Isomorfeus::Puppetmaster::CertificateError.new(message) unless @ignore_https_errors
222
+ elsif message.include?('Error: getaddrinfo ENOTFOUND')
223
+ Isomorfeus::Puppetmaster::DNSError.new(message)
224
+ elsif message.include?('Unknown key: ')
225
+ Isomorfeus::Puppetmaster::KeyError.new(message)
226
+ elsif message.include?('Execution context was destroyed, most likely because of a navigation.')
227
+ Isomorfeus::Puppetmaster::ExecutionContextError.new(message)
228
+ elsif message.include?('Unable to find ')
229
+ Isomorfeus::Puppetmaster::ElementNotFound.new(message)
230
+ elsif (message.include?('SyntaxError:') && (message.include?('unknown pseudo-class selector') || message.include?('is not a valid selector'))) || message.include?('invalid xpath query')
231
+ Isomorfeus::Puppetmaster::DOMException.new(message)
232
+ else
233
+ Isomorfeus::Puppetmaster::JavaScriptError.new(message)
234
+ end
235
+ end
236
+
237
+ def execution_finished?
238
+ @context.eval 'LastExecutionFinished'
239
+ end
240
+
241
+ def get_result
242
+ res, err_msg = @context.eval 'GetLastResult()'
243
+ raise determine_error(err_msg) if err_msg
244
+ res
245
+ end
246
+
247
+ def jsdom_launch
248
+ <<~JAVASCRIPT
249
+ const canvas = require('canvas')
250
+ const jsdom = require('jsdom');
251
+ const Cookie = jsdom.toughCookie.Cookie;
252
+ const MemoryCookieStore = jsdom.toughCookie.MemoryCookieStore;
253
+ const { JSDOM } = jsdom;
254
+
255
+ const JSDOMOptions = {pretendToBeVisual: true, resources: 'usable', runScripts: 'dangerously'};
256
+
257
+ var LastResponse = null;
258
+ var LastResult = null;
259
+ var LastErr = null;
260
+ var LastExecutionFinished = false;
261
+ var LastHandleId = 0;
262
+
263
+ var AllDomHandles = {};
264
+ var AllElementHandles = {};
265
+ var AllConsoleHandles = {};
266
+ var ConsoleMessages = {};
267
+
268
+ var ModalText = null;
269
+ var ModalTextMatched = false;
270
+
271
+ const GetLastResult = function() {
272
+ if (LastExecutionFinished === true) {
273
+ var err = LastErr;
274
+ var res = LastResult;
275
+
276
+ LastErr = null;
277
+ LastRes = null;
278
+ LastExecutionFinished = false;
279
+
280
+ if (err) { return [null, err.message]; }
281
+ else { return [res, null]; }
282
+
283
+ } else {
284
+ return [null, (new Error('Last command did not yet finish execution!')).message];
285
+ }
286
+ };
287
+
288
+ const DialogAcceptHandler = async (dialog) => {
289
+ var msg = dialog.message()
290
+ ModalTextMatched = (ModalText === msg);
291
+ ModalText = msg;
292
+ await dialog.accept();
293
+ }
294
+
295
+ const DialogDismissHandler = async (dialog) => {
296
+ var msg = dialog.message()
297
+ ModalTextMatched = (ModalText === msg);
298
+ ModalText = msg;
299
+ await dialog.dismiss();
300
+ }
301
+
302
+ const RegisterElementHandle = function(element_handle) {
303
+ var entries = Object.entries(AllElementHandles);
304
+ for(var i = 0; i < entries.length; i++) {
305
+ if (entries[i][1] === element_handle) { return entries[i][0]; }
306
+ }
307
+ LastHandleId++;
308
+ AllElementHandles[LastHandleId] = element_handle;
309
+ return LastHandleId;
310
+ };
311
+
312
+ const RegisterElementHandleArray = function(element_handle_array) {
313
+ var registered_handles = [];
314
+ element_handle_array.forEach(function(handle){
315
+ registered_handles.push(RegisterElementHandle(handle));
316
+ });
317
+ return registered_handles;
318
+ };
319
+
320
+ const RegisterCon = function(con) {
321
+ var entries = Object.entries(ConsoleMessages);
322
+ for(var i = 0; i < entries.length; i++) {
323
+ if (entries[i][1] === con) { return entries[i][0]; }
324
+ }
325
+ LastHandleId++;
326
+ AllConsoleHandles[LastHandleId] = con;
327
+ ConsoleMessages[LastHandleId] = [];
328
+ return LastHandleId;
329
+ };
330
+ const RegisterDom = function(dom, handle_id) {
331
+ var entries = Object.entries(AllDomHandles);
332
+ for(var i = 0; i < entries.length; i++) {
333
+ if (entries[i][1] === dom) { return entries[i][0]; }
334
+ }
335
+ AllDomHandles[handle_id] = dom;
336
+ return handle_id;
337
+ };
338
+
339
+ (async () => {
340
+ try {
341
+ var con = new jsdom.VirtualConsole();
342
+ var jar = new jsdom.CookieJar(new MemoryCookieStore(), {rejectPublicSuffixes: false, looseMode: true});
343
+ var handle_id = RegisterCon(con);
344
+ con.on('error', (msg) => { ConsoleMessages[handle_id].push({level: 'error', location: '', text: msg}); });
345
+ con.on('warn', (msg) => { ConsoleMessages[handle_id].push({level: 'warn', location: '', text: msg}); });
346
+ con.on('info', (msg) => { ConsoleMessages[handle_id].push({level: 'info', location: '', text: msg}); });
347
+ con.on('log', (msg) => { ConsoleMessages[handle_id].push({level: 'dir', location: '', text: msg}); });
348
+ con.on('debug', (msg) => { ConsoleMessages[handle_id].push({level: 'dir', location: '', text: msg}); });
349
+ var dom = new JSDOM('', Object.assign({}, JSDOMOptions, { virtualConsole: con }));
350
+ var browser = dom.window.navigator.userAgent;
351
+ LastResult = [RegisterDom(dom, handle_id), browser];
352
+ LastExecutionFinished = true;
353
+ } catch (err) {
354
+ LastErr = err;
355
+ LastExecutionFinished = true;
356
+ }
357
+ })();
358
+ JAVASCRIPT
359
+ end
360
+
361
+ def timed_out?(start_time)
362
+ if (Time.now - start_time) > @timeout
363
+ raise "Command Execution timed out!"
364
+ end
365
+ false
366
+ end
367
+ end
368
+ end
369
+ end
370
+ end
@@ -0,0 +1,908 @@
1
+ module Isomorfeus
2
+ module Puppetmaster
3
+ module Driver
4
+ module JsdomDocument
5
+ def document_accept_alert(document, **options, &block)
6
+ raise Isomorfeus::Puppetmaster::NotSupported
7
+ # TODO maybe wrap in mutex
8
+ # text = options.has_key?(:text) ? "`#{options[:text]}`" : 'null'
9
+ # @context.exec <<~JAVASCRIPT
10
+ # ModalText = #{text};
11
+ # AllDomHandles[#{document.handle}].on('dialog', DialogAcceptHandler);
12
+ # JAVASCRIPT
13
+ # block.call
14
+ # sleep @reaction_timeout
15
+ # @context.eval 'ModalText'
16
+ # ensure
17
+ # matched = await <<~JAVASCRIPT
18
+ # LastResult = ModalTextMatched;
19
+ # ModalTextMatched = false;
20
+ # ModalText = null;
21
+ # AllDomHandles[#{document.handle}].removeListener('dialog', DialogAcceptHandler);
22
+ # JAVASCRIPT
23
+ # raise Isomorfeus::Puppetmaster::NoModalError if options.has_key?(:text) && !matched
24
+ end
25
+
26
+ def document_accept_confirm(document, **options, &block)
27
+ raise Isomorfeus::Puppetmaster::NotSupported
28
+ # TODO maybe wrap in mutex
29
+ # text = options.has_key?(:text) ? "`#{options[:text]}`" : 'null'
30
+ # @context.exec <<~JAVASCRIPT
31
+ # ModalText = #{text};
32
+ # AllDomHandles[#{document.handle}].on('dialog', DialogAcceptHandler);
33
+ # JAVASCRIPT
34
+ # block.call
35
+ # sleep @reaction_timeout
36
+ # @context.eval 'ModalText'
37
+ # ensure
38
+ # matched = await <<~JAVASCRIPT
39
+ # LastResult = ModalTextMatched;
40
+ # ModalTextMatched = false;
41
+ # ModalText = null;
42
+ # AllDomHandles[#{document.handle}].removeListener('dialog', DialogAcceptHandler);
43
+ # JAVASCRIPT
44
+ # raise Isomorfeus::Puppetmaster::NoModalError if options.has_key?(:text) && !matched
45
+ end
46
+
47
+ def document_accept_leave_page(document, **options, &block)
48
+ raise Isomorfeus::Puppetmaster::NotSupported
49
+ # TODO maybe wrap in mutex
50
+ # text = options.has_key?(:text) ? "`#{options[:text]}`" : 'null'
51
+ # @context.exec <<~JAVASCRIPT
52
+ # ModalText = #{text};
53
+ # AllDomHandles[#{document.handle}].on('dialog', DialogAcceptHandler);
54
+ # JAVASCRIPT
55
+ # block.call
56
+ # sleep @reaction_timeout
57
+ # @context.eval 'ModalText'
58
+ # ensure
59
+ # matched = await <<~JAVASCRIPT
60
+ # LastResult = ModalTextMatched;
61
+ # ModalTextMatched = false;
62
+ # ModalText = null;
63
+ # AllDomHandles[#{document.handle}].removeListener('dialog', DialogAcceptHandler);
64
+ # JAVASCRIPT
65
+ # raise Isomorfeus::Puppetmaster::NoModalError if options.has_key?(:text) && !matched
66
+ end
67
+
68
+ def document_accept_prompt(document, **options, &block)
69
+ raise Isomorfeus::Puppetmaster::NotSupported
70
+ # # TODO maybe wrap in mutex
71
+ # text = options.has_key?(:text) ? "`#{options[:text]}`" : 'null'
72
+ # @context.exec <<~JAVASCRIPT
73
+ # ModalText = #{text};
74
+ # AllDomHandles[#{document.handle}].on('dialog', DialogAcceptHandler);
75
+ # JAVASCRIPT
76
+ # block.call
77
+ # sleep @reaction_timeout
78
+ # @context.eval 'ModalText'
79
+ # ensure
80
+ # matched = await <<~JAVASCRIPT
81
+ # LastResult = ModalTextMatched;
82
+ # ModalTextMatched = false;
83
+ # ModalText = null;
84
+ # AllDomHandles[#{document.handle}].removeListener('dialog', DialogAcceptHandler);
85
+ # JAVASCRIPT
86
+ # raise Isomorfeus::Puppetmaster::NoModalError if options.has_key?(:text) && !matched
87
+ end
88
+
89
+ def document_all_text(document)
90
+ @context.eval "AllDomHandles[#{document.handle}].window.document.documentElement.textContent"
91
+ end
92
+
93
+ def document_body(document)
94
+ node_data = @context.exec <<~JAVASCRIPT
95
+ var node = AllDomHandles[#{document.handle}].window.document.body;
96
+ var node_handle = RegisterElementHandle(node);
97
+ var tag = node.tagName.toLowerCase();
98
+ return {handle: node_handle, tag: tag, type: null, content_editable: node.isContentEditable};
99
+ JAVASCRIPT
100
+ if node_data
101
+ node_data[:css_selector] = 'body'
102
+ Isomorfeus::Puppetmaster::Node.new_by_tag(self, document, node_data)
103
+ end
104
+ end
105
+
106
+ def document_bring_to_front(_document); end
107
+
108
+ def document_clear_authentication_credentials(document)
109
+ raise Isomorfeus::Puppetmaster::NotSupported
110
+ end
111
+
112
+ def document_clear_cookies(document)
113
+ @context.exec "AllDomHandles[#{document.handle}].cookieJar.removeAllCookiesSync()"
114
+ end
115
+
116
+ def document_clear_extra_headers(document)
117
+ raise Isomorfeus::Puppetmaster::NotSupported
118
+ end
119
+
120
+ def document_clear_url_blacklist(document)
121
+ raise Isomorfeus::Puppetmaster::NotSupported
122
+ end
123
+
124
+ def document_click(document, x: nil, y: nil, modifiers: nil)
125
+ # modifier_keys: :alt, :control, :meta, :shift
126
+ # raise Isomorfeus::Pupppetmaster::InvalidActionError.new(:click) unless visible?
127
+ modifiers = [modifiers] if modifiers.is_a?(Symbol)
128
+ modifiers = [] unless modifiers
129
+ modifiers = modifiers.map {|key| key.to_s.camelize(:lower) }
130
+ @context.exec <<~JAVASCRIPT
131
+ var options = {button: 0, bubbles: true, cancelable: true};
132
+ var window = AllDomHandles[#{document.handle}].window;
133
+ var modifiers = #{modifiers};
134
+ if (modifiers.includes('meta')) { options['metaKey'] = true; }
135
+ if (modifiers.includes('control')) { options['ctrlKey'] = true; }
136
+ if (modifiers.includes('shift')) { options['shiftKey'] = true; }
137
+ if (modifiers.includes('alt')) { options['altKey'] = true; }
138
+ var x = #{x ? x : 'null'};
139
+ var y = #{y ? y : 'null'};
140
+ if (x && y) {
141
+ options['clientX'] = x;
142
+ options['clientY'] = y;
143
+ }
144
+ window.document.dispatchEvent(new window.MouseEvent('mousedown', options));
145
+ window.document.dispatchEvent(new window.MouseEvent('mouseup', options));
146
+ window.document.dispatchEvent(new window.MouseEvent('click', options));
147
+ JAVASCRIPT
148
+ end
149
+
150
+ def document_close(document)
151
+ await <<~JAVASCRIPT
152
+ delete AllDomHandles[#{document.handle}];
153
+ delete AllConsoleHandles[#{document.handle}];
154
+ delete ConsoleMessages[#{document.handle}];
155
+ JAVASCRIPT
156
+ end
157
+
158
+ def document_console(document)
159
+ messages = @context.eval "ConsoleMessages[#{document.handle}]"
160
+ messages.map {|m| Isomorfeus::Puppetmaster::ConsoleMessage.new(m)}
161
+ end
162
+
163
+ def document_cookies(document)
164
+ uri = document_url(document)
165
+ if uri == 'about:blank'
166
+ uri = if Isomorfeus::Puppetmaster.server_host
167
+ u = URI.new
168
+ u.scheme = Isomorfeus::Puppetmaster.server_scheme if Isomorfeus::Puppetmaster.server_scheme
169
+ u.host = Isomorfeus::Puppetmaster.server_host
170
+ u.to_s
171
+ else
172
+ 'http://127.0.0.1'
173
+ end
174
+ end
175
+ result = @context.eval "AllDomHandles[#{document.handle}].cookieJar.getCookiesSync('#{uri.to_s}')"
176
+ result_hash = {}
177
+ result.each do |cookie|
178
+ cookie['name'] = cookie['key']
179
+ cookie['expires'] = DateTime.parse(cookie['expires']).to_time if cookie.has_key?('expires')
180
+ result_hash[cookie['name']] = Isomorfeus::Puppetmaster::Cookie.new(cookie)
181
+ end
182
+ result_hash
183
+ end
184
+
185
+ def document_dismiss_confirm(document, **options, &block)
186
+ # TODO
187
+ text = options.has_key?(:text) ? "`#{options[:text]}`" : 'null'
188
+ @context.exec <<~JAVASCRIPT
189
+ ModalText = #{text};
190
+ AllDomHandles[#{document.handle}].on('dialog', DialogDismissHandler);
191
+ JAVASCRIPT
192
+ block.call
193
+ sleep @reaction_timeout
194
+ @context.eval 'ModalText'
195
+ ensure
196
+ matched = await <<~JAVASCRIPT
197
+ LastResult = ModalTextMatched;
198
+ ModalTextMatched = false;
199
+ ModalText = null;
200
+ AllDomHandles[#{document.handle}].removeListener('dialog', DialogDismissHandler);
201
+ JAVASCRIPT
202
+ raise Isomorfeus::Puppetmaster::ModalNotFound if options.has_key?(:text) && !matched
203
+ end
204
+
205
+ def document_dismiss_leave_page(document, **options, &block)
206
+ # TODO
207
+ text = options.has_key?(:text) ? "`#{options[:text]}`" : 'null'
208
+ @context.exec <<~JAVASCRIPT
209
+ ModalText = #{text};
210
+ AllDomHandles[#{document.handle}].on('dialog', DialogDismissHandler);
211
+ JAVASCRIPT
212
+ block.call
213
+ sleep @reaction_timeout
214
+ @context.eval 'ModalText'
215
+ ensure
216
+ matched = await <<~JAVASCRIPT
217
+ LastResult = ModalTextMatched;
218
+ ModalTextMatched = false;
219
+ ModalText = null;
220
+ AllDomHandles[#{document.handle}].removeListener('dialog', DialogDismissHandler);
221
+ JAVASCRIPT
222
+ raise Isomorfeus::Puppetmaster::ModalNotFound if options.has_key?(:text) && !matched
223
+ end
224
+
225
+ def document_dismiss_prompt(document, **options, &block)
226
+ # TODO
227
+ text = options.has_key?(:text) ? "`#{options[:text]}`" : 'null'
228
+ @context.exec <<~JAVASCRIPT
229
+ ModalText = #{text};
230
+ AllDomHandles[#{document.handle}].on('dialog', DialogDismissHandler);
231
+ JAVASCRIPT
232
+ block.call
233
+ sleep @reaction_timeout
234
+ @context.eval 'ModalText'
235
+ ensure
236
+ matched = await <<~JAVASCRIPT
237
+ LastResult = ModalTextMatched;
238
+ ModalTextMatched = false;
239
+ ModalText = null;
240
+ AllDomHandles[#{document.handle}].removeListener('dialog', DialogDismissHandler);
241
+ JAVASCRIPT
242
+ raise Isomorfeus::Puppetmaster::ModalNotFound if options.has_key?(:text) && !matched
243
+ end
244
+
245
+ def document_dispatch_event(document, name, event_type = nil, **options)
246
+ raise ArgumentError, 'Unknown event' unless EVENTS.key?(name.to_sym) || event_type
247
+ event_type, opts = *EVENTS[name.to_s.downcase.tr('_', '').to_sym] if event_type.nil?
248
+ opts.merge!(options)
249
+ final_options = options.map { |k,v| "#{k}: '#{v}'" }
250
+ @context.exec <<~JAVASCRIPT
251
+ var window = AllDomHandles[#{document.handle}].window;
252
+ var event = new window.#{event_type}('#{name}', { #{final_options.join(', ')} });
253
+ window.document.dispatchEvent(event);
254
+ JAVASCRIPT
255
+ end
256
+
257
+ def document_double_click(document, x: nil, y: nil, modifiers: nil)
258
+ # modifier_keys: :alt, :control, :meta, :shift
259
+ modifiers = [modifiers] if modifiers.is_a?(Symbol)
260
+ modifiers = [] unless modifiers
261
+ modifiers = modifiers.map {|key| key.to_s.camelize(:lower) }
262
+ await <<~JAVASCRIPT
263
+ var options = {button: 0, bubbles: true, cancelable: true};
264
+ var window = AllDomHandles[#{document.handle}].window;
265
+ var modifiers = #{modifiers};
266
+ if (modifiers.includes('meta')) { options['metaKey'] = true; }
267
+ if (modifiers.includes('control')) { options['ctrlKey'] = true; }
268
+ if (modifiers.includes('shift')) { options['shiftKey'] = true; }
269
+ if (modifiers.includes('alt')) { options['altKey'] = true; }
270
+ var x = #{x ? x : 'null'};
271
+ var y = #{y ? y : 'null'};
272
+ if (x && y) {
273
+ options['clientX'] = x;
274
+ options['clientY'] = y;
275
+ }
276
+ window.document.dispatchEvent(new window.MouseEvent('mousedown', options));
277
+ window.document.dispatchEvent(new window.MouseEvent('mouseup', options));
278
+ window.document.dispatchEvent(new window.MouseEvent('dblclick', options));
279
+ JAVASCRIPT
280
+ end
281
+
282
+ def document_evaluate_script(document, script, *args)
283
+ @context.eval <<~JAVASCRIPT
284
+ AllDomHandles[#{document.handle}].window.eval(
285
+ `var arguments = #{args};
286
+ #{script}`
287
+ )
288
+ JAVASCRIPT
289
+ rescue ExecJS::ProgramError => e
290
+ raise determine_error(e.message)
291
+ end
292
+
293
+ def document_execute_script(document, script, *args)
294
+ @context.eval <<~JAVASCRIPT
295
+ AllDomHandles[#{document.handle}].window.eval(
296
+ `(function() { var arguments = #{args};
297
+ #{script}
298
+ })()`
299
+ )
300
+ JAVASCRIPT
301
+ rescue ExecJS::ProgramError => e
302
+ raise determine_error(e.message)
303
+ end
304
+
305
+ def document_find(document, selector)
306
+ js_escaped_selector = selector.gsub('\\', '\\\\\\').gsub('"', '\"')
307
+ node_data = @context.exec <<~JAVASCRIPT
308
+ var node = AllDomHandles[#{document.handle}].window.document.querySelector("#{js_escaped_selector}");
309
+ if (node) {
310
+ var node_handle = RegisterElementHandle(node);
311
+ var tag = node.tagName.toLowerCase();
312
+ var type = null;
313
+ if (tag === 'input') { type = node.getAttribute('type'); }
314
+ return {handle: node_handle, tag: tag, type: type, content_editable: node.isContentEditable};
315
+ }
316
+ JAVASCRIPT
317
+ if node_data
318
+ node_data[:css_selector] = selector
319
+ Isomorfeus::Puppetmaster::Node.new_by_tag(self, document, node_data)
320
+ else
321
+ raise Isomorfeus::Puppetmaster::ElementNotFound.new(selector)
322
+ end
323
+ rescue Exception => e
324
+ raise determine_error(e.message)
325
+ end
326
+
327
+ def document_find_all(document, selector)
328
+ js_escaped_selector = selector.gsub('\\', '\\\\\\').gsub('"', '\"')
329
+ node_data_array = @context.exec <<~JAVASCRIPT
330
+ var node_array = AllDomHandles[#{document.handle}].window.document.querySelectorAll("#{js_escaped_selector}");
331
+ var node_data_array = [];
332
+ if (node_array) {
333
+ for (var i=0; i<node_array.length; i++) {
334
+ var node_handle = RegisterElementHandle(node_array[i]);
335
+ var tag = node.tagName.toLowerCase();
336
+ var type = null;
337
+ if (tag === 'input') { type = node.getAttribute('type'); }
338
+ node_data_array.push({handle: node_handle, tag: tag, type: type, content_editable: node.isContentEditable});
339
+ }
340
+ }
341
+ return node_data_array;
342
+ JAVASCRIPT
343
+ node_data_array.map do |node_data|
344
+ node_data[:css_selector] = selector
345
+ Isomorfeus::Puppetmaster::Node.new_by_tag(self, document, node_data)
346
+ end
347
+ end
348
+
349
+ def document_find_all_xpath(document, query)
350
+ js_escaped_query = query.gsub('\\', '\\\\\\').gsub('"', '\"')
351
+ node_data_array = @context.exec <<~JAVASCRIPT
352
+ var window = AllDomHandles[#{document.handle}].window;
353
+ var document = window.document;
354
+ var xpath_result = document.evaluate("#{js_escaped_query}", document, null, window.XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
355
+ var node;
356
+ var node_data_array = [];
357
+ while (node = xpath_result.iterateNext) {
358
+ var node_handle = RegisterElementHandle(node);
359
+ var tag = node.tagName.toLowerCase();
360
+ var type = null;
361
+ if (tag === 'input') { type = node.getAttribute('type'); }
362
+ node_data_array.push({handle: node_handle, tag: tag, type: type, content_editable: node.isContentEditable});
363
+ }
364
+ return node_data_array;
365
+ JAVASCRIPT
366
+ node_data_array.map do |node_data|
367
+ node_data[:xpath_query] = query
368
+ Isomorfeus::Puppetmaster::Node.new_by_tag(self, document, node_data)
369
+ end
370
+ end
371
+
372
+ def document_find_xpath(document, query)
373
+ js_escaped_query = query.gsub('\\', '\\\\\\').gsub('"', '\"')
374
+ node_data = @context.exec <<~JAVASCRIPT
375
+ var window = AllDomHandles[#{document.handle}].window;
376
+ var document = window.document;
377
+ var xpath_result = document.evaluate("#{js_escaped_query}", document, null, window.XPathResult.FIRST_ORDERED_NODE_TYPE, null);
378
+ var node = xpath_result.singleNodeValue;
379
+ if (node) {
380
+ var node_handle = RegisterElementHandle(node);
381
+ var tag = node.tagName.toLowerCase();
382
+ var type = null;
383
+ if (tag === 'input') { type = node.getAttribute('type'); }
384
+ return {handle: node_handle, tag: tag, type: type, content_editable: node.isContentEditable};
385
+ }
386
+ JAVASCRIPT
387
+ if node_data
388
+ node_data[:xpath_query] = query
389
+ Isomorfeus::Puppetmaster::Node.new_by_tag(self, document, node_data)
390
+ else
391
+ raise Isomorfeus::Puppetmaster::ElementNotFound.new(query)
392
+ end
393
+ rescue ExecJS::ProgramError => e
394
+ raise determine_error('invalid xpath query')
395
+ end
396
+
397
+ def document_go_back(document)
398
+ raise 'Browser history not supported.'
399
+ @context.eval "AllDomHandles[#{document.handle}].window.history.back()"
400
+ end
401
+
402
+ def document_go_forward(document)
403
+ raise 'Browser history not supported.'
404
+ @context.eval "AllDomHandles[#{document.handle}].window.history.forward()"
405
+ end
406
+
407
+ def document_goto(document, uri)
408
+ parsed_uri = URI.parse(uri)
409
+ parsed_uri.host = @app.host unless parsed_uri.host
410
+ parsed_uri.port = @app.port unless parsed_uri.port
411
+ parsed_uri.scheme = @app.scheme unless parsed_uri.scheme
412
+ response_hash, messages = await <<~JAVASCRIPT
413
+ ConsoleMessages[#{document.handle}] = [];
414
+ var cookie_jar = AllDomHandles[#{document.handle}].cookieJar.cloneSync(new MemoryCookieStore());
415
+ cookie_jar.rejectPublicSuffixes = false;
416
+ var con = new jsdom.VirtualConsole()
417
+ con.on('error', (msg) => { ConsoleMessages[#{document.handle}].push({level: 'error', location: '', text: msg}); });
418
+ con.on('warn', (msg) => { ConsoleMessages[#{document.handle}].push({level: 'warn', location: '', text: msg}); });
419
+ con.on('info', (msg) => { ConsoleMessages[#{document.handle}].push({level: 'info', location: '', text: msg}); });
420
+ con.on('log', (msg) => { ConsoleMessages[#{document.handle}].push({level: 'dir', location: '', text: msg}); });
421
+ con.on('debug', (msg) => { ConsoleMessages[#{document.handle}].push({level: 'dir', location: '', text: msg}); });
422
+ AllConsoleHandles[#{document.handle}] = con;
423
+ try {
424
+ var new_dom = await JSDOM.fromURL('#{parsed_uri.to_s}', Object.assign({}, JSDOMOptions, { cookieJar: cookie_jar, virtualConsole: con }));
425
+ AllDomHandles[#{document.handle}] = new_dom;
426
+ var formatted_response = {
427
+ headers: {},
428
+ ok: true,
429
+ remote_address: '#{parsed_uri.to_s}',
430
+ request: {},
431
+ status: 200,
432
+ status_text: '',
433
+ text: '',
434
+ url: '#{parsed_uri.to_s}'
435
+ };
436
+ LastResult = [formatted_response, ConsoleMessages[#{document.handle}]];
437
+ } catch (err) {
438
+ var formatted_error_response = {
439
+ headers: err.options.headers,
440
+ ok: false,
441
+ remote_address: '#{parsed_uri.to_s}',
442
+ request: {},
443
+ status: err.statusCode ? err.statusCode : 500,
444
+ status_text: err.response ? err.response.statusMessage : '',
445
+ text: err.message ? err.message : '',
446
+ url: '#{parsed_uri.to_s}'
447
+ };
448
+ LastResult = [formatted_error_response, ConsoleMessages[#{document.handle}]];
449
+ }
450
+ JAVASCRIPT
451
+ con_messages = messages.map {|m| Isomorfeus::Puppetmaster::ConsoleMessage.new(m)}
452
+ # STDERR.puts 'M', con_messages
453
+ # STDERR.puts 'R', response_hash
454
+ con_messages.each { |m| raise determine_error(m.text) if m.level == 'error' && !m.text.start_with?('Failed to load resource:') }
455
+ if response_hash
456
+ response = Isomorfeus::Puppetmaster::Response.new(response_hash)
457
+ if response.status == 500 && response.text.start_with?('Error:')
458
+ error = determine_error(response.text)
459
+ raise error if error
460
+ end
461
+ document.instance_variable_set(:@response, response)
462
+ end
463
+ document.response
464
+ end
465
+
466
+ def document_head(document)
467
+ node_data = @context.exec <<~JAVASCRIPT
468
+ var node = AllDomHandles[#{document.handle}].window.document.head;
469
+ var node_handle = RegisterElementHandle(node);
470
+ var tag = node.tagName.toLowerCase();
471
+ return {handle: node_handle, tag: tag, type: null, content_editable: node.isContentEditable};
472
+ JAVASCRIPT
473
+ if node_data
474
+ node_data[:css_selector] = 'body'
475
+ Isomorfeus::Puppetmaster::Node.new_by_tag(self, document, node_data)
476
+ end
477
+ end
478
+
479
+ def document_html(document)
480
+ @context.eval "AllDomHandles[#{document.handle}].serialize()"
481
+ end
482
+
483
+ def document_open_new_document(_document, uri = nil)
484
+ if !uri || uri == 'about:blank'
485
+ parsed_uri = 'about:blank'
486
+ else
487
+ parsed_uri = URI.parse(uri)
488
+ parsed_uri.host = @app.host unless parsed_uri.host
489
+ parsed_uri.port = @app.port unless parsed_uri.port
490
+ parsed_uri.scheme = @app.scheme unless parsed_uri.scheme
491
+ end
492
+ handle, response_hash, messages = await <<~JAVASCRIPT
493
+ var con = new jsdom.VirtualConsole();
494
+ var jar = new jsdom.CookieJar(new MemoryCookieStore(), {rejectPublicSuffixes: false, looseMode: true});
495
+ var handle_id = RegisterCon(con);
496
+ con.on('error', (msg) => { ConsoleMessages[handle_id].push({level: 'error', location: '', text: msg}); });
497
+ con.on('warn', (msg) => { ConsoleMessages[handle_id].push({level: 'warn', location: '', text: msg}); });
498
+ con.on('info', (msg) => { ConsoleMessages[handle_id].push({level: 'info', location: '', text: msg}); });
499
+ con.on('log', (msg) => { ConsoleMessages[handle_id].push({level: 'dir', location: '', text: msg}); });
500
+ con.on('debug', (msg) => { ConsoleMessages[handle_id].push({level: 'dir', location: '', text: msg}); });
501
+ try {
502
+ var new_dom;
503
+ var uri = '#{parsed_uri.to_s}';
504
+ if (uri === 'about:blank') {
505
+ new_dom = new JSDOM('', Object.assign({}, JSDOMOptions, { cookieJar: jar, virtualConsole: con }));
506
+ } else {
507
+ new_dom = await JSDOM.fromURL(uri, Object.assign({}, JSDOMOptions, { cookieJar: jar, virtualConsole: con }));
508
+ }
509
+ AllDomHandles[handle_id] = new_dom;
510
+ var formatted_response = {
511
+ headers: {},
512
+ ok: false,
513
+ remote_address: '#{parsed_uri.to_s}',
514
+ request: {},
515
+ status: 200,
516
+ status_text: '',
517
+ text: '',
518
+ url: '#{parsed_uri.to_s}'
519
+ };
520
+ LastResult = [handle_id, formatted_response, ConsoleMessages[handle_id]];
521
+ } catch (err) {
522
+ var formatted_response = {
523
+ headers: err.options.headers,
524
+ ok: true,
525
+ remote_address: '#{parsed_uri.to_s}',
526
+ request: {},
527
+ status: err.statusCode,
528
+ status_text: err.response ? err.response.statusMessage : '',
529
+ text: '',
530
+ url: '#{parsed_uri.to_s}'
531
+ };
532
+ LastResult = [handle_id, formatted_response, ConsoleMessages[handle_id]];
533
+ }
534
+ JAVASCRIPT
535
+ # STDERR.puts 'R', response_hash
536
+ # STDERR.puts 'C', messages
537
+ con_messages = messages.map {|m| Isomorfeus::Puppetmaster::ConsoleMessage.new(m)}
538
+ con_messages.each { |m| raise determine_error(m.text) if m.level == 'error' && !m.text.start_with?('Failed to load resource:') }
539
+ response = Isomorfeus::Puppetmaster::Response.new(response_hash)
540
+ if response.status == 500 && response.text.start_with?('Error:')
541
+ error = determine_error(response.text)
542
+ raise error if error
543
+ end
544
+ Isomorfeus::Puppetmaster::Document.new(self, handle, response)
545
+ end
546
+
547
+ def document_reload(document)
548
+ document_goto(document, document_url(document))
549
+ end
550
+
551
+ def document_remove_cookie(document, name)
552
+ uri = document_url(document)
553
+ if uri == 'about:blank'
554
+ uri = if Isomorfeus::Puppetmaster.server_host
555
+ u = URI.new
556
+ u.scheme = Isomorfeus::Puppetmaster.server_scheme if Isomorfeus::Puppetmaster.server_scheme
557
+ u.host = Isomorfeus::Puppetmaster.server_host
558
+ u.to_s
559
+ else
560
+ 'http://127.0.0.1'
561
+ end
562
+ end
563
+ domain = URI.parse(uri).host
564
+ await <<~JAVASCRIPT
565
+ var cookies = AllDomHandles[#{document.handle}].cookieJar.getCookiesSync('#{uri.to_s}')
566
+ var path = '/';
567
+ for(i=0; i<cookies.length; i++) {
568
+ if (cookies[i].key === '#{name}' && cookies[i].domain === '#{domain}') {
569
+ var path = cookies[i].path;
570
+ break;
571
+ }
572
+ }
573
+ var promise = new Promise(function(resolve, reject) {
574
+ AllDomHandles[#{document.handle}].cookieJar.store.removeCookie('#{domain}', path, '#{name}', function(err){ resolve(true); });
575
+ })
576
+ await promise;
577
+ JAVASCRIPT
578
+ end
579
+
580
+ def document_render_base64(document, **options)
581
+ raise Isomorfeus::Puppetmaster::NotSupported
582
+ end
583
+
584
+ def document_reset_user_agent(document)
585
+ raise Isomorfeus::Puppetmaster::NotSupported
586
+ end
587
+
588
+ def document_right_click(document, x: nil, y: nil, modifiers: nil)
589
+ # modifier_keys: :alt, :control, :meta, :shift
590
+ modifiers = [modifiers] if modifiers.is_a?(Symbol)
591
+ modifiers = [] unless modifiers
592
+ modifiers = modifiers.map {|key| key.to_s.camelize(:lower) }
593
+ await <<~JAVASCRIPT
594
+ var options = {button: 2, bubbles: true, cancelable: true};
595
+ var window = AllDomHandles[#{document.handle}].window;
596
+ var modifiers = #{modifiers};
597
+ if (modifiers.includes('meta')) { options['metaKey'] = true; }
598
+ if (modifiers.includes('control')) { options['ctrlKey'] = true; }
599
+ if (modifiers.includes('shift')) { options['shiftKey'] = true; }
600
+ if (modifiers.includes('alt')) { options['altKey'] = true; }
601
+ var x = #{x ? x : 'null'};
602
+ var y = #{y ? y : 'null'};
603
+ if (x && y) {
604
+ options['clientX'] = x;
605
+ options['clientY'] = y;
606
+ }
607
+ window.document.dispatchEvent(new window.MouseEvent('mousedown', options));
608
+ window.document.dispatchEvent(new window.MouseEvent('mouseup', options));
609
+ window.document.dispatchEvent(new window.MouseEvent('contextmenu', options));
610
+ JAVASCRIPT
611
+ end
612
+
613
+ def document_save_pdf(document, **options)
614
+ raise Isomorfeus::Puppetmaster::NotSupported
615
+ end
616
+
617
+ def document_save_screenshot(document, file, **options)
618
+ raise Isomorfeus::Puppetmaster::NotSupported
619
+ end
620
+
621
+ def document_scroll_by(document, x, y)
622
+ @context.exec <<~JAVASCRIPT
623
+ AllDomHandles[#{document.handle}].window.scrollX = AllDomHandles[#{document.handle}].window.scrollX + #{x};
624
+ AllDomHandles[#{document.handle}].window.scrollY = AllDomHandles[#{document.handle}].window.scrollY + #{y};
625
+ JAVASCRIPT
626
+ end
627
+
628
+ def document_scroll_to(document, x, y)
629
+ @context.exec <<~JAVASCRIPT
630
+ AllDomHandles[#{document.handle}].window.scrollX = #{x};
631
+ AllDomHandles[#{document.handle}].window.scrollY = #{y};
632
+ JAVASCRIPT
633
+ end
634
+
635
+ def document_set_authentication_credentials(document, username, password)
636
+ raise Isomorfeus::Puppetmaster::NotSupported
637
+ end
638
+
639
+ def document_set_cookie(document, name, value, **options)
640
+ options[:key] ||= name
641
+ options[:value] ||= value
642
+ uri = document_url(document)
643
+ if uri == 'about:blank'
644
+ uri = if Isomorfeus::Puppetmaster.server_host
645
+ u = URI.new
646
+ u.scheme = Isomorfeus::Puppetmaster.server_scheme if Isomorfeus::Puppetmaster.server_scheme
647
+ u.host = Isomorfeus::Puppetmaster.server_host
648
+ u.to_s
649
+ else
650
+ 'http://127.0.0.1'
651
+ end
652
+ end
653
+ options[:domain] ||= URI.parse(uri).host
654
+ final_options = []
655
+ final_options << "expires: new Date('#{options.delete(:expires).to_s}')" if options.has_key?(:expires)
656
+ final_options << "httpOnly: #{options.delete(:http_only)}" if options.has_key?(:http_only)
657
+ final_options << "secure: #{options.delete(:secure)}" if options.has_key?(:secure)
658
+ final_options << "sameSite: '#{options.delete(:same_site)}'" if options.has_key?(:same_site)
659
+ options.each do |k,v|
660
+ final_options << "#{k}: '#{v}'"
661
+ end
662
+ @context.exec "AllDomHandles[#{document.handle}].cookieJar.setCookieSync(new Cookie({#{final_options.join(', ')}}), '#{uri.to_s}', {ignoreError: true})"
663
+ end
664
+
665
+ def document_set_extra_headers(document, headers_hash)
666
+ raise Isomorfeus::Puppetmaster::NotSupported
667
+ end
668
+
669
+ def document_set_user_agent(document, agent_string)
670
+ raise Isomorfeus::Puppetmaster::NotSupported
671
+ end
672
+
673
+ def document_title(document)
674
+ @context.eval "AllDomHandles[#{document.handle}].window.document.title"
675
+ end
676
+
677
+ def document_type_keys(document, *keys)
678
+ cjs = <<~JAVASCRIPT
679
+ var window = AllDomHandles[#{document.handle}].window;
680
+ var events = [];
681
+ var options = {bubbles: true, cancelable: true};
682
+ JAVASCRIPT
683
+ top_modifiers = []
684
+ keys.each do |key|
685
+ if key.is_a?(String)
686
+ key.each_char do |c|
687
+ shift = !! /[[:upper:]]/.match(c)
688
+ cjs << <<~JAVASCRIPT
689
+ events.push(new window.KeyboardEvent('keydown', { bubbles: true, cancelable: true, key: '#{c}'.charCodeAt(0), char: '#{c}',
690
+ altKey: #{need_alt?(top_modifiers)}, ctrlKey: #{need_control?(top_modifiers)}, metaKey: #{need_meta?(top_modifiers)},
691
+ shiftKey: #{shift || need_shift?(top_modifiers)}}));
692
+ JAVASCRIPT
693
+ cjs << <<~JAVASCRIPT
694
+ events.push(new window.KeyboardEvent('keypress', { bubbles: true, cancelable: true, key: '#{c}'.charCodeAt(0), char: '#{c}',
695
+ altKey: #{need_alt?(top_modifiers)}, ctrlKey: #{need_control?(top_modifiers)}, metaKey: #{need_meta?(top_modifiers)},
696
+ shiftKey: #{shift || need_shift?(top_modifiers)}}));
697
+ JAVASCRIPT
698
+ cjs << <<~JAVASCRIPT
699
+ events.push(new window.KeyboardEvent('keyup', { bubbles: true, cancelable: true, key: '#{c}'.charCodeAt(0), char: '#{c}',
700
+ altKey: #{need_alt?(top_modifiers)}, ctrlKey: #{need_control?(top_modifiers)}, metaKey: #{need_meta?(top_modifiers)},
701
+ shiftKey: #{shift || need_shift?(top_modifiers)}}));
702
+ JAVASCRIPT
703
+ end
704
+ elsif key.is_a?(Symbol)
705
+ if %i[ctrl Ctrl].include?(key)
706
+ key = :control
707
+ elsif %i[command Command Meta].include?(key)
708
+ key = :meta
709
+ elsif %i[divide Divide].include?(key)
710
+ key = :numpad_divide
711
+ elsif %i[decimal Decimal].include?(key)
712
+ key = :numpad_decimal
713
+ elsif %i[left right up down].include?(key)
714
+ key = "arrow_#{key}".to_sym
715
+ end
716
+ if %i[alt alt_left alt_right control control_left control_rigth meta meta_left meta_right shift shift_left shift_right].include?(key)
717
+ top_modifiers << key
718
+ cjs << <<~JAVASCRIPT
719
+ events.push(new window.KeyboardEvent('keydown', { bubbles: true, cancelable: true, key: '#{key.to_s.camelize}', code: '#{key.to_s.camelize}',
720
+ altKey: #{need_alt?(top_modifiers)}, ctrlKey: #{need_control?(top_modifiers)}, metaKey: #{need_meta?(top_modifiers)},
721
+ shiftKey: #{need_shift?(top_modifiers)}}));
722
+ JAVASCRIPT
723
+ else
724
+ cjs << <<~JAVASCRIPT
725
+ events.push(new window.KeyboardEvent('keydown', { bubbles: true, cancelable: true, key: '#{key.to_s.camelize}', code: '#{key.to_s.camelize}',
726
+ altKey: #{need_alt?(top_modifiers)}, ctrlKey: #{need_control?(top_modifiers)}, metaKey: #{need_meta?(top_modifiers)},
727
+ shiftKey: #{need_shift?(top_modifiers)}}));
728
+ JAVASCRIPT
729
+ cjs << <<~JAVASCRIPT
730
+ events.push(new window.KeyboardEvent('keypress', { bubbles: true, cancelable: true, key: '#{key.to_s.camelize}', code: '#{key.to_s.camelize}',
731
+ altKey: #{need_alt?(top_modifiers)}, ctrlKey: #{need_control?(top_modifiers)}, metaKey: #{need_meta?(top_modifiers)},
732
+ shiftKey: #{need_shift?(top_modifiers)}}));
733
+ JAVASCRIPT
734
+ cjs << <<~JAVASCRIPT
735
+ events.push(new window.KeyboardEvent('keyup', { bubbles: true, cancelable: true, key: '#{key.to_s.camelize}', code: '#{key.to_s.camelize}',
736
+ altKey: #{need_alt?(top_modifiers)}, ctrlKey: #{need_control?(top_modifiers)}, metaKey: #{need_meta?(top_modifiers)},
737
+ shiftKey: #{need_shift?(top_modifiers)}}));
738
+ JAVASCRIPT
739
+ end
740
+ elsif key.is_a?(Array)
741
+ modifiers = []
742
+ key.each do |k|
743
+ if k.is_a?(Symbol)
744
+ if %i[ctrl Ctrl].include?(k)
745
+ k = :control
746
+ elsif %i[command Command Meta].include?(k)
747
+ k = :meta
748
+ elsif %i[divide Divide].include?(k)
749
+ k = :numpad_divide
750
+ elsif %i[decimal Decimal].include?(k)
751
+ k = :numpad_decimal
752
+ elsif %i[left right up down].include?(key)
753
+ k = "arrow_#{key}".to_sym
754
+ end
755
+ if %i[alt alt_left alt_right control control_left control_rigth meta meta_left meta_right shift shift_left shift_right].include?(k)
756
+ modifiers << k
757
+ cjs << <<~JAVASCRIPT
758
+ events.push(new window.KeyboardEvent('keydown', { bubbles: true, cancelable: true, key: '#{k.to_s.camelize}', code: '#{k.to_s.camelize}',
759
+ altKey: #{need_alt?(modifiers)}, ctrlKey: #{need_control?(modifiers)}, metaKey: #{need_meta?(modifiers)},
760
+ shiftKey: #{need_shift?(modifiers)}}));
761
+ JAVASCRIPT
762
+ else
763
+ cjs << <<~JAVASCRIPT
764
+ events.push(new window.KeyboardEvent('keydown', { bubbles: true, cancelable: true, key: '#{k.to_s.camelize}', code: '#{k.to_s.camelize}',
765
+ altKey: #{need_alt?(modifiers)}, ctrlKey: #{need_control?(modifiers)}, metaKey: #{need_meta?(modifiers)},
766
+ shiftKey: #{need_shift?(modifiers)}}));
767
+ JAVASCRIPT
768
+ cjs << <<~JAVASCRIPT
769
+ events.push(new window.KeyboardEvent('keypress', { bubbles: true, cancelable: true, key: '#{k.to_s.camelize}', code: '#{k.to_s.camelize}',
770
+ altKey: #{need_alt?(modifiers)}, ctrlKey: #{need_control?(modifiers)}, metaKey: #{need_meta?(modifiers)},
771
+ shiftKey: #{need_shift?(modifiers)}}));
772
+ JAVASCRIPT
773
+ cjs << <<~JAVASCRIPT
774
+ events.push(new window.KeyboardEvent('keyup', { bubbles: true, cancelable: true, key: '#{k.to_s.camelize}', code: '#{k.to_s.camelize}',
775
+ altKey: #{need_alt?(modifiers)}, ctrlKey: #{need_control?(modifiers)}, metaKey: #{need_meta?(modifiers)},
776
+ shiftKey: #{need_shift?(modifiers)}}));
777
+ JAVASCRIPT
778
+ end
779
+ elsif k.is_a?(String)
780
+ k.each_char do |c|
781
+ shift = !! /[[:upper:]]/.match(c)
782
+ cjs << <<~JAVASCRIPT
783
+ events.push(new window.KeyboardEvent('keydown', { bubbles: true, cancelable: true, key: '#{c}'.charCodeAt(0), char: '#{c}',
784
+ altKey: #{need_alt?(modifiers)}, ctrlKey: #{need_control?(modifiers)}, metaKey: #{need_meta?(modifiers)},
785
+ shiftKey: #{shift || need_shift?(modifiers)}}));
786
+ JAVASCRIPT
787
+ cjs << <<~JAVASCRIPT
788
+ events.push(new window.KeyboardEvent('keypress', { bubbles: true, cancelable: true, key: '#{c}'.charCodeAt(0), char: '#{c}',
789
+ altKey: #{need_alt?(modifiers)}, ctrlKey: #{need_control?(modifiers)}, metaKey: #{need_meta?(modifiers)},
790
+ shiftKey: #{shift || need_shift?(modifiers)}}));
791
+ JAVASCRIPT
792
+ cjs << <<~JAVASCRIPT
793
+ events.push(new window.KeyboardEvent('keyup', { bubbles: true, cancelable: true, key: '#{c}'.charCodeAt(0), char: '#{c}',
794
+ altKey: #{need_alt?(modifiers)}, ctrlKey: #{need_control?(modifiers)}, metaKey: #{need_meta?(modifiers)},
795
+ shiftKey: #{shift || need_shift?(modifiers)}}));
796
+ JAVASCRIPT
797
+ end
798
+ end
799
+ end
800
+ modifiers.reverse.each do |k|
801
+ cjs << <<~JAVASCRIPT
802
+ events.push(new window.KeyboardEvent('keyup', { bubbles: true, cancelable: true, key: '#{k.to_s.camelize}', code: '#{k.to_s.camelize}',
803
+ altKey: #{need_alt?(modifiers)}, ctrlKey: #{need_control?(modifiers)}, metaKey: #{need_meta?(modifiers)},
804
+ shiftKey: #{need_shift?(modifiers)}}));
805
+ JAVASCRIPT
806
+ end
807
+ end
808
+ end
809
+ top_modifiers.reverse.each do |key|
810
+ cjs << <<~JAVASCRIPT
811
+ events.push(new window.KeyboardEvent('keyup', { bubbles: true, cancelable: true, key: '#{key.to_s.camelize}', code: '#{key.to_s.camelize}',
812
+ altKey: #{need_alt?(top_modifiers)}, ctrlKey: #{need_control?(top_modifiers)}, metaKey: #{need_meta?(top_modifiers)},
813
+ shiftKey: #{need_shift?(top_modifiers)}}));
814
+ JAVASCRIPT
815
+ end
816
+ cjs << <<~JAVASCRIPT
817
+ for (i=0; i<events.length; i++) {
818
+ window.document.dispatchEvent(events[i]);
819
+ }
820
+ JAVASCRIPT
821
+ @context.exec cjs
822
+ end
823
+
824
+ def document_url(document)
825
+ @context.eval "AllDomHandles[#{document.handle}].window.location.href"
826
+ end
827
+
828
+ def document_user_agent(document)
829
+ @context.eval "AllDomHandles[#{document.handle}].window.navigator.userAgent"
830
+ end
831
+
832
+ def document_viewport_maximize(document)
833
+ document_viewport_resize(document, @max_width, @max_height)
834
+ end
835
+
836
+ def document_viewport_resize(document, width, height)
837
+ width = @max_width if width > @max_width
838
+ height = @max_height if width > @max_height
839
+ @context.exec <<~JAVASCRIPT
840
+ AllDomHandles[#{document.handle}].window.innerWidth = #{width};
841
+ AllDomHandles[#{document.handle}].window.innerHeight = #{height};
842
+ JAVASCRIPT
843
+ end
844
+
845
+ def document_viewport_size(document)
846
+ @context.eval "[AllDomHandles[#{document.handle}].window.innerWidth, AllDomHandles[#{document.handle}].window.innerHeight]"
847
+ end
848
+
849
+ def document_wait_for(document, selector)
850
+ js_escaped_selector = selector.gsub('\\', '\\\\\\').gsub('"', '\"')
851
+ node_data = await <<~JAVASCRIPT
852
+ var node = null;
853
+ var start_time = new Date();
854
+ var resolver = function(resolve) {
855
+ node = AllDomHandles[#{document.handle}].window.document.querySelector("#{js_escaped_selector}");
856
+ if (node) {
857
+ var node_handle = RegisterElementHandle(node);
858
+ var tag = node.tagName.toLowerCase();
859
+ var type = null;
860
+ if (tag === 'input') { type = node.getAttribute('type'); }
861
+ LastResult = {handle: node_handle, tag: tag, type: type, content_editable: node.isContentEditable};
862
+ resolve(true);
863
+ }
864
+ else if ((new Date() - start_time) > #{@jsdom_timeout}) { resolve(true); }
865
+ else { setTimeout(resolver, #{@jsdom_reaction_timeout}, resolve) }
866
+ };
867
+ var promise = new Promise(function(resolve, reject){ resolver(resolve); });
868
+ await promise;
869
+ JAVASCRIPT
870
+ if node_data
871
+ node_data[:css_selector] = selector
872
+ Isomorfeus::Puppetmaster::Node.new_by_tag(self, document, node_data)
873
+ end
874
+ end
875
+
876
+ def document_wait_for_xpath(document, query)
877
+ js_escaped_query = query.gsub('\\', '\\\\\\').gsub('"', '\"')
878
+ node_data = await <<~JAVASCRIPT
879
+ var node = null;
880
+ var start_time = new Date();
881
+ var resolver = function(resolve) {
882
+ var window = AllDomHandles[#{document.handle}].window;
883
+ var document = window.document;
884
+ var xpath_result = document.evaluate("#{js_escaped_query}", document, null, window.XPathResult.FIRST_ORDERED_NODE_TYPE, null);
885
+ node = xpath_result.singleNodeValue;
886
+ if (node) {
887
+ var node_handle = RegisterElementHandle(node);
888
+ var tag = node.tagName.toLowerCase();
889
+ var type = null;
890
+ if (tag === 'input') { type = node.getAttribute('type'); }
891
+ LastResult = {handle: node_handle, tag: tag, type: type, content_editable: node.isContentEditable};
892
+ resolve(true);
893
+ }
894
+ else if ((new Date() - start_time) > #{@jsdom_timeout}) { resolve(true); }
895
+ else { setTimeout(resolver, #{@jsdom_reaction_timeout}, resolve) }
896
+ };
897
+ var promise = new Promise(function(resolve, reject){ resolver(resolve); });
898
+ await promise;
899
+ JAVASCRIPT
900
+ if node_data
901
+ node_data[:xpath_query] = query
902
+ Isomorfeus::Puppetmaster::Node.new_by_tag(self, document, node_data)
903
+ end
904
+ end
905
+ end
906
+ end
907
+ end
908
+ end