From 10aa164703031f0cb6756bfad34d971c91d5d937 Mon Sep 17 00:00:00 2001 From: Dmitry Vorotilin Date: Sun, 25 Oct 2026 20:25:13 +0300 Subject: [PATCH 1/6] ref: move to Ferrum's new JavaScript API where it maps directly --- lib/capybara/cuprite/browser.rb | 41 +++++++++++++++++++++++---------- lib/capybara/cuprite/page.rb | 15 ++++++------ 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/lib/capybara/cuprite/browser.rb b/lib/capybara/cuprite/browser.rb index 401f0e84..35683e3e 100644 --- a/lib/capybara/cuprite/browser.rb +++ b/lib/capybara/cuprite/browser.rb @@ -8,6 +8,19 @@ class Browser < Ferrum::Browser extend Forwardable DRAG_HTML5_JS = File.read(File.expand_path("javascripts/drag.js", __dir__)) + CALLBACK_JS = <<~JS + function(...args) { + return new Promise((resolve, reject) => { + try { + (function() { + SCRIPT + }).apply(this, [...args, resolve]); + } catch (error) { + reject(error); + } + }); + } + JS delegate %i[send_keys select set hover trigger before_click switch_to_frame find_modal accept_confirm dismiss_confirm accept_prompt @@ -138,6 +151,10 @@ def close_window(target_id) targets.delete(target_id) # page.close is async, delete target asap end + def evaluate_async(script, wait, *args) + evaluate(CALLBACK_JS.sub("SCRIPT") { script }, *args, timeout: wait) + end + def active_element evaluate("document.activeElement") end @@ -166,7 +183,7 @@ def drag(node, other, drop_modifiers, options = {}) mouse.down sleep delay if delay - html5 = !evaluate_on(node: node, expression: "_cuprite.legacyDragCheck(this)") if html5.nil? + html5 = !node.evaluate("_cuprite.legacyDragCheck(this)") if html5.nil? if html5 drag_html5(node, other, drop_modifiers, delay, drop_offset) @@ -204,10 +221,10 @@ def drop(node, *args) execute("_cuprite.attachDropInput()") input = find(:css, "#_cuprite_drop_file").first select_file(input, args) - evaluate_on(node: node, expression: "_cuprite.dropFile(this)") + node.evaluate("_cuprite.dropFile(this)") else strings = args.flat_map { |arg| arg.map { |type, data| { "type" => type, "data" => data } } } - evaluate_on(node: node, expression: "_cuprite.dropString(#{strings.to_json}, this)") + node.evaluate("_cuprite.dropString(#{strings.to_json}, this)") end end @@ -220,40 +237,40 @@ def parents(node) end def visible_text(node) - evaluate_on(node: node, expression: "_cuprite.visibleText(this)") + node.evaluate("_cuprite.visibleText(this)") end def delete_text(node) - evaluate_on(node: node, expression: "_cuprite.deleteText(this)") + node.evaluate("_cuprite.deleteText(this)") end def attributes(node) - value = evaluate_on(node: node, expression: "_cuprite.getAttributes(this)") + value = node.evaluate("_cuprite.getAttributes(this)") JSON.parse(value) end def attribute(node, name) - evaluate_on(node: node, expression: %(_cuprite.getAttribute(this, "#{name}"))) + node.evaluate(%(_cuprite.getAttribute(this, "#{name}"))) end def value(node) - evaluate_on(node: node, expression: "_cuprite.value(this)") + node.evaluate("_cuprite.value(this)") end def visible?(node) - evaluate_on(node: node, expression: "_cuprite.isVisible(this)") + node.evaluate("_cuprite.isVisible(this)") end def disabled?(node) - evaluate_on(node: node, expression: "_cuprite.isDisabled(this)") + node.evaluate("_cuprite.isDisabled(this)") end def path(node) - evaluate_on(node: node, expression: "_cuprite.path(this)") + node.evaluate("_cuprite.path(this)") end def obscured?(node) - evaluate_on(node: node, expression: "_cuprite.isObscured(this)") + node.evaluate("_cuprite.isObscured(this)") end def all_text(node) diff --git a/lib/capybara/cuprite/page.rb b/lib/capybara/cuprite/page.rb index 5774cee3..c43a46eb 100644 --- a/lib/capybara/cuprite/page.rb +++ b/lib/capybara/cuprite/page.rb @@ -19,7 +19,7 @@ class Page < Ferrum::Page delegate %i[at_css at_xpath css xpath current_url current_title body execution_id execution_id! - evaluate evaluate_on evaluate_async execute] => :active_frame + evaluate evaluate_on execute] => :active_frame def initialize(...) @frame_stack = [] @@ -50,12 +50,11 @@ def set_viewport(width: 0, height: 0, scale_factor: @options.scale_factor, mobil end def set(node, value) - object_id = command("DOM.resolveNode", nodeId: node.node_id).dig("object", "objectId") - evaluate("_cuprite.set(arguments[0], arguments[1])", { "objectId" => object_id }, value) + node.evaluate("_cuprite.set(this, arguments[0])", value) end def select(node, value) - evaluate_on(node: node, expression: "_cuprite.select(this, #{value})") + node.evaluate("_cuprite.select(this, #{value})") end def trigger(node, event) @@ -65,13 +64,13 @@ def trigger(node, event) end def hover(node) - evaluate_on(node: node, expression: "_cuprite.scrollIntoViewport(this)") + node.evaluate("_cuprite.scrollIntoViewport(this)") x, y = find_position(node) command("Input.dispatchMouseEvent", type: "mouseMoved", x: x, y: y) end def send_keys(node, keys) - unless evaluate_on(node: node, expression: %(_cuprite.containsSelection(this) || _cuprite.hasFocus(this))) + unless node.evaluate(%(_cuprite.containsSelection(this) || _cuprite.hasFocus(this))) before_click(node, "click") node.click(mode: :left, keys: keys) end @@ -125,14 +124,14 @@ def reset_modals end def before_click(node, name, _keys = [], offset = {}) - evaluate_on(node: node, expression: "_cuprite.scrollIntoViewport(this)") + node.evaluate("_cuprite.scrollIntoViewport(this)") # If offset is given it may go outside of the element and likely error # will be raised that we detected another element at this position. return true if offset[:x] || offset[:y] x, y = find_position(node, **offset) - evaluate_on(node: node, expression: "_cuprite.mouseEventTest(this, '#{name}', #{x}, #{y})") + node.evaluate("_cuprite.mouseEventTest(this, '#{name}', #{x}, #{y})") true rescue Ferrum::JavaScriptError => e raise MouseEventFailed, e.message if e.class_name == "MouseEventFailed" From 3651a7795c5011dd61c47fd524846b92d3ebb1f7 Mon Sep 17 00:00:00 2001 From: Dmitry Vorotilin Date: Sun, 25 Oct 2026 20:35:41 +0300 Subject: [PATCH 2/6] ref: read parents through a function declaration, which returns nodes on every Ferrum version --- lib/capybara/cuprite/browser.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/capybara/cuprite/browser.rb b/lib/capybara/cuprite/browser.rb index 35683e3e..972f1f88 100644 --- a/lib/capybara/cuprite/browser.rb +++ b/lib/capybara/cuprite/browser.rb @@ -233,7 +233,7 @@ def select_file(node, value) end def parents(node) - evaluate_on(node: node, expression: "_cuprite.parents(this)", by_value: false) + node.evaluate("function() { return _cuprite.parents(this) }") end def visible_text(node) From edfc4f301a259afacc8b735e6fb1c297054b8b5b Mon Sep 17 00:00:00 2001 From: Dmitry Vorotilin Date: Sun, 25 Oct 2026 22:01:57 +0300 Subject: [PATCH 3/6] ref: wait for a triggered click's navigation with Ferrum's wait_for_navigation! --- lib/capybara/cuprite/page.rb | 9 +++++---- spec/features/session_spec.rb | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/lib/capybara/cuprite/page.rb b/lib/capybara/cuprite/page.rb index c43a46eb..92b854b0 100644 --- a/lib/capybara/cuprite/page.rb +++ b/lib/capybara/cuprite/page.rb @@ -19,7 +19,7 @@ class Page < Ferrum::Page delegate %i[at_css at_xpath css xpath current_url current_title body execution_id execution_id! - evaluate evaluate_on execute] => :active_frame + evaluate execute] => :active_frame def initialize(...) @frame_stack = [] @@ -58,9 +58,10 @@ def select(node, value) end def trigger(node, event) - options = {} - options.merge!(wait: TRIGGER_CLICK_WAIT) if event.to_s == "click" && TRIGGER_CLICK_WAIT.positive? - evaluate_on(node: node, expression: %(_cuprite.trigger(this, "#{event}")), **options) + script = %(_cuprite.trigger(this, "#{event}")) + return node.evaluate(script) unless event.to_s == "click" && TRIGGER_CLICK_WAIT.positive? + + wait_for_navigation!(TRIGGER_CLICK_WAIT) { node.evaluate(script) } end def hover(node) diff --git a/spec/features/session_spec.rb b/spec/features/session_spec.rb index 7d57d877..09e7fdd3 100644 --- a/spec/features/session_spec.rb +++ b/spec/features/session_spec.rb @@ -219,6 +219,20 @@ input.trigger("custom:ping") expect(output.text).to eq("custom:ping") end + + it "waits for the page a triggered click opens" do + page = @session.driver.browser.page + navigated = loaded = false + page.on("Page.frameNavigated") { |params| navigated = params.dig("frame", "url").end_with?("/cuprite/slow") } + page.on("Page.loadEventFired") { loaded = navigated } + @session.execute_script(<<~JS) + document.body.insertAdjacentHTML("beforeend", 'slow') + JS + + @session.find(:css, "#slow").trigger(:click) + + expect(loaded).to be(true) + end end describe "Node#set" do From 62aca71600bb4c72cad59c03a927acd0f7fd5734 Mon Sep 17 00:00:00 2001 From: Dmitry Vorotilin Date: Sun, 25 Oct 2026 22:26:44 +0300 Subject: [PATCH 4/6] fix(node): read the attribute unless the property is a string, number or boolean --- lib/capybara/cuprite/node.rb | 8 ++++---- spec/features/session_spec.rb | 4 ++++ spec/support/views/attributes_properties.erb | 2 ++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/capybara/cuprite/node.rb b/lib/capybara/cuprite/node.rb index 81607039..b581c03c 100644 --- a/lib/capybara/cuprite/node.rb +++ b/lib/capybara/cuprite/node.rb @@ -79,10 +79,10 @@ def [](name) return command(:attribute, name) && command(:property, name) end - value = property(name) - value = command(:attribute, name) if value.nil? || value.is_a?(Hash) - - value + case (value = property(name)) + when String, Numeric, true, false then value + else command(:attribute, name) + end end def attributes diff --git a/spec/features/session_spec.rb b/spec/features/session_spec.rb index 09e7fdd3..fd5ae729 100644 --- a/spec/features/session_spec.rb +++ b/spec/features/session_spec.rb @@ -532,6 +532,10 @@ expect(@session.find_field("checked")["checked"]).to be true expect(@session.find_field("unchecked")["checked"]).to be false end + + it "gets the attribute for a property holding a node" do + expect(@session.find_field("outside_form")["form"]).to eq "outer_form" + end end describe "Node#==" do diff --git a/spec/support/views/attributes_properties.erb b/spec/support/views/attributes_properties.erb index 77d493e2..ac639f25 100644 --- a/spec/support/views/attributes_properties.erb +++ b/spec/support/views/attributes_properties.erb @@ -6,5 +6,7 @@ Loop +
+ From c9d8c3f047adbf7a4aab4d16750cc6b8bcd68e5b Mon Sep 17 00:00:00 2001 From: Dmitry Vorotilin Date: Sun, 25 Oct 2026 22:50:51 +0300 Subject: [PATCH 5/6] ref: scroll in drag_by with Ferrum's mouse.scroll_by --- lib/capybara/cuprite/browser.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/capybara/cuprite/browser.rb b/lib/capybara/cuprite/browser.rb index 972f1f88..e2e283cc 100644 --- a/lib/capybara/cuprite/browser.rb +++ b/lib/capybara/cuprite/browser.rb @@ -209,7 +209,7 @@ def drag_by(node, dx, dy, steps, delay = nil, scroll = true) sleep delay if delay - evaluate("window.scrollBy(#{dx}, #{dy})") if scroll # should be extracted to Mouse#scroll_by in ferrum + mouse.scroll_by(dx, dy) if scroll x2, y2 = node.find_position mouse.move(x: x2 + dx, y: y2 + dy, steps: steps) From edf4d466a619cd251579c60fddbfb35074034904 Mon Sep 17 00:00:00 2001 From: Dmitry Vorotilin Date: Mon, 26 Oct 2026 15:44:58 +0300 Subject: [PATCH 6/6] test: expect cyclic references marked inside the returned object --- spec/features/session_spec.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/features/session_spec.rb b/spec/features/session_spec.rb index fd5ae729..ecbb2308 100644 --- a/spec/features/session_spec.rb +++ b/spec/features/session_spec.rb @@ -649,7 +649,8 @@ })() JS - expect(@session.evaluate_script(code)).to eq(Ferrum::CyclicObject.instance) + cyclic = Ferrum::CyclicObject.instance + expect(@session.evaluate_script(code)).to eq("a" => cyclic, "b" => {}, "c" => { "a" => cyclic }) end it "synchronises page loads properly" do