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.
- checksums.yaml +4 -4
- data/.circleci/config.yml +32 -22
- data/.github/ISSUE_TEMPLATE/bug_report.md +17 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +15 -0
- data/.github/workflows/docs.yml +2 -2
- data/.github/workflows/reviewdog.yml +4 -1
- data/.github/workflows/windows_check.yml +40 -0
- data/.gitignore +2 -0
- data/CHANGELOG.md +42 -1
- data/Dockerfile +1 -1
- data/README.md +25 -9
- data/lib/puppeteer.rb +3 -0
- data/lib/puppeteer/aria_query_handler.rb +71 -0
- data/lib/puppeteer/browser_runner.rb +1 -1
- data/lib/puppeteer/custom_query_handler.rb +51 -0
- data/lib/puppeteer/define_async_method.rb +15 -6
- data/lib/puppeteer/dom_world.rb +372 -228
- data/lib/puppeteer/element_handle.rb +28 -33
- data/lib/puppeteer/env.rb +4 -0
- data/lib/puppeteer/execution_context.rb +12 -0
- data/lib/puppeteer/frame.rb +31 -24
- data/lib/puppeteer/launcher/base.rb +8 -0
- data/lib/puppeteer/launcher/chrome.rb +4 -1
- data/lib/puppeteer/page.rb +127 -104
- data/lib/puppeteer/query_handler_manager.rb +65 -0
- data/lib/puppeteer/remote_object.rb +12 -0
- data/lib/puppeteer/version.rb +1 -1
- data/lib/puppeteer/wait_task.rb +16 -4
- data/lib/puppeteer/web_socket.rb +2 -0
- data/lib/puppeteer/web_socket_transport.rb +2 -0
- data/puppeteer-ruby.gemspec +5 -4
- metadata +30 -10
@@ -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
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
data/lib/puppeteer/dom_world.rb
CHANGED
@@ -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.
|
129
|
+
# `$()` in JavaScript.
|
79
130
|
# @param {string} selector
|
80
131
|
# @return {!Promise<?Puppeteer.ElementHandle>}
|
81
|
-
def
|
82
|
-
document.
|
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.
|
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
|
116
|
-
document.
|
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.
|
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
|
125
|
-
document.
|
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.
|
182
|
+
# `$$()` in JavaScript.
|
129
183
|
# @param {string} selector
|
130
184
|
# @return {!Promise<!Array<!Puppeteer.ElementHandle>>}
|
131
|
-
def
|
132
|
-
document.
|
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
|
-
#
|
180
|
-
#
|
181
|
-
#
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
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
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
#
|
298
|
-
|
299
|
-
#
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
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 =
|
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 =
|
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 =
|
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 =
|
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 =
|
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
|
-
|
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
|
-
|
385
|
-
|
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
|
-
|
406
|
-
|
407
|
-
|
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
|
-
|
410
|
-
|
411
|
-
|
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
|
-
|
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:
|
417
|
-
title:
|
418
|
-
polling:
|
520
|
+
predicate_body: selector_predicate,
|
521
|
+
title: title,
|
522
|
+
polling: polling,
|
419
523
|
timeout: option_timeout,
|
420
|
-
args:
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
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
|
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
|
-
|
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 = "
|
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:
|
562
|
+
predicate_body: xpath_predicate,
|
451
563
|
title: title,
|
452
564
|
polling: polling,
|
453
565
|
timeout: option_timeout,
|
454
|
-
args: [
|
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
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
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
|