module Ferrum::Frame::Runtime

Constants

INTERMITTENT_ATTEMPTS
INTERMITTENT_SLEEP
SCRIPT_SRC_TAG
SCRIPT_TEXT_TAG
STYLE_TAG

Public Instance Methods

add_script_tag(url: nil, path: nil, content: nil, type: "text/javascript") click to toggle source
# File lib/ferrum/frame/runtime.rb, line 90
def add_script_tag(url: nil, path: nil, content: nil, type: "text/javascript")
  expr, *args = if url
                  [SCRIPT_SRC_TAG, url, type]
                elsif path || content
                  if path
                    content = File.read(path)
                    content += "\n//# sourceURL=#{path}"
                  end
                  [SCRIPT_TEXT_TAG, content, type]
                end

  evaluate_async(expr, @page.timeout, *args)
end
add_style_tag(url: nil, path: nil, content: nil) click to toggle source
# File lib/ferrum/frame/runtime.rb, line 104
def add_style_tag(url: nil, path: nil, content: nil)
  expr, *args = if url
                  [LINK_TAG, url]
                elsif path || content
                  if path
                    content = File.read(path)
                    content += "\n//# sourceURL=#{path}"
                  end
                  [STYLE_TAG, content]
                end

  evaluate_async(expr, @page.timeout, *args)
end
evaluate(expression, *args) click to toggle source
# File lib/ferrum/frame/runtime.rb, line 48
def evaluate(expression, *args)
  expression = "function() { return %s }" % expression
  call(expression: expression, arguments: args)
end
evaluate_async(expression, wait, *args) click to toggle source
# File lib/ferrum/frame/runtime.rb, line 53
      def evaluate_async(expression, wait, *args)
        template = <<~JS
          function() {
            return new Promise((__f, __r) => {
              try {
                arguments[arguments.length] = r => __f(r);
                arguments.length = arguments.length + 1;
                setTimeout(() => __r(new Error("timed out promise")), %s);
                %s
              } catch(error) {
                __r(error);
              }
            });
          }
        JS

        expression = template % [wait * 1000, expression]
        call(expression: expression, arguments: args, awaitPromise: true)
      end
evaluate_func(expression, *args, on: nil) click to toggle source
# File lib/ferrum/frame/runtime.rb, line 79
def evaluate_func(expression, *args, on: nil)
  call(expression: expression, arguments: args, on: on)
end
evaluate_on(node:, expression:, by_value: true, wait: 0) click to toggle source
# File lib/ferrum/frame/runtime.rb, line 83
def evaluate_on(node:, expression:, by_value: true, wait: 0)
  options = { handle: true }
  expression = "function() { return %s }" % expression
  options = { handle: false, returnByValue: true } if by_value
  call(expression: expression, on: node, wait: wait, **options)
end
execute(expression, *args) click to toggle source
# File lib/ferrum/frame/runtime.rb, line 73
def execute(expression, *args)
  expression = "function() { %s }" % expression
  call(expression: expression, arguments: args, handle: false, returnByValue: true)
  true
end

Private Instance Methods

call(expression:, arguments: [], on: nil, wait: 0, handle: true, **options) click to toggle source
# File lib/ferrum/frame/runtime.rb, line 120
def call(expression:, arguments: [], on: nil, wait: 0, handle: true, **options)
  errors = [NodeNotFoundError, NoExecutionContextError]
  attempts, sleep = INTERMITTENT_ATTEMPTS, INTERMITTENT_SLEEP

  Ferrum.with_attempts(errors: errors, max: attempts, wait: sleep) do
    params = options.dup

    if on
      response = @page.command("DOM.resolveNode", nodeId: on.node_id)
      object_id = response.dig("object", "objectId")
      params = params.merge(objectId: object_id)
    end

    if params[:executionContextId].nil? && params[:objectId].nil?
      params = params.merge(executionContextId: execution_id)
    end

    response = @page.command("Runtime.callFunctionOn",
                             wait: wait, slowmoable: true,
                             **params.merge(functionDeclaration: expression,
                                            arguments: prepare_args(arguments)))
    handle_error(response)
    response = response["result"]

    handle ? handle_response(response) : response.dig("value")
  end
