diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4a0382cc..631cfa76 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,20 @@
-## [Unreleased](https://github.com/rubycdp/ferrum/compare/v0.12...main) ##
+## [Unreleased](https://github.com/rubycdp/ferrum/compare/v0.13...main) ##
+
+### Added
+- `Ferrum::Network::Exchange#xhr?` determines if the exchange is XHR
+- `Ferrum::Network::Request#xhr?` determines if the request is XHR
+- `Ferrum::Network::Response#loaded?` returns true if the response is fully loaded
+- `Ferrum::Node#in_viewport?` checks if the element in viewport (optional argument `scope` as `Ferrum::Node`)
+- `Ferrum::Node#scroll_into_view` - scrolls to element if needed (when it's not in the viewport)
+
+### Changed
+
+### Fixed
+- `Ferrum::Network::Exchange#finished?` returns `true` only fully loaded responses
+
+### Removed
+
+## [0.13](https://github.com/rubycdp/ferrum/compare/v0.12...v0.13) - (Nov 12, 2022) ##
### Added
diff --git a/README.md b/README.md
index 89704468..3b3c3754 100644
--- a/README.md
+++ b/README.md
@@ -1128,6 +1128,8 @@ frame.at_css("//a[text() = 'Log in']") # => Node
#### evaluate
#### selected : `Array`
#### select
+#### scroll_into_view
+#### in_viewport?(of: `Node | nil`) : `Boolean`
(chainable) Selects options by passed attribute.
diff --git a/lib/ferrum/browser/client.rb b/lib/ferrum/browser/client.rb
index b5284e00..d7dc6c44 100644
--- a/lib/ferrum/browser/client.rb
+++ b/lib/ferrum/browser/client.rb
@@ -43,7 +43,7 @@ def command(method, params = {})
@pendings.delete(message[:id])
raise DeadBrowserError if data.nil? && @ws.messages.closed?
- raise TimeoutError unless data
+ raise TimeoutError.new() unless data
error, response = data.values_at("error", "result")
raise_browser_error(error) if error
diff --git a/lib/ferrum/errors.rb b/lib/ferrum/errors.rb
index 2c46071f..ce28a1b0 100644
--- a/lib/ferrum/errors.rb
+++ b/lib/ferrum/errors.rb
@@ -27,11 +27,17 @@ def initialize(url, pendings = [])
end
class TimeoutError < Error
- def message
- "Timed out waiting for response. It's possible that this happened " \
- "because something took a very long time (for example a page load " \
- "was slow). If so, setting the :timeout option to a higher value might " \
- "help."
+ attr_reader :pending_connections_info
+
+ def initialize(pending_connections_info = [])
+ @pending_connections_info = pending_connections_info
+
+ message = "Timed out waiting for response. It's possible that this happened " \
+ "because something took a very long time (for example a page load " \
+ "was slow). If so, setting the :timeout option to a higher value might " \
+ "help."
+ message = "#{message}\nConnections still pending:\n #{pending_connections_info.join(', ')}" unless pending_connections_info.empty?
+ super(message)
end
end
diff --git a/lib/ferrum/frame.rb b/lib/ferrum/frame.rb
index d2530c7e..5c3266c9 100644
--- a/lib/ferrum/frame.rb
+++ b/lib/ferrum/frame.rb
@@ -113,6 +113,7 @@ def content=(html)
document.close();
arguments[1](true);
), @page.timeout, html)
+ @page.document_node_id
end
alias set_content content=
diff --git a/lib/ferrum/network.rb b/lib/ferrum/network.rb
index 623751ad..9c9ac324 100644
--- a/lib/ferrum/network.rb
+++ b/lib/ferrum/network.rb
@@ -11,9 +11,10 @@ module Ferrum
class Network
CLEAR_TYPE = %i[traffic cache].freeze
AUTHORIZE_TYPE = %i[server proxy].freeze
- RESOURCE_TYPES = %w[Document Stylesheet Image Media Font Script TextTrack
- XHR Fetch EventSource WebSocket Manifest
- SignedExchange Ping CSPViolationReport Other].freeze
+ REQUEST_STAGES = %i[Request Response].freeze
+ RESOURCE_TYPES = %i[Document Stylesheet Image Media Font Script TextTrack
+ XHR Fetch Prefetch EventSource WebSocket Manifest
+ SignedExchange Ping CSPViolationReport Preflight Other].freeze
AUTHORIZE_BLOCK_MISSING = "Block is missing, call `authorize(...) { |r| r.continue } " \
"or subscribe to `on(:request)` events before calling it"
AUTHORIZE_TYPE_WRONG = ":type should be in #{AUTHORIZE_TYPE}"
@@ -62,7 +63,15 @@ def wait_for_idle(connections: 0, duration: 0.05, timeout: @page.browser.timeout
start = Utils::ElapsedTime.monotonic_time
until idle?(connections)
- raise TimeoutError if Utils::ElapsedTime.timeout?(start, timeout)
+ if Utils::ElapsedTime.timeout?(start, timeout)
+ if @page.browser.options.pending_connection_errors
+ pending_connections = traffic.select(&:pending?)
+ pending_connections_info = pending_connections.map(&:response).map do |connection|
+ {status: connection.status, status_text: connection.status_text, url: connection.url } unless connection.nil?
+ end
+ end
+ raise TimeoutError.new(pending_connections_info = pending_connections_info)
+ end
sleep(duration)
end
@@ -187,11 +196,20 @@ def whitelist=(patterns)
# end
# browser.go_to("https://google.com")
#
- def intercept(pattern: "*", resource_type: nil)
+ def intercept(pattern: "*", resource_type: nil, request_stage: nil, handle_auth_requests: true)
pattern = { urlPattern: pattern }
- pattern[:resourceType] = resource_type if resource_type && RESOURCE_TYPES.include?(resource_type.to_s)
- @page.command("Fetch.enable", handleAuthRequests: true, patterns: [pattern])
+ if resource_type && RESOURCE_TYPES.none?(resource_type.to_sym)
+ raise ArgumentError, "Unknown resource type '#{resource_type}' must be #{RESOURCE_TYPES.join(' | ')}"
+ end
+
+ if request_stage && REQUEST_STAGES.none?(request_stage.to_sym)
+ raise ArgumentError, "Unknown request stage '#{request_stage}' must be #{REQUEST_STAGES.join(' | ')}"
+ end
+
+ pattern[:resourceType] = resource_type if resource_type
+ pattern[:requestStage] = request_stage if request_stage
+ @page.command("Fetch.enable", patterns: [pattern], handleAuthRequests: handle_auth_requests)
end
#
@@ -374,8 +392,12 @@ def subscribe_response_received
def subscribe_loading_finished
@page.on("Network.loadingFinished") do |params|
- exchange = select(params["requestId"]).last
- exchange.response.body_size = params["encodedDataLength"] if exchange&.response
+ response = select(params["requestId"]).last&.response
+
+ if response
+ response.loaded = true
+ response.body_size = params["encodedDataLength"]
+ end
end
end
diff --git a/lib/ferrum/network/exchange.rb b/lib/ferrum/network/exchange.rb
index f6f3b7d2..a4bc60cf 100644
--- a/lib/ferrum/network/exchange.rb
+++ b/lib/ferrum/network/exchange.rb
@@ -79,7 +79,7 @@ def blocked?
# @return [Boolean]
#
def finished?
- blocked? || !response.nil? || !error.nil?
+ blocked? || response&.loaded? || !error.nil?
end
#
@@ -100,6 +100,15 @@ def intercepted?
!intercepted_request.nil?
end
+ #
+ # Determines if the exchange is XHR.
+ #
+ # @return [Boolean]
+ #
+ def xhr?
+ !!request&.xhr?
+ end
+
#
# Returns request's URL.
#
diff --git a/lib/ferrum/network/request.rb b/lib/ferrum/network/request.rb
index 40c203ac..800c9b15 100644
--- a/lib/ferrum/network/request.rb
+++ b/lib/ferrum/network/request.rb
@@ -50,6 +50,15 @@ def type?(value)
type.downcase == value.to_s.downcase
end
+ #
+ # Determines if the request is XHR.
+ #
+ # @return [Boolean]
+ #
+ def xhr?
+ type?("xhr")
+ end
+
#
# The frame ID of the request.
#
diff --git a/lib/ferrum/network/response.rb b/lib/ferrum/network/response.rb
index f3bca622..2a02b389 100644
--- a/lib/ferrum/network/response.rb
+++ b/lib/ferrum/network/response.rb
@@ -18,8 +18,13 @@ class Response
# @return [Hash{String => Object}]
attr_reader :params
+ # The response is fully loaded by the browser.
#
- # Initializes the respones object.
+ # @return [Boolean]
+ attr_writer :loaded
+
+ #
+ # Initializes the responses object.
#
# @param [Page] page
# The page associated with the network response.
@@ -121,9 +126,8 @@ def body_size=(size)
#
def body
@body ||= begin
- body, encoded = @page
- .command("Network.getResponseBody", requestId: id)
- .values_at("body", "base64Encoded")
+ body, encoded = @page.command("Network.getResponseBody", requestId: id)
+ .values_at("body", "base64Encoded")
encoded ? Base64.decode64(body) : body
end
end
@@ -135,6 +139,13 @@ def main?
@page.network.response == self
end
+ # The response is fully loaded by the browser or not.
+ #
+ # @return [Boolean]
+ def loaded?
+ @loaded
+ end
+
#
# Comapres the respones ID to another response's ID.
#
diff --git a/lib/ferrum/node.rb b/lib/ferrum/node.rb
index 9758023b..1c94456e 100644
--- a/lib/ferrum/node.rb
+++ b/lib/ferrum/node.rb
@@ -88,6 +88,26 @@ def hover
raise NotImplementedError
end
+ def scroll_into_view
+ tap { page.command("DOM.scrollIntoViewIfNeeded", nodeId: node_id) }
+ end
+
+ def in_viewport?(of: nil)
+ function = <<~JS
+ function(element, scope) {
+ const rect = element.getBoundingClientRect();
+ const [height, width] = scope
+ ? [scope.offsetHeight, scope.offsetWidth]
+ : [window.innerHeight, window.innerWidth];
+ return rect.top >= 0 &&
+ rect.left >= 0 &&
+ rect.bottom <= height &&
+ rect.right <= width;
+ }
+ JS
+ page.evaluate_func(function, self, of)
+ end
+
def select_file(value)
page.command("DOM.setFileInputFiles", slowmoable: true, nodeId: node_id, files: Array(value))
end
diff --git a/lib/ferrum/page.rb b/lib/ferrum/page.rb
index 5ca03c54..169954be 100644
--- a/lib/ferrum/page.rb
+++ b/lib/ferrum/page.rb
@@ -282,7 +282,7 @@ def command(method, wait: 0, slowmoable: false, **params)
@event.wait(wait)
if iteration != @event.iteration
set = @event.wait(timeout)
- raise TimeoutError unless set
+ raise TimeoutError.new() unless set
end
end
result
@@ -325,6 +325,10 @@ def use_authorized_proxy?
use_proxy? && @proxy_user && @proxy_password
end
+ def document_node_id
+ command("DOM.getDocument", depth: 0).dig("root", "nodeId")
+ end
+
private
def subscribe
@@ -441,10 +445,6 @@ def combine_url!(url_or_path)
(nil_or_relative ? @browser.base_url.join(url.to_s) : url).to_s
end
- def document_node_id
- command("DOM.getDocument", depth: 0).dig("root", "nodeId")
- end
-
def ws_url
"ws://#{@browser.process.host}:#{@browser.process.port}/devtools/page/#{@target_id}"
end
diff --git a/spec/frame_spec.rb b/spec/frame_spec.rb
index 9bba6c96..247c92f3 100644
--- a/spec/frame_spec.rb
+++ b/spec/frame_spec.rb
@@ -181,9 +181,10 @@
end
it "can set page content" do
- browser.content = "Voila!"
+ browser.content = "Voila! Link"
expect(browser.body).to include("Voila!")
+ expect(browser.at_css("a").text).to eq("Link")
end
it "gets page doctype" do
diff --git a/spec/network/response_spec.rb b/spec/network/response_spec.rb
index d34874a7..d513a42f 100644
--- a/spec/network/response_spec.rb
+++ b/spec/network/response_spec.rb
@@ -81,7 +81,7 @@
%r{/ferrum/jquery.min.js$} => File.size("#{PROJECT_ROOT}/spec/support/public/jquery-1.11.3.min.js"),
%r{/ferrum/jquery-ui.min.js$} => File.size("#{PROJECT_ROOT}/spec/support/public/jquery-ui-1.11.4.min.js"),
%r{/ferrum/test.js$} => File.size("#{PROJECT_ROOT}/spec/support/public/test.js"),
- %r{/ferrum/with_js$} => 2343
+ %r{/ferrum/with_js$} => 2312
}
resources_size.each do |resource, size|
diff --git a/spec/network_spec.rb b/spec/network_spec.rb
index 1b207aa3..ce89d506 100644
--- a/spec/network_spec.rb
+++ b/spec/network_spec.rb
@@ -302,6 +302,55 @@
end
describe "#intercept" do
+ it "supports :pattern argument" do
+ network.intercept(pattern: "*/ferrum/frame_child")
+ page.on(:request) do |request|
+ request.respond(body: "
hello
")
+ end
+
+ page.go_to("/ferrum/frame_parent")
+
+ expect(network.status).to eq(200)
+ frame = page.at_xpath("//iframe").frame
+ expect(frame.body).to include("hello")
+ end
+
+ context "with :resource_type argument" do
+ it "raises an error with wrong type" do
+ expect { network.intercept(resource_type: :BlaBla) }.to raise_error(ArgumentError)
+ end
+
+ it "intercepts only given type" do
+ network.intercept(resource_type: :Document)
+ page.on(:request) do |request|
+ request.respond(body: "
hello
")
+ end
+
+ page.go_to("/ferrum/non_existing")
+
+ expect(network.status).to eq(200)
+ expect(page.body).to include("hello")
+ end
+ end
+
+ context "with :request_stage argument" do
+ it "raises an error with wrong stage" do
+ expect { network.intercept(request_stage: :BlaBla) }.to raise_error(ArgumentError)
+ end
+
+ it "intercepts only given stage" do
+ network.intercept(request_stage: :Response)
+ page.on(:request) do |request|
+ request.respond(body: "
hello
")
+ end
+
+ page.go_to("/ferrum/index")
+
+ expect(network.status).to eq(200)
+ expect(page.body).to include("hello")
+ end
+ end
+
it "supports custom responses" do
network.intercept
page.on(:request) do |request|
diff --git a/spec/node_spec.rb b/spec/node_spec.rb
index 963a13a8..14143d8e 100644
--- a/spec/node_spec.rb
+++ b/spec/node_spec.rb
@@ -72,19 +72,33 @@
end
end
- context "when the element is not in the viewport of parent element", skip: true do
- before do
- browser.go_to("/ferrum/scroll")
- end
+ context "when the element is not in the viewport of parent element" do
+ before { page.go_to("/ferrum/scroll") }
+
+ it "scrolls into view if element outside viewport" do
+ link = page.at_xpath("//a[text() = 'Link outside viewport']")
+ link.click
+ expect(page.current_url).to eq(base_url("/ferrum/scroll"))
- it "scrolls into view", skip: "needs fix" do
- browser.at_xpath("//a[text() = 'Link outside viewport']").click
- expect(browser.current_url).to eq("/")
+ expect(link.in_viewport?).to eq(true)
+ box = page.at_xpath("//div[@id='overflow-box']")
+ expect(link.in_viewport?(of: box)).to eq(false)
+
+ link.scroll_into_view
+ expect(link.in_viewport?(of: box)).to eq(true)
+ link.click
+ expect(page.current_url).to eq(base_url("/"))
end
- it "scrolls into view if scrollIntoViewIfNeeded fails" do
- browser.click_link "Below the fold"
- expect(browser.current_path).to eq("/")
+ it "scrolls into view if element below the fold" do
+ link = page.at_xpath("//a[*//text() = 'Below the fold']")
+ expect(link.in_viewport?).to eq(false)
+
+ link.scroll_into_view
+
+ expect(link.in_viewport?).to eq(true)
+ link.click
+ expect(page.current_url).to eq(base_url("/"))
end
end
end
diff --git a/spec/support/views/animated.erb b/spec/support/views/animated.erb
index 770cb485..559e5c7a 100644
--- a/spec/support/views/animated.erb
+++ b/spec/support/views/animated.erb
@@ -1,3 +1,4 @@
+
diff --git a/spec/support/views/attach_file.erb b/spec/support/views/attach_file.erb
index 1bcad007..8544e318 100644
--- a/spec/support/views/attach_file.erb
+++ b/spec/support/views/attach_file.erb
@@ -1,10 +1,10 @@
diff --git a/spec/support/views/auto_refresh.erb b/spec/support/views/auto_refresh.erb
index 43c63de9..bec9b959 100644
--- a/spec/support/views/auto_refresh.erb
+++ b/spec/support/views/auto_refresh.erb
@@ -1,4 +1,4 @@
-
+
diff --git a/spec/support/views/buttons.erb b/spec/support/views/buttons.erb
index 3276fb67..65c61ab1 100644
--- a/spec/support/views/buttons.erb
+++ b/spec/support/views/buttons.erb
@@ -1,4 +1,3 @@
-