puppeteer-bidi 0.0.1.beta1
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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +13 -0
- data/CLAUDE/README.md +158 -0
- data/CLAUDE/async_programming.md +158 -0
- data/CLAUDE/click_implementation.md +340 -0
- data/CLAUDE/core_layer_gotchas.md +136 -0
- data/CLAUDE/error_handling.md +232 -0
- data/CLAUDE/file_chooser.md +95 -0
- data/CLAUDE/frame_architecture.md +346 -0
- data/CLAUDE/javascript_evaluation.md +341 -0
- data/CLAUDE/jshandle_implementation.md +505 -0
- data/CLAUDE/keyboard_implementation.md +250 -0
- data/CLAUDE/mouse_implementation.md +140 -0
- data/CLAUDE/navigation_waiting.md +234 -0
- data/CLAUDE/porting_puppeteer.md +214 -0
- data/CLAUDE/query_handler.md +194 -0
- data/CLAUDE/rspec_pending_vs_skip.md +262 -0
- data/CLAUDE/selector_evaluation.md +198 -0
- data/CLAUDE/test_server_routes.md +263 -0
- data/CLAUDE/testing_strategy.md +236 -0
- data/CLAUDE/two_layer_architecture.md +180 -0
- data/CLAUDE/wrapped_element_click.md +247 -0
- data/CLAUDE.md +185 -0
- data/LICENSE.txt +21 -0
- data/README.md +488 -0
- data/Rakefile +21 -0
- data/lib/puppeteer/bidi/async_utils.rb +151 -0
- data/lib/puppeteer/bidi/browser.rb +285 -0
- data/lib/puppeteer/bidi/browser_context.rb +53 -0
- data/lib/puppeteer/bidi/browser_launcher.rb +240 -0
- data/lib/puppeteer/bidi/connection.rb +182 -0
- data/lib/puppeteer/bidi/core/README.md +169 -0
- data/lib/puppeteer/bidi/core/browser.rb +230 -0
- data/lib/puppeteer/bidi/core/browsing_context.rb +601 -0
- data/lib/puppeteer/bidi/core/disposable.rb +69 -0
- data/lib/puppeteer/bidi/core/errors.rb +64 -0
- data/lib/puppeteer/bidi/core/event_emitter.rb +83 -0
- data/lib/puppeteer/bidi/core/navigation.rb +128 -0
- data/lib/puppeteer/bidi/core/realm.rb +315 -0
- data/lib/puppeteer/bidi/core/request.rb +300 -0
- data/lib/puppeteer/bidi/core/session.rb +153 -0
- data/lib/puppeteer/bidi/core/user_context.rb +208 -0
- data/lib/puppeteer/bidi/core/user_prompt.rb +102 -0
- data/lib/puppeteer/bidi/core.rb +45 -0
- data/lib/puppeteer/bidi/deserializer.rb +132 -0
- data/lib/puppeteer/bidi/element_handle.rb +602 -0
- data/lib/puppeteer/bidi/errors.rb +42 -0
- data/lib/puppeteer/bidi/file_chooser.rb +52 -0
- data/lib/puppeteer/bidi/frame.rb +597 -0
- data/lib/puppeteer/bidi/http_response.rb +23 -0
- data/lib/puppeteer/bidi/injected.js +1 -0
- data/lib/puppeteer/bidi/injected_source.rb +21 -0
- data/lib/puppeteer/bidi/js_handle.rb +302 -0
- data/lib/puppeteer/bidi/keyboard.rb +265 -0
- data/lib/puppeteer/bidi/lazy_arg.rb +23 -0
- data/lib/puppeteer/bidi/mouse.rb +170 -0
- data/lib/puppeteer/bidi/page.rb +613 -0
- data/lib/puppeteer/bidi/query_handler.rb +397 -0
- data/lib/puppeteer/bidi/realm.rb +242 -0
- data/lib/puppeteer/bidi/serializer.rb +139 -0
- data/lib/puppeteer/bidi/target.rb +81 -0
- data/lib/puppeteer/bidi/task_manager.rb +44 -0
- data/lib/puppeteer/bidi/timeout_settings.rb +20 -0
- data/lib/puppeteer/bidi/transport.rb +129 -0
- data/lib/puppeteer/bidi/version.rb +7 -0
- data/lib/puppeteer/bidi/wait_task.rb +322 -0
- data/lib/puppeteer/bidi.rb +49 -0
- data/scripts/update_injected_source.rb +57 -0
- data/sig/puppeteer/bidi/browser.rbs +80 -0
- data/sig/puppeteer/bidi/element_handle.rbs +238 -0
- data/sig/puppeteer/bidi/frame.rbs +205 -0
- data/sig/puppeteer/bidi/js_handle.rbs +90 -0
- data/sig/puppeteer/bidi/page.rbs +247 -0
- data/sig/puppeteer/bidi.rbs +15 -0
- metadata +176 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
require 'singleton'
|
|
2
|
+
|
|
3
|
+
module Puppeteer
|
|
4
|
+
module Bidi
|
|
5
|
+
# @api private
|
|
6
|
+
class QueryHandler
|
|
7
|
+
include Singleton
|
|
8
|
+
|
|
9
|
+
QUERY_SEPARATORS = %w[= /].freeze
|
|
10
|
+
BUILTIN_QUERY_HANDLERS = {
|
|
11
|
+
'aria' => 'ARIAQueryHandler',
|
|
12
|
+
'pierce' => 'PierceQueryHandler',
|
|
13
|
+
'xpath' => 'XPathQueryHandler',
|
|
14
|
+
'text' => 'TextQueryHandler'
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
Result = Data.define(:updated_selector, :polling, :query_handler)
|
|
18
|
+
|
|
19
|
+
def get_query_handler_and_selector(selector)
|
|
20
|
+
builtin_query_handler_entries.each do |name, handler|
|
|
21
|
+
if (result = detect_handler_from_selector(name, handler, selector))
|
|
22
|
+
return result
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
analyze_default_query_handler(selector)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
class SelectorAnalysis
|
|
32
|
+
attr_reader :selector
|
|
33
|
+
|
|
34
|
+
def initialize(selector)
|
|
35
|
+
@selector = selector
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def requires_raf_polling?
|
|
39
|
+
pseudo_class_present?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def pseudo_class_present?
|
|
45
|
+
in_string = nil
|
|
46
|
+
escape = false
|
|
47
|
+
bracket_depth = 0
|
|
48
|
+
|
|
49
|
+
selector.each_char.with_index do |char, index|
|
|
50
|
+
if escape
|
|
51
|
+
escape = false
|
|
52
|
+
next
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
if in_string
|
|
56
|
+
if char == '\\'
|
|
57
|
+
escape = true
|
|
58
|
+
elsif char == in_string
|
|
59
|
+
in_string = nil
|
|
60
|
+
end
|
|
61
|
+
next
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
case char
|
|
65
|
+
when '"', "'"
|
|
66
|
+
in_string = char
|
|
67
|
+
when '['
|
|
68
|
+
bracket_depth += 1
|
|
69
|
+
when ']'
|
|
70
|
+
bracket_depth -= 1 if bracket_depth.positive?
|
|
71
|
+
when '\\'
|
|
72
|
+
escape = true
|
|
73
|
+
when ':'
|
|
74
|
+
next_char = selector[index + 1]
|
|
75
|
+
next if next_char == ':'
|
|
76
|
+
return true if bracket_depth.zero?
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
false
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def builtin_query_handler_entries
|
|
85
|
+
Enumerator.new do |y|
|
|
86
|
+
BUILTIN_QUERY_HANDLERS.each do |name, const_name|
|
|
87
|
+
if (handler = resolve_handler_constant(const_name))
|
|
88
|
+
y << [name, handler]
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def detect_handler_from_selector(name, handler, selector)
|
|
95
|
+
QUERY_SEPARATORS.each do |separator|
|
|
96
|
+
prefix = "#{name}#{separator}"
|
|
97
|
+
next unless selector.start_with?(prefix)
|
|
98
|
+
|
|
99
|
+
updated_selector = selector[prefix.length..]
|
|
100
|
+
return Result.new(
|
|
101
|
+
updated_selector: updated_selector,
|
|
102
|
+
polling: (name == 'aria' ? 'raf' : 'mutation'),
|
|
103
|
+
query_handler: handler,
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
nil
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def analyze_default_query_handler(selector)
|
|
111
|
+
analysis = SelectorAnalysis.new(selector)
|
|
112
|
+
polling = analysis.requires_raf_polling? ? 'raf' : 'mutation'
|
|
113
|
+
|
|
114
|
+
Result.new(
|
|
115
|
+
updated_selector: selector,
|
|
116
|
+
polling: polling,
|
|
117
|
+
query_handler: resolve_handler_constant('CSSQueryHandler'),
|
|
118
|
+
)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def default_query_handler_result(selector)
|
|
122
|
+
Result.new(
|
|
123
|
+
updated_selector: selector,
|
|
124
|
+
polling: 'mutation',
|
|
125
|
+
query_handler: resolve_handler_constant('CSSQueryHandler'),
|
|
126
|
+
)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def resolve_handler_constant(const_name)
|
|
130
|
+
return const_name if const_name.is_a?(Module)
|
|
131
|
+
|
|
132
|
+
Puppeteer::Bidi.const_get(const_name, false)
|
|
133
|
+
rescue NameError
|
|
134
|
+
nil
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def build_query_handler_result(handler, selector, handler_name)
|
|
138
|
+
{
|
|
139
|
+
updated_selector: selector,
|
|
140
|
+
polling: (handler_name == 'aria' ? 'raf' : 'mutation'),
|
|
141
|
+
query_handler: handler,
|
|
142
|
+
}
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
class BaseQueryHandler
|
|
147
|
+
# Query for a single element matching the selector
|
|
148
|
+
# @param element [ElementHandle] Element to query from
|
|
149
|
+
# @param selector [String] Selector to match
|
|
150
|
+
# @return [ElementHandle, nil] Found element or nil
|
|
151
|
+
def run_query_one(element, selector)
|
|
152
|
+
realm = element.frame.isolated_realm
|
|
153
|
+
|
|
154
|
+
# Adopt the element into the isolated realm first.
|
|
155
|
+
# This ensures the realm is valid and triggers puppeteer_util reset if needed
|
|
156
|
+
# after navigation (mirrors Puppeteer's @bindIsolatedHandle decorator pattern).
|
|
157
|
+
adopted_element = realm.adopt_handle(element)
|
|
158
|
+
|
|
159
|
+
result = realm.call_function(
|
|
160
|
+
query_one_script,
|
|
161
|
+
false,
|
|
162
|
+
arguments: [
|
|
163
|
+
Serializer.serialize(realm.puppeteer_util_lazy_arg),
|
|
164
|
+
adopted_element.remote_value,
|
|
165
|
+
Serializer.serialize(selector)
|
|
166
|
+
]
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
return nil if result['type'] == 'exception'
|
|
170
|
+
|
|
171
|
+
result_value = result['result']
|
|
172
|
+
return nil if result_value['type'] == 'null' || result_value['type'] == 'undefined'
|
|
173
|
+
|
|
174
|
+
handle = JSHandle.from(result_value, realm.core_realm)
|
|
175
|
+
return nil unless handle.is_a?(ElementHandle)
|
|
176
|
+
|
|
177
|
+
element.frame.main_realm.transfer_handle(handle)
|
|
178
|
+
ensure
|
|
179
|
+
adopted_element&.dispose
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Query for all elements matching the selector
|
|
183
|
+
# @param element [ElementHandle] Element to query from
|
|
184
|
+
# @param selector [String] Selector to match
|
|
185
|
+
# @return [Array<ElementHandle>] Array of found elements
|
|
186
|
+
def run_query_all(element, selector)
|
|
187
|
+
realm = element.frame.isolated_realm
|
|
188
|
+
|
|
189
|
+
# Adopt the element into the isolated realm first.
|
|
190
|
+
# This ensures the realm is valid and triggers puppeteer_util reset if needed
|
|
191
|
+
# after navigation (mirrors Puppeteer's @bindIsolatedHandle decorator pattern).
|
|
192
|
+
adopted_element = realm.adopt_handle(element)
|
|
193
|
+
|
|
194
|
+
result = realm.call_function(
|
|
195
|
+
query_all_script,
|
|
196
|
+
true,
|
|
197
|
+
arguments: [
|
|
198
|
+
Serializer.serialize(realm.puppeteer_util_lazy_arg),
|
|
199
|
+
adopted_element.remote_value,
|
|
200
|
+
Serializer.serialize(selector)
|
|
201
|
+
]
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
return [] if result['type'] == 'exception'
|
|
205
|
+
|
|
206
|
+
result_value = result['result']
|
|
207
|
+
return [] unless result_value['type'] == 'array'
|
|
208
|
+
|
|
209
|
+
handles = result_value['value'].map do |element_value|
|
|
210
|
+
JSHandle.from(element_value, realm.core_realm)
|
|
211
|
+
end.select { |h| h.is_a?(ElementHandle) }
|
|
212
|
+
|
|
213
|
+
handles.map { |h| element.frame.main_realm.transfer_handle(h) }
|
|
214
|
+
ensure
|
|
215
|
+
adopted_element&.dispose
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def wait_for(element_or_frame, selector, visible: nil, hidden: nil, timeout: nil, polling: nil, &block)
|
|
219
|
+
if element_or_frame.is_a?(Frame)
|
|
220
|
+
wait_for_in_frame(element_or_frame, nil, selector, visible: visible, hidden: hidden, timeout: timeout, polling: polling, &block)
|
|
221
|
+
elsif element_or_frame.is_a?(ElementHandle)
|
|
222
|
+
frame = element_or_frame.frame
|
|
223
|
+
root = frame.isolated_realm.adopt_handle(element_or_frame)
|
|
224
|
+
wait_for_in_frame(frame, root, selector, visible: visible, hidden: hidden, timeout: timeout, polling: polling, &block)
|
|
225
|
+
else
|
|
226
|
+
raise ArgumentError, "Unsupported query root: #{element_or_frame.class}"
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
private
|
|
231
|
+
|
|
232
|
+
def query_one_script
|
|
233
|
+
raise NotImplementedError, "#{self.class}#query_one_script must be implemented"
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def query_all_script
|
|
237
|
+
raise NotImplementedError, "#{self.class}#query_all_script must be implemented"
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def wait_for_selector_script
|
|
241
|
+
raise NotImplementedError, "#{self.class}#wait_for_selector_script must be implemented"
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def wait_for_in_frame(frame, root, selector, visible:, hidden:, timeout:, polling:, &block)
|
|
245
|
+
raise FrameDetachedError if frame.detached?
|
|
246
|
+
|
|
247
|
+
visibility = if visible
|
|
248
|
+
true
|
|
249
|
+
elsif hidden
|
|
250
|
+
false
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
resolved_polling = (visible || hidden ? 'raf' : polling)
|
|
254
|
+
|
|
255
|
+
options = {}
|
|
256
|
+
options[:polling] = resolved_polling if resolved_polling
|
|
257
|
+
options[:timeout] = timeout if timeout
|
|
258
|
+
|
|
259
|
+
begin
|
|
260
|
+
handle = frame.isolated_realm.wait_for_function(
|
|
261
|
+
wait_for_selector_script,
|
|
262
|
+
options,
|
|
263
|
+
frame.isolated_realm.puppeteer_util_lazy_arg,
|
|
264
|
+
selector,
|
|
265
|
+
root,
|
|
266
|
+
visibility,
|
|
267
|
+
&block
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
return nil unless handle
|
|
271
|
+
|
|
272
|
+
unless handle.is_a?(ElementHandle)
|
|
273
|
+
begin
|
|
274
|
+
handle.dispose
|
|
275
|
+
rescue StandardError
|
|
276
|
+
# Ignored: primitive handles may not support dispose.
|
|
277
|
+
end
|
|
278
|
+
return nil
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
frame.main_realm.transfer_handle(handle)
|
|
282
|
+
rescue Puppeteer::Bidi::TimeoutError => e
|
|
283
|
+
raise Puppeteer::Bidi::TimeoutError,
|
|
284
|
+
"Waiting for selector `#{selector}` failed: Waiting failed: #{e.message.split(': ').last}"
|
|
285
|
+
rescue StandardError => e
|
|
286
|
+
message = "Waiting for selector `#{selector}` failed"
|
|
287
|
+
alias_selector = selector.sub('//*', '//')
|
|
288
|
+
if alias_selector != selector
|
|
289
|
+
message = "#{message} | alias: Waiting for selector `#{alias_selector}` failed"
|
|
290
|
+
end
|
|
291
|
+
raise StandardError.new(message), cause: e
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
class CSSQueryHandler < BaseQueryHandler
|
|
297
|
+
private
|
|
298
|
+
|
|
299
|
+
def query_one_script
|
|
300
|
+
<<~JAVASCRIPT
|
|
301
|
+
(PuppeteerUtil, element, selector) => {
|
|
302
|
+
return PuppeteerUtil.cssQuerySelector(element, selector);
|
|
303
|
+
}
|
|
304
|
+
JAVASCRIPT
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def query_all_script
|
|
308
|
+
<<~JAVASCRIPT
|
|
309
|
+
async (PuppeteerUtil, element, selector) => {
|
|
310
|
+
return [...PuppeteerUtil.cssQuerySelectorAll(element, selector)];
|
|
311
|
+
}
|
|
312
|
+
JAVASCRIPT
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def wait_for_selector_script
|
|
316
|
+
<<~JAVASCRIPT
|
|
317
|
+
(PuppeteerUtil, selector, root, visibility) => {
|
|
318
|
+
const element = PuppeteerUtil.cssQuerySelector(root || document, selector);
|
|
319
|
+
return PuppeteerUtil.checkVisibility(element, visibility === null ? undefined : visibility);
|
|
320
|
+
}
|
|
321
|
+
JAVASCRIPT
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
class XPathQueryHandler < BaseQueryHandler
|
|
326
|
+
private
|
|
327
|
+
|
|
328
|
+
def query_one_script
|
|
329
|
+
<<~JAVASCRIPT
|
|
330
|
+
(PuppeteerUtil, element, selector) => {
|
|
331
|
+
for (const result of PuppeteerUtil.xpathQuerySelectorAll(element, selector, 1)) {
|
|
332
|
+
return result;
|
|
333
|
+
}
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
JAVASCRIPT
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def query_all_script
|
|
340
|
+
<<~JAVASCRIPT
|
|
341
|
+
async (PuppeteerUtil, element, selector) => {
|
|
342
|
+
return [...PuppeteerUtil.xpathQuerySelectorAll(element, selector)];
|
|
343
|
+
}
|
|
344
|
+
JAVASCRIPT
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def wait_for_selector_script
|
|
348
|
+
<<~JAVASCRIPT
|
|
349
|
+
(PuppeteerUtil, selector, root, visibility) => {
|
|
350
|
+
let element = null;
|
|
351
|
+
for (const result of PuppeteerUtil.xpathQuerySelectorAll(root || document, selector, 1)) {
|
|
352
|
+
element = result;
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
return PuppeteerUtil.checkVisibility(element, visibility === null ? undefined : visibility);
|
|
356
|
+
}
|
|
357
|
+
JAVASCRIPT
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
class TextQueryHandler < BaseQueryHandler
|
|
362
|
+
private
|
|
363
|
+
|
|
364
|
+
def query_one_script
|
|
365
|
+
<<~JAVASCRIPT
|
|
366
|
+
(PuppeteerUtil, element, selector) => {
|
|
367
|
+
for (const result of PuppeteerUtil.textQuerySelectorAll(element, selector)) {
|
|
368
|
+
return result;
|
|
369
|
+
}
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
JAVASCRIPT
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def query_all_script
|
|
376
|
+
<<~JAVASCRIPT
|
|
377
|
+
async (PuppeteerUtil, element, selector) => {
|
|
378
|
+
return [...PuppeteerUtil.textQuerySelectorAll(element, selector)];
|
|
379
|
+
}
|
|
380
|
+
JAVASCRIPT
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def wait_for_selector_script
|
|
384
|
+
<<~JAVASCRIPT
|
|
385
|
+
(PuppeteerUtil, selector, root, visibility) => {
|
|
386
|
+
let element = null;
|
|
387
|
+
for (const result of PuppeteerUtil.textQuerySelectorAll(root || document, selector)) {
|
|
388
|
+
element = result;
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
return PuppeteerUtil.checkVisibility(element, visibility === null ? undefined : visibility);
|
|
392
|
+
}
|
|
393
|
+
JAVASCRIPT
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
end
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Puppeteer
|
|
4
|
+
module Bidi
|
|
5
|
+
# Base realm abstraction that mirrors Puppeteer's Realm class hierarchy.
|
|
6
|
+
# Provides shared lifecycle management for WaitTask instances.
|
|
7
|
+
# https://github.com/puppeteer/puppeteer/blob/main/packages/puppeteer-core/src/api/Realm.ts
|
|
8
|
+
class Realm
|
|
9
|
+
attr_reader :task_manager
|
|
10
|
+
|
|
11
|
+
def initialize(timeout_settings)
|
|
12
|
+
@timeout_settings = timeout_settings
|
|
13
|
+
@task_manager = TaskManager.new
|
|
14
|
+
@disposed = false
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def environment
|
|
18
|
+
raise NotImplementedError, 'Subclass must expose its environment object'
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def page
|
|
22
|
+
env = environment
|
|
23
|
+
return env.page if env.respond_to?(:page)
|
|
24
|
+
|
|
25
|
+
raise NotImplementedError, 'Environment must expose a page reference'
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def default_timeout
|
|
29
|
+
@timeout_settings.timeout
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def wait_for_function(page_function, options = {}, *args, &block)
|
|
33
|
+
ensure_environment_active!
|
|
34
|
+
|
|
35
|
+
polling = options[:polling] || 'raf'
|
|
36
|
+
if polling.is_a?(Numeric) && polling < 0
|
|
37
|
+
raise ArgumentError, "Cannot poll with non-positive interval: #{polling}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
timeout = options.key?(:timeout) ? options[:timeout] : default_timeout
|
|
41
|
+
wait_task_options = {
|
|
42
|
+
polling: polling,
|
|
43
|
+
timeout: timeout,
|
|
44
|
+
root: options[:root]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
result = WaitTask.new(self, wait_task_options, page_function, *args).result
|
|
48
|
+
|
|
49
|
+
Async(&block).wait if block
|
|
50
|
+
|
|
51
|
+
result.wait
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def dispose
|
|
55
|
+
return if @disposed
|
|
56
|
+
|
|
57
|
+
@disposed = true
|
|
58
|
+
@task_manager.terminate_all(Error.new('waitForFunction failed: frame got detached.'))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def disposed?
|
|
62
|
+
@disposed
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Adopt a handle from another realm into this realm.
|
|
66
|
+
# Mirrors Puppeteer's BidiRealm#adoptHandle implementation.
|
|
67
|
+
# @param handle [JSHandle] The handle to adopt
|
|
68
|
+
# @return [JSHandle] Handle that belongs to this realm
|
|
69
|
+
def adopt_handle(handle)
|
|
70
|
+
raise ArgumentError, 'handle must be a JSHandle' unless handle.is_a?(JSHandle)
|
|
71
|
+
|
|
72
|
+
evaluate_handle('(node) => node', handle)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Transfer a handle into this realm, disposing of the original.
|
|
76
|
+
# Mirrors Puppeteer's BidiRealm#transferHandle implementation.
|
|
77
|
+
# @param handle [JSHandle] Handle that may belong to another realm
|
|
78
|
+
# @return [JSHandle] Handle adopted into this realm
|
|
79
|
+
def transfer_handle(handle)
|
|
80
|
+
raise ArgumentError, 'handle must be a JSHandle' unless handle.is_a?(JSHandle)
|
|
81
|
+
|
|
82
|
+
return handle if handle.realm.equal?(self)
|
|
83
|
+
|
|
84
|
+
adopted = adopt_handle(handle)
|
|
85
|
+
handle.dispose
|
|
86
|
+
adopted
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def ensure_environment_active!
|
|
92
|
+
env = environment
|
|
93
|
+
return unless env.respond_to?(:detached?)
|
|
94
|
+
|
|
95
|
+
raise FrameDetachedError if env.detached?
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Concrete realm that wraps the default window realm for a Frame.
|
|
100
|
+
# https://github.com/puppeteer/puppeteer/blob/main/packages/puppeteer-core/src/bidi/Realm.ts
|
|
101
|
+
class FrameRealm < Realm
|
|
102
|
+
attr_reader :core_realm
|
|
103
|
+
|
|
104
|
+
def initialize(frame, core_realm)
|
|
105
|
+
@frame = frame
|
|
106
|
+
@core_realm = core_realm
|
|
107
|
+
@puppeteer_util_handle = nil
|
|
108
|
+
@puppeteer_util_lazy_arg = nil
|
|
109
|
+
super(frame.page.timeout_settings)
|
|
110
|
+
|
|
111
|
+
setup_core_realm_callbacks
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def environment
|
|
115
|
+
@frame
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def page
|
|
119
|
+
@frame.page
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def evaluate(script, *args)
|
|
123
|
+
ensure_environment_active!
|
|
124
|
+
|
|
125
|
+
result = execute_with_core(script, args).wait
|
|
126
|
+
handle_evaluation_exception(result) if result['type'] == 'exception'
|
|
127
|
+
|
|
128
|
+
actual_result = result['result'] || result
|
|
129
|
+
Deserializer.deserialize(actual_result)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def evaluate_handle(script, *args)
|
|
133
|
+
ensure_environment_active!
|
|
134
|
+
|
|
135
|
+
result = execute_with_core(script, args).wait
|
|
136
|
+
handle_evaluation_exception(result) if result['type'] == 'exception'
|
|
137
|
+
|
|
138
|
+
JSHandle.from(result['result'], @core_realm)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def call_function(function_declaration, await_promise, **options)
|
|
142
|
+
ensure_environment_active!
|
|
143
|
+
|
|
144
|
+
result = @core_realm.call_function(function_declaration, await_promise, **options).wait
|
|
145
|
+
handle_evaluation_exception(result) if result['type'] == 'exception'
|
|
146
|
+
|
|
147
|
+
result
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def puppeteer_util
|
|
151
|
+
return @puppeteer_util_handle if @puppeteer_util_handle
|
|
152
|
+
|
|
153
|
+
script = "(function() { const module = { exports: {} }; #{PUPPETEER_INJECTED_SOURCE}; return module.exports.default; })()"
|
|
154
|
+
@puppeteer_util_handle = evaluate_handle(script)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def puppeteer_util_lazy_arg
|
|
158
|
+
@puppeteer_util_lazy_arg ||= LazyArg.create { puppeteer_util }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def reset_puppeteer_util_handle!
|
|
162
|
+
handle = @puppeteer_util_handle
|
|
163
|
+
@puppeteer_util_handle = nil
|
|
164
|
+
@puppeteer_util_lazy_arg = nil
|
|
165
|
+
return unless handle
|
|
166
|
+
|
|
167
|
+
begin
|
|
168
|
+
handle.dispose
|
|
169
|
+
rescue StandardError
|
|
170
|
+
# The realm might already be gone; ignore cleanup failures.
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def wait_for_function(page_function, options = {}, *args, &block)
|
|
175
|
+
raise FrameDetachedError if @frame.detached?
|
|
176
|
+
|
|
177
|
+
super
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def dispose
|
|
181
|
+
reset_puppeteer_util_handle!
|
|
182
|
+
super
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
private
|
|
186
|
+
|
|
187
|
+
def execute_with_core(script, args)
|
|
188
|
+
script_trimmed = script.strip
|
|
189
|
+
|
|
190
|
+
is_iife = script_trimmed.match?(/\)\s*\(\s*\)\s*\z/)
|
|
191
|
+
is_function = !is_iife && (
|
|
192
|
+
script_trimmed.match?(/\A\s*(?:async\s+)?(?:\(.*?\)|[a-zA-Z_$][\w$]*)\s*=>/) ||
|
|
193
|
+
script_trimmed.match?(/\A\s*(?:async\s+)?function\s*\w*\s*\(/)
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
if is_function
|
|
197
|
+
serialized_args = args.map { |arg| Serializer.serialize(arg) }
|
|
198
|
+
options = {}
|
|
199
|
+
options[:arguments] = serialized_args unless serialized_args.empty?
|
|
200
|
+
@core_realm.call_function(script_trimmed, true, **options)
|
|
201
|
+
else
|
|
202
|
+
@core_realm.evaluate(script_trimmed, true)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def handle_evaluation_exception(result)
|
|
207
|
+
exception_details = result['exceptionDetails']
|
|
208
|
+
return unless exception_details
|
|
209
|
+
|
|
210
|
+
text = exception_details['text'] || 'Evaluation failed'
|
|
211
|
+
exception = exception_details['exception']
|
|
212
|
+
|
|
213
|
+
error_message = text
|
|
214
|
+
|
|
215
|
+
if exception && exception['type'] != 'error'
|
|
216
|
+
thrown_value = Deserializer.deserialize(exception)
|
|
217
|
+
error_message = "Evaluation failed: #{thrown_value}"
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
raise error_message
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def setup_core_realm_callbacks
|
|
224
|
+
@core_realm.environment = @frame if @core_realm.respond_to?(:environment=)
|
|
225
|
+
|
|
226
|
+
destroyed_listener = proc do |payload|
|
|
227
|
+
reason = payload.is_a?(Hash) ? payload[:reason] : payload
|
|
228
|
+
task_manager.terminate_all(Error.new(reason || 'Realm destroyed'))
|
|
229
|
+
dispose
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
updated_listener = proc do
|
|
233
|
+
reset_puppeteer_util_handle!
|
|
234
|
+
task_manager.rerun_all
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
@core_realm.on(:destroyed, &destroyed_listener)
|
|
238
|
+
@core_realm.on(:updated, &updated_listener)
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|