end
cyclic?(object_id) click to toggle source
# File lib/ferrum/frame/runtime.rb, line 225
      def cyclic?(object_id)
        @page.command(
          "Runtime.callFunctionOn",
          objectId: object_id,
          returnByValue: true,
          functionDeclaration: <<~JS
            function() {
              if (Array.isArray(this) &&
                  this.every(e => e instanceof Node)) {
                return false;
              }

              function detectCycle(obj, seen) {
                if (typeof obj === "object") {
                  if (seen.indexOf(obj) !== -1) {
                    return true;
                  }
                  for (let key in obj) {
                    if (obj.hasOwnProperty(key) && detectCycle(obj[key], seen.concat([obj]))) {
                      return true;
                    }
                  }
                }

                return false;
              }

              return detectCycle(this, []);
            }
          JS
        )
      end
cyclic_object() click to toggle source
# File lib/ferrum/frame/runtime.rb, line 258
def cyclic_object
  CyclicObject.instance
end
handle_error(response) click to toggle source

FIXME: We should have a central place to handle all type of errors

# File lib/ferrum/frame/runtime.rb, line 149
def handle_error(response)
  result = response["result"]
  return if result["subtype"] != "error"

  case result["description"]
  when /\AError: timed out promise/
    raise ScriptTimeoutError
  else
    raise JavaScriptError.new(result)
  end
end
handle_response(response) click to toggle source
# File lib/ferrum/frame/runtime.rb, line 161
def handle_response(response)
  case response["type"]
  when "boolean", "number", "string"
    response["value"]
  when "undefined"
    nil
  when "function"
    {}
  when "object"
    object_id = response["objectId"]

    case response["subtype"]
    when "node"
        # We cannot store object_id in the node because page can be reloaded
        # and node destroyed so we need to retrieve it each time for given id.
        # Though we can try to subscribe to `DOM.childNodeRemoved` and
        # `DOM.childNodeInserted` in the future.
        node_id = @page.command("DOM.requestNode", objectId: object_id)["nodeId"]
        description = @page.command("DOM.describeNode", nodeId: node_id)["node"]
        Node.new(self, @page.target_id, node_id, description)
    when "array"
      reduce_props(object_id, []) do |memo, key, value|
        next(memo) unless (Integer(key) rescue nil)
        value = value["objectId"] ? handle_response(value) : value["value"]
        memo.insert(key.to_i, value)
      end.compact
    when "date"
      response["description"]
    when "null"
      nil
    else
      reduce_props(object_id, {}) do |memo, key, value|
        value = value["objectId"] ? handle_response(value) : value["value"]
        memo.merge(key => value)
      end
    end
  end
end
prepare_args(args) click to toggle source
# File lib/ferrum/frame/runtime.rb, line 200
def prepare_args(args)
  args.map do |arg|
    if arg.is_a?(Node)
      resolved = @page.command("DOM.resolveNode", nodeId: arg.node_id)
      { objectId: resolved["object"]["objectId"] }
    elsif arg.is_a?(Hash) && arg["objectId"]
      { objectId: arg["objectId"] }
    else
      { value: arg }
    end
  end
end
reduce_props(object_id, to) { |memo, prop, prop| ... } click to toggle source
# File lib/ferrum/frame/runtime.rb, line 213
def reduce_props(object_id, to)
  if cyclic?(object_id).dig("result", "value")
    return to.is_a?(Array) ? [cyclic_object] : cyclic_object
  else
    props = @page.command("Runtime.getProperties", ownProperties: true, objectId: object_id)
    props["result"].reduce(to) do |memo, prop|
      next(memo) unless prop["enumerable"]
      yield(memo, prop["name"], prop["value"])
    end
  end
end