From f8add788a2fabc1fdcbf36c6f883ac5360412d3e Mon Sep 17 00:00:00 2001 From: Dmitry Vorotilin Date: Mon, 19 Oct 2026 22:11:48 +0300 Subject: [PATCH 1/8] test: clean up skipped specs Unskip specs that pass now: saving an attachment to an absolute path (Chromium 1444729 is fixed), clicking inside an svg, and window positioning on headless macOS. Fix specs with wrong assertions: network latency emulation, the change event that fires only on blur, whitespace that Chrome keeps in text and inner text, and the empty Network#request, #response and #status placeholders. Remove specs for the Capybara API that Ferrum doesn't have: attach_file, drag_to, native, MouseEventFailed for obscured clicks, and a datepicker spec with no view. --- spec/browser_spec.rb | 3 -- spec/mouse_spec.rb | 68 +----------------------------- spec/network_spec.rb | 17 +++++--- spec/node_spec.rb | 67 ++++------------------------- spec/page_spec.rb | 2 +- spec/support/views/attach_file.erb | 17 -------- spec/support/views/drag.erb | 54 ------------------------ 7 files changed, 24 insertions(+), 204 deletions(-) delete mode 100644 spec/support/views/attach_file.erb delete mode 100644 spec/support/views/drag.erb diff --git a/spec/browser_spec.rb b/spec/browser_spec.rb index 2ec9237a..4cc45020 100644 --- a/spec/browser_spec.rb +++ b/spec/browser_spec.rb @@ -631,9 +631,6 @@ def proxy_tunnel(socket, request, browser_url) let(:save_path) { "/tmp/ferrum" } it "saves an attachment" do - # Also https://github.com/puppeteer/puppeteer/issues/10161 - skip "https://bugs.chromium.org/p/chromium/issues/detail?id=1444729" - browser.go_to("/#{filename}") browser.downloads.wait diff --git a/spec/mouse_spec.rb b/spec/mouse_spec.rb index 3b292ff8..0d3e2afe 100644 --- a/spec/mouse_spec.rb +++ b/spec/mouse_spec.rb @@ -142,7 +142,7 @@ end end - context "mouse support", skip: true do + context "mouse support" do before do browser.go_to("/click_test") end @@ -152,7 +152,7 @@ browser.reset end - it "scrolls around so that elements can be clicked" do + it "scrolls around so that elements can be clicked", skip: true do browser.resize(width: 200, height: 200) log = browser.at_css("#log") @@ -164,74 +164,10 @@ end end - it "fixes some weird layout issue that we are not entirely sure about the reason for" do - browser.go_to("/datepicker") - browser.at_css("#datepicker").set("2012-05-11") - browser.at_xpath("//a[text() = 'some link']").click - end - it "can click an element inside an svg" do expect { browser.at_css("#myrect").click }.not_to raise_error end - context "with #two overlapping #one" do - before do - browser.execute <<-JS - var two = document.getElementById("two") - two.style.position = "absolute" - two.style.left = "0px" - two.style.top = "0px" - JS - end - - it "detects if an element is obscured when clicking" do - expect do - browser.at_css("#one").click - end.to raise_error(Ferrum::MouseEventFailed) { |error| - expect(error.selector).to eq("html body div#two.box") - expect(error.message).to include("[200.0, 200.0]") - } - end - - it "clicks in the center of an element" do - expect do - browser.at_css("#one").click - end.to raise_error(Ferrum::MouseEventFailed) { |error| - expect(error.position).to eq([200, 200]) - } - end - - it "clicks in the center of an element within the viewport, if part is outside the viewport" do - browser.resize(width: 200, height: 200) - - expect do - browser.at_css("#one").click - end.to raise_error(Ferrum::MouseEventFailed) { |error| - expect(error.position.first).to eq(100) - } - end - end - - context "with #svg overlapping #one" do - before do - browser.execute <<-JS - var two = document.getElementById("svg") - two.style.position = "absolute" - two.style.left = "0px" - two.style.top = "0px" - JS - end - - it "detects if an element is obscured when clicking" do - expect do - browser.at_css("#one").click - end.to raise_error(Ferrum::MouseEventFailed) { |error| - expect(error.selector).to eq("html body svg#svg.box") - expect(error.message).to include("[200.0, 200.0]") - } - end - end - context "with image maps", skip: true do before { browser.go_to("/image_map") } diff --git a/spec/network_spec.rb b/spec/network_spec.rb index 8a415120..3465995b 100644 --- a/spec/network_spec.rb +++ b/spec/network_spec.rb @@ -164,15 +164,22 @@ end it "#request" do - skip + page.go_to("/with_js") + + expect(network.request.url).to eq(base_url("/with_js")) end it "#response" do - skip + page.go_to("/with_js") + + expect(network.response.url).to eq(base_url("/with_js")) + expect(network.response.status).to eq(200) end it "#status" do - skip + page.go_to("/arbitrary_path/404/missing") + + expect(network.status).to eq(404) end describe "#clear" do @@ -585,13 +592,13 @@ end end - it "#emulate_network_conditions", skip: "doesn't work for now" do + it "#emulate_network_conditions" do page.network.emulate_network_conditions(latency: 500) start = Ferrum::Utils::ElapsedTime.monotonic_time page.go_to("/with_js") - expect(Ferrum::Utils::ElapsedTime.elapsed_time(start)).to eq(2000) + expect(Ferrum::Utils::ElapsedTime.elapsed_time(start)).to be >= 0.5 end it "#offline_mode" do diff --git a/spec/node_spec.rb b/spec/node_spec.rb index 763e89be..c9fe00f3 100644 --- a/spec/node_spec.rb +++ b/spec/node_spec.rb @@ -107,12 +107,6 @@ browser.go_to("/with_js") end - it "raises a MouseEventFailed error", skip: "needs fix" do - expect do - browser.at_xpath("//a[text() = 'O hai']").click - end.to raise_error(Ferrum::MouseEventFailed) - end - it "clicks properly when then brought in with animate" do browser.execute %($("#off-the-left").animate({left: "10"});) @@ -458,7 +452,7 @@ end end - context "whitespace stripping tests", skip: true do + context "with whitespace" do before do browser.go_to("/filter_text_test") end @@ -467,16 +461,16 @@ expect(browser.at_css("#foo").text).to eq "foo" end - it "gets text stripped whitespace" do + it "gets inner text with stripped whitespace" do expect(browser.at_css("#bar").inner_text).to eq "bar" end - it "gets text stripped whitespace and then converts nbsp to space" do - expect(browser.at_css("#baz").inner_text).to eq " baz " + it "keeps non-breaking spaces in inner text" do + expect(browser.at_css("#baz").inner_text).to eq "\u00a0baz \u00a0 \u00a0" end - it "gets text stripped whitespace" do - expect(browser.at_css("#qux").text).to eq " \u3000 qux \u3000 " + it "keeps all whitespace in text" do + expect(browser.at_css("#qux").text).to eq " \u00a0 \u3000 qux \u3000 \u00a0 \u3000 " end end @@ -787,7 +781,9 @@ change_me.focus.type("Hello!") end - it "fires the change event", skip: true do + it "fires the change event on blur" do + change_me.blur + expect(browser.at_css("#changes").text).to eq("Hello!") end @@ -881,24 +877,6 @@ end end - describe "#attach_file", skip: true do - it "handles obsolete node" do - browser.go_to("/attach_file") - browser.attach_file "file", __FILE__ - end - - it "attaches a file when passed a Pathname" do - filename = Pathname.new("spec/tmp/a_test_pathname").expand_path - File.write(filename, "text") - - element = browser.at_css("#change_me_file") - element.set(filename) - expect(element.value).to eq("C:\\fakepath\\a_test_pathname") - ensure - FileUtils.rm_f(filename) - end - end - describe "#select_file" do it "selects a file" do browser.go_to("/form") @@ -910,33 +888,6 @@ end end - describe "#drag_to", skip: true do - before { browser.go_to("/drag") } - - it "supports drag_to" do - draggable = browser.at_css("#drag_to #draggable") - droppable = browser.at_css("#drag_to #droppable") - - draggable.drag_to(droppable) - expect(droppable).to have_content("Dropped") - end - - it "supports drag_by on native element" do - draggable = browser.at_css("#drag_by .draggable") - - top_before = browser.evaluate(%($("#drag_by .draggable").position().top)) - left_before = browser.evaluate(%($("#drag_by .draggable").position().left)) - - draggable.native.drag_by(15, 15) - - top_after = browser.evaluate(%($("#drag_by .draggable").position().top)) - left_after = browser.evaluate(%($("#drag_by .draggable").position().left)) - - expect(top_after).to eq(top_before + 15) - expect(left_after).to eq(left_before + 15) - end - end - context "with disappearing node" do it "raises an error if the element was on a previous page" do browser.go_to("/index") diff --git a/spec/page_spec.rb b/spec/page_spec.rb index 4e113164..4c58fe6d 100644 --- a/spec/page_spec.rb +++ b/spec/page_spec.rb @@ -57,7 +57,7 @@ describe "#position=" do it "allows the window to be positioned" do - skip if Ferrum::Utils::Platform.mac? + skip if Ferrum::Utils::Platform.mac? && !browser.options.headless expect do page.position = { left: 10, top: 20 } diff --git a/spec/support/views/attach_file.erb b/spec/support/views/attach_file.erb deleted file mode 100644 index 00855cd4..00000000 --- a/spec/support/views/attach_file.erb +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - diff --git a/spec/support/views/drag.erb b/spec/support/views/drag.erb deleted file mode 100644 index c56d2901..00000000 --- a/spec/support/views/drag.erb +++ /dev/null @@ -1,54 +0,0 @@ - - - - - ferrum drag_to and drag_by - - - - - - - -
-
-

