puppeteer-ruby 0.0.26 → 0.31.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.
@@ -0,0 +1,51 @@
1
+ class Puppeteer::CustomQueryHandler
2
+ # @param query_one [String] JS function (element: Element | Document, selector: string) => Element | null;
3
+ # @param query_all [String] JS function (element: Element | Document, selector: string) => Element[] | NodeListOf<Element>;
4
+ def initialize(query_one: nil, query_all: nil)
5
+ @query_one = query_one
6
+ @query_all = query_all
7
+ end
8
+
9
+ def query_one(element, selector)
10
+ unless @query_one
11
+ raise NotImplementedError.new("#{self.class}##{__method__} is not implemented.")
12
+ end
13
+
14
+ handle = element.evaluate_handle(@query_one, selector)
15
+ element = handle.as_element
16
+
17
+ if element
18
+ return element
19
+ end
20
+ handle.dispose
21
+ nil
22
+ end
23
+
24
+ def wait_for(dom_world, selector, visible: nil, hidden: nil, timeout: nil)
25
+ unless @query_one
26
+ raise NotImplementedError.new("#{self.class}##{__method__} is not implemented.")
27
+ end
28
+
29
+ dom_world.send(:wait_for_selector_in_page, @query_one, selector, visible: visible, hidden: hidden, timeout: timeout)
30
+ end
31
+
32
+ def query_all(element, selector)
33
+ unless @query_all
34
+ raise NotImplementedError.new("#{self.class}##{__method__} is not implemented.")
35
+ end
36
+
37
+ handles = element.evaluate_handle(@query_all, selector)
38
+ properties = handles.properties
39
+ handles.dispose
40
+ properties.values.map(&:as_element).compact
41
+ end
42
+
43
+ def query_all_array(element, selector)
44
+ unless @query_all
45
+ raise NotImplementedError.new("#{self.class}##{__method__} is not implemented.")
46
+ end
47
+
48
+ handles = element.evaluate_handle(@query_all, selector)
49
+ handles.evaluate_handle('(res) => Array.from(res)')
50
+ end
51
+ end
@@ -10,12 +10,21 @@ module Puppeteer::DefineAsyncMethod
10
10
  end
11
11
 
12
12
  original_method = instance_method(async_method_name[6..-1])
13
- define_method(async_method_name) do |*args|
14
- Concurrent::Promises.future do
15
- original_method.bind(self).call(*args)
16
- rescue => err
17
- Logger.new($stderr).warn(err)
18
- raise err
13
+ define_method(async_method_name) do |*args, **kwargs|
14
+ if kwargs.empty? # for Ruby < 2.7
15
+ Concurrent::Promises.future do
16
+ original_method.bind(self).call(*args)
17
+ rescue => err
18
+ Logger.new($stderr).warn(err)
19
+ raise err
20
+ end
21
+ else
22
+ Concurrent::Promises.future do
23
+ original_method.bind(self).call(*args, **kwargs)
24
+ rescue => err
25
+ Logger.new($stderr).warn(err)
26
+ raise err
27
+ end
19
28
  end
20
29
  end
21
30
  end
@@ -4,6 +4,47 @@ require 'thread'
4
4
  class Puppeteer::DOMWorld
5
5
  using Puppeteer::DefineAsyncMethod
6
6
 
7
+ class BindingFunction
8
+ def initialize(name:, proc:)
9
+ @name = name
10
+ @proc = proc
11
+ end
12
+
13
+ def call(*args)
14
+ @proc.call(*args)
15
+ end
16
+
17
+ attr_reader :name
18
+
19
+ def page_binding_init_string
20
+ <<~JAVASCRIPT
21
+ (type, bindingName) => {
22
+ /* Cast window to any here as we're about to add properties to it
23
+ * via win[bindingName] which TypeScript doesn't like.
24
+ */
25
+ const win = window;
26
+ const binding = win[bindingName];
27
+
28
+ win[bindingName] = (...args) => {
29
+ const me = window[bindingName];
30
+ let callbacks = me.callbacks;
31
+ if (!callbacks) {
32
+ callbacks = new Map();
33
+ me.callbacks = callbacks;
34
+ }
35
+ const seq = (me.lastSeq || 0) + 1;
36
+ me.lastSeq = seq;
37
+ const promise = new Promise((resolve, reject) =>
38
+ callbacks.set(seq, { resolve, reject })
39
+ );
40
+ binding(JSON.stringify({ type, name: bindingName, seq, args }));
41
+ return promise;
42
+ };
43
+ }
44
+ JAVASCRIPT
45
+ end
46
+ end
47
+
7
48
  # @param {!Puppeteer.FrameManager} frameManager
