From 047296bc3c3ddb779caace4eddae56879bf1746f Mon Sep 17 00:00:00 2001 From: Dmitry Vorotilin Date: Sun, 18 Oct 2026 21:19:52 +0300 Subject: [PATCH 1/2] fix(page): drop cached node ids after DOM.getDocument DOM.getDocument resets every node id on the target without firing DOM.documentUpdated, so nodes found before frame.content= on an iframe, a whole-page accessibility.query or the document request on frameStoppedLoading kept a dead id and raised NodeNotFoundError. Bump document_version after it so their ids are requested again. Refs #650 --- CHANGELOG.md | 3 +++ lib/ferrum/page.rb | 14 +++++++++----- spec/node_spec.rb | 24 ++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bc64b01..ba6eda65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,9 @@ - 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] +- Replacing an iframe's content with `frame.content=` or querying the whole page with `accessibility.query` broke every + node found before it with `Ferrum::NodeNotFoundError`, even though the nodes were still on the page. Their ids are + requested again now [#650] - Behind a proxy with credentials, `request.abort` in `on(:request)` and `network.blacklist`/`whitelist` were ignored because Ferrum's own proxy handler continued every request first. It now only continues requests nobody else handles [#332] diff --git a/lib/ferrum/page.rb b/lib/ferrum/page.rb index 6d51c09f..f7d69470 100644 --- a/lib/ferrum/page.rb +++ b/lib/ferrum/page.rb @@ -87,8 +87,8 @@ class Page # @return [Downloads] attr_reader :downloads - # Bumped every time Chrome reports `DOM.documentUpdated`, which - # invalidates every node id handed out before it. + # Bumped every time Chrome reports `DOM.documentUpdated` and after every + # `DOM.getDocument`, both invalidate every node id handed out before them. # # @return [Integer] attr_reader :document_version @@ -521,9 +521,13 @@ def use_authorized_proxy? # The root node id, or `true` when sent asynchronously. # def document_node_id(async: false) - return client.command("DOM.getDocument", async: true, depth: 0) if async - - command("DOM.getDocument", depth: 0).dig("root", "nodeId") + result = if async + client.command("DOM.getDocument", async: true, depth: 0) + else + command("DOM.getDocument", depth: 0).dig("root", "nodeId") + end + @document_version += 1 + result end private diff --git a/spec/node_spec.rb b/spec/node_spec.rb index 59a631b2..763e89be 100644 --- a/spec/node_spec.rb +++ b/spec/node_spec.rb @@ -61,6 +61,30 @@ expect(browser.evaluate("document.activeElement.id")).not_to eq("customer_email") end + it "keeps working after an iframe's content is replaced" do + browser.go_to("/form") + node = browser.at_css("#customer_email") + node.focus + node.blur + + browser.frame_by(name: "frame").content = "

new

" + node.focus + + expect(browser.evaluate("document.activeElement.id")).to eq("customer_email") + end + + it "keeps working after an accessibility query of the whole page" do + browser.go_to("/form") + node = browser.at_css("#customer_email") + node.focus + node.blur + + browser.accessibility.query(role: "textbox") + node.focus + + expect(browser.evaluate("document.activeElement.id")).to eq("customer_email") + end + it "raises an error if the element is not visible" do browser.go_to("/index") From d0b4a7c7ce23eb806bd108a958f45be0ff65b8da Mon Sep 17 00:00:00 2001 From: Dmitry Vorotilin Date: Sun, 18 Oct 2026 21:19:52 +0300 Subject: [PATCH 2/2] fix(node): say the page is still loading when Chrome returns no node id --- CHANGELOG.md | 2 +- lib/ferrum/node.rb | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba6eda65..230b0a36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ on `DOM.documentUpdated` and such nodes raise `Ferrum::NodeNotFoundError` [#86] - Replacing an iframe's content with `frame.content=` or querying the whole page with `accessibility.query` broke every node found before it with `Ferrum::NodeNotFoundError`, even though the nodes were still on the page. Their ids are - requested again now [#650] + requested again now. A node used while the page is still loading a new document says so in its error [#650] - Behind a proxy with credentials, `request.abort` in `on(:request)` and `network.blacklist`/`whitelist` were ignored because Ferrum's own proxy handler continued every request first. It now only continues requests nobody else handles [#332] diff --git a/lib/ferrum/node.rb b/lib/ferrum/node.rb index 65967184..99051a8f 100644 --- a/lib/ferrum/node.rb +++ b/lib/ferrum/node.rb @@ -35,7 +35,9 @@ def node_id document_version = page.document_version @node_id = begin id = page.command("DOM.requestNode", objectId: @object_id)["nodeId"] - raise NodeNotFoundError, { "message" => "node is not trackable" } if id.zero? + if id.zero? + raise NodeNotFoundError, { "message" => "node is not trackable, the page is still loading a new document" } + end id rescue NoExecutionContextError