From ebc41214205cd8d48f3a6ac984faf8a6b401511c Mon Sep 17 00:00:00 2001 From: Dmitry Vorotilin Date: Mon, 19 Oct 2026 17:33:46 +0300 Subject: [PATCH] feat(runtime): finish the move to the new JavaScript evaluation API for 0.21 Removes `Runtime::Legacy`, which kept old-style scripts running as before and warned about what changes here, along with `#evaluate_async`, `#evaluate_func`, `#evaluate_on` and `Utils::Deprecate`. Every call now takes the new path: - a returned promise or thenable is awaited, and a rejection raises - keywords are always named parameters, and mixing them with positional arguments raises `ArgumentError` - a script of several statements raises `SyntaxError`, and one after a leading line break runs - a thrown value that isn't an `Error` raises `JavaScriptError` - `Node#evaluate` resolves nodes and dates instead of returning `{}` Positional arguments keep working through `arguments[n]`. --- CHANGELOG.md | 6 +- README.md | 6 +- docs/13-javascript.md | 10 +- lib/ferrum.rb | 1 - lib/ferrum/browser.rb | 3 +- lib/ferrum/frame/runtime.rb | 7 - lib/ferrum/frame/runtime/legacy.rb | 345 ---------------------------- lib/ferrum/frame/script.rb | 27 +-- lib/ferrum/page.rb | 3 +- lib/ferrum/utils/deprecate.rb | 77 ------- sig/ferrum/frame/runtime.rbs | 2 - sig/ferrum/frame/runtime/legacy.rbs | 61 ----- sig/ferrum/frame/script.rbs | 6 +- sig/ferrum/utils/deprecate.rbs | 13 -- spec/frame/runtime/legacy_spec.rb | 195 ---------------- spec/frame/runtime_spec.rb | 45 ++++ 16 files changed, 59 insertions(+), 748 deletions(-) delete mode 100644 lib/ferrum/frame/runtime/legacy.rb delete mode 100644 lib/ferrum/utils/deprecate.rb delete mode 100644 sig/ferrum/frame/runtime/legacy.rbs delete mode 100644 sig/ferrum/utils/deprecate.rbs delete mode 100644 spec/frame/runtime/legacy_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 090a57b5..2e675d9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,7 @@ - `Browser::Process#default_user_agent`, `#browser_version`, `#protocol_version`, `#v8_version` and `#webkit_version`, use `Browser#default_user_agent` and `Browser#version` instead - Ruby 3.1 support, Ferrum requires Ruby 3.2 or newer +- `#evaluate_async`, `#evaluate_func` and `#evaluate_on`, use `#evaluate` and `Node#evaluate` ## [0.19.1](https://github.com/rubycdp/ferrum/compare/v0.19.0...v0.19.1) (October 16, 2026) ## @@ -122,8 +123,9 @@ - `Ferrum::Page::Stream#stream` now closes the CDP stream handle (`IO.close`) once it's been fully read, so streams opened for `Ferrum::Browser#pdf` and `Ferrum::Page::Tracing#record` no longer keep their backing storage alive in the browser; a failed `IO.close` is raised to the caller. -- `#evaluate_async`, `#evaluate_func` and `#evaluate_on` are deprecated, and scripts whose result will change in the upcoming - 0.21 warn with a rewrite, see [upgrading](docs/13-javascript.md#upgrading-to-021) +- `#evaluate`/`#execute` await a returned promise or thenable, keywords are always named parameters and can't be + mixed with positional arguments, a thrown non-`Error` raises, and `Node#evaluate` resolves nodes and dates, see + [upgrading](docs/13-javascript.md#upgrading-to-021) - `Ferrum::ScriptTimeoutError` is a `Ferrum::TimeoutError` ### Fixed diff --git a/README.md b/README.md index 1c4f1ca6..524dd303 100644 --- a/README.md +++ b/README.md @@ -105,8 +105,7 @@ page.evaluate(<<~JS, a: 1, b: 2) JS ``` -`await` works directly, and a function declaration's returned promise is -awaited: +`await` works directly, and a returned promise is awaited: ```ruby page.evaluate("await fetch(url).then(r => r.text())", url: "/api") @@ -134,8 +133,7 @@ page.evaluate("Array.from(nodes).map(n => n.textContent)", nodes: list) page.at_css("input").evaluate("this.value") ``` -Scripts written for the old API keep working, and Ferrum warns about any whose -result will change in the next release, 0.21, with a suggested rewrite. See +Coming from 0.20 or earlier? See [upgrading to 0.21](docs/13-javascript.md#upgrading-to-021). Do any mouse movements you like: diff --git a/docs/13-javascript.md b/docs/13-javascript.md index b1909904..d53d5ad9 100644 --- a/docs/13-javascript.md +++ b/docs/13-javascript.md @@ -57,17 +57,15 @@ page.at_css("input").execute("this.value = text", text: "hello") #### Upgrading to 0.21 -The next release, Ferrum 0.21, finishes the move to the API above. Until then, scripts written for the old API run -exactly as before, and Ferrum warns, pointing at your code, about any whose result will change. `evaluate_async`, -`evaluate_func` and `evaluate_on` are deprecated and will be removed in 0.21. Set -`FERRUM_DEPRECATION_WARNINGS=raise` to turn the warnings into errors while migrating, or `0` to silence them. +0.21 finishes the move to the API above. Ferrum 0.20 runs old-style scripts as before and warns about each change below, +pointing at your code, so upgrade to 0.20 first and fix its warnings. -| Warning | In 0.20 | From 0.21 | Rewrite | +| Script | 0.20 | 0.21 | Rewrite | |---|---|---|---| | Keyword arguments read through `arguments` | one object in `arguments[0]` | named parameters | `evaluate("options.foo", options: { foo: 1 })` | | Mixing positional and keyword arguments | keywords appended as an object | `ArgumentError` | pass the object in braces, or name every argument | | More than one expression, `"a(); b"` | returns `a()`, skips the rest | `SyntaxError` | `evaluate("function() { a(); return b }")` | -| Never runs, a line break before the script | `nil`, nothing runs | runs and returns its value | check it's safe to run, or remove it | +| A line break before the script | `nil`, nothing runs | runs and returns its value | check it's safe to run, or remove it | | Returns a promise | `{}`, not awaited | awaited, raises if rejected | prefix with `void` to keep it from waiting | | Returns an object with a `then` method | the object | awaited like a promise | wrap it, `({ value: object })` | | Throws something that isn't an `Error` | the thrown value | `Ferrum::JavaScriptError` | return the value instead | diff --git a/lib/ferrum.rb b/lib/ferrum.rb index db746feb..676de110 100644 --- a/lib/ferrum.rb +++ b/lib/ferrum.rb @@ -7,7 +7,6 @@ require "ferrum/utils/platform" require "ferrum/utils/elapsed_time" require "ferrum/utils/attempt" -require "ferrum/utils/deprecate" require "ferrum/errors" require "ferrum/browser" require "ferrum/remote_object" diff --git a/lib/ferrum/browser.rb b/lib/ferrum/browser.rb index 92378c43..bfa04216 100644 --- a/lib/ferrum/browser.rb +++ b/lib/ferrum/browser.rb @@ -36,8 +36,7 @@ class Browser screenshot pdf mhtml viewport_size device_pixel_ratio start_screencast stop_screencast frames frame_by main_frame - evaluate evaluate_handle execute - evaluate_on evaluate_async evaluate_func isolated_world + evaluate evaluate_handle execute isolated_world add_script_tag add_style_tag bypass_csp evaluate_on_new_document remove_script_on_new_document on position position= diff --git a/lib/ferrum/frame/runtime.rb b/lib/ferrum/frame/runtime.rb index e3769317..aa968e74 100644 --- a/lib/ferrum/frame/runtime.rb +++ b/lib/ferrum/frame/runtime.rb @@ -2,7 +2,6 @@ require "singleton" require "ferrum/frame/script" -require "ferrum/frame/runtime/legacy" module Ferrum # @@ -81,8 +80,6 @@ class Frame # it run for as long as it takes. # module Runtime - include Legacy - INTERMITTENT_ATTEMPTS = ENV.fetch("FERRUM_INTERMITTENT_ATTEMPTS", 6).to_i INTERMITTENT_SLEEP = ENV.fetch("FERRUM_INTERMITTENT_SLEEP", 0.1).to_f @@ -199,10 +196,6 @@ def evaluate_in(object, expression, positional, keywords, mode: :value) # def run(expression, positional, keywords, mode:, on: nil) script = Script.new(expression) - if legacy?(script, positional, keywords, mode: mode, on: on) - return run_legacy(script, positional, keywords, mode: mode, on: on) - end - named = keywords.except(:timeout, :args) params, values = arguments_for(script, positional, named_arguments(keywords[:args], named), mode) call(wrap(script, params, mode), values, on: on, mode: mode, timeout: script_timeout(keywords[:timeout])) diff --git a/lib/ferrum/frame/runtime/legacy.rb b/lib/ferrum/frame/runtime/legacy.rb deleted file mode 100644 index b41b7e2d..00000000 --- a/lib/ferrum/frame/runtime/legacy.rb +++ /dev/null @@ -1,345 +0,0 @@ -# frozen_string_literal: true - -module Ferrum - class Frame - module Runtime - # - # The JavaScript evaluation API as it was before named arguments, kept - # so that scripts written for it return exactly what they used to. A - # call takes this path unless it uses something only the new API has: - # named arguments, a function declaration, `await`, or one of the new - # methods. - # - # Whatever runs here is checked for what Ferrum 0.21 will do - # differently, which is warned about with a suggested rewrite. Nothing - # in this module will be kept in 0.21. - # - module Legacy - # Marker rejected browser-side by {#evaluate_async} when it times out. - TIMEOUT_MARKER = "timed out promise" - - NEXT_VERSION = "Ferrum 0.21" - - # Scripts already checked for whether they parse the 0.21 way. - PARSES = Concurrent::Map.new - - # Inspects an object result for the differences 0.21 would make. - INSPECT_RESULT = <<~JS - function() { - try { - return [ - typeof this.then === "function", - Array.isArray(this) && this.some(e => e instanceof Node) - ]; - } catch (e) { - return [false, false]; - } - } - JS - - # - # @deprecated Will be removed in Ferrum 0.21. Use {#evaluate}, which awaits - # promises: `page.evaluate("await thing()", timeout: wait)`. - # - # Evaluates an asynchronous expression, appending a resolve callback as - # the last entry of `arguments`. - # - # @param [String] expression - # The JavaScript to evaluate. - # - # @param [Integer] wait - # How long to wait for the promise to settle, in seconds. - # - # @param [Array] args - # Additional arguments, reachable as `arguments[0]` and up. The - # resolve callback follows them. - # - # @return [Object] - # - def evaluate_async(expression, wait, *args) - Utils::Deprecate.warn( - "#{self.class}#evaluate_async will be removed in #{NEXT_VERSION}. Use #evaluate, which waits for a " \ - "returned promise, instead of calling the last of `arguments`: " \ - "evaluate(\"await new Promise(resolve => setTimeout(resolve, 100))\", timeout: #{wait})." - ) - - template = <<~JS - function() { - return new Promise((__f, __r) => { - try { - arguments[arguments.length] = r => __f(r); - arguments.length = arguments.length + 1; - setTimeout(() => __r(new Error("#{TIMEOUT_MARKER}")), %s); - %s - } catch(error) { - __r(error); - } - }); - } - JS - - expression = format(template, wait * 1000, expression) - legacy_call(expression: expression, arguments: args, awaitPromise: true) - end - - # - # @deprecated Will be removed in Ferrum 0.21. Use {#evaluate}, which takes a - # function declaration and named arguments, or {Node#evaluate}. - # - # Evaluates a raw JS function declaration, optionally on a specific - # remote object instead of the frame's global execution context. - # - # @param [String] expression - # A JS function declaration, e.g. `"function(a, b) { return a + b }"`. - # - # @param [Array] args - # Arguments to pass to the function. - # - # @param [Node, nil] on - # Remote object to invoke the function on. - # - # @return [Object] - # - def evaluate_func(expression, *args, on: nil) - Utils::Deprecate.warn( - "#{self.class}#evaluate_func will be removed in #{NEXT_VERSION}. Use #evaluate, which takes a function " \ - "declaration and named arguments: evaluate(\"function(a, b) { return a + b }\", a: 1, b: 2). " \ - "For on:, use node.evaluate." - ) - - legacy_call(expression: expression, arguments: args, on: on) - end - - # - # @deprecated Will be removed in Ferrum 0.21. Use {Node#evaluate}. - # - # Evaluates an expression against a given node's remote object (+this+ - # refers to the node). - # - # @param [Node] node - # The node to evaluate the expression on. - # - # @param [String] expression - # The JavaScript to evaluate. - # - # @param [Boolean] by_value - # Whether to return the plain JS value instead of resolving it. - # - # @param [Integer] wait - # Passed through to the underlying `Runtime.callFunctionOn` command. - # - # @return [Object] - # - def evaluate_on(node:, expression:, by_value: true, wait: 0) - Utils::Deprecate.warn( - "#{self.class}#evaluate_on will be removed in #{NEXT_VERSION}. Use node.evaluate(\"this.value\") instead." - ) - - options = { handle: true } - expression = format("function() { return %s }", expression) - options = { handle: false, returnByValue: true } if by_value - legacy_call(expression: expression, on: node, wait: wait, **options) - end - - private - - # Calls that only the new API could have made go the new way. - def legacy?(script, positional, keywords, mode:, on:) - return false if mode == :handle || (on && (mode == :none || !on.is_a?(Node))) - return false if function?(script, mode) || script.awaits? - - keywords.empty? || positional.any? || script.uses?("arguments") - end - - # Keywords used to be collected into a trailing Hash, `timeout:` and - # `args:` included, so they're passed on exactly that way. - def run_legacy(script, positional, keywords, mode:, on:) - warn_about_keywords(script, positional, keywords) - arguments = keywords.empty? ? positional : [*positional, keywords] - - if mode == :none - legacy_execute(script, arguments) - elsif on - legacy_evaluate_on(script, arguments, on) - else - legacy_evaluate(script, arguments) - end - end - - def legacy_evaluate(script, arguments) - warn_about_line_break(script) - response = legacy_call(expression: format("function() { return %s }", script.source), - arguments: arguments, handle: :raw) - legacy_handle_error(response) - warn_about_result(script, response, :value) - warn_about_statements(script) - handle_response(response["result"]) - end - - def legacy_evaluate_on(script, arguments, node) - warn_about_line_break(script) - response = legacy_call(expression: format("function() { return %s }", script.source), - arguments: arguments, on: node, handle: :raw) - legacy_handle_error(response) - warn_about_result(script, response, :node) - warn_about_statements(script) - serialize(response["result"]) - end - - def legacy_execute(script, arguments) - response = legacy_call(expression: format("function() { %s }", script.source), - arguments: arguments, handle: :raw) - legacy_handle_error(response) - warn_about_result(script, response, :none) - release(response["result"]) - true - end - - # What `returnByValue` used to give, fetched afterwards so that the - # result could be inspected first. - def serialize(result) - object_id = result["objectId"] - return result["value"] unless object_id - - response = @page.command("Runtime.callFunctionOn", objectId: object_id, returnByValue: true, - functionDeclaration: "function() { return this }") - response.dig("result", "value") - ensure - release(result) - end - - def release(result) - object_id = result&.dig("objectId") - @page.command("Runtime.releaseObject", objectId: object_id) if object_id - rescue BrowserError - nil - end - - def legacy_call(expression:, arguments: [], on: nil, wait: 0, handle: true, **options) - # do not rescue -> retry if we operate on an existing node - errors = on ? [] : [NodeNotFoundError, NoExecutionContextError] - - Utils::Attempt.with_retry(errors: errors, max: INTERMITTENT_ATTEMPTS, wait: INTERMITTENT_SLEEP) do - params = options.dup - - params = params.merge(objectId: resolve_node(on)) if on - - 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))) - next response if handle == :raw - - legacy_handle_error(response) - response = response["result"] - - handle ? handle_response(response) : response["value"] - end - end - - # Only an `Error` raised: anything else thrown came back as the result. - def legacy_handle_error(response) - result = response["result"] - return if result["subtype"] != "error" - - raise ScriptTimeoutError if result["description"].to_s.start_with?("Error: #{TIMEOUT_MARKER}") - - raise JavaScriptError, response["exceptionDetails"] || { "exception" => result } - end - - def warn_about_keywords(script, positional, keywords) - return if keywords.empty? - - if positional.any? - warn_about(script, "Mixing positional and keyword arguments will raise ArgumentError in #{NEXT_VERSION}, " \ - "where keywords will become named parameters instead of a trailing object. Pass the " \ - "object in braces, evaluate(\"…\", 1, { foo: 1 }), or name every argument.") - else - warn_about(script, "Keyword arguments will become named parameters in #{NEXT_VERSION} instead of " \ - "arriving as one object in `arguments[0]`. Name the object instead: " \ - "evaluate(\"options.foo\", options: { foo: 1 }).") - end - end - - def warn_about_line_break(script) - return unless script.leading_line_break? - - warn_about(script, "This script never runs: the line break before it ends the `return` it's placed " \ - "after, so the result is always nil. #{NEXT_VERSION} will run it and return its value. " \ - "Check it's safe to run, or remove it.") - end - - def warn_about_statements(script) - return if script.leading_line_break? || !script.code.match?(/[;\n]/) - return if PARSES.compute_if_absent(script.code) { parses?(wrap(script, [], :value)) } - - warn_about(script, "This script is more than one expression, which will be a SyntaxError in " \ - "#{NEXT_VERSION}; today only the first statement runs. Use a function declaration " \ - "and return the value: evaluate(\"function() { a(); return b }\").") - end - - def parses?(declaration) - response = @page.command("Runtime.compileScript", expression: "(#{declaration})", sourceURL: "", - persistScript: false, executionContextId: execution_id!) - response["exceptionDetails"].nil? - end - - def warn_about_result(script, response, kind) - result = response["result"] - return warn_about_throw(script, result) if response["exceptionDetails"] - - case result["subtype"] - when "promise" then warn_about_promise(script, kind) - when "node" then warn_about_node(script, "a DOM node", "Ferrum::Node") if kind == :node - when "date" then warn_about_node(script, "a Date", "its string") if kind == :node - when "array" then warn_about_object(script, result, kind) if kind == :node - when nil, "proxy" then warn_about_object(script, result, kind) if result["type"] == "object" - end - end - - def warn_about_throw(script, result) - warn_about(script, "This script throws #{result['value'].inspect}, which comes back as its result " \ - "today and will raise Ferrum::JavaScriptError in #{NEXT_VERSION}. Return the value " \ - "instead, or rescue the error.") - end - - def warn_about_promise(script, kind) - if kind == :none - warn_about(script, "This script returns a promise, which #execute will wait for in #{NEXT_VERSION}. " \ - "Drop the `return`, or use `void`, to keep it from waiting.") - else - warn_about(script, "This script returns a promise, which comes back as {} today. #{NEXT_VERSION} " \ - "will wait for it and return its value, or raise if it rejects. Prefix the " \ - "expression with `void` to keep it from waiting.") - end - end - - def warn_about_node(script, value, replacement) - warn_about(script, "Node#evaluate returns #{value} as {} today, and will return #{replacement} in " \ - "#{NEXT_VERSION}. Check the code that uses the result.") - end - - def warn_about_object(script, result, kind) - response = @page.command("Runtime.callFunctionOn", objectId: result["objectId"], returnByValue: true, - functionDeclaration: INSPECT_RESULT) - thenable, nodes = response.dig("result", "value") - if thenable - warn_about(script, "This script returns an object with a `then` method, which #{NEXT_VERSION} " \ - "will wait for like a promise. Wrap it, e.g. `({ value: object })`, to get the object.") - end - warn_about_node(script, "DOM nodes in an array", "Ferrum::Node") if nodes && kind == :node - end - - def warn_about(script, message) - excerpt = script.code.lines.first.to_s.strip - excerpt = "#{excerpt[0, 60]}…" if excerpt.length > 60 || script.code.lines.size > 1 - Utils::Deprecate.warn("#{message}\n script: #{excerpt}", user_code: true) - end - end - end - end -end diff --git a/lib/ferrum/frame/script.rb b/lib/ferrum/frame/script.rb index 9cb2bdf1..07f425d9 100644 --- a/lib/ferrum/frame/script.rb +++ b/lib/ferrum/frame/script.rb @@ -44,9 +44,6 @@ class Script # Words after which a `/` starts a regular expression, not a division. REGEXP_KEYWORDS = %w[return typeof instanceof in of new delete void throw case do else yield await].freeze - # @return [String] the script as given. - attr_reader :source - # @return [String] the script without surrounding whitespace, comments # and trailing semicolons. attr_reader :code @@ -91,29 +88,7 @@ def named_function? # @return [Boolean] # def awaits? - uses?("await") - end - - # - # Whether the script mentions the given word as code, rather than as a - # property name or inside a string or comment. - # - # @param [String] word - # - # @return [Boolean] - # - def uses?(word) - @words.include?(word) - end - - # - # Whether a line break comes before the code, which right after a - # `return` ends that statement. - # - # @return [Boolean] - # - def leading_line_break? - !@code.empty? && @source[0...@code_start].include?("\n") + @words.include?("await") end # diff --git a/lib/ferrum/page.rb b/lib/ferrum/page.rb index 01590045..c81fa3c1 100644 --- a/lib/ferrum/page.rb +++ b/lib/ferrum/page.rb @@ -34,8 +34,7 @@ class Page delegate %i[at_css at_xpath css xpath wait_for_selector current_url current_title url title body doctype content= - execution_id execution_id! evaluate evaluate_handle execute - evaluate_on evaluate_async evaluate_func isolated_world + execution_id execution_id! evaluate evaluate_handle execute isolated_world add_script_tag add_style_tag] => :main_frame delegate %i[base_url default_user_agent timeout timeout=] => :@options diff --git a/lib/ferrum/utils/deprecate.rb b/lib/ferrum/utils/deprecate.rb deleted file mode 100644 index 5f2fdda8..00000000 --- a/lib/ferrum/utils/deprecate.rb +++ /dev/null @@ -1,77 +0,0 @@ -# frozen_string_literal: true - -module Ferrum - module Utils - # - # Emits deprecation warnings for the JavaScript evaluation API that - # will change in the next release, Ferrum 0.21. - # - # Warnings are printed once per unique message and call site so that a - # deprecated call inside a loop doesn't flood the output. Set - # `FERRUM_DEPRECATION_WARNINGS=0` to silence them entirely, or - # `FERRUM_DEPRECATION_WARNINGS=raise` to turn them into errors while - # migrating a suite. - # - module Deprecate - @seen = Concurrent::Set.new - - module_function - - # - # Warns about a deprecation, naming where it comes from. - # - # @param [String] message - # What changes, and what to do instead. - # - # @param [Boolean] user_code - # Whether to name the first caller outside any installed gem, which is - # where a script passed through a library like Capybara was written, - # rather than the direct caller. - # - # @return [void] - # - # @raise [Ferrum::Error] - # When `FERRUM_DEPRECATION_WARNINGS=raise`. - # - def warn(message, user_code: false) - mode = ENV.fetch("FERRUM_DEPRECATION_WARNINGS", "warn") - return if %w[0 false].include?(mode) - - location = caller_locations(1)&.find { |frame| !skip?(frame.path.to_s, user_code) } - message = "[Ferrum] DEPRECATION: #{message}" - message += "\n called from #{location.path}:#{location.lineno}" if location - - raise Ferrum::Error, message if mode == "raise" - return unless @seen.add?(message) - - Kernel.warn(message) - end - - # - # Forgets which warnings have already been printed. Only useful in tests. - # - # @return [void] - # - def reset! - @seen.clear - end - - # - # Whether a caller's path is left out when naming where a deprecated - # call comes from. - # - # @param [String] path - # - # @param [Boolean] user_code - # Whether to leave out installed gems as well as Ferrum itself. - # - # @return [Boolean] - # - def skip?(path, user_code) - return true if path.include?("/lib/ferrum/") || path.end_with?("forwardable.rb") || path.start_with?("<") - - user_code && Gem.path.any? { |gem_path| path.start_with?(File.join(gem_path, "")) } - end - end - end -end diff --git a/sig/ferrum/frame/runtime.rbs b/sig/ferrum/frame/runtime.rbs index ee8d447b..8b81605d 100644 --- a/sig/ferrum/frame/runtime.rbs +++ b/sig/ferrum/frame/runtime.rbs @@ -7,8 +7,6 @@ module Ferrum class Frame module Runtime - include Legacy - INTERMITTENT_ATTEMPTS: untyped INTERMITTENT_SLEEP: untyped diff --git a/sig/ferrum/frame/runtime/legacy.rbs b/sig/ferrum/frame/runtime/legacy.rbs deleted file mode 100644 index 856f786e..00000000 --- a/sig/ferrum/frame/runtime/legacy.rbs +++ /dev/null @@ -1,61 +0,0 @@ -module Ferrum - class Frame - module Runtime - module Legacy : Runtime - TIMEOUT_MARKER: ::String - - NEXT_VERSION: ::String - - PARSES: Concurrent::Map[::String, bool] - - INSPECT_RESULT: ::String - - %a{deprecated} def evaluate_async: (untyped expression, untyped wait, *untyped args) -> untyped - - %a{deprecated} def evaluate_func: (untyped expression, *untyped args, ?on: untyped?) -> untyped - - %a{deprecated} def evaluate_on: (node: untyped, expression: untyped, ?by_value: bool, ?wait: ::Integer) -> untyped - - private - - def legacy?: (Script script, ::Array[untyped] positional, ::Hash[::Symbol, untyped] keywords, mode: ::Symbol, on: (Node | RemoteObject)?) -> bool - - def run_legacy: (Script script, ::Array[untyped] positional, ::Hash[::Symbol, untyped] keywords, mode: ::Symbol, on: (Node | RemoteObject)?) -> untyped - - def legacy_evaluate: (Script script, ::Array[untyped] arguments) -> untyped - - def legacy_evaluate_on: (Script script, ::Array[untyped] arguments, Node node) -> untyped - - def legacy_execute: (Script script, ::Array[untyped] arguments) -> true - - def serialize: (untyped result) -> untyped - - def release: (untyped result) -> void - - def legacy_call: (expression: ::String, ?arguments: ::Array[untyped], ?on: Node?, ?wait: ::Integer, ?handle: (bool | :raw), **untyped options) -> untyped - - def legacy_handle_error: (untyped response) -> void - - def warn_about_keywords: (Script script, ::Array[untyped] positional, ::Hash[::Symbol, untyped] keywords) -> void - - def warn_about_line_break: (Script script) -> void - - def warn_about_statements: (Script script) -> void - - def parses?: (::String declaration) -> bool - - def warn_about_result: (Script script, untyped response, ::Symbol kind) -> void - - def warn_about_throw: (Script script, untyped result) -> void - - def warn_about_promise: (Script script, ::Symbol kind) -> void - - def warn_about_node: (Script script, ::String value, ::String replacement) -> void - - def warn_about_object: (Script script, untyped result, ::Symbol kind) -> void - - def warn_about: (Script script, ::String message) -> void - end - end - end -end diff --git a/sig/ferrum/frame/script.rbs b/sig/ferrum/frame/script.rbs index 4add4ea7..ffb51230 100644 --- a/sig/ferrum/frame/script.rbs +++ b/sig/ferrum/frame/script.rbs @@ -45,7 +45,7 @@ module Ferrum @words: ::Set[::String] - attr_reader source: ::String + @source: ::String attr_reader code: ::String @@ -57,10 +57,6 @@ module Ferrum def awaits?: () -> bool - def uses?: (::String word) -> bool - - def leading_line_break?: () -> bool - def parameters: () -> ::Array[::String]? private diff --git a/sig/ferrum/utils/deprecate.rbs b/sig/ferrum/utils/deprecate.rbs deleted file mode 100644 index e58cf030..00000000 --- a/sig/ferrum/utils/deprecate.rbs +++ /dev/null @@ -1,13 +0,0 @@ -module Ferrum - module Utils - module Deprecate - self.@seen: Concurrent::Set[::String] - - def self?.warn: (::String message, ?user_code: bool) -> void - - def self?.reset!: () -> void - - def self?.skip?: (::String path, bool user_code) -> bool - end - end -end diff --git a/spec/frame/runtime/legacy_spec.rb b/spec/frame/runtime/legacy_spec.rb deleted file mode 100644 index b271ebd9..00000000 --- a/spec/frame/runtime/legacy_spec.rb +++ /dev/null @@ -1,195 +0,0 @@ -# frozen_string_literal: true - -describe Ferrum::Frame::Runtime::Legacy do - around do |example| - Ferrum::Utils::Deprecate.reset! - example.run - Ferrum::Utils::Deprecate.reset! - end - - def evaluate_warning(message, &block) - result = nil - expect { result = block.call }.to output(message).to_stderr - result - end - - describe "calls that keep working unchanged" do - it "doesn't warn about plain expressions and positional arguments" do - browser.go_to("/index") - - expect do - browser.evaluate("1 + 1") - browser.evaluate("arguments[0] + arguments[1]", 1, 2) - browser.evaluate("arguments[0].a", { a: 1 }) - browser.execute("window.x = arguments[0]", 1) - browser.at_xpath(".//a").evaluate("this.id") - end.not_to output.to_stderr - end - end - - describe "calls that change in 0.21" do - it "passes keyword arguments as one object to a script reading arguments" do - result = evaluate_warning(/become named parameters.*\n script: arguments\[0\]\.foo\n called from #{__FILE__}/) do - browser.evaluate("arguments[0].foo", foo: "bar") - end - - expect(result).to eq("bar") - end - - it "passes reserved keywords along with the rest" do - result = evaluate_warning(/become named parameters/) { browser.evaluate("arguments[0]", timeout: 2) } - - expect(result).to eq("timeout" => 2) - end - - it "appends keyword arguments mixed with positional ones" do - result = evaluate_warning(/Mixing positional and keyword arguments will raise ArgumentError/) do - browser.evaluate("[arguments[0], arguments[1]]", 1, foo: 2) - end - - expect(result).to eq([1, { "foo" => 2 }]) - end - - it "returns the first of several statements" do - result = evaluate_warning(/more than one expression, which will be a SyntaxError/) { browser.evaluate("1; 2") } - - expect(result).to eq(1) - end - - it "doesn't run a script after a leading line break" do - result = evaluate_warning(/This script never runs/) { browser.evaluate("\n window.ran = true\n") } - - expect(result).to be_nil - expect(browser.evaluate("window.ran")).to be_nil - end - - it "returns a promise as an empty object" do - result = evaluate_warning(/returns a promise, which comes back as \{\} today/) do - browser.evaluate("Promise.resolve(42)") - end - - expect(result).to eq({}) - end - - it "doesn't wait for a promise #execute returns" do - result = evaluate_warning(/returns a promise, which #execute will wait for/) do - browser.execute("return new Promise(() => {})") - end - - expect(result).to be(true) - end - - it "returns an object with a then method as it is" do - result = evaluate_warning(/object with a `then` method/) { browser.evaluate("({ then() {}, a: 1 })") } - - expect(result).to eq("then" => {}, "a" => 1) - end - - it "returns a thrown value that isn't an Error" do - result = evaluate_warning(/throws "boom", which comes back as its result/) do - browser.evaluate("(() => { throw 'boom' })()") - end - - expect(result).to eq("boom") - end - - it "returns a DOM node from Node#evaluate as an empty object" do - browser.go_to("/index") - node = browser.at_xpath(".//a") - - result = evaluate_warning(/returns a DOM node as \{\} today/) { node.evaluate("this.parentNode") } - - expect(result).to eq({}) - end - - it "returns a Date from Node#evaluate as an empty object" do - browser.go_to("/index") - node = browser.at_xpath(".//a") - - result = evaluate_warning(/returns a Date as \{\} today/) { node.evaluate("new Date(0)") } - - expect(result).to eq({}) - end - end - - describe "methods removed in 0.21" do - it "keeps #evaluate_async" do - result = evaluate_warning(/#evaluate_async will be removed in Ferrum 0.21/) do - browser.evaluate_async("arguments[0](1)", 1) - end - - expect(result).to eq(1) - end - - it "handles values with #evaluate_async" do - ENV["FERRUM_DEPRECATION_WARNINGS"] = "0" - - expect(browser.evaluate_async("arguments[0](null)", 5)).to be_nil - expect(browser.evaluate_async("arguments[0](false)", 5)).to be false - expect(browser.evaluate_async("arguments[0](true)", 5)).to be true - expect(browser.evaluate_async(%(arguments[0]({foo: "bar"})), 5)).to eq("foo" => "bar") - ensure - ENV.delete("FERRUM_DEPRECATION_WARNINGS") - end - - it "times out with #evaluate_async" do - ENV["FERRUM_DEPRECATION_WARNINGS"] = "0" - - expect do - browser.evaluate_async("var callback=arguments[0]; setTimeout(function(){callback(true)}, 4000)", 1) - end.to raise_error(Ferrum::ScriptTimeoutError) - ensure - ENV.delete("FERRUM_DEPRECATION_WARNINGS") - end - - it "keeps #evaluate_func" do - result = evaluate_warning(/#evaluate_func will be removed in Ferrum 0.21/) do - browser.evaluate_func("function(a) { return a }", 1) - end - - expect(result).to eq(1) - end - - it "keeps #evaluate_on" do - browser.go_to("/index") - node = browser.at_xpath(".//a") - - result = evaluate_warning(/#evaluate_on will be removed in Ferrum 0.21/) do - browser.evaluate_on(node: node, expression: "this.tagName") - end - - expect(result).to eq("A") - end - end - - describe "warnings" do - it "warns once per call site" do - stderr = StringIO.new - original = $stderr - $stderr = stderr - begin - 3.times { browser.evaluate("1; 2") } - ensure - $stderr = original - end - - expect(stderr.string.scan("DEPRECATION").size).to eq(1) - end - - it "raises when FERRUM_DEPRECATION_WARNINGS=raise" do - ENV["FERRUM_DEPRECATION_WARNINGS"] = "raise" - - expect { browser.evaluate("1; 2") }.to raise_error(Ferrum::Error, /DEPRECATION/) - ensure - ENV.delete("FERRUM_DEPRECATION_WARNINGS") - end - - it "stays quiet when FERRUM_DEPRECATION_WARNINGS=0" do - ENV["FERRUM_DEPRECATION_WARNINGS"] = "0" - - expect { browser.evaluate("1; 2") }.not_to output.to_stderr - ensure - ENV.delete("FERRUM_DEPRECATION_WARNINGS") - end - end -end diff --git a/spec/frame/runtime_spec.rb b/spec/frame/runtime_spec.rb index 8fc8fc27..b2ccdb5d 100644 --- a/spec/frame/runtime_spec.rb +++ b/spec/frame/runtime_spec.rb @@ -410,6 +410,51 @@ end end + describe "#evaluate in 0.21" do + it "awaits a returned promise and raises when it rejects" do + expect(browser.evaluate("new Promise(resolve => setTimeout(() => resolve(42), 100))")).to eq(42) + expect { browser.evaluate("Promise.reject(new Error('nope'))") }.to raise_error(Ferrum::JavaScriptError, /nope/) + end + + it "awaits a returned object with a then method" do + expect(browser.evaluate("({ then(resolve) { resolve(42) } })")).to eq(42) + end + + it "makes keywords parameters even when the script reads arguments" do + expect(browser.evaluate("[foo, arguments[0]]", foo: 1)).to eq([1, 1]) + end + + it "rejects mixing positional and named arguments" do + expect { browser.evaluate("a", 1, a: 2) }.to raise_error(ArgumentError, /either positionally or by name/) + end + + it "raises for more than one expression" do + expect { browser.evaluate("1; 2") }.to raise_error(Ferrum::JavaScriptError, /SyntaxError/) + end + + it "runs a script after a leading line break" do + expect(browser.evaluate("\n 1 + 1\n")).to eq(2) + end + + it "raises when a value that isn't an Error is thrown" do + expect { browser.evaluate("(() => { throw 'boom' })()") }.to raise_error(Ferrum::JavaScriptError, /"boom"/) + end + + it "waits for a promise returned to #execute" do + browser.execute("return new Promise(resolve => setTimeout(() => { window.done = true; resolve() }, 100))") + + expect(browser.evaluate("window.done")).to be(true) + end + + it "resolves nodes and dates from Node#evaluate" do + browser.go_to("/index") + node = browser.at_xpath(".//a") + + expect(node.evaluate("this.parentNode")).to be_a(Ferrum::Node) + expect(node.evaluate("new Date(0)")).to be_a(String) + end + end + describe "#evaluate_handle" do it "returns a handle that can be passed back in" do browser.go_to("/index")