8
49
  # @param {!Puppeteer.Frame} frame
9
50
  # @param {!Puppeteer.TimeoutSettings} timeoutSettings
@@ -13,19 +54,29 @@ class Puppeteer::DOMWorld
13
54
  @timeout_settings = timeout_settings
14
55
  @context_promise = resolvable_future
15
56
  @wait_tasks = Set.new
57
+ @bound_functions = {}
58
+ @ctx_bindings = Set.new
16
59
  @detached = false
60
+
61
+ frame_manager.client.on_event('Runtime.bindingCalled', &method(:handle_binding_called))
17
62
  end
18
63
 
19
64
  attr_reader :frame
20
65
 
21
66
  # only used in Puppeteer::WaitTask#initialize
22
- def _wait_tasks
67
+ private def _wait_tasks
23
68
  @wait_tasks
24
69
  end
25
70
 
71
+ # only used in Puppeteer::WaitTask#initialize
72
+ private def _bound_functions
73
+ @bound_functions
74
+ end
75
+
26
76
  # @param context [Puppeteer::ExecutionContext]
27
77
  def context=(context)
28
78
  if context
79
+ @ctx_bindings.clear
29
80
  unless @context_promise.resolved?
30
81
  @context_promise.fulfill(context)
31
82
  end
@@ -75,12 +126,13 @@ class Puppeteer::DOMWorld
75
126
  execution_context.evaluate(page_function, *args)
76
127
  end
77
128
 
78
- # `$()` in JavaScript. $ is not allowed to use as a method name in Ruby.
129
+ # `$()` in JavaScript.
79
130
  # @param {string} selector
80
131
  # @return {!Promise<?Puppeteer.ElementHandle>}
81
- def S(selector)
82
- document.S(selector)
132
+ def query_selector(selector)
133
+ document.query_selector(selector)
83
134
  end
135
+ alias_method :S, :query_selector
84
136
 
85
137
  private def evaluate_document
86
138
  # sometimes execution_context.evaluate_handle('document') returns null object.
@@ -107,30 +159,33 @@ class Puppeteer::DOMWorld
107
159
  document.Sx(expression)
108
160
  end
109
161
 
110
- # `$eval()` in JavaScript. $ is not allowed to use as a method name in Ruby.
162
+ # `$eval()` in JavaScript.
111
163
  # @param {string} selector
112
164
  # @param {Function|string} pageFunction
113
165
  # @param {!Array<*>} args
114
166
  # @return {!Promise<(!Object|undefined)>}
115
- def Seval(selector, page_function, *args)
116
- document.Seval(selector, page_function, *args)
167
+ def eval_on_selector(selector, page_function, *args)
168
+ document.eval_on_selector(selector, page_function, *args)
117
169
  end
170
+ alias_method :Seval, :eval_on_selector
118
171
 
119
- # `$$eval()` in JavaScript. $ is not allowed to use as a method name in Ruby.
172
+ # `$$eval()` in JavaScript.
120
173
  # @param {string} selector
121
174
  # @param {Function|string} pageFunction
122
175
  # @param {!Array<*>} args
123
176
  # @return {!Promise<(!Object|undefined)>}
124
- def SSeval(selector, page_function, *args)
125
- document.SSeval(selector, page_function, *args)
177
+ def eval_on_selector_all(selector, page_function, *args)
178
+ document.eval_on_selector_all(selector, page_function, *args)
126
179
  end
180
+ alias_method :SSeval, :eval_on_selector_all
127
181
 
