diff --git a/CHANGELOG.md b/CHANGELOG.md index cb1c5263..889ef328 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ### Added +- `Ferrum::Browser#wait_for_css`: returns instance of `Ferrum::Node` that's matched by provided css selector. +- `Ferrum::Browser#wait_for_xpath`: returns instance of `Ferrum::Node` that's matched by provided XPath selector. - Alias `Ferrum::Frame#content=` to `Ferrum::Frame#set_content` - Alias `Ferrum::Node#propery` to `Ferrum::Node#[]` - Implement `Ferrum::Network#blacklist=` and `Ferrum::Network#whitelist=` diff --git a/README.md b/README.md index f1a3d378..18b50446 100644 --- a/README.md +++ b/README.md @@ -340,6 +340,26 @@ browser.go_to("https://google.com/") browser.body # => '
... ``` +#### wait_for_css : `Node` + +```ruby +browser.wait_for_css("body", timeout: 3000, interval: 100) # => [Node] +``` +* css `String` - CSS selector for a target element. +* options `Hash` + * `:timeout` (Integer) - timeout for the selector-waiting in milliseconds, 3000 ms by default. + * `:interval` (Integer) - interval that used to compute attempts for the selector-waiting (max attempts = timeout / interval), 100 by default. + +#### wait_for_xpath : `Node` + +```ruby +browser.wait_for_xpath("//body", timeout: 3000, interval: 100) # => [Node] +``` + +* xpath `String` - XPath selector for a target element. +* options `Hash` + * `:timeout` (Integer) - timeout for the selector-waiting in milliseconds, 3000 ms by default. + * `:interval` (Integer) - interval that used to compute attempts for the selector-waiting (max attempts = timeout / interval), 100 by default. ## Screenshots diff --git a/lib/ferrum/browser.rb b/lib/ferrum/browser.rb index 8926195f..6456c49d 100644 --- a/lib/ferrum/browser.rb +++ b/lib/ferrum/browser.rb @@ -28,7 +28,7 @@ class Browser evaluate evaluate_on evaluate_async execute evaluate_func add_script_tag add_style_tag bypass_csp on goto position position= - playback_rate playback_rate=] => :page + playback_rate playback_rate= wait_for_css wait_for_xpath] => :page delegate %i[default_user_agent] => :process attr_reader :client, :process, :contexts, :logger, :js_errors, :pending_connection_errors, diff --git a/lib/ferrum/frame/dom.rb b/lib/ferrum/frame/dom.rb index b60c6b0a..10647182 100644 --- a/lib/ferrum/frame/dom.rb +++ b/lib/ferrum/frame/dom.rb @@ -65,6 +65,16 @@ def at_xpath(selector, within: nil) evaluate_func(expr, selector, within) end + def wait_for_xpath(xpath, **options) + expr = <<~JS + function(selector) { + return document.evaluate(selector, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; + } + JS + + wait_for_selector(xpath, expr, **options) + end + def css(selector, within: nil) expr = <<~JS function(selector, within) { @@ -86,6 +96,48 @@ def at_css(selector, within: nil) evaluate_func(expr, selector, within) end + + def wait_for_css(css, **options) + expr = <<~JS + function(selector) { + return document.querySelector(selector); + } + JS + + wait_for_selector(css, expr, **options) + end + + private + + def wait_for_selector(selector, find_element_expression, timeout: 3000, interval: 100) + expr = <<~JS + function(selector, findElementExpression, timeout, interval) { + var attempts = 0; + var max = timeout / interval; + var findElement = new Function(function(expression) { + return "{ return " + expression + " };"; + }(findElementExpression))(); + function waitForElement(resolve, reject) { + if (attempts > ((max < 1) ? 1 : max)) { + return reject(new Error("Not found element match the selector: " + selector)); + } + var element = findElement(selector); + if (element !== null) { + return resolve(element); + } + setTimeout(function () { + waitForElement(resolve, reject); + }, interval); + attempts++; + } + return new Promise(function (resolve, reject) { + waitForElement(resolve, reject); + }); + } + JS + + evaluate_func(expr, selector, find_element_expression, timeout, interval, awaitPromise: true) + end end end end diff --git a/lib/ferrum/frame/runtime.rb b/lib/ferrum/frame/runtime.rb index 926cc783..cbd351fe 100644 --- a/lib/ferrum/frame/runtime.rb +++ b/lib/ferrum/frame/runtime.rb @@ -76,8 +76,8 @@ def execute(expression, *args) true end - def evaluate_func(expression, *args, on: nil) - call(expression: expression, arguments: args, on: on) + def evaluate_func(expression, *args, on: nil, **options) + call(expression: expression, arguments: args, on: on, **options) end def evaluate_on(node:, expression:, by_value: true, wait: 0) diff --git a/lib/ferrum/page.rb b/lib/ferrum/page.rb index fe66fe5d..36c16b7c 100644 --- a/lib/ferrum/page.rb +++ b/lib/ferrum/page.rb @@ -34,7 +34,7 @@ def reset delegate %i[at_css at_xpath css xpath current_url current_title url title body doctype content= execution_id evaluate evaluate_on evaluate_async execute evaluate_func - add_script_tag add_style_tag] => :main_frame + add_script_tag add_style_tag wait_for_css wait_for_xpath] => :main_frame include Animation include Screenshot diff --git a/spec/browser_spec.rb b/spec/browser_spec.rb index 7bc6672e..9249bb1a 100644 --- a/spec/browser_spec.rb +++ b/spec/browser_spec.rb @@ -504,6 +504,112 @@ module Ferrum expect(browser.evaluate("window.last_hashchange")).to eq("#foo") end + context "wait_for_css" do + before do + browser.go_to("/ferrum/with_js") + end + + it "waits for provided css selector" do + expect(browser.wait_for_css("div#wait_for_selector")).to be_kind_of(Ferrum::Node) + end + + it "waits for provided css of hidden element" do + expect(browser.wait_for_css("div#wait_for_hidden_selector")).to be_kind_of(Ferrum::Node) + end + + it "raises error when default timeout exceed" do + expect do + browser.wait_for_css("div#not_existed_element") + end.to raise_error(Ferrum::JavaScriptError, /Not found element match the selector/) + end + + it "raises error when timeout exceed" do + expect do + browser.wait_for_css("div#wait_for_selector", timeout: 800) + end.to raise_error(Ferrum::JavaScriptError, /Not found element match the selector/) + end + + it "raises error when provided invalid css selector" do + expect do + browser.wait_for_css("//div[@id='wait_for_selector']") + end.to raise_error(Ferrum::JavaScriptError, /Failed to execute 'querySelector' on 'Document'/) + end + + it "waits less than provided timeout when node found" do + Timeout.timeout(1) do + expect(browser.wait_for_css("div#wait_for_selector", timeout: 2000)).to be_kind_of(Ferrum::Node) + end + end + + it "waits for selector within frame" do + browser.execute <<-JS + setTimeout(function(){ + document.body.innerHTML += "