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,401 @@
1
+ module Isomorfeus
2
+ module Puppetmaster
3
+ module Driver
4
+ class Puppeteer
5
+ include Isomorfeus::Puppetmaster::Driver::PuppeteerDocument
6
+ include Isomorfeus::Puppetmaster::Driver::PuppeteerNode
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 :app, :default_document, :url_blacklist
36
+
37
+ def initialize(options = {})
38
+ # https://pptr.dev/#?product=Puppeteer&version=v1.12.2&show=api-puppeteerlaunchoptions
39
+ # init ExecJs context
40
+ @app = options.delete(:app)
41
+ @options = options.dup
42
+ @browser_type = @options.delete(:browser_type) { :chromium }
43
+ @max_width = @options.delete(:max_width) { VIEWPORT_MAX_WIDTH }
44
+ @max_height = @options.delete(:max_height) { VIEWPORT_MAX_HEIGHT }
45
+ @width = @options.delete(:width) { VIEWPORT_DEFAULT_WIDTH > @max_width ? @max_width : VIEWPORT_DEFAULT_WIDTH }
46
+ @height = @options.delete(:height) { VIEWPORT_DEFAULT_HEIGHT > @max_height ? @max_height : VIEWPORT_DEFAULT_HEIGHT }
47
+ @timeout = @options.delete(:timeout) { TIMEOUT }
48
+ @max_wait = @options.delete(:max_wait) { @timeout + 1 }
49
+ @reaction_timeout = @options.delete(:reaction_timeout) { REACTION_TIMEOUT }
50
+ @puppeteer_timeout = @timeout * 1000
51
+ @puppeteer_reaction_timeout = @reaction_timeout * 1000
52
+ @url_blacklist = @options.delete(:url_blacklist) { [] }
53
+ @context = ExecJS.permissive_compile(puppeteer_launch)
54
+ page_handle = await_result
55
+ @default_document = Isomorfeus::Puppetmaster::Document.new(self, page_handle, Isomorfeus::Puppetmaster::Response.new('status' => 200))
56
+ ObjectSpace.define_finalizer(self, self.class.close_browser(self))
57
+ end
58
+
59
+ def self.document_handle_disposer(driver, handle)
60
+ cjs = <<~JAVASCRIPT
61
+ if (AllPageHandles[#{handle}]) { AllPageHandles[#{handle}].close(); }
62
+ delete AllPageHandles[#{handle}];
63
+ delete ConsoleMessages[#{handle}];
64
+ JAVASCRIPT
65
+ proc { driver.execute_script(cjs) }
66
+ end
67
+
68
+ def self.node_handle_disposer(driver, handle)
69
+ cjs = <<~JAVASCRIPT
70
+ if (AllElementHandles[#{handle}]) { AllElementHandles[#{handle}].dispose(); }
71
+ delete AllElementHandles[#{handle}];
72
+ JAVASCRIPT
73
+ proc { driver.execute_script(cjs) }
74
+ end
75
+
76
+ def browser
77
+ await('LastResult = await CurrentBrowser.userAgent();')
78
+ end
79
+
80
+ def document_handles
81
+ await <<~JAVASCRIPT
82
+ var pages = await CurrentBrowser.pages();
83
+ var handles = [];
84
+ for (i=0; i< pages.length; i++) {
85
+ handles.push(RegisterPage(pages[i]));
86
+ }
87
+ LastResult = handles;
88
+ JAVASCRIPT
89
+ end
90
+
91
+ ##### frame, all todo
92
+
93
+ def frame_all_text(frame)
94
+ await <<~JAVASCRIPT
95
+ LastResult = await AllElementHandles[#{frame.handle}].executionContext().evaluate((frame) => {
96
+ return frame.contentDocument.documentElement.textContent;
97
+ }, AllElementHandles[#{frame.handle}]);
98
+ JAVASCRIPT
99
+ end
100
+
101
+ def frame_body(frame)
102
+ node_data = await <<~JAVASCRIPT
103
+ var tt = await AllElementHandles[#{frame.handle}].executionContext().evaluate((frame) => {
104
+ node = frame.contentDocument.body;
105
+ var tag = node.tagName.toLowerCase();
106
+ var type = null;
107
+ if (tag === 'input') { type = node.getAttribute('type'); }
108
+ return [tag, type];
109
+ }, AllElementHandles[#{frame.handle}]);
110
+ LastResult = {handle: node_handle, tag: tt[0], type: tt[1]};
111
+ JAVASCRIPT
112
+ if node_data
113
+ node_data[:css_selector] = 'body'
114
+ Isomorfeus::Puppetmaster::Node.new_by_tag(self, document, node_data)
115
+ end
116
+ end
117
+
118
+ def frame_focus(frame)
119
+ await <<~JAVASCRIPT
120
+ await AllElementHandles[#{frame.handle}].executionContext().evaluate((frame) => {
121
+ frame.contentDocument.documentElement.focus();
122
+ }, AllElementHandles[#{frame.handle}]);
123
+ JAVASCRIPT
124
+ end
125
+
126
+ def frame_head(frame)
127
+ node_data = await <<~JAVASCRIPT
128
+ var tt = await AllElementHandles[#{frame.handle}].executionContext().evaluate((frame) => {
129
+ node = frame.contentDocument.head;
130
+ var tag = node.tagName.toLowerCase();
131
+ var type = null;
132
+ if (tag === 'input') { type = node.getAttribute('type'); }
133
+ return [tag, type];
134
+ }, AllElementHandles[#{frame.handle}]);
135
+ LastResult = {handle: node_handle, tag: tt[0], type: tt[1]};
136
+ JAVASCRIPT
137
+ if node_data
138
+ node_data[:css_selector] = 'body'
139
+ Isomorfeus::Puppetmaster::Node.new_by_tag(self, document, node_data)
140
+ end
141
+ end
142
+
143
+ def frame_html(frame)
144
+ await <<~JAVASCRIPT
145
+ LastResult = await AllElementHandles[#{frame.handle}].executionContext().evaluate((frame) => {
146
+ return frame.contentDocument.documentElement.outerHTML;
147
+ }, AllElementHandles[#{frame.handle}]);
148
+ JAVASCRIPT
149
+ end
150
+
151
+ def frame_title(frame)
152
+ await <<~JAVASCRIPT
153
+ LastResult = await AllElementHandles[#{frame.handle}].executionContext().evaluate((frame) => {
154
+ return frame.contentDocument.title;
155
+ }, AllElementHandles[#{frame.handle}]);
156
+ JAVASCRIPT
157
+ end
158
+
159
+ def frame_url(frame)
160
+ await <<~JAVASCRIPT
161
+ LastResult = await AllElementHandles[#{frame.handle}].executionContext().evaluate((frame) => {
162
+ return frame.contentDocument.location.href;
163
+ }, AllElementHandles[#{frame.handle}]);
164
+ JAVASCRIPT
165
+ end
166
+
167
+ def frame_visible_text(frame)
168
+ # if node is AREA, check visibility of relevant image
169
+ text = await <<~JAVASCRIPT
170
+ LastResult = await AllElementHandles[#{frame.handle}].executionContext().evaluate((frame) => {
171
+ var node = frame.contentDocument.body;
172
+ var temp_node = node;
173
+ while (temp_node) {
174
+ style = window.getComputedStyle(node);
175
+ if (style.display === "none" || style.visibility === "hidden" || parseFloat(style.opacity) === 0) { return ''; }
176
+ temp_node = temp_node.parentElement;
177
+ }
178
+ if (node.nodeName == "TEXTAREA" || node instanceof SVGElement) { return node.textContent; }
179
+ else { return node.innerText; }
180
+ }, AllElementHandles[#{frame.handle}]);
181
+ JAVASCRIPT
182
+ text.gsub(/\A[[:space:]&&[^\u00a0]]+/, "").gsub(/[[:space:]&&[^\u00a0]]+\z/, "").gsub(/\n+/, "\n").tr("\u00a0", " ")
183
+ end
184
+
185
+ private
186
+
187
+ def self.close_browser(driver)
188
+ cjs = <<~JAVASCRIPT
189
+ CurrentBrowser.close()
190
+ JAVASCRIPT
191
+ proc { driver.await(cjs) }
192
+ end
193
+
194
+ def await(script)
195
+ @context.eval <<~JAVASCRIPT
196
+ (async () => {
197
+ try {
198
+ LastExecutionFinished = false;
199
+ LastResult = null;
200
+ LastErr = null;
201
+ #{script}
202
+ LastExecutionFinished = true;
203
+ } catch(err) {
204
+ LastResult = null;
205
+ LastErr = err;
206
+ LastExecutionFinished = true;
207
+ }
208
+ })()
209
+ JAVASCRIPT
210
+ await_result
211
+ end
212
+
213
+ def await_result
214
+ start_time = Time.now
215
+ while !execution_finished? && !timed_out?(start_time)
216
+ sleep 0.01
217
+ end
218
+ get_result
219
+ end
220
+
221
+ def chromium_require
222
+ <<~JAVASCRIPT
223
+ const MasterPuppeteer = require('puppeteer');
224
+ JAVASCRIPT
225
+ end
226
+
227
+ def determine_error(message)
228
+ if message.include?('net::ERR_CERT_') || message.include?('SEC_ERROR_EXPIRED_CERTIFICATE')
229
+ Isomorfeus::Puppetmaster::CertificateError.new(message)
230
+ elsif message.include?('net::ERR_NAME_') || message.include?('NS_ERROR_UNKNOWN_HOST')
231
+ Isomorfeus::Puppetmaster::DNSError.new(message)
232
+ elsif message.include?('Unknown key: ')
233
+ Isomorfeus::Puppetmaster::KeyError.new(message)
234
+ elsif message.include?('Execution context was destroyed, most likely because of a navigation.')
235
+ Isomorfeus::Puppetmaster::ExecutionContextError.new(message)
236
+ elsif message.include?('Evaluation failed: DOMException:') || (message.include?('Evaluation failed:') && (message.include?('is not a valid selector') || message.include?('is not a legal expression')))
237
+ Isomorfeus::Puppetmaster::DOMException.new(message)
238
+ else
239
+ Isomorfeus::Puppetmaster::JavaScriptError.new(message)
240
+ end
241
+ end
242
+
243
+ def execution_finished?
244
+ @context.eval 'LastExecutionFinished'
245
+ end
246
+
247
+ def firefox_require
248
+ <<~JAVASCRIPT
249
+ const MasterPuppeteer = require('puppeteer-firefox');
250
+ JAVASCRIPT
251
+ end
252
+
253
+ def get_result
254
+ res, err_msg = @context.eval 'GetLastResult()'
255
+ raise determine_error(err_msg) if err_msg
256
+ res
257
+ end
258
+
259
+ def launch_line
260
+ string_options = []
261
+ options = @options.dup
262
+ string_options << "ignoreHTTPSErrors: #{options.delete(:ignore_https_errors)}" if options.has_key?(:ignore_https_errors)
263
+ string_options << "executablePath: '#{options.delete(:executable_path)}'" if options.has_key?(:executable_path)
264
+ options.each do |option, value|
265
+ string_options << "#{option.to_s.camelize(:lower)}: #{value}"
266
+ end
267
+ string_options << "userDataDir: '#{Dir.mktmpdir}'" unless @options.has_key?(:user_data_dir)
268
+ string_options << "defaultViewport: { width: #{@width}, height: #{@height} }"
269
+ string_options << "pipe: true"
270
+ # string_options << "args: ['--disable-popup-blocking']"
271
+ line = 'await MasterPuppeteer.launch('
272
+ unless string_options.empty?
273
+ line << '{'
274
+ line << string_options.join(', ') if string_options.size > 1
275
+ line << '}'
276
+ end
277
+ line << ')'
278
+ end
279
+
280
+ def puppeteer_launch
281
+ # todo target_handle, puppeteer save path
282
+ puppeteer_require = case @browser_type
283
+ when :firefox then firefox_require
284
+ when :chrome then chromium_require
285
+ when :chromium then chromium_require
286
+ else
287
+ raise "Browser type #{@browser_type} not supported! Browser type must be one of: chrome, firefox."
288
+ end
289
+ <<~JAVASCRIPT
290
+ #{puppeteer_require}
291
+ var BrowserType = '#{@browser_type.to_s}';
292
+ var LastResult = null;
293
+ var LastErr = null;
294
+ var LastExecutionFinished = false;
295
+ var LastHandleId = 0;
296
+
297
+ var AllPageHandles = {};
298
+ var AllElementHandles = {};
299
+
300
+ var CurrentBrowser = null;
301
+ var ConsoleMessages = {};
302
+
303
+ var ModalText = null;
304
+ var ModalTextMatched = false;
305
+
306
+ const GetLastResult = function() {
307
+ if (LastExecutionFinished === true) {
308
+ var err = LastErr;
309
+ var res = LastResult;
310
+
311
+ LastErr = null;
312
+ LastRes = null;
313
+ LastExecutionFinished = false;
314
+
315
+ if (err) { return [null, err.message]; }
316
+ else { return [res, null]; }
317
+
318
+ } else {
319
+ return [null, (new Error('Last command did not yet finish execution!')).message];
320
+ }
321
+ };
322
+
323
+ const DialogAcceptHandler = async (dialog) => {
324
+ var msg = dialog.message()
325
+ ModalTextMatched = (ModalText === msg);
326
+ ModalText = msg;
327
+ await dialog.accept();
328
+ }
329
+
330
+ const DialogDismissHandler = async (dialog) => {
331
+ var msg = dialog.message()
332
+ ModalTextMatched = (ModalText === msg);
333
+ ModalText = msg;
334
+ await dialog.dismiss();
335
+ }
336
+
337
+ const RegisterElementHandle = function(element_handle) {
338
+ var entries = Object.entries(AllElementHandles);
339
+ for(var i = 0; i < entries.length; i++) {
340
+ if (entries[i][1] === element_handle) { return entries[i][0]; }
341
+ }
342
+ LastHandleId++;
343
+ var handle_id = LastHandleId;
344
+ AllElementHandles[handle_id] = element_handle;
345
+ return handle_id;
346
+ };
347
+
348
+ const RegisterPage = function(page) {
349
+ var entries = Object.entries(AllPageHandles);
350
+ for(var i = 0; i < entries.length; i++) {
351
+ if (entries[i][1] === page) { return entries[i][0]; }
352
+ }
353
+ LastHandleId++;
354
+ var handle_id = LastHandleId;
355
+ AllPageHandles[handle_id] = page;
356
+ ConsoleMessages[handle_id] = [];
357
+ AllPageHandles[handle_id].on('console', (msg) => {
358
+ ConsoleMessages[handle_id].push({level: msg.type(), location: msg.location(), text: msg.text()});
359
+ });
360
+ AllPageHandles[handle_id].on('pageerror', (error) => {
361
+ ConsoleMessages[handle_id].push({level: 'error', location: '', text: error.message});
362
+ });
363
+ return handle_id;
364
+ };
365
+
366
+ (async () => {
367
+ try {
368
+ CurrentBrowser = #{launch_line}
369
+ var page = (await CurrentBrowser.pages())[0];
370
+ page.setDefaultTimeout(#{@puppeteer_timeout});
371
+ if (!(BrowserType === 'firefox')) {
372
+ var target = page.target();
373
+ var cdp_session = await target.createCDPSession();
374
+ await cdp_session.send('Page.setDownloadBehavior', {behavior: 'allow', downloadPath: '#{Isomorfeus::Puppetmaster.save_path}'});
375
+ if (#{@url_blacklist}.length > 0) { await cdp_session.send('Network.setBlockedURLs', {urls: #{@url_blacklist}}); }
376
+ await cdp_session.detach();
377
+ }
378
+ LastResult = RegisterPage(page);
379
+ LastExecutionFinished = true;
380
+ } catch (err) {
381
+ LastErr = err;
382
+ LastExecutionFinished = true;
383
+ }
384
+ })();
385
+ JAVASCRIPT
386
+ end
387
+
388
+ def session
389
+ @session
390
+ end
391
+
392
+ def timed_out?(start_time)
393
+ if (Time.now - start_time) > @timeout
394
+ raise "Command Execution timed out!"
395
+ end
396
+ false
397
+ end
398
+ end
399
+ end
400
+ end
401
+ end
@@ -0,0 +1,944 @@
1
+ module Isomorfeus
2
+ module Puppetmaster
3
+ module Driver
4
+ module PuppeteerDocument
5
+ def document_accept_alert(document, **options, &block)
6
+ # TODO maybe wrap in mutex
7
+ text = options.has_key?(:text) ? "`#{options[:text]}`" : 'null'
8
+ @context.exec <<~JAVASCRIPT
9
+ ModalText = #{text};
10
+ AllPageHandles[#{document.handle}].on('dialog', DialogAcceptHandler);
11
+ JAVASCRIPT
12
+ block.call
13
+ sleep @reaction_timeout
14
+ @context.eval 'ModalText'
15
+ ensure
16
+ matched = await <<~JAVASCRIPT
17
+ LastResult = ModalTextMatched;
18
+ ModalTextMatched = false;
19
+ ModalText = null;
20
+ AllPageHandles[#{document.handle}].removeListener('dialog', DialogAcceptHandler);
21
+ JAVASCRIPT
22
+ raise Isomorfeus::Puppetmaster::NoModalError if options.has_key?(:text) && !matched
23
+ end
24
+
25
+ def document_accept_confirm(document, **options, &block)
26
+ # TODO maybe wrap in mutex
27
+ text = options.has_key?(:text) ? "`#{options[:text]}`" : 'null'
28
+ @context.exec <<~JAVASCRIPT
29
+ ModalText = #{text};
30
+ AllPageHandles[#{document.handle}].on('dialog', DialogAcceptHandler);
31
+ JAVASCRIPT
32
+ block.call
33
+ sleep @reaction_timeout
34
+ @context.eval 'ModalText'
35
+ ensure
36
+ matched = await <<~JAVASCRIPT
37
+ LastResult = ModalTextMatched;
38
+ ModalTextMatched = false;
39
+ ModalText = null;
40
+ AllPageHandles[#{document.handle}].removeListener('dialog', DialogAcceptHandler);
41
+ JAVASCRIPT
42
+ raise Isomorfeus::Puppetmaster::NoModalError if options.has_key?(:text) && !matched
43
+ end
44
+
45
+ def document_accept_leave_page(document, **options, &block)
46
+ # TODO maybe wrap in mutex
47
+ text = options.has_key?(:text) ? "`#{options[:text]}`" : 'null'
48
+ @context.exec <<~JAVASCRIPT
49
+ ModalText = #{text};
50
+ AllPageHandles[#{document.handle}].on('dialog', DialogAcceptHandler);
51
+ JAVASCRIPT
52
+ block.call
53
+ sleep @reaction_timeout
54
+ @context.eval 'ModalText'
55
+ ensure
56
+ matched = await <<~JAVASCRIPT
57
+ LastResult = ModalTextMatched;
58
+ ModalTextMatched = false;
59
+ ModalText = null;
60
+ AllPageHandles[#{document.handle}].removeListener('dialog', DialogAcceptHandler);
61
+ JAVASCRIPT
62
+ raise Isomorfeus::Puppetmaster::NoModalError if options.has_key?(:text) && !matched
63
+ end
64
+
65
+ def document_accept_prompt(document, **options, &block)
66
+ # TODO maybe wrap in mutex
67
+ text = options.has_key?(:text) ? "`#{options[:text]}`" : 'null'
68
+ @context.exec <<~JAVASCRIPT
69
+ ModalText = #{text};
70
+ AllPageHandles[#{document.handle}].on('dialog', DialogAcceptHandler);
71
+ JAVASCRIPT
72
+ block.call
73
+ sleep @reaction_timeout
74
+ @context.eval 'ModalText'
75
+ ensure
76
+ matched = await <<~JAVASCRIPT
77
+ LastResult = ModalTextMatched;
78
+ ModalTextMatched = false;
79
+ ModalText = null;
80
+ AllPageHandles[#{document.handle}].removeListener('dialog', DialogAcceptHandler);
81
+ JAVASCRIPT
82
+ raise Isomorfeus::Puppetmaster::NoModalError if options.has_key?(:text) && !matched
83
+ end
84
+
85
+ def document_all_text(document)
86
+ await("LastResult = AllPageHandles[#{document.handle}].evaluate(function(){ return document.documentElement.textContent; });")
87
+ end
88
+
89
+ def document_body(document)
90
+ node_data = await <<~JAVASCRIPT
91
+ var element_handle = await AllPageHandles[#{document.handle}].$('body');
92
+ if (element_handle) {
93
+ var node_handle = RegisterElementHandle(element_handle);
94
+ var tt = await AllElementHandles[node_handle].executionContext().evaluate((node) => {
95
+ var tag = node.tagName.toLowerCase();
96
+ return [tag, null, node.isContentEditable];
97
+ }, AllElementHandles[node_handle]);
98
+ LastResult = {handle: node_handle, tag: tt[0], type: tt[1], content_editable: tt[2]};
99
+ }
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 document_bring_to_front(document)
108
+ await("await AllPageHandles[#{document.handle}].bringToFront();")
109
+ end
110
+
111
+ def document_clear_authentication_credentials(document)
112
+ await("AllPageHandles[#{document.handle}].authenticate(null);")
113
+ end
114
+
115
+ def document_clear_cookies(document)
116
+ await <<~JAVASCRIPT
117
+ var cookies = await AllPageHandles[#{document.handle}].cookies();
118
+ cookies.forEach(async(cookie) => {await AllPageHandles[#{document.handle}].deleteCookie(cookie);});
119
+ JAVASCRIPT
120
+ end
121
+
122
+ def document_clear_extra_headers(document)
123
+ await ("AllPageHandles[#{document.handle}].setExtraHTTPHeaders({});")
124
+ end
125
+
126
+ def document_clear_url_blacklist(document)
127
+ await <<~JAVASCRIPT
128
+ if (!(BrowserType === 'firefox')) {
129
+ var cdp_session = await AllPageHandles[#{document.handle}].target().createCDPSession();
130
+ await cdp_session.send('Network.setBlockedURLs', {urls: []});
131
+ await cdp_session.detach();
132
+ }
133
+ JAVASCRIPT
134
+ end
135
+
136
+ def document_click(document, x: nil, y: nil, modifiers: nil)
137
+ # modifier_keys: :alt, :control, :meta, :shift
138
+ # await "await AllPageHandles[#{document.handle}].mouse.click(#{x},#{y},{button: 'left'});"
139
+ # raise Isomorfeus::Pupppetmaster::InvalidActionError.new(:click) unless visible?
140
+ modifiers = [modifiers] if modifiers.is_a?(Symbol)
141
+ modifiers = [] unless modifiers
142
+ modifiers = modifiers.map {|key| key.to_s.camelize(:lower) }
143
+ await <<~JAVASCRIPT
144
+ var response_event_occurred = false;
145
+ var response_handler = function(event){ response_event_occurred = true; };
146
+ var response_watcher = new Promise(function(resolve, reject){
147
+ setTimeout(function(){
148
+ if (!response_event_occurred) { resolve(true); }
149
+ else { setTimeout(function(){ resolve(true); }, #{@puppeteer_timeout}); }
150
+ AllPageHandles[#{document.handle}].removeListener('response', response_handler);
151
+ }, #{@puppeteer_reaction_timeout});
152
+ });
153
+ AllPageHandles[#{document.handle}].on('response', response_handler);
154
+ var navigation_watcher;
155
+ if (BrowserType === 'firefox') {
156
+ navigation_watcher = AllPageHandles[#{document.handle}].waitFor(1000);
157
+ } else {
158
+ navigation_watcher = AllPageHandles[#{document.handle}].waitForNavigation();
159
+ }
160
+ await AllPageHandles[#{document.handle}].evaluate(function(){
161
+ var options = {button: 0, bubbles: true, cancelable: true};
162
+ var node = window;
163
+ var x = #{x ? x : 'null'};
164
+ var y = #{y ? y : 'null'};
165
+ var modifiers = #{modifiers};
166
+ if (x && y) {
167
+ options['clientX'] = x;
168
+ options['clientY'] = y;
169
+ var n = document.elementFromPoint(x, y);
170
+ if (n) { node = n };
171
+ }
172
+ if (modifiers.includes('meta')) { options['metaKey'] = true; }
173
+ if (modifiers.includes('control')) { options['ctrlKey'] = true; }
174
+ if (modifiers.includes('shift')) { options['shiftKey'] = true; }
175
+ if (modifiers.includes('alt')) { options['altKey'] = true; }
176
+ node.dispatchEvent(new MouseEvent('mousedown', options));
177
+ node.dispatchEvent(new MouseEvent('mouseup', options));
178
+ node.dispatchEvent(new MouseEvent('click', options));
179
+ });
180
+ await Promise.race([response_watcher, navigation_watcher]);
181
+ JAVASCRIPT
182
+ end
183
+
184
+ def document_close(document)
185
+ await <<~JAVASCRIPT
186
+ await AllPageHandles[#{document.handle}].close();
187
+ delete AllPageHandles[#{document.handle}];
188
+ delete ConsoleMessages[#{document.handle}];
189
+ JAVASCRIPT
190
+ end
191
+
192
+ def document_console(document)
193
+ messages = @context.exec "return ConsoleMessages[#{document.handle}]"
194
+ messages.map {|m| Isomorfeus::Puppetmaster::ConsoleMessage.new(m)}
195
+ end
196
+
197
+ def document_cookies(document)
198
+ result = await("LastResult = await AllPageHandles[#{document.handle}].cookies();")
199
+ result_hash = {}
200
+ result.each { |cookie| result_hash[cookie['name']] = Isomorfeus::Puppetmaster::Cookie.new(cookie) }
201
+ result_hash
202
+ end
203
+
204
+ def document_dismiss_confirm(document, **options, &block)
205
+ # TODO
206
+ text = options.has_key?(:text) ? "`#{options[:text]}`" : 'null'
207
+ @context.exec <<~JAVASCRIPT
208
+ ModalText = #{text};
209
+ AllPageHandles[#{document.handle}].on('dialog', DialogDismissHandler);
210
+ JAVASCRIPT
211
+ block.call
212
+ sleep @reaction_timeout
213
+ @context.eval 'ModalText'
214
+ ensure
215
+ matched = await <<~JAVASCRIPT
216
+ LastResult = ModalTextMatched;
217
+ ModalTextMatched = false;
218
+ ModalText = null;
219
+ AllPageHandles[#{document.handle}].removeListener('dialog', DialogDismissHandler);
220
+ JAVASCRIPT
221
+ raise Isomorfeus::Puppetmaster::ModalNotFound if options.has_key?(:text) && !matched
222
+ end
223
+
224
+ def document_dismiss_leave_page(document, **options, &block)
225
+ # TODO
226
+ text = options.has_key?(:text) ? "`#{options[:text]}`" : 'null'
227
+ @context.exec <<~JAVASCRIPT
228
+ ModalText = #{text};
229
+ AllPageHandles[#{document.handle}].on('dialog', DialogDismissHandler);
230
+ JAVASCRIPT
231
+ block.call
232
+ sleep @reaction_timeout
233
+ @context.eval 'ModalText'
234
+ ensure
235
+ matched = await <<~JAVASCRIPT
236
+ LastResult = ModalTextMatched;
237
+ ModalTextMatched = false;
238
+ ModalText = null;
239
+ AllPageHandles[#{document.handle}].removeListener('dialog', DialogDismissHandler);
240
+ JAVASCRIPT
241
+ raise Isomorfeus::Puppetmaster::ModalNotFound if options.has_key?(:text) && !matched
242
+ end
243
+
244
+ def document_dismiss_prompt(document, **options, &block)
245
+ # TODO
246
+ text = options.has_key?(:text) ? "`#{options[:text]}`" : 'null'
247
+ @context.exec <<~JAVASCRIPT
248
+ ModalText = #{text};
249
+ AllPageHandles[#{document.handle}].on('dialog', DialogDismissHandler);
250
+ JAVASCRIPT
251
+ block.call
252
+ sleep @reaction_timeout
253
+ @context.eval 'ModalText'
254
+ ensure
255
+ matched = await <<~JAVASCRIPT
256
+ LastResult = ModalTextMatched;
257
+ ModalTextMatched = false;
258
+ ModalText = null;
259
+ AllPageHandles[#{document.handle}].removeListener('dialog', DialogDismissHandler);
260
+ JAVASCRIPT
261
+ raise Isomorfeus::Puppetmaster::ModalNotFound if options.has_key?(:text) && !matched
262
+ end
263
+
264
+ def document_dispatch_event(document, name, event_type = nil, **options)
265
+ raise ArgumentError, 'Unknown event' unless EVENTS.key?(name.to_sym) || event_type
266
+ event_type, opts = *EVENTS[name.to_s.downcase.tr('_', '').to_sym] if event_type.nil?
267
+ opts.merge!(options)
268
+ await <<~JAVASCRIPT
269
+ handle = await AllPageHandles[#{document.handle}].evaluate(function(node){
270
+ var event = new #{event_type}('#{name}'#{opts.empty? ? '' : opts});
271
+ document.dispatchEvent(event);
272
+ });
273
+ JAVASCRIPT
274
+ end
275
+
276
+ def document_double_click(document, x: nil, y: nil, modifiers: nil)
277
+ # modifier_keys: :alt, :control, :meta, :shift
278
+ modifiers = [modifiers] if modifiers.is_a?(Symbol)
279
+ modifiers = [] unless modifiers
280
+ modifiers = modifiers.map {|key| key.to_s.camelize(:lower) }
281
+ await <<~JAVASCRIPT
282
+ var response_event_occurred = false;
283
+ var response_handler = function(event){ response_event_occurred = true; };
284
+ var response_watcher = new Promise(function(resolve, reject){
285
+ setTimeout(function(){
286
+ if (!response_event_occurred) { resolve(true); }
287
+ else { setTimeout(function(){ resolve(true); }, #{@puppeteer_timeout}); }
288
+ AllPageHandles[#{document.handle}].removeListener('response', response_handler);
289
+ }, #{@puppeteer_reaction_timeout});
290
+ });
291
+ AllPageHandlers[#{document.handle}].on('response', response_handler);
292
+ var navigation_watcher;
293
+ if (BrowserType === 'firefox') {
294
+ navigation_watcher = AllPageHandles[#{document.handle}].waitFor(1000);
295
+ } else {
296
+ navigation_watcher = AllPageHandles[#{document.handle}].waitForNavigation();
297
+ }
298
+ await AllPageHandles[#{document.handle}].evaluate(function(){
299
+ var options = {button: 0, bubbles: true, cancelable: true};
300
+ var node = window;
301
+ var x = #{x ? x : 'null'};
302
+ var y = #{y ? y : 'null'};
303
+ var modifiers = #{modifiers};
304
+ if (x && y) {
305
+ options['clientX'] = x;
306
+ options['clientY'] = y;
307
+ var n = document.elementFromPoint(x, y);
308
+ if (n) { node = n };
309
+ }
310
+ if (modifiers.includes('meta')) { options['metaKey'] = true; }
311
+ if (modifiers.includes('control')) { options['ctrlKey'] = true; }
312
+ if (modifiers.includes('shift')) { options['shiftKey'] = true; }
313
+ if (modifiers.includes('alt')) { options['altKey'] = true; }
314
+ node.dispatchEvent(new MouseEvent('mousedown', options));
315
+ node.dispatchEvent(new MouseEvent('mouseup', options));
316
+ node.dispatchEvent(new MouseEvent('dblclick', options));
317
+ });
318
+ await Promise.race([response_watcher, navigation_watcher]);
319
+ JAVASCRIPT
320
+ end
321
+
322
+ def document_evaluate_script(document, script, *args)
323
+ await <<~JAVASCRIPT
324
+ LastResult = await AllPageHandles[#{document.handle}].evaluate((arguments) => {
325
+ return #{script}
326
+ }, #{args});
327
+ JAVASCRIPT
328
+ end
329
+
330
+ def document_execute_script(document, script, *args)
331
+ await <<~JAVASCRIPT
332
+ LastResult = await AllPageHandles[#{document.handle}].evaluate((arguments) => {
333
+ #{script}
334
+ }, #{args});
335
+ JAVASCRIPT
336
+ end
337
+
338
+ def document_find(document, selector)
339
+ js_escaped_selector = selector.gsub('\\', '\\\\\\').gsub('"', '\"')
340
+ node_data = await <<~JAVASCRIPT
341
+ var element_handle = await AllPageHandles[#{document.handle}].$("#{js_escaped_selector}");
342
+ if (element_handle) {
343
+ var node_handle = RegisterElementHandle(element_handle);
344
+ var tt = await AllPageHandles[#{document.handle}].evaluate((node) => {
345
+ var tag = node.tagName.toLowerCase();
346
+ var type = null;
347
+ if (tag === 'input') { type = node.getAttribute('type'); }
348
+ return [tag, type, node.isContentEditable];
349
+ }, AllElementHandles[node_handle]);
350
+ LastResult = {handle: node_handle, tag: tt[0], type: tt[1], content_editable: tt[2]};
351
+ }
352
+ JAVASCRIPT
353
+ if node_data
354
+ node_data[:css_selector] = selector
355
+ Isomorfeus::Puppetmaster::Node.new_by_tag(self, document, node_data)
356
+ else
357
+ raise Isomorfeus::Puppetmaster::ElementNotFound.new(selector)
358
+ end
359
+ end
360
+
361
+ def document_find_all(document, selector)
362
+ js_escaped_selector = selector.gsub('\\', '\\\\\\').gsub('"', '\"')
363
+ node_data_array = await <<~JAVASCRIPT
364
+ var node_data_array = [];
365
+ var element_handle_array = await AllPageHandles[#{document.handle}].$$("#{js_escaped_selector}");
366
+ if (element_handle_array) {
367
+ for (var i=0; i<element_handle_array.length; i++) {
368
+ var node_handle = RegisterElementHandle(element_handle_array[i]);
369
+ var tt = await AllPageHandles[#{document.handle}].evaluate((node) => {
370
+ var tag = node.tagName.toLowerCase();
371
+ var type = null;
372
+ if (tag === 'input') { type = node.getAttribute('type'); }
373
+ return [tag, type, node.isContentEditable];
374
+ }, AllElementHandles[node_handle]);
375
+ node_data_array.push({handle: node_handle, tag: tt[0], type: tt[1], content_editable: tt[2]});
376
+ }
377
+ }
378
+ LastResult = node_data_array;
379
+ JAVASCRIPT
380
+ node_data_array.map do |node_data|
381
+ node_data[:css_selector] = selector
382
+ Isomorfeus::Puppetmaster::Node.new_by_tag(self, document, node_data)
383
+ end
384
+ end
385
+
386
+ def document_find_all_xpath(document, query)
387
+ js_escaped_query = query.gsub('\\', '\\\\\\').gsub('"', '\"')
388
+ node_data_array = await <<~JAVASCRIPT
389
+ var node_data_array = [];
390
+ var element_handle_array = await AllPageHandles[#{document.handle}].$x("#{js_escaped_query}");
391
+ if (element_handle_array) {
392
+ for (var i=0; i<element_handle_array.length; i++) {
393
+ var node_handle = RegisterElementHandle(element_handle_array[i]);
394
+ var tt = await AllPageHandles[#{document.handle}].evaluate((node) => {
395
+ var tag = node.tagName.toLowerCase();
396
+ var type = null;
397
+ if (tag === 'input') { type = node.getAttribute('type'); }
398
+ return [tag, type, node.isContentEditable];
399
+ }, AllElementHandles[node_handle]);
400
+ node_data_array.push({handle: node_handle, tag: tt[0], type: tt[1], content_editable: tt[2]});
401
+ }
402
+ }
403
+ LastResult = node_data_array;
404
+ JAVASCRIPT
405
+ node_data_array.map do |node_data|
406
+ node_data[:xpath_query] = query
407
+ Isomorfeus::Puppetmaster::Node.new_by_tag(self, document, node_data)
408
+ end
409
+ end
410
+
411
+ def document_find_xpath(document, query)
412
+ js_escaped_query = query.gsub('\\', '\\\\\\').gsub('"', '\"')
413
+ node_data = await <<~JAVASCRIPT
414
+ var element_handle_array = await AllPageHandles[#{document.handle}].$x("#{js_escaped_query}");
415
+ var element_handle = (element_handle_array) ? element_handle_array[0] : null;
416
+ if (element_handle) {
417
+ var node_handle = RegisterElementHandle(element_handle);
418
+ var tt = await AllPageHandles[#{document.handle}].evaluate((node) => {
419
+ var tag = node.tagName.toLowerCase();
420
+ var type = null;
421
+ if (tag === 'input') { type = node.getAttribute('type'); }
422
+ return [tag, type, node.isContentEditable];
423
+ }, AllElementHandles[node_handle]);
424
+ LastResult = {handle: node_handle, tag: tt[0], type: tt[1], content_editable: tt[2]};
425
+ }
426
+ JAVASCRIPT
427
+ if node_data
428
+ node_data[:xpath_query] = query
429
+ Isomorfeus::Puppetmaster::Node.new_by_tag(self, document, node_data)
430
+ else
431
+ raise Isomorfeus::Puppetmaster::ElementNotFound.new(query)
432
+ end
433
+ end
434
+
435
+ def document_go_back(document)
436
+ response_hash, messages = await <<~JAVASCRIPT
437
+ ConsoleMessages[#{document.handle}] = [];
438
+ var response = await AllPageHandles[#{document.handle}].goBack();
439
+ if (response) {
440
+ var request = response.request();
441
+ var formatted_response = {
442
+ headers: response.headers(),
443
+ ok: response.ok(),
444
+ remote_address: response.remoteAddress(),
445
+ request: {
446
+ failure: request.failure(),
447
+ headers: request.headers(),
448
+ method: request.method(),
449
+ post_data: request.postData(),
450
+ resource_type: request.resourceType(),
451
+ url: request.url()
452
+ },
453
+ status: response.status(),
454
+ status_text: response.statusText(),
455
+ text: response.text(),
456
+ url: response.url()
457
+ };
458
+ LastResult = [formatted_response, ConsoleMessages[#{document.handle}]];
459
+ } else {
460
+ LastResult = [null, ConsoleMessages[#{document.handle}]];
461
+ }
462
+ JAVASCRIPT
463
+ con_messages = messages.map {|m| Isomorfeus::Puppetmaster::ConsoleMessage.new(m)}
464
+ con_messages.each { |m| raise determine_error(m.text) if m.level == 'error' && !m.text.start_with?('Failed to load resource:') }
465
+ if response_hash
466
+ response = Isomorfeus::Puppetmaster::Response.new(response_hash)
467
+ document.instance_variable_set(:@response, response)
468
+ end
469
+ document.response
470
+ end
471
+
472
+ def document_go_forward(document)
473
+ response_hash, messages = await <<~JAVASCRIPT
474
+ ConsoleMessages[#{document.handle}] = [];
475
+ var response = await AllPageHandles[#{document.handle}].goForward();
476
+ if (response) {
477
+ var request = response.request();
478
+ var formatted_response = {
479
+ headers: response.headers(),
480
+ ok: response.ok(),
481
+ remote_address: response.remoteAddress(),
482
+ request: {
483
+ failure: request.failure(),
484
+ headers: request.headers(),
485
+ method: request.method(),
486
+ post_data: request.postData(),
487
+ resource_type: request.resourceType(),
488
+ url: request.url()
489
+ },
490
+ status: response.status(),
491
+ status_text: response.statusText(),
492
+ text: response.text(),
493
+ url: response.url()
494
+ };
495
+ LastResult = [formatted_response, ConsoleMessages[#{document.handle}]];
496
+ } else {
497
+ LastResult = [null, ConsoleMessages[#{document.handle}]];
498
+ }
499
+ JAVASCRIPT
500
+ con_messages = messages.map {|m| Isomorfeus::Puppetmaster::ConsoleMessage.new(m)}
501
+ con_messages.each { |m| raise determine_error(m.text) if m.level == 'error' && !m.text.start_with?('Failed to load resource:') }
502
+ if response_hash
503
+ response = Isomorfeus::Puppetmaster::Response.new(response_hash)
504
+ document.instance_variable_set(:@response, response)
505
+ end
506
+ document.response
507
+ end
508
+
509
+ def document_goto(document, uri)
510
+ parsed_uri = URI.parse(uri)
511
+ parsed_uri.host = @app.host unless parsed_uri.host
512
+ parsed_uri.port = @app.port unless parsed_uri.port
513
+ parsed_uri.scheme = @app.scheme unless parsed_uri.scheme
514
+ response_hash, messages = await <<~JAVASCRIPT
515
+ ConsoleMessages[#{document.handle}] = [];
516
+ var response = await AllPageHandles[#{document.handle}].goto('#{parsed_uri.to_s}');
517
+ if (response) {
518
+ var request = response.request();
519
+ var formatted_response = {
520
+ headers: response.headers(),
521
+ ok: response.ok(),
522
+ remote_address: response.remoteAddress(),
523
+ request: {
524
+ failure: request.failure(),
525
+ headers: request.headers(),
526
+ method: request.method(),
527
+ post_data: request.postData(),
528
+ resource_type: request.resourceType(),
529
+ url: request.url()
530
+ },
531
+ status: response.status(),
532
+ status_text: response.statusText(),
533
+ text: response.text(),
534
+ url: response.url()
535
+ };
536
+ LastResult = [formatted_response, ConsoleMessages[#{document.handle}]];
537
+ } else {
538
+ LastResult = [null, ConsoleMessages[#{document.handle}]];
539
+ }
540
+ JAVASCRIPT
541
+ con_messages = messages.map {|m| Isomorfeus::Puppetmaster::ConsoleMessage.new(m)}
542
+ con_messages.each { |m| raise determine_error(m.text) if m.level == 'error' && !m.text.start_with?('Failed to load resource:') }
543
+ if response_hash
544
+ response = Isomorfeus::Puppetmaster::Response.new(response_hash)
545
+ document.instance_variable_set(:@response, response)
546
+ end
547
+ document.response
548
+ end
549
+
550
+ def document_head(document)
551
+ node_data = await <<~JAVASCRIPT
552
+ var element_handle = await AllPageHandles[#{document.handle}].$('head');
553
+ if (element_handle) {
554
+ var node_handle = RegisterElementHandle(element_handle);
555
+ var tt = await AllElementHandles[node_handle].executionContext().evaluate((node) => {
556
+ var tag = node.tagName.toLower();
557
+ return [tag, null, node.isContentEditable];
558
+ }, AllElementHandles[node_handle]);
559
+ LastResult = {handle: node_handle, tag: tt[0], type: tt[1], content_editable: tt[2]};
560
+ }
561
+ JAVASCRIPT
562
+ if node_data
563
+ node_data[:css_selector] = selector
564
+ Isomorfeus::Puppetmaster::Node.new_by_tag(self, document, node_data)
565
+ end
566
+ end
567
+
568
+ def document_html(document)
569
+ await "LastResult = await AllPageHandles[#{document.handle}].content();"
570
+ end
571
+
572
+ def document_open_new_document(_document, uri = nil)
573
+ if !uri || uri == 'about:blank'
574
+ parsed_uri = 'about:blank'
575
+ else
576
+ parsed_uri = URI.parse(uri)
577
+ parsed_uri.host = @app.host unless parsed_uri.host
578
+ parsed_uri.port = @app.port unless parsed_uri.port
579
+ parsed_uri.scheme = @app.scheme unless parsed_uri.scheme
580
+ end
581
+ handle, response_hash, messages = await <<~JAVASCRIPT
582
+ var new_page = await CurrentBrowser.newPage();
583
+ var url = '#{parsed_uri.to_s}';
584
+ new_page.setDefaultTimeout(#{@puppeteer_timeout});
585
+ await new_page.setViewport({width: #{@width}, height: #{@height}});
586
+ if (!(BrowserType === 'firefox')) {
587
+ var new_target = new_page.target();
588
+ var cdp_session = await new_target.createCDPSession();
589
+ await cdp_session.send('Page.setDownloadBehavior', {behavior: 'allow', downloadPath: '#{Isomorfeus::Puppetmaster.save_path}'});
590
+ if (#{@url_blacklist}.length > 0) { await cdp_session.send('Network.setBlockedURLs', {urls: #{@url_blacklist}}); }
591
+ await cdp_session.detach();
592
+ }
593
+ var page_handle = RegisterPage(new_page);
594
+ var result_response = null;
595
+ if (url && url !== '') {
596
+ var response = await new_page.goto(url);
597
+ if (response) {
598
+ var request = response.request();
599
+ result_response = {
600
+ headers: response.headers(),
601
+ ok: response.ok(),
602
+ remote_address: response.remoteAddress(),
603
+ request: {
604
+ failure: request.failure(),
605
+ headers: request.headers(),
606
+ method: request.method(),
607
+ post_data: request.postData(),
608
+ resource_type: request.resourceType(),
609
+ url: request.url()
610
+ },
611
+ status: response.status(),
612
+ status_text: response.statusText(),
613
+ text: response.text(),
614
+ url: response.url()
615
+ }
616
+ }
617
+ };
618
+ LastResult = [page_handle, result_response, ConsoleMessages[page_handle]];
619
+ JAVASCRIPT
620
+ con_messages = messages.map {|m| Isomorfeus::Puppetmaster::ConsoleMessage.new(m)}
621
+ con_messages.each { |m| raise determine_error(m.text) if m.level == 'error' && !m.text.start_with?('Failed to load resource:') }
622
+ Isomorfeus::Puppetmaster::Document.new(self, handle, Isomorfeus::Puppetmaster::Response.new(response_hash))
623
+ end
624
+
625
+ def document_reload(document)
626
+ response_hash = await"LastResult = await AllPageHandles[#{document.handle}].reload();"
627
+ Isomorfeus::Puppetmaster::Response.new(response_hash)
628
+ end
629
+
630
+ def document_remove_cookie(document, name)
631
+ await "await AllPageHandles[#{document.handle}].deleteCookie({name: '#{name}'})"
632
+ end
633
+
634
+ def document_render_base64(document, **options)
635
+ # todo
636
+ # https://pptr.dev/#?product=Puppeteer&version=v1.12.2&show=api-pagescreenshotoptions
637
+ final_options = ["encoding: 'base64'"]
638
+ if options.has_key?(:format)
639
+ options[:format] = 'jpeg' if options[:format].to_s.downcase == 'jpg'
640
+ final_options << "type: '#{options.delete(:format).to_s.downcase}'"
641
+ end
642
+ final_options << "quality: #{options.delete(:quality)}" if options.has_key?(:quality)
643
+ final_options << "fullPage: #{options.delete(:full)}" if options.has_key?(:full)
644
+ options.each do |k,v|
645
+ final_options << "#{k.to_s.camelize(:lower)}: #{v}"
646
+ end
647
+ await "LastResult = await AllPageHandles[#{document.handle}].screenshot({#{final_options.join(', ')}});"
648
+ end
649
+
650
+ def document_reset_user_agent(document)
651
+ await <<~JAVASCRIPT
652
+ var original_user_agent = await CurrentBrowser.userAgent();
653
+ await AllPageHandles[#{document.handle}].setUserAgent(original_user_agent);
654
+ JAVASCRIPT
655
+ end
656
+
657
+ def document_right_click(document, x: nil, y: nil, modifiers: nil)
658
+ # modifier_keys: :alt, :control, :meta, :shift
659
+ # offset: { x: int, y: int }
660
+ modifiers = [modifiers] if modifiers.is_a?(Symbol)
661
+ modifiers = [] unless modifiers
662
+ modifiers = modifiers.map {|key| key.to_s.camelize(:lower) }
663
+ await <<~JAVASCRIPT
664
+ var response_event_occurred = false;
665
+ var response_handler = function(event){ response_event_occurred = true; };
666
+ var response_watcher = new Promise(function(resolve, reject){
667
+ setTimeout(function(){
668
+ if (!response_event_occurred) { resolve(true); }
669
+ else { setTimeout(function(){ resolve(true); }, #{@puppeteer_timeout}); }
670
+ AllPageHandles[#{document.handle}].removeListener('response', response_handler);
671
+ }, #{@puppeteer_reaction_timeout});
672
+ });
673
+ AllPageHandles[#{document.handle}].on('response', response_handler);
674
+ var navigation_watcher;
675
+ if (BrowserType === 'firefox') {
676
+ navigation_watcher = AllPageHandles[#{document.handle}].waitFor(1000);
677
+ } else {
678
+ navigation_watcher = AllPageHandles[#{document.handle}].waitForNavigation();
679
+ }
680
+ await AllPageHandles[#{document.handle}].evaluate(function(){
681
+ var options = {button: 2, bubbles: true, cancelable: true};
682
+ var node = window;
683
+ var x = #{x ? x : 'null'};
684
+ var y = #{y ? y : 'null'};
685
+ var modifiers = #{modifiers};
686
+ if (x && y) {
687
+ options['clientX'] = x;
688
+ options['clientY'] = y;
689
+ var n = document.elementFromPoint(x, y);
690
+ if (n) { node = n };
691
+ }
692
+ if (modifiers.includes('meta')) { options['metaKey'] = true; }
693
+ if (modifiers.includes('control')) { options['ctrlKey'] = true; }
694
+ if (modifiers.includes('shift')) { options['shiftKey'] = true; }
695
+ if (modifiers.includes('alt')) { options['altKey'] = true; }
696
+ node.dispatchEvent(new MouseEvent('mousedown', options));
697
+ node.dispatchEvent(new MouseEvent('mouseup', options));
698
+ node.dispatchEvent(new MouseEvent('contextmenu', options));
699
+ });
700
+ await Promise.race([response_watcher, navigation_watcher]);
701
+ JAVASCRIPT
702
+ end
703
+
704
+ def document_save_pdf(document, path, **options)
705
+ # todo
706
+ # https://pptr.dev/#?product=Puppeteer&version=v1.12.2&show=api-pagepdfoptions
707
+ absolute_path = File.absolute_path(path)
708
+ final_options = ["path: '#{absolute_path}'"]
709
+ final_options << "format: '#{options.delete(:format)}'" if options.has_key?(:format)
710
+ final_options << "headerTemplate: `#{options.delete(:header_template)}`" if options.has_key?(:header_template)
711
+ final_options << "footerTemplate: `#{options.delete(:footer_template)}`" if options.has_key?(:footer_template)
712
+ final_options << "pageRanges: '#{options.delete(:page_ranges)}'" if options.has_key?(:page_ranges)
713
+ final_options << "width: '#{options.delete(:width)}'" if options.has_key?(:width)
714
+ final_options << "height: '#{options.delete(:height)}'" if options.has_key?(:height)
715
+ options.each do |k,v|
716
+ final_options << "#{k.to_s.camelize(:lower)}: #{v}"
717
+ end
718
+ await "await AllPageHandles[#{document.handle}].pdf({#{final_options.join(', ')}});"
719
+ end
720
+
721
+ def document_save_screenshot(document, path, **options)
722
+ # todo
723
+ # https://pptr.dev/#?product=Puppeteer&version=v1.12.2&show=api-pagescreenshotoptions
724
+ absolute_path = File.absolute_path(path)
725
+ final_options = ["path: '#{absolute_path}'"]
726
+ if options.has_key?(:format)
727
+ options[:format] = 'jpeg' if options[:format].to_s.downcase == 'jpg'
728
+ final_options << "type: '#{options.delete(:format).to_s.downcase}'"
729
+ end
730
+ final_options << "quality: #{options.delete(:quality)}" if options.has_key?(:quality)
731
+ final_options << "fullPage: #{options.delete(:full)}" if options.has_key?(:full)
732
+ options.each do |k,v|
733
+ final_options << "#{k.to_s.camelize(:lower)}: #{v}"
734
+ end
735
+ await "await AllPageHandles[#{document.handle}].screenshot({#{final_options.join(', ')}});"
736
+ end
737
+
738
+ def document_scroll_by(document, x, y)
739
+ await "await AllPageHandles[#{document.handle}].evaluate('window.scrollBy(#{x}, #{y})');"
740
+ end
741
+
742
+ def document_scroll_to(document, x, y)
743
+ await "await AllPageHandles[#{document.handle}].evaluate('window.scrollTo(#{x}, #{y})');"
744
+ end
745
+
746
+ def document_set_authentication_credentials(document, username, password)
747
+ await "await AllPageHandles[#{document.handle}].authenticate({username: '#{username}', password: '#{password}'});"
748
+ end
749
+
750
+ def document_set_cookie(document, name, value, **options)
751
+ options[:name] ||= name
752
+ options[:value] ||= value
753
+ uri = document_url(document)
754
+ if uri == 'about:blank'
755
+ uri = if Isomorfeus::Puppetmaster.server_host
756
+ u = URI.new
757
+ u.scheme = Isomorfeus::Puppetmaster.server_scheme if Isomorfeus::Puppetmaster.server_scheme
758
+ u.host = Isomorfeus::Puppetmaster.server_host
759
+ u.to_s
760
+ else
761
+ 'http://127.0.0.1'
762
+ end
763
+ end
764
+ options[:domain] ||= URI.parse(uri).host
765
+ final_options = []
766
+ final_options << "expires: #{options.delete(:expires).to_i}" if options.has_key?(:expires)
767
+ final_options << "httpOnly: #{options.delete(:http_only)}" if options.has_key?(:http_only)
768
+ final_options << "secure: #{options.delete(:secure)}" if options.has_key?(:secure)
769
+ final_options << "sameSite: '#{options.delete(:same_site)}'" if options.has_key?(:same_site)
770
+ options.each do |k,v|
771
+ final_options << "#{k}: '#{v}'"
772
+ end
773
+ await "await AllPageHandles[#{document.handle}].setCookie({#{final_options.join(', ')}});"
774
+ end
775
+
776
+ def document_set_extra_headers(document, headers_hash)
777
+ await "await AllPageHandles[#{document.handle}].setExtraHTTPHeaders({#{headers_hash.map { |k, v| "'#{k}': '#{v}'" }.join(', ')}});"
778
+ end
779
+
780
+ def document_set_url_blacklist(document, url_array)
781
+ # https://www.chromium.org/administrators/url-blacklist-filter-format
782
+ @url_blacklist = url_array
783
+ await <<~JAVASCRIPT
784
+ if (!(BrowserType === 'firefox')) {
785
+ var cdp_session = await AllPageHandles[#{document.handle}].target().createCDPSession();
786
+ await cdp_session.send('Network.setBlockedURLs', {urls: #{url_array}});
787
+ await cdp_session.detach();
788
+ }
789
+ JAVASCRIPT
790
+ end
791
+
792
+ def document_set_user_agent(document, agent_string)
793
+ await "await AllPageHandles[#{document.handle}].setUserAgent('#{agent_string}');"
794
+ end
795
+
796
+ def document_title(document)
797
+ await "LastResult = await AllPageHandles[#{document.handle}].title();"
798
+ end
799
+
800
+ def document_type_keys(document, *keys)
801
+ cjs = "await AllPageHandles[#{document.handle}].bringToFront();"
802
+ top_modifiers = []
803
+ keys.each do |key|
804
+ if key.is_a?(String)
805
+ key.each_char do |c|
806
+ need_shift = /[[:upper:]]/.match(c)
807
+ cjs << "await AllPageHandles[#{document.handle}].keyboard.down('Shift');\n" if need_shift
808
+ c = "Key#{c.upcase}" if /[[:alpha:]]/.match(c)
809
+ cjs << "await AllPageHandles[#{document.handle}].keyboard.down('#{c}');\n"
810
+ cjs << "await AllPageHandles[#{document.handle}].keyboard.up('#{c}');\n"
811
+ cjs << "await AllPageHandles[#{document.handle}].keyboard.up('Shift');\n" if need_shift
812
+ end
813
+ elsif key.is_a?(Symbol)
814
+ if %i[ctrl Ctrl].include?(key)
815
+ key = :control
816
+ elsif %i[command Command Meta].include?(key)
817
+ key = :meta
818
+ elsif %i[divide Divide].include?(key)
819
+ key = :numpad_divide
820
+ elsif %i[decimal Decimal].include?(key)
821
+ key = :numpad_decimal
822
+ elsif %i[left right up down].include?(key)
823
+ key = "arrow_#{key}".to_sym
824
+ end
825
+ if %i[alt alt_left alt_right control control_left control_rigth meta meta_left meta_right shift shift_left shift_right].include?(key)
826
+ top_modifiers << key
827
+ cjs << "await AllPageHandles[#{document.handle}].keyboard.down('#{key.to_s.camelize}');\n"
828
+ else
829
+ cjs << "await AllPageHandles[#{document.handle}].keyboard.press('#{key.to_s.camelize}');\n"
830
+ end
831
+ elsif key.is_a?(Array)
832
+ modifiers = []
833
+ key.each do |k|
834
+ if k.is_a?(Symbol)
835
+ if %i[ctrl Ctrl].include?(k)
836
+ k = :control
837
+ elsif %i[command Command Meta].include?(k)
838
+ k = :meta
839
+ elsif %i[divide Divide].include?(k)
840
+ k = :numpad_divide
841
+ elsif %i[decimal Decimal].include?(k)
842
+ k = :numpad_decimal
843
+ elsif %i[left right up down].include?(key)
844
+ k = "arrow_#{key}".to_sym
845
+ end
846
+ if %i[alt alt_left alt_right control control_left control_rigth meta meta_left meta_right shift shift_left shift_right].include?(k)
847
+ modifiers << k
848
+ cjs << "await AllPageHandles[#{document.handle}].keyboard.down('#{k.to_s.camelize}');\n"
849
+ else
850
+ cjs << "await AllPageHandles[#{document.handle}].keyboard.press('#{k.to_s.camelize}');\n"
851
+ end
852
+ elsif k.is_a?(String)
853
+ k.each_char do |c|
854
+ need_shift = /[[:upper:]]/.match(c)
855
+ cjs << "await AllPageHandles[#{document.handle}].keyboard.down('Shift');\n" if need_shift
856
+ c = "Key#{c.upcase}" if /[[:alpha:]]/.match(c)
857
+ cjs << "await AllPageHandles[#{document.handle}].keyboard.press('#{c}');\n"
858
+ cjs << "await AllPageHandles[#{document.handle}].keyboard.up('Shift');\n" if need_shift
859
+ end
860
+ end
861
+ end
862
+ modifiers.reverse.each do |k|
863
+ cjs << "await AllPageHandles[#{document.handle}].keyboard.up('#{k.to_s.camelize}');\n"
864
+ end
865
+ end
866
+ end
867
+ top_modifiers.reverse.each do |key|
868
+ cjs << "await AllPageHandles[#{document.handle}].keyboard.up('#{key.to_s.camelize}');\n"
869
+ end
870
+ await(cjs)
871
+ end
872
+
873
+ def document_url(document)
874
+ await "LastResult = await AllPageHandles[#{document.handle}].evaluate('window.location.href');"
875
+ end
876
+
877
+ def document_user_agent(document)
878
+ await "LastResult = await AllPageHandles[#{document.handle}].evaluate('window.navigator.userAgent');"
879
+ end
880
+
881
+ def document_viewport_maximize(document)
882
+ document_viewport_resize(document, @max_width, @max_height)
883
+ end
884
+
885
+ def document_viewport_resize(document, width, height)
886
+ width = @max_width if width > @max_width
887
+ height = @max_height if width > @max_height
888
+ await "await AllPageHandles[#{document.handle}].setViewport({width: #{width}, height: #{height}});"
889
+ end
890
+
891
+ def document_viewport_size(document)
892
+ viewport = @context.eval "AllPageHandles[#{document.handle}].viewport()"
893
+ [viewport['width'], viewport['height']]
894
+ end
895
+
896
+ def document_wait_for(document, selector)
897
+ js_escaped_selector = selector.gsub('\\', '\\\\\\').gsub('"', '\"')
898
+ node_data = await <<~JAVASCRIPT
899
+ var element_handle = await AllPageHandles[#{document.handle}].waitForSelector("#{js_escaped_selector}");
900
+ if (element_handle) {
901
+ var node_handle = RegisterElementHandle(element_handle);
902
+ var tt = await AllElementHandles[node_handle].executionContext().evaluate((node) => {
903
+ var tag = node.tagName.toLowerCase();
904
+ var type = null;
905
+ if (tag === 'input') { type = node.getAttribute('type'); }
906
+ return [tag, type, node.isContentEditable];
907
+ }, AllElementHandles[node_handle]);
908
+ LastResult = {handle: node_handle, tag: tt[0], type: tt[1], content_editable: tt[2]};
909
+ }
910
+ JAVASCRIPT
911
+ if node_data
912
+ node_data[:css_selector] = selector
913
+ Isomorfeus::Puppetmaster::Node.new_by_tag(self, document, node_data)
914
+ else
915
+ raise Isomorfeus::Puppetmaster::ElementNotFound.new(selector)
916
+ end
917
+ end
918
+
919
+ def document_wait_for_xpath(document, query)
920
+ js_escaped_query = query.gsub('\\', '\\\\\\').gsub('"', '\"')
921
+ node_data = await <<~JAVASCRIPT
922
+ var element_handle = await AllPageHandles[#{document.handle}].waitForXPath("#{js_escaped_query}");
923
+ if (element_handle) {
924
+ var node_handle = RegisterElementHandle(element_handle);
925
+ var tt = await AllElementHandles[node_handle].executionContext().evaluate((node) => {
926
+ var tag = node.tagName.toLowerCase();
927
+ var type = null;
928
+ if (tag === 'input') { type = node.getAttribute('type'); }
929
+ return [tag, type, node.isContentEditable];
930
+ }, AllElementHandles[node_handle]);
931
+ LastResult = {handle: node_handle, tag: tt[0], type: tt[1], content_editable: tt[2]};
932
+ }
933
+ JAVASCRIPT
934
+ if node_data
935
+ node_data[:xpath_query] = query
936
+ Isomorfeus::Puppetmaster::Node.new_by_tag(self, document, node_data)
937
+ else
938
+ raise Isomorfeus::Puppetmaster::ElementNotFound.new(selector)
939
+ end
940
+ end
941
+ end
942
+ end
943
+ end
944
+ end