128
- # `$$()` in JavaScript. $ is not allowed to use as a method name in Ruby.
182
+ # `$$()` in JavaScript.
129
183
  # @param {string} selector
130
184
  # @return {!Promise<!Array<!Puppeteer.ElementHandle>>}
131
- def SS(selector)
132
- document.SS(selector)
185
+ def query_selector_all(selector)
186
+ document.query_selector_all(selector)
133
187
  end
188
+ alias_method :SS, :query_selector_all
134
189
 
135
190
  # @return [String]
136
191
  def content
@@ -175,144 +230,127 @@ class Puppeteer::DOMWorld
175
230
  end
176
231
  end
177
232
 
178
- # /**
179
- # * @param {!{url?: string, path?: string, content?: string, type?: string}} options
180
- # * @return {!Promise<!Puppeteer.ElementHandle>}
181
- # */
182
- # async addScriptTag(options) {
183
- # const {
184
- # url = null,
185
- # path = null,
186
- # content = null,
187
- # type = ''
188
- # } = options;
189
- # if (url !== null) {
190
- # try {
191
- # const context = await this.executionContext();
192
- # return (await context.evaluateHandle(addScriptUrl, url, type)).asElement();
193
- # } catch (error) {
194
- # throw new Error(`Loading script from ${url} failed`);
195
- # }
196
- # }
197
-
198
- # if (path !== null) {
199
- # let contents = await readFileAsync(path, 'utf8');
200
- # contents += '//# sourceURL=' + path.replace(/\n/g, '');
201
- # const context = await this.executionContext();
202
- # return (await context.evaluateHandle(addScriptContent, contents, type)).asElement();
203
- # }
204
-
205
- # if (content !== null) {
206
- # const context = await this.executionContext();
207
- # return (await context.evaluateHandle(addScriptContent, content, type)).asElement();
208
- # }
209
-
210
- # throw new Error('Provide an object with a `url`, `path` or `content` property');
211
-
212
- # /**
213
- # * @param {string} url
214
- # * @param {string} type
215
- # * @return {!Promise<!HTMLElement>}
216
- # */
217
- # async function addScriptUrl(url, type) {
218
- # const script = document.createElement('script');
219
- # script.src = url;
220
- # if (type)
221
- # script.type = type;
222
- # const promise = new Promise((res, rej) => {
223
- # script.onload = res;
224
- # script.onerror = rej;
225
- # });
226
- # document.head.appendChild(script);
227
- # await promise;
228
- # return script;
229
- # }
230
-
231
- # /**
232
- # * @param {string} content
233
- # * @param {string} type
234
- # * @return {!HTMLElement}
235
- # */
236
- # function addScriptContent(content, type = 'text/javascript') {
237
- # const script = document.createElement('script');
238
- # script.type = type;
239
- # script.text = content;
240
- # let error = null;
241
- # script.onerror = e => error = e;
242
- # document.head.appendChild(script);
243
- # if (error)
244
- # throw error;
245
- # return script;
246
- # }
247
- # }
233
+ # @param url [String?]
234
+ # @param path [String?]
235
+ # @param content [String?]
236
+ # @param type [String?]
237
+ def add_script_tag(url: nil, path: nil, content: nil, type: nil)
238
+ if url
239
+ begin
240
+ return execution_context.
241
+ evaluate_handle(ADD_SCRIPT_URL, url, type || '').
242
+ as_element
243
+ rescue Puppeteer::ExecutionContext::EvaluationError # for Chrome
244
+ raise "Loading script from #{url} failed"
245
+ rescue Puppeteer::Connection::ProtocolError # for Firefox
246
+ raise "Loading script from #{url} failed"
247
+ end
248
+ end
248
249
 
