diff --git a/CHANGELOG.md b/CHANGELOG.md index f545c544..1f2503b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ ### Fixed - Headers can be added on a remote browser whose `/json/version` has no `User-Agent`, such as browserless v2, instead of failing with `Invalid parameters` [rubycdp/cuprite#300] +- A `Ferrum::Node` found before a navigation or reload kept its cached CDP node id, which Chrome reuses for the new + document, so `focus`, `click`, `scroll_into_view` and friends silently hit another element. Node ids are now dropped + on `DOM.documentUpdated` and such nodes raise `Ferrum::NodeNotFoundError` [#86] ### Removed - `Browser::Process#default_user_agent`, `#browser_version`, `#protocol_version`, `#v8_version` and `#webkit_version`, diff --git a/lib/ferrum/node.rb b/lib/ferrum/node.rb index 00803e2a..65967184 100644 --- a/lib/ferrum/node.rb +++ b/lib/ferrum/node.rb @@ -23,20 +23,26 @@ def initialize(frame, target_id, description, object_id: nil, node_id: nil) @tag_name = description["nodeName"].downcase @object_id = object_id @node_id = node_id + @document_version = @page.document_version end # Frontend node id is resolved lazily, on first actual need (focus, click, scroll_into_view, etc.) # We can try to subscribe to `DOM.childNodeRemoved` and `DOM.childNodeInserted` in the future # to keep track of nodes. def node_id - @node_id ||= begin + return @node_id if @node_id && @document_version == page.document_version + + document_version = page.document_version + @node_id = begin id = page.command("DOM.requestNode", objectId: @object_id)["nodeId"] - raise NodeNotFoundError, "node is not trackable" if id.zero? + raise NodeNotFoundError, { "message" => "node is not trackable" } if id.zero? id rescue NoExecutionContextError - raise NodeNotFoundError, "node is not trackable" + raise NodeNotFoundError, { "message" => "node is not trackable" } end + @document_version = document_version + @node_id end # Whether this is an element node, as opposed to e.g. a text node. diff --git a/lib/ferrum/page.rb b/lib/ferrum/page.rb index f813114a..3ed20a3e 100644 --- a/lib/ferrum/page.rb +++ b/lib/ferrum/page.rb @@ -87,6 +87,12 @@ class Page # @return [Downloads] attr_reader :downloads + # Bumped every time Chrome reports `DOM.documentUpdated`, which + # invalidates every node id handed out before it. + # + # @return [Integer] + attr_reader :document_version + def initialize(client, context_id:, target_id:, proxy: nil) @client = client @context_id = context_id @@ -96,6 +102,7 @@ def initialize(client, context_id:, target_id:, proxy: nil) @frames = Concurrent::Map.new @main_frame = Frame.new(nil, self) @event = Utils::Event.new.tap(&:set) + @document_version = 0 self.proxy = proxy @mouse = Mouse.new(self) @@ -526,6 +533,8 @@ def subscribe network.subscribe downloads.subscribe + on("DOM.documentUpdated") { @document_version += 1 } + if @options.logger on("Runtime.consoleAPICalled") do |params| log_console_api(params) diff --git a/sig/ferrum/node.rbs b/sig/ferrum/node.rbs index 1e0fbb09..b47d5a28 100644 --- a/sig/ferrum/node.rbs +++ b/sig/ferrum/node.rbs @@ -6,6 +6,7 @@ module Ferrum @node_id: untyped @object_id: untyped + @document_version: ::Integer attr_reader page: untyped diff --git a/sig/ferrum/page.rbs b/sig/ferrum/page.rbs index eb7b4bad..51553300 100644 --- a/sig/ferrum/page.rbs +++ b/sig/ferrum/page.rbs @@ -23,6 +23,7 @@ module Ferrum attr_reader headers: Headers attr_reader cookies: Cookies attr_reader downloads: Downloads + attr_reader document_version: ::Integer attr_reader client: (Client | SessionClient) @client: (Client | SessionClient) @@ -40,6 +41,7 @@ module Ferrum @tracing: Page::Tracing @downloads: Downloads @referrer: String? + @document_version: ::Integer def initialize: ((Client | SessionClient) client, context_id: String?, target_id: String, ?proxy: { host: String, port: ::Integer, user: String?, password: String? }?) -> void diff --git a/spec/node_spec.rb b/spec/node_spec.rb index 3c86263d..59a631b2 100644 --- a/spec/node_spec.rb +++ b/spec/node_spec.rb @@ -47,6 +47,20 @@ expect { node.text }.to raise_error(Ferrum::NodeNotFoundError) end + it "raises an error instead of focusing a node from the new document after reload" do + browser.go_to("/form") + node = browser.at_css("#customer_email") + node.focus + + browser.refresh + browser.at_css("#customer_email").focus + browser.at_css("#customer_email").blur + + expect { node.focus }.to raise_error(Ferrum::NodeNotFoundError, "node is not trackable") + expect { node.focus }.to raise_error(Ferrum::NodeNotFoundError, "node is not trackable") + expect(browser.evaluate("document.activeElement.id")).not_to eq("customer_email") + end + it "raises an error if the element is not visible" do browser.go_to("/index")