Class: Puppeteer::Bidi::ExposedFunction

Inherits:
Object
  • Object
show all
Defined in:
lib/puppeteer/bidi/exposed_function.rb,
sig/puppeteer/bidi/exposed_function.rbs

Overview

ExposedFunction manages the lifecycle of a function exposed to the page. It uses script.message to receive calls from the page.

Defined Under Namespace

Classes: ThrownValue

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(frame, name, apply, isolate: false) ⇒ ExposedFunction

Returns a new instance of ExposedFunction.

Parameters:

  • frame (Frame)
  • name (String)
  • apply (Proc)
  • isolate: (Boolean) (defaults to: false)


47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/puppeteer/bidi/exposed_function.rb', line 47

def initialize(frame, name, apply, isolate: false)
  @frame = frame
  @name = name
  @apply = apply
  @isolate = isolate
  @channel = "__puppeteer__#{@frame._id}_page_exposeFunction_#{@name}"
  @scripts = []
  @disposed = false
  @listener = nil
  @frame_listener = nil
  @injected_contexts = {}
end

Instance Attribute Details

#nameString (readonly)

: String

Returns:

  • (String)


40
41
42
# File 'lib/puppeteer/bidi/exposed_function.rb', line 40

def name
  @name
end

Class Method Details

.from(frame, name, apply = nil, isolate: false, &block) ⇒ void

This method returns an undefined value.

Create and initialize an exposed function.

Parameters:

  • frame (Frame)
  • name (String)
  • apply (Proc, nil) (defaults to: nil)
  • isolate: (Boolean) (defaults to: false)


29
30
31
32
33
34
35
36
37
38
# File 'lib/puppeteer/bidi/exposed_function.rb', line 29

def self.from(frame, name, apply = nil, isolate: false, &block)
  handler = apply || block
  unless handler&.respond_to?(:call)
    raise ArgumentError, "expose_function requires a callable"
  end

  func = new(frame, name, handler, isolate: isolate)
  func.send(:setup)
  func
end

Instance Method Details

#build_function_declarationString

Build the JavaScript function declaration for exposeFunction.

Returns:

  • (String)


111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/puppeteer/bidi/exposed_function.rb', line 111

def build_function_declaration
  name_literal = JSON.generate(@name)
  <<~JS
    (callback) => {
      Object.assign(globalThis, {
        [#{name_literal}]: function (...args) {
          return new Promise((resolve, reject) => {
            callback([resolve, reject, args]);
          });
        },
      });
    }
  JS
end

#channel_argumentHash[Symbol, untyped]

Build the channel argument for script.addPreloadScript/callFunction.

Returns:

  • (Hash[Symbol, untyped])


128
129
130
131
132
133
134
135
136
# File 'lib/puppeteer/bidi/exposed_function.rb', line 128

def channel_argument
  {
    type: "channel",
    value: {
      channel: @channel,
      ownership: "root"
    }
  }
end

#debug_error(error) ⇒ Object

Parameters:

  • error (Object)

Returns:

  • (Object)


383
384
385
386
387
# File 'lib/puppeteer/bidi/exposed_function.rb', line 383

def debug_error(error)
  return unless ENV["DEBUG_BIDI_COMMAND"]

  warn(error.full_message)
end

#disposevoid

This method returns an undefined value.

Dispose this exposed function, removing it from the page.



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/puppeteer/bidi/exposed_function.rb', line 68

def dispose
  return if @disposed

  @disposed = true

  session.off("script.message", &@listener) if @listener
  @listener = nil
  if @frame_listener
    @frame.page.off(:frameattached, &@frame_listener)
    @frame_listener = nil
  end

  remove_binding_from_frame(@frame)

  @scripts.each do |frame, script_id|
    begin
      frame.browsing_context.remove_preload_script(script_id).wait
    rescue StandardError => e
      debug_error(e)
    end
  end
end

#dispose_call_handles(args_handle, handles) ⇒ void

This method returns an undefined value.

Dispose call handles after processing.

Parameters:



312
313
314
315
316
317
318
319
320
321
322
323
324
325
# File 'lib/puppeteer/bidi/exposed_function.rb', line 312

def dispose_call_handles(args_handle, handles)
  begin
    args_handle.dispose
  rescue StandardError
    nil
  end
  handles.each do |handle|
    begin
      handle.dispose
    rescue StandardError
      nil
    end
  end
end

#disposed?Boolean

Check if this exposed function is disposed.

Returns:

  • (Boolean)


62
63
64
# File 'lib/puppeteer/bidi/exposed_function.rb', line 62

def disposed?
  @disposed
end

#find_frame(context_id) ⇒ Frame?

Find frame by browsing context ID.

Parameters:

  • context_id (String)

Returns:



330
331
332
333
334
335
336
337
# File 'lib/puppeteer/bidi/exposed_function.rb', line 330

def find_frame(context_id)
  frames = [@frame]
  frames.each do |frame|
    return frame if frame.browsing_context.id == context_id
    frames.concat(frame.child_frames)
  end
  nil
end

#find_realm(frame, realm_id) ⇒ FrameRealm?

Find frame realm by realm ID.

Parameters:

  • frame (Frame)
  • realm_id (String)

Returns:



343
344
345
346
347
348
# File 'lib/puppeteer/bidi/exposed_function.rb', line 343

def find_realm(frame, realm_id)
  return frame.main_realm if frame.main_realm.core_realm.id == realm_id
  return frame.isolated_realm if frame.isolated_realm.core_realm.id == realm_id

  nil
end

#frame_in_scope?(frame) ⇒ Boolean

Check if a frame belongs to the current frame subtree.

Parameters:

Returns:

  • (Boolean)


353
354
355
356
357
358
359
360
# File 'lib/puppeteer/bidi/exposed_function.rb', line 353

def frame_in_scope?(frame)
  current = frame
  while current
    return true if current == @frame
    current = current.parent_frame
  end
  false
end

#handle_message(params) ⇒ void

This method returns an undefined value.

Handle script.message events from the page.

Parameters:

  • params (Hash[String, untyped])


189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/puppeteer/bidi/exposed_function.rb', line 189

def handle_message(params)
  return if @disposed
  return unless params.is_a?(Hash)
  return unless params["channel"] == @channel

  source = params["source"] || {}
  frame = find_frame(source["context"])
  return unless frame

  realm = find_realm(frame, source["realm"])
  return unless realm

  data_handle = JSHandle.from(params["data"], realm.core_realm)
  begin
    process_call(data_handle)
  ensure
    begin
      data_handle.dispose
    rescue StandardError
      nil
    end
  end
rescue StandardError => e
  debug_error(e)
end

#inject_into_frame(frame) ⇒ void

This method returns an undefined value.

Inject the function into a single frame.

Parameters:



151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/puppeteer/bidi/exposed_function.rb', line 151

def inject_into_frame(frame)
  return if @injected_contexts[frame.browsing_context.id]

  function_declaration = build_function_declaration
  channel = channel_argument
  realm = @isolate ? frame.isolated_realm : frame.main_realm
  script_id = nil

  if frame.browsing_context.parent.nil?
    begin
      script_id = frame.browsing_context.add_preload_script(
        function_declaration,
        arguments: [channel],
        sandbox: realm.core_realm.sandbox
      ).wait
    rescue StandardError => e
      debug_error(e)
    end
  end

  begin
    realm.core_realm.call_function(
      function_declaration,
      false,
      arguments: [channel]
    ).wait
  rescue StandardError => e
    debug_error(e)
    return
  end

  @scripts << [frame, script_id] if script_id
  @injected_contexts[frame.browsing_context.id] = true
end

#inject_into_framesvoid

This method returns an undefined value.

Inject the function into existing frames.



140
141
142
143
144
145
146
# File 'lib/puppeteer/bidi/exposed_function.rb', line 140

def inject_into_frames
  frames = [@frame]
  frames.each do |frame|
    frames.concat(frame.child_frames)
    inject_into_frame(frame)
  end
end