249
- # /**
250
- # * @param {!{url?: string, path?: string, content?: string}} options
251
- # * @return {!Promise<!Puppeteer.ElementHandle>}
252
- # */
253
- # async addStyleTag(options) {
254
- # const {
255
- # url = null,
256
- # path = null,
257
- # content = null
258
- # } = options;
259
- # if (url !== null) {
260
- # try {
261
- # const context = await this.executionContext();
262
- # return (await context.evaluateHandle(addStyleUrl, url)).asElement();
263
- # } catch (error) {
264
- # throw new Error(`Loading style from ${url} failed`);
265
- # }
266
- # }
267
-
268
- # if (path !== null) {
269
- # let contents = await readFileAsync(path, 'utf8');
270
- # contents += '/*# sourceURL=' + path.replace(/\n/g, '') + '*/';
271
- # const context = await this.executionContext();
272
- # return (await context.evaluateHandle(addStyleContent, contents)).asElement();
273
- # }
274
-
275
- # if (content !== null) {
276
- # const context = await this.executionContext();
277
- # return (await context.evaluateHandle(addStyleContent, content)).asElement();
278
- # }
279
-
280
- # throw new Error('Provide an object with a `url`, `path` or `content` property');
281
-
282
- # /**
283
- # * @param {string} url
284
- # * @return {!Promise<!HTMLElement>}
285
- # */
286
- # async function addStyleUrl(url) {
287
- # const link = document.createElement('link');
288
- # link.rel = 'stylesheet';
289
- # link.href = url;
290
- # const promise = new Promise((res, rej) => {
291
- # link.onload = res;
292
- # link.onerror = rej;
293
- # });
294
- # document.head.appendChild(link);
295
- # await promise;
296
- # return link;
297
- # }
298
-
299
- # /**
300
- # * @param {string} content
301
- # * @return {!Promise<!HTMLElement>}
302
- # */
303
- # async function addStyleContent(content) {
304
- # const style = document.createElement('style');
305
- # style.type = 'text/css';
306
- # style.appendChild(document.createTextNode(content));
307
- # const promise = new Promise((res, rej) => {
308
- # style.onload = res;
309
- # style.onerror = rej;
310
- # });
311
- # document.head.appendChild(style);
312
- # await promise;
313
- # return style;
314
- # }
315
- # }
250
+ if path
251
+ contents = File.read(path)
252
+ contents += "//# sourceURL=#{path.gsub(/\n/, '')}"
253
+ return execution_context.
254
+ evaluate_handle(ADD_SCRIPT_CONTENT, contents, type || '').
255
+ as_element
256
+ end
257
+
258
+ if content
259
+ return execution_context.
260
+ evaluate_handle(ADD_SCRIPT_CONTENT, content, type || '').
261
+ as_element
262
+ end
263
+
264
+ raise ArgumentError.new('Provide an object with a `url`, `path` or `content` property')
265
+ end
266
+
267
+ ADD_SCRIPT_URL = <<~JAVASCRIPT
268
+ async (url, type) => {
269
+ const script = document.createElement('script');
270
+ script.src = url;
271
+ if (type)
272
+ script.type = type;
273
+ const promise = new Promise((res, rej) => {
274
+ script.onload = res;
275
+ script.onerror = rej;
276
+ });
277
+ document.head.appendChild(script);
278
+ await promise;
279
+ return script;
280
+ }
281
+ JAVASCRIPT
282
+
283
+ ADD_SCRIPT_CONTENT = <<~JAVASCRIPT
284
+ (content, type) => {
285
+ if (type === undefined) type = 'text/javascript';
286
+ const script = document.createElement('script');
287
+ script.type = type;
288
+ script.text = content;
289
+ let error = null;
290
+ script.onerror = e => error = e;
291
+ document.head.appendChild(script);
292
+ if (error)
293
+ throw error;
294
+ return script;
295
+ }
296
+ JAVASCRIPT
297
+
298
+ # @param url [String?]
299
+ # @param path [String?]
300
+ # @param content [String?]
301
+ def add_style_tag(url: nil, path: nil, content: nil)
302
+ if url
303
+ begin
304
+ return execution_context.evaluate_handle(ADD_STYLE_URL, url).as_element
305
+ rescue Puppeteer::ExecutionContext::EvaluationError # for Chrome
306
+ raise "Loading style from #{url} failed"
307
+ rescue Puppeteer::Connection::ProtocolError # for Firefox
308
+ raise "Loading style from #{url} failed"
309
+ end
310
+ end
311
+
312
+ if path
313
+ contents = File.read(path)
314
+ contents += "/*# sourceURL=#{path.gsub(/\n/, '')}*/"
315
+ return execution_context.evaluate_handle(ADD_STYLE_CONTENT, contents).as_element
316
+ end
317
+
318
+ if content
319
+ return execution_context.evaluate_handle(ADD_STYLE_CONTENT, content).as_element
320
+ end
321
+
322
+ raise ArgumentError.new('Provide an object with a `url`, `path` or `content` property')
323
+ end
324
+
325
+ ADD_STYLE_URL = <<~JAVASCRIPT
326
+ async (url) => {
327
+ const link = document.createElement('link');
328
+ link.rel = 'stylesheet';
329
+ link.href = url;
330
+ const promise = new Promise((res, rej) => {
331
+ link.onload = res;
332
+ link.onerror = rej;
333
+ });
334
+ document.head.appendChild(link);
335
+ await promise;
336
+ return link;
337
+ }
338
+ JAVASCRIPT
339
+
340
+ ADD_STYLE_CONTENT = <<~JAVASCRIPT
341
+ async (content) => {
342
+ const style = document.createElement('style');
343
+ style.type = 'text/css';
344
+ style.appendChild(document.createTextNode(content));
345
+ const promise = new Promise((res, rej) => {
346
+ style.onload = res;
347
+ style.onerror = rej;
348
+ });
349
+ document.head.appendChild(style);
350
+ await promise;
351
+ return style;
352
+ }
353
+ JAVASCRIPT
316
354
 