Drag me

-
-
-

Drop here

-
-
- -
-
-

Drag me by

-
-
- - From b1319de36fcd2de95a8064c2f3c7afe61a0f7a53 Mon Sep 17 00:00:00 2001 From: Dmitry Vorotilin Date: Mon, 19 Oct 2026 22:15:14 +0300 Subject: [PATCH 2/8] test(headers): remove skipped specs for headers propagating across windows --- spec/headers_spec.rb | 80 ---------------------------- spec/support/views/popup_headers.erb | 9 ---- 2 files changed, 89 deletions(-) delete mode 100644 spec/support/views/popup_headers.erb diff --git a/spec/headers_spec.rb b/spec/headers_spec.rb index 0d3d459c..a4d9eee1 100644 --- a/spec/headers_spec.rb +++ b/spec/headers_spec.rb @@ -83,84 +83,4 @@ expect(browser.body).to include("X_CUSTOM_HEADER: 1") end end - - context "multiple windows", skip: true do - it "persists headers across popup windows" do - browser.headers.set( - "Cookie" => "foo=bar", - "Host" => "foo.com", - "User-Agent" => "foo" - ) - browser.go_to("/popup_headers") - browser.at_xpath("//a[text()='pop up']").click - - page, = browser.windows(:last) - - expect(page.body).to include("USER_AGENT: foo") - expect(page.body).to include("COOKIE: foo=bar") - expect(page.body).to include("HOST: foo.com") - end - - it "sets headers in existing windows" do - page = browser.create_page - page.headers.set( - "Cookie" => "foo=bar", - "Host" => "foo.com", - "User-Agent" => "foo" - ) - page.goto("/headers") - expect(page.body).to include("USER_AGENT: foo") - expect(page.body).to include("COOKIE: foo=bar") - expect(page.body).to include("HOST: foo.com") - - browser.switch_to_window browser.windows.last - browser.go_to("/headers") - expect(browser.body).to include("USER_AGENT: foo") - expect(browser.body).to include("COOKIE: foo=bar") - expect(browser.body).to include("HOST: foo.com") - end - - it "keeps temporary headers local to the current window" do - browser.create_page - browser.headers.add("X-Custom-Header" => "1", permanent: false) - - browser.switch_to_window browser.windows.last - browser.go_to("/headers") - expect(browser.body).not_to include("X_CUSTOM_HEADER: 1") - - browser.switch_to_window browser.windows.first - browser.go_to("/headers") - expect(browser.body).to include("X_CUSTOM_HEADER: 1") - end - - it "does not mix temporary headers with permanent ones when propagating to other windows" do - browser.create_page - browser.headers.add("X-Custom-Header" => "1", permanent: false) - browser.headers.add("Host" => "foo.com") - - browser.switch_to_window browser.windows.last - browser.go_to("/headers") - expect(browser.body).to include("HOST: foo.com") - expect(browser.body).not_to include("X_CUSTOM_HEADER: 1") - - browser.switch_to_window browser.windows.first - browser.go_to("/headers") - expect(browser.body).to include("HOST: foo.com") - expect(browser.body).to include("X_CUSTOM_HEADER: 1") - end - - it "does not propagate temporary headers to new windows" do - browser.go_to - browser.headers.add("X-Custom-Header" => "1", permanent: false) - browser.create_page - - browser.switch_to_window browser.windows.last - browser.go_to("/headers") - expect(browser.body).not_to include("X_CUSTOM_HEADER: 1") - - browser.switch_to_window browser.windows.first - browser.go_to("/headers") - expect(browser.body).to include("X_CUSTOM_HEADER: 1") - end - end end diff --git a/spec/support/views/popup_headers.erb b/spec/support/views/popup_headers.erb deleted file mode 100644 index 8772afe4..00000000 --- a/spec/support/views/popup_headers.erb +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - pop up - - From 9571f832269587a275c99846c0f67ac54156cff3 Mon Sep 17 00:00:00 2001 From: Dmitry Vorotilin Date: Mon, 19 Oct 2026 22:47:23 +0300 Subject: [PATCH 3/8] test(mouse): scroll elements into view explicitly before clicking them --- spec/mouse_spec.rb | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/spec/mouse_spec.rb b/spec/mouse_spec.rb index 0d3e2afe..be2edc4b 100644 --- a/spec/mouse_spec.rb +++ b/spec/mouse_spec.rb @@ -8,14 +8,14 @@ expect(browser.body).to include("x: 100, y: 150") end - it "has no trouble clicking elements when the size of a document changes", skip: true do + it "has no trouble clicking elements when the size of a document changes" do browser.go_to("/long_page") - browser.at_css("#penultimate").click + browser.at_css("#penultimate").scroll_into_view.click browser.execute <<~JS el = document.getElementById("penultimate") el.parentNode.removeChild(el) JS - browser.at_xpath("//a[text() = 'Phasellus blandit velit']").click + browser.at_xpath("//a[text() = 'Phasellus blandit velit']").scroll_into_view.click expect(browser.body).to include("Hello") end @@ -152,14 +152,13 @@ browser.reset end - it "scrolls around so that elements can be clicked", skip: true do + it "clicks elements scrolled into view" do browser.resize(width: 200, height: 200) log = browser.at_css("#log") instructions = %w[one four one two three] instructions.each do |instruction| - browser.at_css("##{instruction}").click - browser.screenshot(path: "a.png") + browser.at_css("##{instruction}").scroll_into_view.click expect(log.text).to eq(instruction) end end From 75854f969e942ac64a1378e54791d3f1cd1f3ce9 Mon Sep 17 00:00:00 2001 From: Dmitry Vorotilin Date: Mon, 19 Oct 2026 22:54:05 +0300 Subject: [PATCH 4/8] fix(node): click the bounding rect fallback in viewport coordinates When Chrome can't compute content quads, Node#click falls back to the bounding rect, but it added the page scroll offset to it, so on a scrolled page the click missed the element. --- CHANGELOG.md | 2 ++ lib/ferrum/node.rb | 4 ++-- sig/ferrum/node.rbs | 2 +- spec/node_spec.rb | 26 ++++++++++++++++++++------ 4 files changed, 25 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e820e75..6c832a45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,8 @@ - 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] - `mouse.click(modifiers:)` sent its `mousemove` without the modifiers [#581] +- When Chrome can't compute an element's content quads, `node.click` falls back to its bounding rect, but it added the + page scroll offset and missed the element on a scrolled page. It clicks in viewport coordinates now ### 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 99051a8f..f8ca9b45 100644 --- a/lib/ferrum/node.rb +++ b/lib/ferrum/node.rb @@ -531,8 +531,8 @@ def exists? def bounding_rect_coordinates evaluate <<~JS - [this.getBoundingClientRect().left + window.pageXOffset + (this.offsetWidth / 2), - this.getBoundingClientRect().top + window.pageYOffset + (this.offsetHeight / 2)] + [this.getBoundingClientRect().left + (this.offsetWidth / 2), + this.getBoundingClientRect().top + (this.offsetHeight / 2)] JS end diff --git a/sig/ferrum/node.rbs b/sig/ferrum/node.rbs index b47d5a28..a8e7cd17 100644 --- a/sig/ferrum/node.rbs +++ b/sig/ferrum/node.rbs @@ -82,7 +82,7 @@ module Ferrum private - def bounding_rect_coordinates: () -> untyped + def bounding_rect_coordinates: () -> [Numeric, Numeric] def content_quads: () -> untyped diff --git a/spec/node_spec.rb b/spec/node_spec.rb index c9fe00f3..a78e8397 100644 --- a/spec/node_spec.rb +++ b/spec/node_spec.rb @@ -20,14 +20,28 @@ expect(browser.network.traffic.length).to be <= 5 end - it "does not run into content quads error" do - browser.go_to("/index") + context "when content quads can't be computed" do + before do + allow_any_instance_of(described_class).to receive(:content_quads) + .and_raise(Ferrum::CoordinatesNotFoundError, "Could not compute content quads") + end - allow_any_instance_of(Ferrum::Node).to receive(:content_quads) - .and_raise(Ferrum::CoordinatesNotFoundError, "Could not compute content quads") + it "clicks the element by its bounding rect" do + browser.go_to("/index") - browser.at_xpath("//a[text() = 'JS redirect']").click - expect(browser.body).to include("Hello world") + browser.at_xpath("//a[text()='JS redirect']").click + + expect(browser.body).to include("Hello world") + end + + it "clicks the element by its bounding rect on a scrolled page" do + browser.go_to("/long_page") + link = browser.at_xpath("//a[text() = 'Phasellus blandit velit']").scroll_into_view + + link.click + + expect(browser.current_url).to eq(base_url("/")) + end end it "synchronizes page loads properly" do From fe99017f8b260a34bcce35b2d3ebd8eaec63112c Mon Sep 17 00:00:00 2001 From: Dmitry Vorotilin Date: Mon, 19 Oct 2026 22:55:28 +0300 Subject: [PATCH 5/8] test(frame): scroll elements into view before clicking them inside frames --- spec/frame_spec.rb | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/spec/frame_spec.rb b/spec/frame_spec.rb index ff31b6dd..712e3d08 100644 --- a/spec/frame_spec.rb +++ b/spec/frame_spec.rb @@ -152,32 +152,20 @@ end end - it "supports clicking in a frame", skip: true do - page.go_to - page.execute <<-JS - document.body.innerHTML += "