#process_call(data_handle) ⇒ void

This method returns an undefined value.

Process a function call from the page.

Parameters:



218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'lib/puppeteer/bidi/exposed_function.rb', line 218

def process_call(data_handle)
  args = []
  handles = []
  args_handle = data_handle.evaluate_handle("([, , args]) => args")

  begin
    args_handle.get_properties.each do |index, handle|
      index_int = begin
        Integer(index, 10)
      rescue ArgumentError, TypeError
        nil
      end
      next unless index_int

      handles << handle
      if handle.is_a?(ElementHandle)
        args[index_int] = handle
      else
        args[index_int] = handle.json_value
      end
    end

    result = @apply.call(*args)
    result = AsyncUtils.await(result)
  rescue StandardError => e
    if e.is_a?(ThrownValue)
      send_thrown_value(data_handle, e.value)
    elsif e.is_a?(TypeError) && e.message.include?("exception class/object expected")
      send_thrown_value(data_handle, nil)
    else
      send_error(data_handle, e)
    end
    dispose_call_handles(args_handle, handles)
    return
  end

  send_result(data_handle, result)
  dispose_call_handles(args_handle, handles)
end

#remove_binding_from_frame(frame) ⇒ void

This method returns an undefined value.

Remove the exposed binding from a frame subtree.

Parameters:



365
366
367
368
369
370
371
372
373
374
375
# File 'lib/puppeteer/bidi/exposed_function.rb', line 365

def remove_binding_from_frame(frame)
  begin
    frame.evaluate("(name) => { delete globalThis[name]; }", @name)
  rescue StandardError => e
    debug_error(e)
  end

  frame.child_frames.each do |child|
    remove_binding_from_frame(child)
  end
end

#send_error(data_handle, error) ⇒ void

This method returns an undefined value.

Send an error response back to the page.

Parameters:

  • data_handle (JSHandle)
  • error (StandardError)


276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
# File 'lib/puppeteer/bidi/exposed_function.rb', line 276

def send_error(data_handle, error)
  name = error.class.name
  message = error.message
  stack = error.backtrace&.join("\n")
  data_handle.evaluate(<<~JS, name, message, stack)
    ([, reject], name, message, stack) => {
      const error = new Error(message);
      error.name = name;
      if (stack) {
        error.stack = stack;
      }
      reject(error);
    }
  JS
rescue StandardError => e
  debug_error(e)
end

#send_result(data_handle, result) ⇒ void

This method returns an undefined value.

Send a successful response back to the page.

Parameters:

  • data_handle (JSHandle)
  • result (Object)


262
263
264
265
266
267
268
269
270
# File 'lib/puppeteer/bidi/exposed_function.rb', line 262

def send_result(data_handle, result)
  data_handle.evaluate(<<~JS, result)
    ([resolve], result) => {
      resolve(result);
    }
  JS
rescue StandardError => e
  debug_error(e)
end

#send_thrown_value(data_handle, value) ⇒ void

This method returns an undefined value.

Send a non-Error rejection value back to the page.

Parameters:



298
299
300
301
302
303
304
305
306
# File 'lib/puppeteer/bidi/exposed_function.rb', line 298

def send_thrown_value(data_handle, value)
  data_handle.evaluate(<<~JS, value)
    ([, reject], value) => {
      reject(value);
    }
  JS
rescue StandardError => e
  debug_error(e)
end

#sessionCore::Session

Get the BiDi session.

Returns:



379
380
381
# File 'lib/puppeteer/bidi/exposed_function.rb', line 379

def session
  @frame.browsing_context.user_context.browser.session
end

#setupvoid

This method returns an undefined value.

Set up the exposed function by injecting it into the page.



95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/puppeteer/bidi/exposed_function.rb', line 95

def setup
  @listener = proc do |params|
    handle_message(params)
  end
  session.on("script.message", &@listener)

  @frame_listener = proc do |frame|
    inject_into_frame(frame) if frame_in_scope?(frame)
  end
  @frame.page.on(:frameattached, &@frame_listener)

  inject_into_frames
end