317
355
  class ElementNotFoundError < StandardError
318
356
  def initialize(selector)
@@ -325,14 +363,14 @@ class Puppeteer::DOMWorld
325
363
  # @param button [String] "left"|"right"|"middle"
326
364
  # @param click_count [Number]
327
365
  def click(selector, delay: nil, button: nil, click_count: nil)
328
- handle = S(selector) or raise ElementNotFoundError.new(selector)
366
+ handle = query_selector(selector) or raise ElementNotFoundError.new(selector)
329
367
  handle.click(delay: delay, button: button, click_count: click_count)
330
368
  handle.dispose
331
369
  end
332
370
 
333
371
  # @param selector [String]
334
372
  def focus(selector)
335
- handle = S(selector) or raise ElementNotFoundError.new(selector)
373
+ handle = query_selector(selector) or raise ElementNotFoundError.new(selector)
336
374
  handle.focus
337
375
  handle.dispose
338
376
  end
@@ -350,7 +388,7 @@ class Puppeteer::DOMWorld
350
388
  # @param selector [String]
351
389
  # @return [Array<String>]
352
390
  def select(selector, *values)
353
- handle = S(selector) or raise ElementNotFoundError.new(selector)
391
+ handle = query_selector(selector) or raise ElementNotFoundError.new(selector)
354
392
  result = handle.select(*values)
355
393
  handle.dispose
356
394
 
@@ -359,7 +397,7 @@ class Puppeteer::DOMWorld
359
397
 
360
398
  # @param selector [String]
361
399
  def tap(selector)
362
- handle = S(selector) or raise ElementNotFoundError.new(selector)
400
+ handle = query_selector(selector) or raise ElementNotFoundError.new(selector)
363
401
  handle.tap
364
402
  handle.dispose
365
403
  end
@@ -368,7 +406,7 @@ class Puppeteer::DOMWorld
368
406
  # @param text [String]
369
407
  # @param delay [Number]
370
408
  def type_text(selector, text, delay: nil)
371
- handle = S(selector) or raise ElementNotFoundError.new(selector)
409
+ handle = query_selector(selector) or raise ElementNotFoundError.new(selector)
372
410
  handle.type_text(text, delay: delay)
373
411
  handle.dispose
374
412
  end
@@ -378,61 +416,127 @@ class Puppeteer::DOMWorld
378
416
  # @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
379
417
  # @param timeout [Integer]
380
418
  def wait_for_selector(selector, visible: nil, hidden: nil, timeout: nil)
