Module: Capybara::Lightpanda::Browser::Runtime

Included in:
Capybara::Lightpanda::Browser
Defined in:
lib/capybara/lightpanda/browser/runtime.rb

Overview

JS evaluation and RemoteObject plumbing: Runtime.evaluate / callFunctionOn dispatch, result serialization (Ferrum’s Frame::Runtime is the peer-gem equivalent).

Instance Method Summary collapse

Instance Method Details

#call_function_on(remote_object_id, function_declaration, *args, return_by_value: true) ⇒ Object

Call a function on a remote object via Runtime.callFunctionOn. Binds ‘this` to the DOM element referenced by remote_object_id.



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/capybara/lightpanda/browser/runtime.rb', line 111

def call_function_on(remote_object_id, function_declaration, *args, return_by_value: true)
  params = {
    objectId: remote_object_id,
    functionDeclaration: function_declaration,
    returnByValue: return_by_value,
    awaitPromise: true,
  }
  params[:arguments] = args.map { |a| serialize_argument(a) } unless args.empty?

  response = page_command("Runtime.callFunctionOn", **params)
  raise_on_js_error!("call_function_on", function_declaration, response)

  result = response["result"]
  return nil if result["type"] == "undefined"

  return_by_value ? result["value"] : result
end

#evaluate(expression, *args) ⇒ Object

Evaluate JS and return a serialized value. No-args fast path uses Runtime.evaluate; with args we wrap as a function and dispatch via Runtime.callFunctionOn so ‘arguments` is bound. Both paths use `returnByValue: false` and unwrap so DOM-node returns come back as `{ “lightpanda_node” => … }` for the Driver to wrap.

The no-args path sends the user’s text verbatim with ‘replMode: true` (V8’s DevTools-console REPL mode — Lightpanda forwards Runtime.evaluate to the V8 inspector, which handles the flag natively). Without it, top-level ‘const`/`let` persist in the global lexical environment across classic scripts — per spec, and Chrome behaves identically —so a second `const sel = …` raises `SyntaxError: Identifier ’sel’ has already been declared`. REPL mode keeps the bindings (visible to later calls, like the DevTools console) but allows redeclaration. Completion-value semantics cover a bare expression (‘’foo’‘), a `throw` statement, and multi-statement scripts alike.



26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/capybara/lightpanda/browser/runtime.rb', line 26

def evaluate(expression, *args)
  if args.empty?
    response = page_command("Runtime.evaluate", expression: expression, returnByValue: false,
                                                awaitPromise: true, replMode: true)
    raise_on_js_error!("evaluate", expression, response)

    return unwrap_call_result(response["result"])
  end

  wrapped = "function() { return #{expression} }"
  call_with_args(wrapped, args)
end

#evaluate_async(expression, *args, wait: @options.timeout) ⇒ Object

Evaluate async JS with a callback. The user’s script receives the callback as its last argument (‘arguments[arguments.length - 1]`), matching Capybara’s evaluate_async_script contract.



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/capybara/lightpanda/browser/runtime.rb', line 80

def evaluate_async(expression, *args, wait: @options.timeout)
  timeout_ms = (wait * 1000).to_i
  wrapped = <<~JS
    function() {
      var __args = Array.prototype.slice.call(arguments);
      return new Promise(function(__resolve, __reject) {
        var __timer = setTimeout(function() {
          __reject(new Error('Async script timeout after #{timeout_ms}ms'));
        }, #{timeout_ms});
        var __done = function(val) { clearTimeout(__timer); __resolve(val); };
        __args.push(__done);
        (function() { #{expression} }).apply(null, __args);
      });
    }
  JS
  call_with_args(wrapped, args)
end

#evaluate_with_ref(expression) ⇒ Object

Evaluate JS and return a RemoteObject reference (for DOM nodes, arrays).



99
100
101
102
103
104
105
106
107
# File 'lib/capybara/lightpanda/browser/runtime.rb', line 99

def evaluate_with_ref(expression)
  response = page_command("Runtime.evaluate", expression: expression, returnByValue: false, awaitPromise: true)
  raise_on_js_error!("evaluate_with_ref", expression, response)

  result = response["result"]
  return nil if result["type"] == "undefined"

  result
end

#execute(expression, *args) ⇒ Object

Execute JS without returning a value.

Like ‘evaluate`, the no-args path uses `replMode: true` so top-level `const`/`let` redeclarations across calls don’t raise. Also raises on JS exceptions so silent failures don’t mask test bugs (the previous fast path swallowed them because ‘awaitPromise: false` was checked but `exceptionDetails` was not).



46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/capybara/lightpanda/browser/runtime.rb', line 46

def execute(expression, *args)
  if args.empty?
    response = page_command("Runtime.evaluate", expression: expression, returnByValue: false,
                                                awaitPromise: false, replMode: true)
    raise_on_js_error!("execute", expression, response)
    return nil
  end

  wrapped = "function() { #{expression} }"
  call_with_args(wrapped, args, return_by_value: false)
  nil
end

#release_object(remote_object_id) ⇒ Object

Release a remote object reference to free V8 memory. Cleanup is best-effort: callers wrap their work in ‘ensure release_object(…)`, so a TimeoutError or transport hiccup here must not propagate out of the ensure block and bury the original failure.



138
139
140
141
142
143
# File 'lib/capybara/lightpanda/browser/runtime.rb', line 138

def release_object(remote_object_id)
  page_command("Runtime.releaseObject", objectId: remote_object_id)
rescue Error
  # Object may already be released, context destroyed, or the CDP call
  # itself timed out / failed in transport.
end