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