381
- wait_for_selector_or_xpath(selector, false, visible: visible, hidden: hidden, timeout: timeout)
419
+ # call wait_for_selector_in_page with custom query selector.
420
+ query_selector_manager = Puppeteer::QueryHandlerManager.instance
421
+ query_selector_manager.detect_query_handler(selector).wait_for(self, visible: visible, hidden: hidden, timeout: timeout)
382
422
  end
383
423
 
384
- # @param xpath [String]
385
- # @param visible [Boolean] Wait for element visible (not 'display: none' nor 'visibility: hidden') on true. default to false.
386
- # @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
387
- # @param timeout [Integer]
388
- def wait_for_xpath(xpath, visible: nil, hidden: nil, timeout: nil)
389
- wait_for_selector_or_xpath(xpath, true, visible: visible, hidden: hidden, timeout: timeout)
424
+ private def binding_identifier(name, context)
425
+ "#{name}_#{context.send(:_context_id)}"
390
426
  end
391
427
 
392
- # /**
393
- # * @param {Function|string} pageFunction
394
- # * @param {!{polling?: string|number, timeout?: number}=} options
395
- # * @return {!Promise<!Puppeteer.JSHandle>}
396
- # */
397
- # waitForFunction(pageFunction, options = {}, ...args) {
398
- # const {
399
- # polling = 'raf',
400
- # timeout = this._timeoutSettings.timeout(),
401
- # } = options;
402
- # return new WaitTask(this, pageFunction, 'function', polling, timeout, ...args).promise;
403
- # }
404
428
 
405
- # @param page_function [String]
406
- # @param args [Array]
407
- # @param polling [Integer|String]
429
+ def add_binding_to_context(context, binding_function)
430
+ return if @ctx_bindings.include?(binding_identifier(binding_function.name, context))
431
+
432
+ expression = binding_function.page_binding_init_string
433
+ begin
434
+ context.client.send_message('Runtime.addBinding',
435
+ name: binding_function.name,
436
+ executionContextName: context.send(:_context_name))
437
+ context.evaluate(expression, 'internal', binding_function.name)
438
+ rescue => err
439
+ # We could have tried to evaluate in a context which was already
440
+ # destroyed. This happens, for example, if the page is navigated while
441
+ # we are trying to add the binding
442
+ allowed = [
443
+ 'Execution context was destroyed',
444
+ 'Cannot find context with specified id',
445
+ ]
446
+ if allowed.any? { |msg| err.message.include?(msg) }
447
+ # ignore
448
+ else
449
+ raise
450
+ end
451
+ end
452
+ @ctx_bindings << binding_identifier(binding_function.name, context)
453
+ end
454
+
455
+ private def handle_binding_called(event)
456
+ return unless has_context?
457
+ payload = JSON.parse(event['payload']) rescue nil
458
+ name = payload['name']
459
+ args = payload['args']
460
+
461
+ # The binding was either called by something in the page or it was
462
+ # called before our wrapper was initialized.
463
+ return unless payload
464
+ return unless payload['type'] == 'internal'
465
+ context = execution_context
466
+ return unless @ctx_bindings.include?(binding_identifier(name, context))
467
+ return unless context.send(:_context_id) == event['executionContextId']
468
+
469
+ result = @bound_functions[name].call(*args)
470
+ deliver_result_js = <<~JAVASCRIPT
471
+ (name, seq, result) => {
472
+ globalThis[name].callbacks.get(seq).resolve(result);
473
+ globalThis[name].callbacks.delete(seq);
474
+ }
475
+ JAVASCRIPT
476
+
477
+ begin
478
+ context.evaluate(deliver_result_js, name, payload['seq'], result)
479
+ rescue => err
480
+ # The WaitTask may already have been resolved by timing out, or the
481
+ # exection context may have been destroyed.
482
+ # In both caes, the promises above are rejected with a protocol error.
483
+ # We can safely ignores these, as the WaitTask is re-installed in
484
+ # the next execution context if needed.
485
+ return if err.message.include?('Protocol error')
486
+ raise
487
+ end
488
+ end
489
+
490
+ # @param query_one [String] JS function (element: Element | Document, selector: string) => Element | null;
491
+ # @param selector [String]
492
+ # @param visible [Boolean] Wait for element visible (not 'display: none' nor 'visibility: hidden') on true. default to false.
493
+ # @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
408
494
  # @param timeout [Integer]
