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,597 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
module Puppeteer
|
|
5
|
+
module Bidi
|
|
6
|
+
# Frame represents a frame (main frame or iframe) in the page
|
|
7
|
+
# This is a high-level wrapper around Core::BrowsingContext
|
|
8
|
+
# Following Puppeteer's BidiFrame implementation
|
|
9
|
+
class Frame
|
|
10
|
+
attr_reader :browsing_context #: Core::BrowsingContext
|
|
11
|
+
|
|
12
|
+
# Factory method following Puppeteer's BidiFrame.from pattern
|
|
13
|
+
# @rbs parent: Page | Frame
|
|
14
|
+
# @rbs browsing_context: Core::BrowsingContext
|
|
15
|
+
# @rbs return: Frame
|
|
16
|
+
def self.from(parent, browsing_context)
|
|
17
|
+
frame = new(parent, browsing_context)
|
|
18
|
+
frame.send(:initialize_frame)
|
|
19
|
+
frame
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @rbs parent: Page | Frame
|
|
23
|
+
# @rbs browsing_context: Core::BrowsingContext
|
|
24
|
+
# @rbs return: void
|
|
25
|
+
def initialize(parent, browsing_context)
|
|
26
|
+
@parent = parent
|
|
27
|
+
@browsing_context = browsing_context
|
|
28
|
+
@frames = {} # Map of browsing context id to Frame (like WeakMap in JS)
|
|
29
|
+
|
|
30
|
+
default_core_realm = @browsing_context.default_realm
|
|
31
|
+
internal_core_realm = @browsing_context.create_window_realm("__puppeteer_internal_#{rand(1..10_000)}")
|
|
32
|
+
|
|
33
|
+
@main_realm = FrameRealm.new(self, default_core_realm)
|
|
34
|
+
@isolated_realm = FrameRealm.new(self, internal_core_realm)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @rbs return: FrameRealm
|
|
38
|
+
def main_realm
|
|
39
|
+
@main_realm
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @rbs return: FrameRealm
|
|
43
|
+
def isolated_realm
|
|
44
|
+
@isolated_realm
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Backwards compatibility for call sites that previously accessed Frame#realm.
|
|
48
|
+
# @rbs return: FrameRealm
|
|
49
|
+
def realm
|
|
50
|
+
main_realm
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Get the page that owns this frame
|
|
54
|
+
# Traverses up the parent chain until reaching a Page
|
|
55
|
+
# @rbs return: Page
|
|
56
|
+
def page
|
|
57
|
+
@parent.is_a?(Page) ? @parent : @parent.page
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Get the parent frame
|
|
61
|
+
# @rbs return: Frame?
|
|
62
|
+
def parent_frame
|
|
63
|
+
@parent.is_a?(Frame) ? @parent : nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Evaluate JavaScript in the frame context
|
|
67
|
+
# @rbs script: String
|
|
68
|
+
# @rbs *args: untyped
|
|
69
|
+
# @rbs return: untyped
|
|
70
|
+
def evaluate(script, *args)
|
|
71
|
+
assert_not_detached
|
|
72
|
+
main_realm.evaluate(script, *args)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Evaluate JavaScript and return a handle to the result
|
|
76
|
+
# @rbs script: String
|
|
77
|
+
# @rbs *args: untyped
|
|
78
|
+
# @rbs return: JSHandle
|
|
79
|
+
def evaluate_handle(script, *args)
|
|
80
|
+
assert_not_detached
|
|
81
|
+
main_realm.evaluate_handle(script, *args)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Get the document element handle
|
|
85
|
+
# @rbs return: ElementHandle
|
|
86
|
+
def document
|
|
87
|
+
assert_not_detached
|
|
88
|
+
handle = main_realm.evaluate_handle('document')
|
|
89
|
+
unless handle.is_a?(ElementHandle)
|
|
90
|
+
handle.dispose
|
|
91
|
+
raise 'Failed to get document'
|
|
92
|
+
end
|
|
93
|
+
handle
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Query for an element matching the selector
|
|
97
|
+
# @rbs selector: String
|
|
98
|
+
# @rbs return: ElementHandle?
|
|
99
|
+
def query_selector(selector)
|
|
100
|
+
doc = document
|
|
101
|
+
begin
|
|
102
|
+
doc.query_selector(selector)
|
|
103
|
+
ensure
|
|
104
|
+
doc.dispose
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Query for all elements matching the selector
|
|
109
|
+
# @rbs selector: String
|
|
110
|
+
# @rbs return: Array[ElementHandle]
|
|
111
|
+
def query_selector_all(selector)
|
|
112
|
+
doc = document
|
|
113
|
+
begin
|
|
114
|
+
doc.query_selector_all(selector)
|
|
115
|
+
ensure
|
|
116
|
+
doc.dispose
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Evaluate a function on the first element matching the selector
|
|
121
|
+
# @rbs selector: String
|
|
122
|
+
# @rbs page_function: String
|
|
123
|
+
# @rbs *args: untyped
|
|
124
|
+
# @rbs return: untyped
|
|
125
|
+
def eval_on_selector(selector, page_function, *args)
|
|
126
|
+
doc = document
|
|
127
|
+
begin
|
|
128
|
+
doc.eval_on_selector(selector, page_function, *args)
|
|
129
|
+
ensure
|
|
130
|
+
doc.dispose
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Evaluate a function on all elements matching the selector
|
|
135
|
+
# @rbs selector: String
|
|
136
|
+
# @rbs page_function: String
|
|
137
|
+
# @rbs *args: untyped
|
|
138
|
+
# @rbs return: untyped
|
|
139
|
+
def eval_on_selector_all(selector, page_function, *args)
|
|
140
|
+
doc = document
|
|
141
|
+
begin
|
|
142
|
+
doc.eval_on_selector_all(selector, page_function, *args)
|
|
143
|
+
ensure
|
|
144
|
+
doc.dispose
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Click an element matching the selector
|
|
149
|
+
# @rbs selector: String
|
|
150
|
+
# @rbs button: String
|
|
151
|
+
# @rbs count: Integer
|
|
152
|
+
# @rbs delay: Numeric?
|
|
153
|
+
# @rbs offset: Hash[Symbol, Numeric]?
|
|
154
|
+
# @rbs return: void
|
|
155
|
+
def click(selector, button: 'left', count: 1, delay: nil, offset: nil)
|
|
156
|
+
assert_not_detached
|
|
157
|
+
|
|
158
|
+
handle = query_selector(selector)
|
|
159
|
+
raise SelectorNotFoundError, selector unless handle
|
|
160
|
+
|
|
161
|
+
begin
|
|
162
|
+
handle.click(button: button, count: count, delay: delay, offset: offset)
|
|
163
|
+
ensure
|
|
164
|
+
handle.dispose
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Type text into an element matching the selector
|
|
169
|
+
# @rbs selector: String
|
|
170
|
+
# @rbs text: String
|
|
171
|
+
# @rbs delay: Numeric
|
|
172
|
+
# @rbs return: void
|
|
173
|
+
def type(selector, text, delay: 0)
|
|
174
|
+
assert_not_detached
|
|
175
|
+
|
|
176
|
+
handle = query_selector(selector)
|
|
177
|
+
raise SelectorNotFoundError, selector unless handle
|
|
178
|
+
|
|
179
|
+
begin
|
|
180
|
+
handle.type(text, delay: delay)
|
|
181
|
+
ensure
|
|
182
|
+
handle.dispose
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Hover over an element matching the selector
|
|
187
|
+
# @rbs selector: String
|
|
188
|
+
# @rbs return: void
|
|
189
|
+
def hover(selector)
|
|
190
|
+
assert_not_detached
|
|
191
|
+
|
|
192
|
+
handle = query_selector(selector)
|
|
193
|
+
raise SelectorNotFoundError, selector unless handle
|
|
194
|
+
|
|
195
|
+
begin
|
|
196
|
+
handle.hover
|
|
197
|
+
ensure
|
|
198
|
+
handle.dispose
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Get the frame URL
|
|
203
|
+
# @rbs return: String
|
|
204
|
+
def url
|
|
205
|
+
@browsing_context.url
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Navigate to a URL
|
|
209
|
+
# @rbs url: String
|
|
210
|
+
# @rbs wait_until: String
|
|
211
|
+
# @rbs timeout: Numeric
|
|
212
|
+
# @rbs return: HTTPResponse?
|
|
213
|
+
def goto(url, wait_until: 'load', timeout: 30000)
|
|
214
|
+
response = wait_for_navigation(timeout: timeout, wait_until: wait_until) do
|
|
215
|
+
@browsing_context.navigate(url, wait: 'interactive').wait
|
|
216
|
+
end
|
|
217
|
+
# Return HTTPResponse with the final URL
|
|
218
|
+
# Note: Currently we don't track HTTP status codes from BiDi protocol
|
|
219
|
+
# Assuming successful navigation (200 OK)
|
|
220
|
+
HTTPResponse.new(url: @browsing_context.url, status: 200)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Set frame content
|
|
224
|
+
# @rbs html: String
|
|
225
|
+
# @rbs wait_until: String
|
|
226
|
+
# @rbs return: void
|
|
227
|
+
def set_content(html, wait_until: 'load')
|
|
228
|
+
assert_not_detached
|
|
229
|
+
|
|
230
|
+
# Puppeteer BiDi implementation:
|
|
231
|
+
# await Promise.all([
|
|
232
|
+
# this.setFrameContent(html),
|
|
233
|
+
# firstValueFrom(combineLatest([this.#waitForLoad$(options), this.#waitForNetworkIdle$(options)]))
|
|
234
|
+
# ]);
|
|
235
|
+
|
|
236
|
+
# IMPORTANT: Register listener BEFORE document.write to avoid race condition
|
|
237
|
+
load_event = case wait_until
|
|
238
|
+
when 'load'
|
|
239
|
+
:load
|
|
240
|
+
when 'domcontentloaded'
|
|
241
|
+
:dom_content_loaded
|
|
242
|
+
else
|
|
243
|
+
raise ArgumentError, "Unknown wait_until value: #{wait_until}"
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
promise = Async::Promise.new
|
|
247
|
+
listener = proc { promise.resolve(nil) }
|
|
248
|
+
@browsing_context.once(load_event, &listener)
|
|
249
|
+
|
|
250
|
+
# Execute both operations: document.write AND wait for load
|
|
251
|
+
# Use promise_all to wait for both to complete (like Puppeteer's Promise.all)
|
|
252
|
+
AsyncUtils.await_promise_all(
|
|
253
|
+
-> { set_frame_content(html) },
|
|
254
|
+
promise
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
nil
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Set frame content using document.open/write/close
|
|
261
|
+
# This is a low-level method that doesn't wait for load events
|
|
262
|
+
# @rbs content: String
|
|
263
|
+
# @rbs return: void
|
|
264
|
+
def set_frame_content(content)
|
|
265
|
+
assert_not_detached
|
|
266
|
+
|
|
267
|
+
evaluate(<<~JS, content)
|
|
268
|
+
html => {
|
|
269
|
+
document.open();
|
|
270
|
+
document.write(html);
|
|
271
|
+
document.close();
|
|
272
|
+
}
|
|
273
|
+
JS
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Get the frame name
|
|
277
|
+
# @rbs return: String
|
|
278
|
+
def name
|
|
279
|
+
@_name || ''
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Check if frame is detached
|
|
283
|
+
# @rbs return: bool
|
|
284
|
+
def detached?
|
|
285
|
+
@browsing_context.closed?
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# Get child frames
|
|
289
|
+
# Returns cached frame instances following Puppeteer's pattern
|
|
290
|
+
# @rbs return: Array[Frame]
|
|
291
|
+
def child_frames
|
|
292
|
+
@browsing_context.children.map do |child_context|
|
|
293
|
+
@frames[child_context.id]
|
|
294
|
+
end.compact
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Get the frame element (iframe/frame DOM element) for this frame
|
|
298
|
+
# Returns nil for the main frame
|
|
299
|
+
# Following Puppeteer's Frame.frameElement() implementation exactly
|
|
300
|
+
# @rbs return: ElementHandle?
|
|
301
|
+
def frame_element
|
|
302
|
+
assert_not_detached
|
|
303
|
+
|
|
304
|
+
parent = parent_frame
|
|
305
|
+
return nil unless parent
|
|
306
|
+
|
|
307
|
+
# Query all iframe and frame elements in the parent frame
|
|
308
|
+
list = parent.isolated_realm.evaluate_handle('() => document.querySelectorAll("iframe,frame")')
|
|
309
|
+
|
|
310
|
+
begin
|
|
311
|
+
# Get the array of elements
|
|
312
|
+
length = list.evaluate('list => list.length')
|
|
313
|
+
|
|
314
|
+
length.times do |i|
|
|
315
|
+
iframe = list.evaluate_handle("(list, i) => list[i]", i)
|
|
316
|
+
begin
|
|
317
|
+
# Check if this iframe's content frame matches our frame
|
|
318
|
+
content_frame = iframe.as_element&.content_frame
|
|
319
|
+
if content_frame&.browsing_context&.id == @browsing_context.id
|
|
320
|
+
# Transfer the handle to the main realm (adopt handle)
|
|
321
|
+
# This ensures the returned handle is in the correct execution context
|
|
322
|
+
return parent.main_realm.transfer_handle(iframe.as_element)
|
|
323
|
+
end
|
|
324
|
+
ensure
|
|
325
|
+
iframe.dispose unless iframe.disposed?
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
nil
|
|
330
|
+
ensure
|
|
331
|
+
list.dispose unless list.disposed?
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Wait for navigation to complete
|
|
336
|
+
# @rbs timeout: Numeric
|
|
337
|
+
# @rbs wait_until: String | Array[String]
|
|
338
|
+
# @rbs &block: (-> void)?
|
|
339
|
+
# @rbs return: HTTPResponse?
|
|
340
|
+
def wait_for_navigation(timeout: 30000, wait_until: 'load', &block)
|
|
341
|
+
assert_not_detached
|
|
342
|
+
|
|
343
|
+
# Normalize wait_until to array
|
|
344
|
+
wait_until_array = wait_until.is_a?(Array) ? wait_until : [wait_until]
|
|
345
|
+
|
|
346
|
+
# Separate lifecycle events from network idle events
|
|
347
|
+
lifecycle_events = wait_until_array.select { |e| ['load', 'domcontentloaded'].include?(e) }
|
|
348
|
+
network_idle_events = wait_until_array.select { |e| ['networkidle0', 'networkidle2'].include?(e) }
|
|
349
|
+
|
|
350
|
+
# Default to 'load' if no lifecycle event specified
|
|
351
|
+
lifecycle_events = ['load'] if lifecycle_events.empty? && network_idle_events.any?
|
|
352
|
+
|
|
353
|
+
# Determine which load event to wait for (use the first one)
|
|
354
|
+
load_event = case lifecycle_events.first
|
|
355
|
+
when 'load'
|
|
356
|
+
:load
|
|
357
|
+
when 'domcontentloaded'
|
|
358
|
+
:dom_content_loaded
|
|
359
|
+
else
|
|
360
|
+
:load # Default
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# Use Async::Promise for signaling (Fiber-based, not Thread-based)
|
|
364
|
+
# This avoids race conditions and follows Puppeteer's Promise-based pattern
|
|
365
|
+
promise = Async::Promise.new
|
|
366
|
+
|
|
367
|
+
# Track navigation type for response creation
|
|
368
|
+
navigation_type = nil # :full_page, :fragment, or :history
|
|
369
|
+
navigation_obj = nil # The navigation object we're waiting for
|
|
370
|
+
|
|
371
|
+
# Helper to set up navigation listeners
|
|
372
|
+
setup_navigation_listeners = proc do |navigation|
|
|
373
|
+
navigation_obj = navigation
|
|
374
|
+
navigation_type = :full_page
|
|
375
|
+
|
|
376
|
+
# Set up listeners for navigation completion
|
|
377
|
+
# Listen for fragment, failed, aborted events
|
|
378
|
+
navigation.once(:fragment) do
|
|
379
|
+
promise.resolve(nil) unless promise.resolved?
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
navigation.once(:failed) do
|
|
383
|
+
promise.resolve(nil) unless promise.resolved?
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
navigation.once(:aborted) do
|
|
387
|
+
next if detached?
|
|
388
|
+
promise.resolve(nil) unless promise.resolved?
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# Also listen for load/domcontentloaded events to complete navigation
|
|
392
|
+
@browsing_context.once(load_event) do
|
|
393
|
+
promise.resolve(:full_page) unless promise.resolved?
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# Listen for navigation events from BrowsingContext
|
|
398
|
+
# This follows Puppeteer's pattern: race between 'navigation', 'historyUpdated', and 'fragmentNavigated'
|
|
399
|
+
navigation_listener = proc do |data|
|
|
400
|
+
# Only handle if we haven't already attached to a navigation
|
|
401
|
+
next if navigation_obj
|
|
402
|
+
|
|
403
|
+
navigation = data[:navigation]
|
|
404
|
+
setup_navigation_listeners.call(navigation)
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
history_listener = proc do
|
|
408
|
+
# History API navigations (without Navigation object)
|
|
409
|
+
# Only resolve if we haven't attached to a navigation
|
|
410
|
+
promise.resolve(nil) unless navigation_obj || promise.resolved?
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
fragment_listener = proc do
|
|
414
|
+
# Fragment navigations (anchor links, hash changes)
|
|
415
|
+
# Only resolve if we haven't attached to a navigation
|
|
416
|
+
promise.resolve(nil) unless navigation_obj || promise.resolved?
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
closed_listener = proc do
|
|
420
|
+
# Handle frame detachment by rejecting the promise
|
|
421
|
+
promise.reject(FrameDetachedError.new('Navigating frame was detached')) unless promise.resolved?
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
@browsing_context.on(:navigation, &navigation_listener)
|
|
425
|
+
@browsing_context.on(:history_updated, &history_listener)
|
|
426
|
+
@browsing_context.on(:fragment_navigated, &fragment_listener)
|
|
427
|
+
@browsing_context.once(:closed, &closed_listener)
|
|
428
|
+
|
|
429
|
+
begin
|
|
430
|
+
# CRITICAL: Check for existing navigation BEFORE executing block
|
|
431
|
+
# This follows Puppeteer's pattern where waitForNavigation can attach to
|
|
432
|
+
# an already-started navigation (e.g., when called after goto)
|
|
433
|
+
existing_nav = @browsing_context.navigation
|
|
434
|
+
if existing_nav && !existing_nav.disposed?
|
|
435
|
+
# Attach to the existing navigation
|
|
436
|
+
setup_navigation_listeners.call(existing_nav)
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
# Execute the block if provided (this may trigger navigation)
|
|
440
|
+
# Block executes in the same Fiber context for cooperative multitasking
|
|
441
|
+
Async(&block).wait if block
|
|
442
|
+
|
|
443
|
+
# Wait for navigation with timeout using Async (Fiber-based)
|
|
444
|
+
if network_idle_events.any?
|
|
445
|
+
# Puppeteer's pattern: wait for both navigation completion AND network idle
|
|
446
|
+
# Determine concurrency based on network idle event
|
|
447
|
+
concurrency = network_idle_events.include?('networkidle0') ? 0 : 2
|
|
448
|
+
|
|
449
|
+
# Wait for both navigation and network idle in parallel using promise_all
|
|
450
|
+
navigation_result, _ = AsyncUtils.async_timeout(timeout, -> do
|
|
451
|
+
AsyncUtils.await_promise_all(
|
|
452
|
+
promise,
|
|
453
|
+
-> { page.wait_for_network_idle(idle_time: 500, timeout: timeout, concurrency: concurrency) }
|
|
454
|
+
)
|
|
455
|
+
end).wait
|
|
456
|
+
|
|
457
|
+
result = navigation_result
|
|
458
|
+
else
|
|
459
|
+
# Only wait for navigation
|
|
460
|
+
result = AsyncUtils.async_timeout(timeout, promise).wait
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
# Return HTTPResponse for full page navigation, nil for fragment/history
|
|
464
|
+
if result == :full_page
|
|
465
|
+
HTTPResponse.new(url: @browsing_context.url, status: 200)
|
|
466
|
+
else
|
|
467
|
+
nil
|
|
468
|
+
end
|
|
469
|
+
rescue Async::TimeoutError
|
|
470
|
+
raise Puppeteer::Bidi::TimeoutError, "Navigation timeout of #{timeout}ms exceeded"
|
|
471
|
+
ensure
|
|
472
|
+
# Clean up listeners
|
|
473
|
+
@browsing_context.off(:navigation, &navigation_listener)
|
|
474
|
+
@browsing_context.off(:history_updated, &history_listener)
|
|
475
|
+
@browsing_context.off(:fragment_navigated, &fragment_listener)
|
|
476
|
+
@browsing_context.off(:closed, &closed_listener)
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
# Wait for a function to return a truthy value
|
|
481
|
+
# @rbs page_function: String
|
|
482
|
+
# @rbs options: Hash[Symbol, untyped]
|
|
483
|
+
# @rbs *args: untyped
|
|
484
|
+
# @rbs &block: ((JSHandle) -> void)?
|
|
485
|
+
# @rbs return: JSHandle
|
|
486
|
+
def wait_for_function(page_function, options = {}, *args, &block)
|
|
487
|
+
main_realm.wait_for_function(page_function, options, *args, &block)
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
# Wait for an element matching the selector to appear in the frame
|
|
491
|
+
# @rbs selector: String
|
|
492
|
+
# @rbs visible: bool?
|
|
493
|
+
# @rbs hidden: bool?
|
|
494
|
+
# @rbs timeout: Numeric?
|
|
495
|
+
# @rbs &block: ((ElementHandle?) -> void)?
|
|
496
|
+
# @rbs return: ElementHandle?
|
|
497
|
+
def wait_for_selector(selector, visible: nil, hidden: nil, timeout: nil, &block)
|
|
498
|
+
result = QueryHandler.instance.get_query_handler_and_selector(selector)
|
|
499
|
+
result.query_handler.new.wait_for(self, result.updated_selector, visible: visible, hidden: hidden, polling: result.polling, timeout: timeout, &block)
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
# Set files on an input element
|
|
503
|
+
# @rbs element: ElementHandle
|
|
504
|
+
# @rbs files: Array[String]
|
|
505
|
+
# @rbs return: void
|
|
506
|
+
def set_files(element, files)
|
|
507
|
+
assert_not_detached
|
|
508
|
+
|
|
509
|
+
@browsing_context.set_files(
|
|
510
|
+
element.remote_value_as_shared_reference,
|
|
511
|
+
files
|
|
512
|
+
).wait
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
# Get the frame ID (browsing context ID)
|
|
516
|
+
# Following Puppeteer's _id pattern
|
|
517
|
+
# @rbs return: String
|
|
518
|
+
def _id
|
|
519
|
+
@browsing_context.id
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
private
|
|
523
|
+
|
|
524
|
+
# Initialize the frame by setting up child frame tracking
|
|
525
|
+
# Following Puppeteer's BidiFrame.#initialize pattern exactly
|
|
526
|
+
# @rbs return: void
|
|
527
|
+
def initialize_frame
|
|
528
|
+
# Create Frame objects for existing child contexts
|
|
529
|
+
@browsing_context.children.each do |child_context|
|
|
530
|
+
create_frame_target(child_context)
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
# Listen for new child frames
|
|
534
|
+
@browsing_context.on(:browsingcontext) do |data|
|
|
535
|
+
create_frame_target(data[:browsing_context])
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
# Emit framedetached when THIS frame's browsing context is closed
|
|
539
|
+
# Following Puppeteer's pattern: this.browsingContext.on('closed', () => {
|
|
540
|
+
# this.page().trustedEmitter.emit(PageEvent.FrameDetached, this);
|
|
541
|
+
# });
|
|
542
|
+
@browsing_context.on(:closed) do
|
|
543
|
+
@frames.clear
|
|
544
|
+
page.emit(:framedetached, self)
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
# Listen for navigation events and emit framenavigated
|
|
548
|
+
# Following Puppeteer's pattern: emit framenavigated on DOMContentLoaded
|
|
549
|
+
@browsing_context.on(:dom_content_loaded) do
|
|
550
|
+
page.emit(:framenavigated, self)
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
# Also emit framenavigated on fragment navigation (anchor links, hash changes)
|
|
554
|
+
# Note: Puppeteer uses navigation.once('fragment'), but we listen to
|
|
555
|
+
# browsingContext's fragment_navigated which is equivalent
|
|
556
|
+
@browsing_context.on(:fragment_navigated) do
|
|
557
|
+
page.emit(:framenavigated, self)
|
|
558
|
+
end
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
# Create a Frame for a child browsing context
|
|
562
|
+
# Following Puppeteer's BidiFrame.#createFrameTarget pattern exactly:
|
|
563
|
+
# const frame = BidiFrame.from(this, browsingContext);
|
|
564
|
+
# this.#frames.set(browsingContext, frame);
|
|
565
|
+
# this.page().trustedEmitter.emit(PageEvent.FrameAttached, frame);
|
|
566
|
+
# browsingContext.on('closed', () => {
|
|
567
|
+
# this.#frames.delete(browsingContext);
|
|
568
|
+
# });
|
|
569
|
+
# Note: FrameDetached is NOT emitted here - it's emitted in #initialize
|
|
570
|
+
# when the frame's own browsing context closes
|
|
571
|
+
# @rbs browsing_context: Core::BrowsingContext
|
|
572
|
+
# @rbs return: Frame
|
|
573
|
+
def create_frame_target(browsing_context)
|
|
574
|
+
frame = Frame.from(self, browsing_context)
|
|
575
|
+
@frames[browsing_context.id] = frame
|
|
576
|
+
|
|
577
|
+
# Emit frameattached event
|
|
578
|
+
page.emit(:frameattached, frame)
|
|
579
|
+
|
|
580
|
+
# Remove frame from parent's frames map when its context is closed
|
|
581
|
+
# Note: FrameDetached is emitted by the frame itself in its initialize_frame
|
|
582
|
+
browsing_context.once(:closed) do
|
|
583
|
+
@frames.delete(browsing_context.id)
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
frame
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
# Check if this frame is detached and raise error if so
|
|
590
|
+
# @rbs return: void
|
|
591
|
+
def assert_not_detached
|
|
592
|
+
raise FrameDetachedError, "Attempted to use detached Frame '#{_id}'." if @browsing_context.closed?
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
end
|
|
596
|
+
end
|
|
597
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Puppeteer
|
|
4
|
+
module Bidi
|
|
5
|
+
# HTTPResponse represents a response to an HTTP request
|
|
6
|
+
class HTTPResponse
|
|
7
|
+
attr_reader :url
|
|
8
|
+
|
|
9
|
+
# @param url [String] Response URL
|
|
10
|
+
# @param status [Integer] HTTP status code
|
|
11
|
+
def initialize(url:, status: 200)
|
|
12
|
+
@url = url
|
|
13
|
+
@status = status
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Check if the response was successful (status code 200-299)
|
|
17
|
+
# @return [Boolean] True if status code is in 2xx range
|
|
18
|
+
def ok?
|
|
19
|
+
@status >= 200 && @status < 300
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var g=Object.defineProperty;var X=Object.getOwnPropertyDescriptor;var B=Object.getOwnPropertyNames;var Y=Object.prototype.hasOwnProperty;var l=(t,e)=>{for(var r in e)g(t,r,{get:e[r],enumerable:!0})},G=(t,e,r,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of B(e))!Y.call(t,s)&&s!==r&&g(t,s,{get:()=>e[s],enumerable:!(o=X(e,s))||o.enumerable});return t};var J=t=>G(g({},"__esModule",{value:!0}),t);var pe={};l(pe,{default:()=>he});module.exports=J(pe);var N=class extends Error{constructor(e,r){super(e,r),this.name=this.constructor.name}get[Symbol.toStringTag](){return this.constructor.name}},p=class extends N{};var c=class t{static create(e){return new t(e)}static async race(e){let r=new Set;try{let o=e.map(s=>s instanceof t?(s.#s&&r.add(s),s.valueOrThrow()):s);return await Promise.race(o)}finally{for(let o of r)o.reject(new Error("Timeout cleared"))}}#e=!1;#r=!1;#o;#t;#a=new Promise(e=>{this.#t=e});#s;#i;constructor(e){e&&e.timeout>0&&(this.#i=new p(e.message),this.#s=setTimeout(()=>{this.reject(this.#i)},e.timeout))}#l(e){clearTimeout(this.#s),this.#o=e,this.#t()}resolve(e){this.#r||this.#e||(this.#e=!0,this.#l(e))}reject(e){this.#r||this.#e||(this.#r=!0,this.#l(e))}resolved(){return this.#e}finished(){return this.#e||this.#r}value(){return this.#o}#n;valueOrThrow(){return this.#n||(this.#n=(async()=>{if(await this.#a,this.#r)throw this.#o;return this.#o})()),this.#n}};var L=new Map,W=t=>{let e=L.get(t);return e||(e=new Function(`return ${t}`)(),L.set(t,e),e)};var b={};l(b,{ariaQuerySelector:()=>z,ariaQuerySelectorAll:()=>x});var z=(t,e)=>globalThis.__ariaQuerySelector(t,e),x=async function*(t,e){yield*await globalThis.__ariaQuerySelectorAll(t,e)};var E={};l(E,{cssQuerySelector:()=>K,cssQuerySelectorAll:()=>Z});var K=(t,e)=>t.querySelector(e),Z=function(t,e){return t.querySelectorAll(e)};var A={};l(A,{customQuerySelectors:()=>P});var v=class{#e=new Map;register(e,r){if(!r.queryOne&&r.queryAll){let o=r.queryAll;r.queryOne=(s,i)=>{for(let n of o(s,i))return n;return null}}else if(r.queryOne&&!r.queryAll){let o=r.queryOne;r.queryAll=(s,i)=>{let n=o(s,i);return n?[n]:[]}}else if(!r.queryOne||!r.queryAll)throw new Error("At least one query method must be defined.");this.#e.set(e,{querySelector:r.queryOne,querySelectorAll:r.queryAll})}unregister(e){this.#e.delete(e)}get(e){return this.#e.get(e)}clear(){this.#e.clear()}},P=new v;var R={};l(R,{pierceQuerySelector:()=>ee,pierceQuerySelectorAll:()=>te});var ee=(t,e)=>{let r=null,o=s=>{let i=document.createTreeWalker(s,NodeFilter.SHOW_ELEMENT);do{let n=i.currentNode;n.shadowRoot&&o(n.shadowRoot),!(n instanceof ShadowRoot)&&n!==s&&!r&&n.matches(e)&&(r=n)}while(!r&&i.nextNode())};return t instanceof Document&&(t=t.documentElement),o(t),r},te=(t,e)=>{let r=[],o=s=>{let i=document.createTreeWalker(s,NodeFilter.SHOW_ELEMENT);do{let n=i.currentNode;n.shadowRoot&&o(n.shadowRoot),!(n instanceof ShadowRoot)&&n!==s&&n.matches(e)&&r.push(n)}while(i.nextNode())};return t instanceof Document&&(t=t.documentElement),o(t),r};var u=(t,e)=>{if(!t)throw new Error(e)};var y=class{#e;#r;#o;#t;constructor(e,r){this.#e=e,this.#r=r}async start(){let e=this.#t=c.create(),r=await this.#e();if(r){e.resolve(r);return}this.#o=new MutationObserver(async()=>{let o=await this.#e();o&&(e.resolve(o),await this.stop())}),this.#o.observe(this.#r,{childList:!0,subtree:!0,attributes:!0})}async stop(){u(this.#t,"Polling never started."),this.#t.finished()||this.#t.reject(new Error("Polling stopped")),this.#o&&(this.#o.disconnect(),this.#o=void 0)}result(){return u(this.#t,"Polling never started."),this.#t.valueOrThrow()}},w=class{#e;#r;constructor(e){this.#e=e}async start(){let e=this.#r=c.create(),r=await this.#e();if(r){e.resolve(r);return}let o=async()=>{if(e.finished())return;let s=await this.#e();if(!s){window.requestAnimationFrame(o);return}e.resolve(s),await this.stop()};window.requestAnimationFrame(o)}async stop(){u(this.#r,"Polling never started."),this.#r.finished()||this.#r.reject(new Error("Polling stopped"))}result(){return u(this.#r,"Polling never started."),this.#r.valueOrThrow()}},T=class{#e;#r;#o;#t;constructor(e,r){this.#e=e,this.#r=r}async start(){let e=this.#t=c.create(),r=await this.#e();if(r){e.resolve(r);return}this.#o=setInterval(async()=>{let o=await this.#e();o&&(e.resolve(o),await this.stop())},this.#r)}async stop(){u(this.#t,"Polling never started."),this.#t.finished()||this.#t.reject(new Error("Polling stopped")),this.#o&&(clearInterval(this.#o),this.#o=void 0)}result(){return u(this.#t,"Polling never started."),this.#t.valueOrThrow()}};var _={};l(_,{PCombinator:()=>H,pQuerySelector:()=>fe,pQuerySelectorAll:()=>$});var a=class{static async*map(e,r){for await(let o of e)yield await r(o)}static async*flatMap(e,r){for await(let o of e)yield*r(o)}static async collect(e){let r=[];for await(let o of e)r.push(o);return r}static async first(e){for await(let r of e)return r}};var C={};l(C,{textQuerySelectorAll:()=>m});var re=new Set(["checkbox","image","radio"]),oe=t=>t instanceof HTMLSelectElement||t instanceof HTMLTextAreaElement||t instanceof HTMLInputElement&&!re.has(t.type),se=new Set(["SCRIPT","STYLE"]),f=t=>!se.has(t.nodeName)&&!document.head?.contains(t),I=new WeakMap,F=t=>{for(;t;)I.delete(t),t instanceof ShadowRoot?t=t.host:t=t.parentNode},j=new WeakSet,ne=new MutationObserver(t=>{for(let e of t)F(e.target)}),d=t=>{let e=I.get(t);if(e||(e={full:"",immediate:[]},!f(t)))return e;let r="";if(oe(t))e.full=t.value,e.immediate.push(t.value),t.addEventListener("input",o=>{F(o.target)},{once:!0,capture:!0});else{for(let o=t.firstChild;o;o=o.nextSibling){if(o.nodeType===Node.TEXT_NODE){e.full+=o.nodeValue??"",r+=o.nodeValue??"";continue}r&&e.immediate.push(r),r="",o.nodeType===Node.ELEMENT_NODE&&(e.full+=d(o).full)}r&&e.immediate.push(r),t instanceof Element&&t.shadowRoot&&(e.full+=d(t.shadowRoot).full),j.has(t)||(ne.observe(t,{childList:!0,characterData:!0,subtree:!0}),j.add(t))}return I.set(t,e),e};var m=function*(t,e){let r=!1;for(let o of t.childNodes)if(o instanceof Element&&f(o)){let s;o.shadowRoot?s=m(o.shadowRoot,e):s=m(o,e);for(let i of s)yield i,r=!0}r||t instanceof Element&&f(t)&&d(t).full.includes(e)&&(yield t)};var k={};l(k,{checkVisibility:()=>le,pierce:()=>S,pierceAll:()=>O});var ie=["hidden","collapse"],le=(t,e)=>{if(!t)return e===!1;if(e===void 0)return t;let r=t.nodeType===Node.TEXT_NODE?t.parentElement:t,o=window.getComputedStyle(r),s=o&&!ie.includes(o.visibility)&&!ae(r);return e===s?t:!1};function ae(t){let e=t.getBoundingClientRect();return e.width===0||e.height===0}var ce=t=>"shadowRoot"in t&&t.shadowRoot instanceof ShadowRoot;function*S(t){ce(t)?yield t.shadowRoot:yield t}function*O(t){t=S(t).next().value,yield t;let e=[document.createTreeWalker(t,NodeFilter.SHOW_ELEMENT)];for(let r of e){let o;for(;o=r.nextNode();)o.shadowRoot&&(yield o.shadowRoot,e.push(document.createTreeWalker(o.shadowRoot,NodeFilter.SHOW_ELEMENT)))}}var D={};l(D,{xpathQuerySelectorAll:()=>q});var q=function*(t,e,r=-1){let s=(t.ownerDocument||document).evaluate(e,t,null,XPathResult.ORDERED_NODE_ITERATOR_TYPE),i=[],n;for(;(n=s.iterateNext())&&(i.push(n),!(r&&i.length===r)););for(let h=0;h<i.length;h++)n=i[h],yield n,delete i[h]};var ue=/[-\w\P{ASCII}*]/u,H=(r=>(r.Descendent=">>>",r.Child=">>>>",r))(H||{}),V=t=>"querySelectorAll"in t,Q=class{#e;#r=[];#o=void 0;elements;constructor(e,r){this.elements=[e],this.#e=r,this.#t()}async run(){if(typeof this.#o=="string")switch(this.#o.trimStart()){case":scope":this.#t();break}for(;this.#o!==void 0;this.#t()){let e=this.#o;typeof e=="string"?e[0]&&ue.test(e[0])?this.elements=a.flatMap(this.elements,async function*(r){V(r)&&(yield*r.querySelectorAll(e))}):this.elements=a.flatMap(this.elements,async function*(r){if(!r.parentElement){if(!V(r))return;yield*r.querySelectorAll(e);return}let o=0;for(let s of r.parentElement.children)if(++o,s===r)break;yield*r.parentElement.querySelectorAll(`:scope>:nth-child(${o})${e}`)}):this.elements=a.flatMap(this.elements,async function*(r){switch(e.name){case"text":yield*m(r,e.value);break;case"xpath":yield*q(r,e.value);break;case"aria":yield*x(r,e.value);break;default:let o=P.get(e.name);if(!o)throw new Error(`Unknown selector type: ${e.name}`);yield*o.querySelectorAll(r,e.value)}})}}#t(){if(this.#r.length!==0){this.#o=this.#r.shift();return}if(this.#e.length===0){this.#o=void 0;return}let e=this.#e.shift();switch(e){case">>>>":{this.elements=a.flatMap(this.elements,S),this.#t();break}case">>>":{this.elements=a.flatMap(this.elements,O),this.#t();break}default:this.#r=e,this.#t();break}}},M=class{#e=new WeakMap;calculate(e,r=[]){if(e===null)return r;e instanceof ShadowRoot&&(e=e.host);let o=this.#e.get(e);if(o)return[...o,...r];let s=0;for(let n=e.previousSibling;n;n=n.previousSibling)++s;let i=this.calculate(e.parentNode,[s]);return this.#e.set(e,i),[...i,...r]}},U=(t,e)=>{if(t.length+e.length===0)return 0;let[r=-1,...o]=t,[s=-1,...i]=e;return r===s?U(o,i):r<s?-1:1},de=async function*(t){let e=new Set;for await(let o of t)e.add(o);let r=new M;yield*[...e.values()].map(o=>[o,r.calculate(o)]).sort(([,o],[,s])=>U(o,s)).map(([o])=>o)},$=function(t,e){let r=JSON.parse(e);if(r.some(o=>{let s=0;return o.some(i=>(typeof i=="string"?++s:s=0,s>1))}))throw new Error("Multiple deep combinators found in sequence.");return de(a.flatMap(r,o=>{let s=new Q(t,o);return s.run(),s.elements}))},fe=async function(t,e){for await(let r of $(t,e))return r;return null};var me=Object.freeze({...b,...A,...R,..._,...C,...k,...D,...E,Deferred:c,createFunction:W,createTextContent:d,IntervalPoller:T,isSuitableNodeForTextMatching:f,MutationPoller:y,RAFPoller:w}),he=me;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Puppeteer
|
|
4
|
+
module Bidi
|
|
5
|
+
# Puppeteer's injected utilities (Poller classes, Deferred, etc.)
|
|
6
|
+
# Source: https://unpkg.com/puppeteer-core@24.31.0/lib/esm/puppeteer/generated/injected.js
|
|
7
|
+
# Version: puppeteer-core@24.31.0
|
|
8
|
+
#
|
|
9
|
+
# To update this file, run:
|
|
10
|
+
# bundle exec ruby scripts/update_injected_source.rb
|
|
11
|
+
#
|
|
12
|
+
# This script provides:
|
|
13
|
+
# - RAFPoller: requestAnimationFrame-based polling
|
|
14
|
+
# - MutationPoller: MutationObserver-based polling
|
|
15
|
+
# - IntervalPoller: setInterval-based polling
|
|
16
|
+
# - Deferred: Promise wrapper
|
|
17
|
+
# - createFunction: Creates function from string
|
|
18
|
+
# - Various query selector utilities
|
|
19
|
+
PUPPETEER_INJECTED_SOURCE = File.read(File.join(__dir__, 'injected.js')).freeze
|
|
20
|
+
end
|
|
21
|
+
end
|