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,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