409
- # @return [Puppeteer::JSHandle]
410
- def wait_for_function(page_function, args: [], polling: nil, timeout: nil)
411
- option_polling = polling || 'raf'
495
+ private def wait_for_selector_in_page(query_one, selector, visible: nil, hidden: nil, timeout: nil, binding_function: nil)
496
+ option_wait_for_visible = visible || false
497
+ option_wait_for_hidden = hidden || false
412
498
  option_timeout = timeout || @timeout_settings.timeout
413
499
 
414
- Puppeteer::WaitTask.new(
500
+ polling =
501
+ if option_wait_for_visible || option_wait_for_hidden
502
+ 'raf'
503
+ else
504
+ 'mutation'
505
+ end
506
+ title = "selector #{selector}#{option_wait_for_hidden ? 'to be hidden' : ''}"
507
+
508
+ selector_predicate = make_predicate_string(
509
+ predicate_arg_def: '(selector, waitForVisible, waitForHidden)',
510
+ predicate_query_handler: query_one,
511
+ async: true,
512
+ predicate_body: <<~JAVASCRIPT
513
+ const node = await predicateQueryHandler(document, selector)
514
+ return checkWaitForOptions(node, waitForVisible, waitForHidden);
515
+ JAVASCRIPT
516
+ )
517
+
518
+ wait_task = Puppeteer::WaitTask.new(
415
519
  dom_world: self,
416
- predicate_body: page_function,
417
- title: 'function',
418
- polling: option_polling,
520
+ predicate_body: selector_predicate,
521
+ title: title,
522
+ polling: polling,
419
523
  timeout: option_timeout,
420
- args: args,
421
- ).await_promise
422
- end
423
-
424
-
425
- # @return [String]
426
- def title
427
- evaluate('() => document.title')
524
+ args: [selector, option_wait_for_visible, option_wait_for_hidden],
525
+ binding_function: binding_function,
526
+ )
527
+ handle = wait_task.await_promise
528
+ unless handle.as_element
529
+ handle.dispose
530
+ return nil
531
+ end
532
+ handle.as_element
428
533
  end
429
534
 
430
- # @param selector_or_xpath [String]
431
- # @param is_xpath [Boolean]
535
+ # @param xpath [String]
432
536
  # @param visible [Boolean] Wait for element visible (not 'display: none' nor 'visibility: hidden') on true. default to false.
433
537
  # @param hidden [Boolean] Wait for element invisible ('display: none' nor 'visibility: hidden') on true. default to false.
434
538
  # @param timeout [Integer]
435
- private def wait_for_selector_or_xpath(selector_or_xpath, is_xpath, visible: nil, hidden: nil, timeout: nil)
539
+ def wait_for_xpath(xpath, visible: nil, hidden: nil, timeout: nil)
436
540
  option_wait_for_visible = visible || false
437
541
  option_wait_for_hidden = hidden || false
438
542
  option_timeout = timeout || @timeout_settings.timeout
@@ -443,15 +547,23 @@ class Puppeteer::DOMWorld
443
547
  else
444
548
  'mutation'
445
549
  end
