diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f35de29..4d8eebc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,10 +3,16 @@ ### Added ### Changed +- Every script runs the new way: keyword arguments are named parameters, promises and thenables are awaited, all + statements run and the last one's value is returned, and `Node#evaluate` returns nodes and dates, see + [upgrading](docs/13-javascript.md#upgrading-to-021) [#673] ### Fixed ### Removed +- `#evaluate_async`, `#evaluate_func` and `#evaluate_on`, use `#evaluate` and `Node#evaluate`, and + `page.wait_for_navigation! { }` for `evaluate_on`'s `wait:` [#673] +- `FERRUM_JS_API` [#673] ## [0.20.1](https://github.com/rubycdp/ferrum/compare/v0.20.0...v0.20.1) (October 29, 2026) ## diff --git a/README.md b/README.md index 6fd5adb3..3d6f6485 100644 --- a/README.md +++ b/README.md @@ -129,9 +129,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, with a suggested -rewrite, about any whose result will change. The next release, 0.21, removes the -old behavior, so run your suite with `FERRUM_JS_API=new` to check it's ready. See +Some scripts written for Ferrum before 0.21 behave differently now, 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 93fc1aaa..4e9e9026 100644 --- a/docs/13-javascript.md +++ b/docs/13-javascript.md @@ -62,16 +62,10 @@ page.at_css("input").execute("this.value = text", text: "hello") #### Upgrading to 0.21 -Scripts written for the old API run exactly as before, and Ferrum warns, pointing at your code, about any whose result -will change. Calls that only the new API can make, e.g. with named arguments, already run the new way. - -* 0.20 runs old-style scripts the old way and warns. Set `FERRUM_JS_API=new` to run everything the new way. -* 0.21 removes the old way, along with `evaluate_async`, `evaluate_func` and `evaluate_on`. There's no switch back. - -Warnings only cover the scripts and results your suite actually runs into, so before upgrading to 0.21, run the suite -once with `FERRUM_JS_API=new`. Set `FERRUM_DEPRECATION_WARNINGS=raise` to turn the warnings into errors while -migrating, or `0` to silence them. A library built on Ferrum should pass named arguments or a function declaration, -which run the new way in both 0.20 and 0.21. +0.21 removes the JavaScript API from before named arguments, along with `evaluate_async`, `evaluate_func` and +`evaluate_on`. Plain expressions and positional arguments, `evaluate("document.title")` or +`evaluate("arguments[0] + 1", 1)`, work as before. Upgrade to 0.20 first and fix what it warns about, or run your +suite on 0.20 with `FERRUM_JS_API=new`, which behaves like 0.21. These scripts behave differently: | Script | < 0.21 | >= 0.21 | Notes | |---|---|---|---| diff --git a/lib/ferrum/browser.rb b/lib/ferrum/browser.rb index 2e56b0ea..361b5cd7 100644 --- a/lib/ferrum/browser.rb +++ b/lib/ferrum/browser.rb @@ -37,8 +37,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 794a7c6c..39da10f4 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 # @@ -140,19 +139,7 @@ class Frame # is sent unchanged, with `this` bound to the node and the arguments # `["#", "!"]`, and comes back as `"#main!"`. # - # ## Scripts written for the old API - # - # Until Ferrum 0.21, a call that only the old API could have made runs - # the old way through {Legacy} and warns when its result will change. - # That's a call without named arguments, or one whose script reads - # `arguments` or also passes positional ones, unless the script is a - # declaration or uses `await`, or the method is {#evaluate_handle} or - # {Node#execute}. - # `FERRUM_JS_API=new` sends every call through the steps above. - # 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 DEEP_SERIALIZATION = { @@ -276,10 +263,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) params, values = arguments_for(script, positional, named) 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 1f33eaf8..00000000 --- a/lib/ferrum/frame/runtime/legacy.rb +++ /dev/null @@ -1,336 +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 `FERRUM_JS_API=new` is set or the call - # 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. 0.21 - # removes this module, leaving only the new way. - # - # The script is parsed by {Script} as for the new way, to route the - # call and to spot what changes. An old-style call then runs as it did - # in Ferrum 0.19: - # - # 1. **Arguments.** Keyword arguments, `timeout:` included, are - # appended to the positional ones as one trailing object. - # 2. **Wrap.** The source is wrapped as it is, in - # `function() { return }`, or `function() { }` for - # {#execute}. A line break after `return` ends that statement, and - # only the first statement of `"a(); b"` runs. - # 3. **Call.** One `Runtime.callFunctionOn` that doesn't await - # promises, bounded by the page timeout, which raises a plain - # {TimeoutError}. {#evaluate_async} races the promise against a - # timer in the page instead. - # 4. **Check.** Before the result is converted, it's compared with what - # 0.21 will do: its subtype for promises, nodes and dates, its - # properties for nodes in an array, and the parsed script for - # statements separated by a semicolon. Nothing is - # run on the result, so getters and proxy traps don't fire. - # 5. **Warn.** Each difference warns once per call site, naming the - # first caller outside installed gems, then the result is converted - # as before: a promise comes back as `{}`. - # - module Legacy - # Marker rejected browser-side by {#evaluate_async} when it times out. - TIMEOUT_MARKER = "timed out promise" - - NEXT_VERSION = "Ferrum 0.21" - - # - # @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 Ferrum 0.21. 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 Ferrum 0.21. Use #evaluate, which takes a function " \ - "declaration: evaluate(\"function(a, b) { return a + b }\", 1, 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) - replacement = 'node.evaluate("this.value")' - replacement = "page.wait_for_navigation!(#{wait}) { #{replacement} }" if wait.positive? - Utils::Deprecate.warn("#{self.class}#evaluate_on will be removed in Ferrum 0.21. Use #{replacement} 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 ENV.fetch("FERRUM_JS_API", nil) == "new" - return false if mode == :handle || (on && (mode == :none || !on.is_a?(Node))) - return false if script.declaration? || script.awaits? - - keywords.empty? || positional.any? || script.uses?("arguments") - end - - # Keywords used to be collected into a trailing Hash, `timeout:` - # 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, serializationOptions: DEEP_SERIALIZATION) - 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 - params = params.merge(serializationOptions: DEEP_SERIALIZATION) if handle == true - - 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 - - def legacy_handle_error(response) - details = response["exceptionDetails"] - return unless details - - description = details.dig("exception", "description").to_s - raise ScriptTimeoutError if description.start_with?("Error: #{TIMEOUT_MARKER}") - - raise JavaScriptError, details - 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.several_statements? - - warn_about(script, "This script is more than one statement, and today only the first one runs. " \ - "#{NEXT_VERSION} will run them all and return the value of the last one. Check the " \ - "rest is safe to run, or remove it.") - end - - def warn_about_result(script, response, kind) - result = response["result"] - 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 ISO 8601 string") if kind == :node - when "array" then warn_about_nodes(script, result) if kind == :node - end - 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_nodes(script, result) - nodes = properties(result, ownProperties: true).any? { |p| p.dig("value", "subtype") == "node" } - warn_about_node(script, "DOM nodes in an array", "Ferrum::Node") if nodes - end - - def properties(result, **options) - @page.command("Runtime.getProperties", objectId: result["objectId"], **options)["result"] - 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} Set FERRUM_JS_API=new to run it the #{NEXT_VERSION} way now." \ - "\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 99bc56d5..846b7ba9 100644 --- a/lib/ferrum/frame/script.rb +++ b/lib/ferrum/frame/script.rb @@ -77,7 +77,7 @@ class Script def initialize(source) @source = source.to_s @code_start = @code_end = 0 - @words = Set.new + @awaits = false @scanned = @source.valid_encoding? && (@source.encoding == Encoding::UTF_8 || @source.ascii_only?) @scanned ? scan : @code_end = @source.bytesize @code = @source.byteslice(@code_start...@code_end) @@ -105,54 +105,23 @@ def declaration? # @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 the code is several statements, separated by a top-level - # semicolon, as in `"a(); b"`, or by a line break that can only end a - # statement, as in `"a()\nb"`, unlike `"a\n.b()"`. - # - # @return [Boolean] - # - def several_statements? - !@last_statement_start.nil? + @awaits end # # The code split before its last statement, so that the value of that - # one can be returned: `"a = 1\nb"` gives `["a = 1\n", "b"]`. + # one can be returned. Statements are separated by a top-level + # semicolon, as in `"a(); b"`, or by a line break that can only end a + # statement, as in `"a = 1\nb"`, which gives `["a = 1\n", "b"]`. # # @return [Array(String, String)] # def split_last_statement - return ["", @code] unless several_statements? + return ["", @code] unless @last_statement_start [@source.byteslice(@code_start...@last_statement_start), @source.byteslice(@last_statement_start...@code_end)] end - # - # Whether a line break comes before the code, which right after a - # `return` ends that statement. - # - # @return [Boolean] - # - def leading_line_break? - @scanned && !@code.empty? && @source.byteslice(0...@code_start).include?("\n") - end - private # Whether the code is one parenthesized group, as in `(function() {})`. @@ -230,7 +199,7 @@ def block_start?(previous) def scan_token(scanner, stack, previous) if scanner.scan(WORD) - @words << scanner.matched unless previous == "." + @awaits ||= scanner.matched == "await" && previous != "." scanner.matched elsif scanner.scan(STRING) || (regexp_allowed?(previous) && scanner.scan(REGEXP)) scanner.matched diff --git a/lib/ferrum/page.rb b/lib/ferrum/page.rb index 3597d5b0..8ca5f87e 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 index 51a0eb07..b231e487 100644 --- a/lib/ferrum/utils/deprecate.rb +++ b/lib/ferrum/utils/deprecate.rb @@ -3,8 +3,7 @@ module Ferrum module Utils # - # Emits deprecation warnings for the JavaScript evaluation API, whose - # old behavior is removed in the next release, Ferrum 0.21. + # Emits deprecation warnings for APIs removed in a later release. # # Warnings are printed once per unique message and call site so that a # deprecated call inside a loop doesn't flood the output. Set diff --git a/sig/ferrum/frame/runtime.rbs b/sig/ferrum/frame/runtime.rbs index ca7718e5..2ad9cb23 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 46c2b656..00000000 --- a/sig/ferrum/frame/runtime/legacy.rbs +++ /dev/null @@ -1,57 +0,0 @@ -module Ferrum - class Frame - module Runtime - module Legacy : Runtime - TIMEOUT_MARKER: ::String - - NEXT_VERSION: ::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 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_nodes: (Script script, untyped result) -> void - - def properties: (untyped result, **untyped options) -> ::Array[untyped] - - 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 ca4d11f6..93925c46 100644 --- a/sig/ferrum/frame/script.rbs +++ b/sig/ferrum/frame/script.rbs @@ -41,8 +41,6 @@ module Ferrum @paren_close: ::Integer? - @words: ::Set[::String] - @block: bool? @block_end: bool? @@ -51,6 +49,8 @@ module Ferrum @condition_end: bool? + @awaits: bool + @scanned: bool @wrapped: Script? @@ -67,13 +67,8 @@ module Ferrum def awaits?: () -> bool - def uses?: (::String word) -> bool - - def several_statements?: () -> bool - def split_last_statement: () -> [::String, ::String] - def leading_line_break?: () -> bool private diff --git a/spec/frame/runtime/legacy_spec.rb b/spec/frame/runtime/legacy_spec.rb deleted file mode 100644 index be29eb9a..00000000 --- a/spec/frame/runtime/legacy_spec.rb +++ /dev/null @@ -1,223 +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") - browser.evaluate("[1,\n 2];\n") - browser.evaluate("(() => { const a = 1; return a })()") - end.not_to output.to_stderr - end - - it "returns an object with a then method as it is" do - expect do - expect(browser.evaluate("({ then() {}, a: 1 })")).to eq("then" => {}, "a" => 1) - expect(browser.execute("return Object.create({ then() {} })")).to be(true) - 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 statement, and today only the first one runs/) { 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 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 - - it "returns DOM nodes in an array from Node#evaluate as empty objects" do - browser.go_to("/index") - node = browser.at_xpath(".//a") - - result = evaluate_warning(/returns DOM nodes in an array as \{\} today/) { node.evaluate("[this]") } - - 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 - - it "points #evaluate_on with wait: at #wait_for_navigation!" do - browser.go_to("/index") - node = browser.at_xpath(".//a") - - evaluate_warning(/Use page\.wait_for_navigation!\(0\.1\) \{ node\.evaluate\("this\.value"\) \} instead/) do - browser.evaluate_on(node: node, expression: "this.tagName", wait: 0.1) - end - end - - it "raises when a string is thrown in #evaluate_async or #evaluate_on" do - ENV["FERRUM_DEPRECATION_WARNINGS"] = "0" - browser.go_to("/table") - node = browser.at_xpath(".//td") - - expect do - browser.evaluate_async(%(throw "smth"), 5) - end.to raise_error(Ferrum::JavaScriptError, "Uncaught (in promise) smth") - expect do - browser.evaluate_on(node: node, expression: %((() => { throw "smth" })())) - end.to raise_error(Ferrum::JavaScriptError, "Uncaught smth") - ensure - ENV.delete("FERRUM_DEPRECATION_WARNINGS") - 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 aff338f4..8df87c3a 100644 --- a/spec/frame/runtime_spec.rb +++ b/spec/frame/runtime_spec.rb @@ -521,15 +521,7 @@ end end - describe "with FERRUM_JS_API=new" do - around do |example| - original = ENV.fetch("FERRUM_JS_API", nil) - ENV["FERRUM_JS_API"] = "new" - example.run - ensure - ENV["FERRUM_JS_API"] = original - end - + describe "scripts written for the old API" 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/) @@ -570,10 +562,6 @@ expect(node.evaluate("this.parentNode")).to be_a(Ferrum::Node) expect(node.evaluate("new Date(0)")).to be_a(String) end - - it "doesn't warn" do - expect { expect(browser.evaluate("Promise.resolve(1)")).to eq(1) }.not_to output.to_stderr - end end describe "#evaluate_handle" do diff --git a/spec/unit/frame/script_spec.rb b/spec/unit/frame/script_spec.rb index 1b0393bd..eabe1ba8 100644 --- a/spec/unit/frame/script_spec.rb +++ b/spec/unit/frame/script_spec.rb @@ -160,76 +160,36 @@ def script(source) end end - describe "#uses?" do + describe "#split_last_statement" do { - "arguments[0]" => true, - "Array.from(arguments)" => true, - "`${arguments.length}`" => true, - "fn.arguments" => false, - "'arguments'" => false, - "// arguments" => false, - "argumentsList" => false + "a(); b" => ["a(); ", "b"], + "a = 1\nb" => ["a = 1\n", "b"], + "a\n.b()" => ["", "a\n.b()"], + "a = 1\n!a" => ["a = 1\n", "!a"], + "a = 1\n~a" => ["a = 1\n", "~a"], + "a = 1\n++a" => ["a = 1\n", "++a"], + "a = 1\n--a" => ["a = 1\n", "--a"], + "x++\nx" => ["x++\n", "x"], + "x--\n--x" => ["x--\n", "--x"], + "x++ / 2" => ["", "x++ / 2"], + "function f() {}\n[1].map(f)" => ["function f() {}\n", "[1].map(f)"], + "class A {}\n(new A)" => ["class A {}\n", "(new A)"], + "if (a) {}\n[b]" => ["if (a) {}\n", "[b]"], + "f = () => {}\n[1].map(f)" => ["f = () => {}\n", "[1].map(f)"], + "o = {}\n[a]" => ["", "o = {}\n[a]"], + "if (a)\n b\nelse\n c" => ["", "if (a)\n b\nelse\n c"], + "while (a)\n b()\nc" => ["while (a)\n b()\n", "c"], + "f(a)\nb" => ["f(a)\n", "b"], + "a\n!= 1" => ["", "a\n!= 1"], + "a\n+ 1" => ["", "a\n+ 1"], + "if (a) { b }\nelse { c }" => ["", "if (a) { b }\nelse { c }"], + "try { a }\ncatch (e) { b }\nfinally { c }" => ["", "try { a }\ncatch (e) { b }\nfinally { c }"], + "x = 1;" => ["", "x = 1"], + "(function() { a(); b() })()" => ["", "(function() { a(); b() })()"], + "for (;;) {}" => ["", "for (;;) {}"] }.each do |source, expected| - it "is #{expected} for #{source.inspect} and arguments" do - expect(script(source).uses?("arguments")).to be(expected) - end - end - end - - describe "#several_statements?" do - { - "a(); b" => true, - "1; 2" => true, - "{ a: 1 }; 2" => true, - "a();\nb()" => true, - "x = 1;" => false, - "a();;" => false, - "a(); // comment" => false, - "(function() { a(); b() })()" => false, - "for (;;) {}" => false, - "`${a; b}`" => false, - "\"a; b\"" => false, - "// a; b\n1" => false, - "a\nb" => true, - "a = 1\n!a" => true, - "a = 1\n~a" => true, - "a = 1\n++a" => true, - "a = 1\n--a" => true, - "a\n!= 1" => false, - "a\n+ 1" => false, - "if (a) { b }\nelse { c }" => false, - "try { a }\ncatch (e) { b }\nfinally { c }" => false, - "x++\nx" => true, - "x++ / 2" => false, - "function f() {}\n[1].map(f)" => true, - "if (a) {}\n[b]" => true, - "o = {}\n[a]" => false, - "if (a)\n b\nelse\n c" => false, - "while (a)\n b()\nc" => true - }.each do |source, expected| - it "is #{expected} for #{source.inspect}" do - expect(script(source).several_statements?).to be(expected) - end - end - end - - describe "#leading_line_break?" do - { - "\n1 + 1" => true, - "\n window.x = 1\n" => true, - " \n 1" => true, - "// comment\n1" => true, - "/* a\n b */ 1" => true, - "1 + 1" => false, - " 1 + 1" => false, - "/* comment */ 1" => false, - "1 +\n1" => false, - "1\n" => false, - "\n" => false, - "" => false - }.each do |source, expected| - it "is #{expected} for #{source.inspect}" do - expect(script(source).leading_line_break?).to be(expected) + it "is #{expected.inspect} for #{source.inspect}" do + expect(script(source).split_last_statement).to eq(expected) end end end @@ -321,7 +281,7 @@ def script(source) result = script(source) expect(result.code.bytes).to eq(source.bytes) - expect([result.declaration?, result.awaits?, result.leading_line_break?]).to eq([false, false, false]) + expect([result.declaration?, result.awaits?]).to eq([false, false]) end end