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.
Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +13 -0
  4. data/CLAUDE/README.md +158 -0
  5. data/CLAUDE/async_programming.md +158 -0
  6. data/CLAUDE/click_implementation.md +340 -0
  7. data/CLAUDE/core_layer_gotchas.md +136 -0
  8. data/CLAUDE/error_handling.md +232 -0
  9. data/CLAUDE/file_chooser.md +95 -0
  10. data/CLAUDE/frame_architecture.md +346 -0
  11. data/CLAUDE/javascript_evaluation.md +341 -0
  12. data/CLAUDE/jshandle_implementation.md +505 -0
  13. data/CLAUDE/keyboard_implementation.md +250 -0
  14. data/CLAUDE/mouse_implementation.md +140 -0
  15. data/CLAUDE/navigation_waiting.md +234 -0
  16. data/CLAUDE/porting_puppeteer.md +214 -0
  17. data/CLAUDE/query_handler.md +194 -0
  18. data/CLAUDE/rspec_pending_vs_skip.md +262 -0
  19. data/CLAUDE/selector_evaluation.md +198 -0
  20. data/CLAUDE/test_server_routes.md +263 -0
  21. data/CLAUDE/testing_strategy.md +236 -0
  22. data/CLAUDE/two_layer_architecture.md +180 -0
  23. data/CLAUDE/wrapped_element_click.md +247 -0
  24. data/CLAUDE.md +185 -0
  25. data/LICENSE.txt +21 -0
  26. data/README.md +488 -0
  27. data/Rakefile +21 -0
  28. data/lib/puppeteer/bidi/async_utils.rb +151 -0
  29. data/lib/puppeteer/bidi/browser.rb +285 -0
  30. data/lib/puppeteer/bidi/browser_context.rb +53 -0
  31. data/lib/puppeteer/bidi/browser_launcher.rb +240 -0
  32. data/lib/puppeteer/bidi/connection.rb +182 -0
  33. data/lib/puppeteer/bidi/core/README.md +169 -0
  34. data/lib/puppeteer/bidi/core/browser.rb +230 -0
  35. data/lib/puppeteer/bidi/core/browsing_context.rb +601 -0
  36. data/lib/puppeteer/bidi/core/disposable.rb +69 -0
  37. data/lib/puppeteer/bidi/core/errors.rb +64 -0
  38. data/lib/puppeteer/bidi/core/event_emitter.rb +83 -0
  39. data/lib/puppeteer/bidi/core/navigation.rb +128 -0
  40. data/lib/puppeteer/bidi/core/realm.rb +315 -0
  41. data/lib/puppeteer/bidi/core/request.rb +300 -0
  42. data/lib/puppeteer/bidi/core/session.rb +153 -0
  43. data/lib/puppeteer/bidi/core/user_context.rb +208 -0
  44. data/lib/puppeteer/bidi/core/user_prompt.rb +102 -0
  45. data/lib/puppeteer/bidi/core.rb +45 -0
  46. data/lib/puppeteer/bidi/deserializer.rb +132 -0
  47. data/lib/puppeteer/bidi/element_handle.rb +602 -0
  48. data/lib/puppeteer/bidi/errors.rb +42 -0
  49. data/lib/puppeteer/bidi/file_chooser.rb +52 -0
  50. data/lib/puppeteer/bidi/frame.rb +597 -0
  51. data/lib/puppeteer/bidi/http_response.rb +23 -0
  52. data/lib/puppeteer/bidi/injected.js +1 -0
  53. data/lib/puppeteer/bidi/injected_source.rb +21 -0
  54. data/lib/puppeteer/bidi/js_handle.rb +302 -0
  55. data/lib/puppeteer/bidi/keyboard.rb +265 -0
  56. data/lib/puppeteer/bidi/lazy_arg.rb +23 -0
  57. data/lib/puppeteer/bidi/mouse.rb +170 -0
  58. data/lib/puppeteer/bidi/page.rb +613 -0
  59. data/lib/puppeteer/bidi/query_handler.rb +397 -0
  60. data/lib/puppeteer/bidi/realm.rb +242 -0
  61. data/lib/puppeteer/bidi/serializer.rb +139 -0
  62. data/lib/puppeteer/bidi/target.rb +81 -0
  63. data/lib/puppeteer/bidi/task_manager.rb +44 -0
  64. data/lib/puppeteer/bidi/timeout_settings.rb +20 -0
  65. data/lib/puppeteer/bidi/transport.rb +129 -0
  66. data/lib/puppeteer/bidi/version.rb +7 -0
  67. data/lib/puppeteer/bidi/wait_task.rb +322 -0
  68. data/lib/puppeteer/bidi.rb +49 -0
  69. data/scripts/update_injected_source.rb +57 -0
  70. data/sig/puppeteer/bidi/browser.rbs +80 -0
  71. data/sig/puppeteer/bidi/element_handle.rbs +238 -0
  72. data/sig/puppeteer/bidi/frame.rbs +205 -0
  73. data/sig/puppeteer/bidi/js_handle.rbs +90 -0
  74. data/sig/puppeteer/bidi/page.rbs +247 -0
  75. data/sig/puppeteer/bidi.rbs +15 -0
  76. 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