446
- title = "#{is_xpath ? :XPath : :selector} #{selector_or_xpath}#{option_wait_for_hidden ? 'to be hidden' : ''}"
550
+ title = "XPath #{xpath}#{option_wait_for_hidden ? 'to be hidden' : ''}"
551
+
552
+ xpath_predicate = make_predicate_string(
553
+ predicate_arg_def: '(selector, waitForVisible, waitForHidden)',
554
+ predicate_body: <<~JAVASCRIPT
555
+ const node = document.evaluate(selector, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
556
+ return checkWaitForOptions(node, waitForVisible, waitForHidden);
557
+ JAVASCRIPT
558
+ )
447
559
 
448
560
  wait_task = Puppeteer::WaitTask.new(
449
561
  dom_world: self,
450
- predicate_body: PREDICATE,
562
+ predicate_body: xpath_predicate,
451
563
  title: title,
452
564
  polling: polling,
453
565
  timeout: option_timeout,
454
- args: [selector_or_xpath, is_xpath, option_wait_for_visible, option_wait_for_hidden],
566
+ args: [xpath, option_wait_for_visible, option_wait_for_hidden],
455
567
  )
456
568
  handle = wait_task.await_promise
457
569
  unless handle.as_element
@@ -461,34 +573,66 @@ class Puppeteer::DOMWorld
461
573
  handle.as_element
462
574
  end
463
575
 
464
- PREDICATE = <<~JAVASCRIPT
465
- /**
466
- * @param {string} selectorOrXPath
467
- * @param {boolean} isXPath
468
- * @param {boolean} waitForVisible
469
- * @param {boolean} waitForHidden
470
- * @return {?Node|boolean}
471
- */
472
- function _(selectorOrXPath, isXPath, waitForVisible, waitForHidden) {
473
- const node = isXPath
474
- ? document.evaluate(selectorOrXPath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue
475
- : document.querySelector(selectorOrXPath);
476
- if (!node)
477
- return waitForHidden;
478
- if (!waitForVisible && !waitForHidden)
479
- return node;
480
- const element = /** @type {Element} */ (node.nodeType === Node.TEXT_NODE ? node.parentElement : node);
481
- const style = window.getComputedStyle(element);
482
- const isVisible = style && style.visibility !== 'hidden' && hasVisibleBoundingBox();
483
- const success = (waitForVisible === isVisible || waitForHidden === !isVisible);
484
- return success ? node : null;
485
- /**
486
- * @return {boolean}
487
- */
488
- function hasVisibleBoundingBox() {
489
- const rect = element.getBoundingClientRect();
490
- return !!(rect.top || rect.bottom || rect.width || rect.height);
491
- }
492
- }
493
- JAVASCRIPT
576
+ # @param page_function [String]
577
+ # @param args [Array]
578
+ # @param polling [Integer|String]
579
+ # @param timeout [Integer]
580
+ # @return [Puppeteer::JSHandle]
581
+ def wait_for_function(page_function, args: [], polling: nil, timeout: nil)
582
+ option_polling = polling || 'raf'
583
+ option_timeout = timeout || @timeout_settings.timeout
584
+
585
+ Puppeteer::WaitTask.new(
586
+ dom_world: self,
587
+ predicate_body: page_function,
588
+ title: 'function',
589
+ polling: option_polling,
590
+ timeout: option_timeout,
591
+ args: args,
592
+ ).await_promise
593
+ end
594
+
595
+
596
+ # @return [String]
597
+ def title
598
+ evaluate('() => document.title')
599
+ end
600
+
601
+ private def make_predicate_string(predicate_arg_def:, predicate_body:, predicate_query_handler: nil, async: false)
602
+ predicate_query_handler_string =
603
+ if predicate_query_handler
604
+ "const predicateQueryHandler = #{predicate_query_handler}"
605
+ else
606
+ ""
607
+ end
608
+
609
+ <<~JAVASCRIPT
610
+ #{async ? 'async ' : ''}function _#{predicate_arg_def} {
611
+ #{predicate_query_handler_string}
612
+ #{predicate_body}
613
+
614
+ function checkWaitForOptions(node, waitForVisible, waitForHidden) {
615
+ if (!node) return waitForHidden;
616
+ if (!waitForVisible && !waitForHidden) return node;
617
+ const element =
618
+ node.nodeType === Node.TEXT_NODE ? node.parentElement : node;
619
+
620
+ const style = window.getComputedStyle(element);
621
+ const isVisible =
622
+ style && style.visibility !== 'hidden' && hasVisibleBoundingBox();
623
+ const success =
624
+ waitForVisible === isVisible || waitForHidden === !isVisible;
625
+ return success ? node : null;
626
+
627
+ /**
628
+ * @return {boolean}
629
+ */
630
+ function hasVisibleBoundingBox() {
631
+ const rect = element.getBoundingClientRect();
632
+ return !!(rect.top || rect.bottom || rect.width || rect.height);
633
+ }
634
+ }
635
+ }
636
+ JAVASCRIPT
637
+ end
494
638
  end