ferrum 0.13 → 0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bde7c8e40700ace2d713cba69eee5828dcb888e5468c07a6b1e5e0d668e4c641
4
- data.tar.gz: d52f7278dd76e670aa50721e6d70adc26297bbfb031ddac259dde5421c817a04
3
+ metadata.gz: 6cbef2f4caac663a8b39afc37b94e46f00c9471cdaea36a1781dcfa99488a546
4
+ data.tar.gz: 58d8f68e84138b71de15ac33368d4cd4f8992db4ab7f321a3e3f03530968502b
5
5
  SHA512:
6
- metadata.gz: a7206d7a92d8483bd106fe262130492e7bf51e2db8b99f73c39ada0a674fb29c84bc948bdbb6f554b672ade9f4e3812a9158447b30d6f976cb4892b5e4e8df30
7
- data.tar.gz: aef76b65c27dca2a5385d9881f9be8caccb85e804b31a26e421e35a43295baa3a5c818de856b49e3a7928139af4aeb588a5fae1cb67275440e5a0b3ddf97f76e
6
+ metadata.gz: a621c8e99fe1ec5e183ad54681da4a6bf09105fc66477c37b76b59cb1842f679ac679690514de1829fbbe18af4b81e0e01a64b564a48db9c49af151fc5b64e51
7
+ data.tar.gz: cc3089d1b9a81d43e8dbeb356b0d677d5377bc1d1d4667e0da9f389034e044ef0497ced614b39812cff53dd70930ec1f66e473f2a30a7edad825dd3424df7e54
data/README.md CHANGED
@@ -134,6 +134,7 @@ In docker as root you must pass the no-sandbox browser option:
134
134
  Ferrum::Browser.new(browser_options: { 'no-sandbox': nil })
135
135
  ```
136
136
 
137
+ It has also been reported that the Chrome process repeatedly crashes when running inside a Docker container on an M1 Mac preventing Ferrum from working. Ferrum should work as expected when deployed to a Docker container on a non-M1 Mac.
137
138
 
138
139
  ## Customization
139
140
 
@@ -144,7 +145,8 @@ Ferrum::Browser.new(options)
144
145
  ```
145
146
 
146
147
  * options `Hash`
147
- * `:headless` (Boolean) - Set browser as headless or not, `true` by default.
148
+ * `:headless` (String | Boolean) - Set browser as headless or not, `true` by default. You can set `"new"` to support
149
+ [new headless mode](https://developer.chrome.com/articles/new-headless/).
148
150
  * `:xvfb` (Boolean) - Run browser in a virtual framebuffer, `false` by default.
149
151
  * `:window_size` (Array) - The dimensions of the browser window in which to
150
152
  test, expressed as a 2-element array, e.g. [1024, 768]. Default: [1024, 768]
@@ -595,9 +597,16 @@ Activates offline mode for a page.
595
597
 
596
598
  ```ruby
597
599
  browser.network.offline_mode
598
- browser.go_to("https://github.com/") # => Ferrum::StatusError (Request to https://github.com/ failed to reach server, check DNS and server status)
600
+ browser.go_to("https://github.com/") # => Ferrum::StatusError (Request to https://github.com/ failed(net::ERR_INTERNET_DISCONNECTED))
599
601
  ```
600
602
 
603
+ #### cache(disable: `Boolean`)
604
+
605
+ Toggles ignoring cache for each request. If true, cache will not be used.
606
+
607
+ ```ruby
608
+ browser.network.cache(disable: true)
609
+ ```
601
610
 
602
611
  ## Proxy
603
612
 
@@ -1128,6 +1137,8 @@ frame.at_css("//a[text() = 'Log in']") # => Node
1128
1137
  #### evaluate
1129
1138
  #### selected : `Array<Node>`
1130
1139
  #### select
1140
+ #### scroll_into_view
1141
+ #### in_viewport?(of: `Node | nil`) : `Boolean`
1131
1142
 
1132
1143
  (chainable) Selects options by passed attribute.
1133
1144
 
@@ -84,7 +84,8 @@ module Ferrum
84
84
  case error["message"]
85
85
  # Node has disappeared while we were trying to get it
86
86
  when "No node with given id found",
87
- "Could not find node with given id"
87
+ "Could not find node with given id",
88
+ "Inspected target navigated or closed"
88
89
  raise NodeNotFoundError, error
89
90
  # Context is lost, page is reloading
90
91
  when "Cannot find context with specified id"
@@ -39,6 +39,10 @@ module Ferrum
39
39
  !!options.xvfb
40
40
  end
41
41
 
42
+ def headless_new?
43
+ @flags["headless"] == "new"
44
+ end
45
+
42
46
  def to_a
43
47
  [path] + @flags.map { |k, v| v.nil? ? "--#{k}" : "--#{k}=#{v}" }
44
48
  end
@@ -19,8 +19,9 @@ module Ferrum
19
19
  "keep-alive-for-test" => nil,
20
20
  "disable-popup-blocking" => nil,
21
21
  "disable-extensions" => nil,
22
+ "disable-component-extensions-with-background-pages" => nil,
22
23
  "disable-hang-monitor" => nil,
23
- "disable-features" => "site-per-process,TranslateUI",
24
+ "disable-features" => "site-per-process,IsolateOrigins,TranslateUI",
24
25
  "disable-translate" => nil,
25
26
  "disable-background-networking" => nil,
26
27
  "enable-features" => "NetworkService,NetworkServiceInProcess",
@@ -32,6 +33,7 @@ module Ferrum
32
33
  "disable-ipc-flooding-protection" => nil,
33
34
  "disable-prompt-on-repost" => nil,
34
35
  "disable-renderer-backgrounding" => nil,
36
+ "disable-site-isolation-trials" => nil,
35
37
  "force-color-profile" => "srgb",
36
38
  "metrics-recording-only" => nil,
37
39
  "safebrowsing-disable-auto-update" => nil,
@@ -74,7 +76,12 @@ module Ferrum
74
76
  end
75
77
 
76
78
  def merge_default(flags, options)
77
- defaults = except("headless", "disable-gpu") unless options.headless
79
+ defaults = case options.headless
80
+ when false
81
+ except("headless", "disable-gpu")
82
+ when "new"
83
+ except("headless").merge("headless" => "new")
84
+ end
78
85
 
79
86
  defaults ||= DEFAULT_OPTIONS
80
87
  defaults.merge(flags)
@@ -137,7 +137,7 @@ module Ferrum
137
137
  output = ""
138
138
  start = Utils::ElapsedTime.monotonic_time
139
139
  max_time = start + timeout
140
- regexp = %r{DevTools listening on (ws://.*)}
140
+ regexp = %r{DevTools listening on (ws://.*[a-zA-Z0-9-]{36})}
141
141
  while (now = Utils::ElapsedTime.monotonic_time) < max_time
142
142
  begin
143
143
  output += read_io.read_nonblock(512)
@@ -7,7 +7,7 @@ require "websocket/driver"
7
7
  module Ferrum
8
8
  class Browser
9
9
  class WebSocket
10
- WEBSOCKET_BUG_SLEEP = 0.01
10
+ WEBSOCKET_BUG_SLEEP = 0.05
11
11
  SKIP_LOGGING_SCREENSHOTS = !ENV["FERRUM_LOGGING_SCREENSHOTS"]
12
12
 
13
13
  attr_reader :url, :messages
@@ -22,7 +22,7 @@ module Ferrum
22
22
  body doctype content=
23
23
  headers cookies network
24
24
  mouse keyboard
25
- screenshot pdf mhtml viewport_size
25
+ screenshot pdf mhtml viewport_size device_pixel_ratio
26
26
  frames frame_by main_frame
27
27
  evaluate evaluate_on evaluate_async execute evaluate_func
28
28
  add_script_tag add_style_tag bypass_csp
@@ -177,7 +177,7 @@ module Ferrum
177
177
  block_given? ? yield(page) : page
178
178
  ensure
179
179
  if block_given?
180
- page.close
180
+ page&.close
181
181
  context.dispose if new_context
182
182
  end
183
183
  end
@@ -237,6 +237,8 @@ module Ferrum
237
237
  end
238
238
 
239
239
  def quit
240
+ return unless @client
241
+
240
242
  @client.close
241
243
  @process.stop
242
244
  @client = @process = @contexts = nil
@@ -262,6 +264,10 @@ module Ferrum
262
264
  VersionInfo.new(command("Browser.getVersion"))
263
265
  end
264
266
 
267
+ def headless_new?
268
+ process&.command&.headless_new?
269
+ end
270
+
265
271
  private
266
272
 
267
273
  def start
@@ -113,6 +113,38 @@ module Ferrum
113
113
  Time.at(attributes["expires"]) if attributes["expires"].positive?
114
114
  end
115
115
 
116
+ #
117
+ # The priority of the cookie.
118
+ #
119
+ # @return [String]
120
+ #
121
+ def priority
122
+ @attributes["priority"]
123
+ end
124
+
125
+ #
126
+ # @return [Boolean]
127
+ #
128
+ def sameparty?
129
+ @attributes["sameParty"]
130
+ end
131
+
132
+ alias same_party? sameparty?
133
+
134
+ #
135
+ # @return [String]
136
+ #
137
+ def source_scheme
138
+ @attributes["sourceScheme"]
139
+ end
140
+
141
+ #
142
+ # @return [Integer]
143
+ #
144
+ def source_port
145
+ @attributes["sourcePort"]
146
+ end
147
+
116
148
  #
117
149
  # Compares different cookie objects.
118
150
  #
@@ -121,6 +153,31 @@ module Ferrum
121
153
  def ==(other)
122
154
  other.class == self.class && other.attributes == attributes
123
155
  end
156
+
157
+ #
158
+ # Converts the cookie back into a raw cookie String.
159
+ #
160
+ # @return [String]
161
+ # The raw cookie string.
162
+ #
163
+ def to_s
164
+ string = String.new("#{@attributes['name']}=#{@attributes['value']}")
165
+
166
+ @attributes.each do |key, value|
167
+ case key
168
+ when "name", "value" # no-op
169
+ when "domain" then string << "; Domain=#{value}"
170
+ when "path" then string << "; Path=#{value}"
171
+ when "expires" then string << "; Expires=#{Time.at(value).httpdate}"
172
+ when "httpOnly" then string << "; httpOnly" if value
173
+ when "secure" then string << "; Secure" if value
174
+ end
175
+ end
176
+
177
+ string
178
+ end
179
+
180
+ alias to_h attributes
124
181
  end
125
182
  end
126
183
  end
@@ -4,10 +4,34 @@ require "ferrum/cookies/cookie"
4
4
 
5
5
  module Ferrum
6
6
  class Cookies
7
+ include Enumerable
8
+
7
9
  def initialize(page)
8
10
  @page = page
9
11
  end
10
12
 
13
+ #
14
+ # Enumerates over all cookies.
15
+ #
16
+ # @yield [cookie]
17
+ # The given block will be passed each cookie.
18
+ #
19
+ # @yieldparam [Cookie] cookie
20
+ # A cookie in the browser.
21
+ #
22
+ # @return [Enumerator]
23
+ # If no block is given, an Enumerator object will be returned.
24
+ #
25
+ def each
26
+ return enum_for(__method__) unless block_given?
27
+
28
+ cookies = @page.command("Network.getAllCookies")["cookies"]
29
+
30
+ cookies.each do |c|
31
+ yield Cookie.new(c)
32
+ end
33
+ end
34
+
11
35
  #
12
36
  # Returns cookies hash.
13
37
  #
@@ -22,8 +46,9 @@ module Ferrum
22
46
  # # }
23
47
  #
24
48
  def all
25
- cookies = @page.command("Network.getAllCookies")["cookies"]
26
- cookies.to_h { |c| [c["name"], Cookie.new(c)] }
49
+ each.to_h do |cookie|
50
+ [cookie.name, cookie]
51
+ end
27
52
  end
28
53
 
29
54
  #
@@ -44,7 +69,7 @@ module Ferrum
44
69
  # # }>
45
70
  #
46
71
  def [](name)
47
- all[name]
72
+ find { |cookie| cookie.name == name }
48
73
  end
49
74
 
50
75
  #
@@ -53,23 +78,34 @@ module Ferrum
53
78
  # @param [Hash{Symbol => Object}, Cookie] options
54
79
  #
55
80
  # @option options [String] :name
81
+ # The cookie param name.
56
82
  #
57
83
  # @option options [String] :value
84
+ # The cookie param value.
58
85
  #
59
86
  # @option options [String] :domain
87
+ # The domain the cookie belongs to.
60
88
  #
61
89
  # @option options [String] :path
90
+ # The path that the cookie is bound to.
62
91
  #
63
92
  # @option options [Integer] :expires
93
+ # When the cookie will expire.
64
94
  #
65
95
  # @option options [Integer] :size
96
+ # The size of the cookie.
66
97
  #
67
98
  # @option options [Boolean] :httponly
99
+ # Specifies whether the cookie `HttpOnly`.
68
100
  #
69
101
  # @option options [Boolean] :secure
102
+ # Specifies whether the cookie is marked as `Secure`.
70
103
  #
71
104
  # @option options [String] :samesite
105
+ # Specifies whether the cookie is `SameSite`.
72
106
  #
107
+ # @option options [Boolean] :session
108
+ # Specifies whether the cookie is a session cookie.
73
109
  #
74
110
  # @example
75
111
  # browser.cookies.set(name: "stealth", value: "omg", domain: "google.com") # => true
data/lib/ferrum/frame.rb CHANGED
@@ -113,6 +113,7 @@ module Ferrum
113
113
  document.close();
114
114
  arguments[1](true);
115
115
  ), @page.timeout, html)
116
+ @page.document_node_id
116
117
  end
117
118
  alias set_content content=
118
119
 
@@ -51,7 +51,7 @@ module Ferrum
51
51
  # @return [Boolean]
52
52
  #
53
53
  def navigation_request?(frame_id)
54
- request.type?(:document) && request&.frame_id == frame_id
54
+ request&.type?(:document) && request&.frame_id == frame_id
55
55
  end
56
56
 
57
57
  #
@@ -79,7 +79,7 @@ module Ferrum
79
79
  # @return [Boolean]
80
80
  #
81
81
  def finished?
82
- blocked? || !response.nil? || !error.nil?
82
+ blocked? || response&.loaded? || !error.nil?
83
83
  end
84
84
 
85
85
  #
@@ -100,6 +100,24 @@ module Ferrum
100
100
  !intercepted_request.nil?
101
101
  end
102
102
 
103
+ #
104
+ # Determines if the exchange is XHR.
105
+ #
106
+ # @return [Boolean]
107
+ #
108
+ def xhr?
109
+ !!request&.xhr?
110
+ end
111
+
112
+ #
113
+ # Determines if the exchange is a redirect.
114
+ #
115
+ # @return [Boolean]
116
+ #
117
+ def redirect?
118
+ response&.redirect?
119
+ end
120
+
103
121
  #
104
122
  # Returns request's URL.
105
123
  #
@@ -1,10 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "ferrum/network/request_params"
3
4
  require "base64"
4
5
 
5
6
  module Ferrum
6
7
  class Network
7
8
  class InterceptedRequest
9
+ include RequestParams
10
+
8
11
  attr_accessor :request_id, :frame_id, :resource_type, :network_id, :status
9
12
 
10
13
  def initialize(page, params)
@@ -54,18 +57,6 @@ module Ferrum
54
57
  @page.command("Fetch.failRequest", requestId: request_id, errorReason: "BlockedByClient")
55
58
  end
56
59
 
57
- def url
58
- @request["url"]
59
- end
60
-
61
- def method
62
- @request["method"]
63
- end
64
-
65
- def headers
66
- @request["headers"]
67
- end
68
-
69
60
  def initial_priority
70
61
  @request["initialPriority"]
71
62
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "ferrum/network/request_params"
3
4
  require "time"
4
5
 
5
6
  module Ferrum
@@ -9,6 +10,8 @@ module Ferrum
9
10
  # object.
10
11
  #
11
12
  class Request
13
+ include RequestParams
14
+
12
15
  #
13
16
  # Initializes the request object.
14
17
  #
@@ -51,48 +54,21 @@ module Ferrum
51
54
  end
52
55
 
53
56
  #
54
- # The frame ID of the request.
55
- #
56
- # @return [String]
57
+ # Determines if the request is XHR.
57
58
  #
58
- def frame_id
59
- @params["frameId"]
60
- end
61
-
62
- #
63
- # The URL for the request.
64
- #
65
- # @return [String]
66
- #
67
- def url
68
- @request["url"]
69
- end
70
-
71
- #
72
- # The URL fragment for the request.
73
- #
74
- # @return [String, nil]
59
+ # @return [Boolean]
75
60
  #
76
- def url_fragment
77
- @request["urlFragment"]
61
+ def xhr?
62
+ type?("xhr")
78
63
  end
79
64
 
80
65
  #
81
- # The request method.
66
+ # The frame ID of the request.
82
67
  #
83
68
  # @return [String]
84
69
  #
85
- def method
86
- @request["method"]
87
- end
88
-
89
- #
90
- # The request headers.
91
- #
92
- # @return [Hash{String => String}]
93
- #
94
- def headers
95
- @request["headers"]
70
+ def frame_id
71
+ @params["frameId"]
96
72
  end
97
73
 
98
74
  #
@@ -105,15 +81,14 @@ module Ferrum
105
81
  end
106
82
 
107
83
  #
108
- # The optional HTTP `POST` form data.
84
+ # Converts the request to a Hash.
109
85
  #
110
- # @return [String, nil]
111
- # The HTTP `POST` form data.
86
+ # @return [Hash{String => Object}]
87
+ # The params of the request.
112
88
  #
113
- def post_data
114
- @request["postData"]
89
+ def to_h
90
+ @params
115
91
  end
116
- alias body post_data
117
92
  end
118
93
  end
119
94
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ferrum
4
+ class Network
5
+ #
6
+ # Common methods used by both {Request} and {InterceptedRequest}.
7
+ #
8
+ module RequestParams
9
+ #
10
+ # The URL for the request.
11
+ #
12
+ # @return [String]
13
+ #
14
+ def url
15
+ @request["url"]
16
+ end
17
+
18
+ #
19
+ # The URL fragment for the request.
20
+ #
21
+ # @return [String, nil]
22
+ #
23
+ def url_fragment
24
+ @request["urlFragment"]
25
+ end
26
+
27
+ #
28
+ # The request method.
29
+ #
30
+ # @return [String]
31
+ #
32
+ def method
33
+ @request["method"]
34
+ end
35
+
36
+ #
37
+ # The request headers.
38
+ #
39
+ # @return [Hash{String => String}]
40
+ #
41
+ def headers
42
+ @request["headers"]
43
+ end
44
+
45
+ #
46
+ # The optional HTTP `POST` form data.
47
+ #
48
+ # @return [String, nil]
49
+ # The HTTP `POST` form data.
50
+ #
51
+ def post_data
52
+ @request["postData"]
53
+ end
54
+ alias body post_data
55
+ end
56
+ end
57
+ end
@@ -18,8 +18,13 @@ module Ferrum
18
18
  # @return [Hash{String => Object}]
19
19
  attr_reader :params
20
20
 
21
+ # The response is fully loaded by the browser.
21
22
  #
22
- # Initializes the respones object.
23
+ # @return [Boolean]
24
+ attr_writer :loaded
25
+
26
+ #
27
+ # Initializes the responses object.
23
28
  #
24
29
  # @param [Page] page
25
30
  # The page associated with the network response.
@@ -121,9 +126,8 @@ module Ferrum
121
126
  #
122
127
  def body
123
128
  @body ||= begin
124
- body, encoded = @page
125
- .command("Network.getResponseBody", requestId: id)
126
- .values_at("body", "base64Encoded")
129
+ body, encoded = @page.command("Network.getResponseBody", requestId: id)
130
+ .values_at("body", "base64Encoded")
127
131
  encoded ? Base64.decode64(body) : body
128
132
  end
129
133
  end
@@ -135,8 +139,22 @@ module Ferrum
135
139
  @page.network.response == self
136
140
  end
137
141
 
142
+ # The response is fully loaded by the browser or not.
143
+ #
144
+ # @return [Boolean]
145
+ def loaded?
146
+ @loaded
147
+ end
148
+
149
+ # Whether the response is a redirect.
150
+ #
151
+ # @return [Boolean]
152
+ def redirect?
153
+ params.key?("redirectResponse")
154
+ end
155
+
138
156
  #
139
- # Comapres the respones ID to another response's ID.
157
+ # Compares the response's ID to another response's ID.
140
158
  #
141
159
  # @return [Boolean]
142
160
  # Indicates whether the response has the same ID as the other response
@@ -154,6 +172,8 @@ module Ferrum
154
172
  def inspect
155
173
  %(#<#{self.class} @params=#{@params.inspect} @response=#{@response.inspect}>)
156
174
  end
175
+
176
+ alias to_h params
157
177
  end
158
178
  end
159
179
  end
@@ -11,9 +11,10 @@ module Ferrum
11
11
  class Network
12
12
  CLEAR_TYPE = %i[traffic cache].freeze
13
13
  AUTHORIZE_TYPE = %i[server proxy].freeze
14
- RESOURCE_TYPES = %w[Document Stylesheet Image Media Font Script TextTrack
15
- XHR Fetch EventSource WebSocket Manifest
16
- SignedExchange Ping CSPViolationReport Other].freeze
14
+ REQUEST_STAGES = %i[Request Response].freeze
15
+ RESOURCE_TYPES = %i[Document Stylesheet Image Media Font Script TextTrack
16
+ XHR Fetch Prefetch EventSource WebSocket Manifest
17
+ SignedExchange Ping CSPViolationReport Preflight Other].freeze
17
18
  AUTHORIZE_BLOCK_MISSING = "Block is missing, call `authorize(...) { |r| r.continue } " \
18
19
  "or subscribe to `on(:request)` events before calling it"
19
20
  AUTHORIZE_TYPE_WRONG = ":type should be in #{AUTHORIZE_TYPE}"
@@ -187,11 +188,20 @@ module Ferrum
187
188
  # end
188
189
  # browser.go_to("https://google.com")
189
190
  #
190
- def intercept(pattern: "*", resource_type: nil)
191
+ def intercept(pattern: "*", resource_type: nil, request_stage: nil, handle_auth_requests: true)
191
192
  pattern = { urlPattern: pattern }
192
- pattern[:resourceType] = resource_type if resource_type && RESOURCE_TYPES.include?(resource_type.to_s)
193
193
 
194
- @page.command("Fetch.enable", handleAuthRequests: true, patterns: [pattern])
194
+ if resource_type && RESOURCE_TYPES.none?(resource_type.to_sym)
195
+ raise ArgumentError, "Unknown resource type '#{resource_type}' must be #{RESOURCE_TYPES.join(' | ')}"
196
+ end
197
+
198
+ if request_stage && REQUEST_STAGES.none?(request_stage.to_sym)
199
+ raise ArgumentError, "Unknown request stage '#{request_stage}' must be #{REQUEST_STAGES.join(' | ')}"
200
+ end
201
+
202
+ pattern[:resourceType] = resource_type if resource_type
203
+ pattern[:requestStage] = request_stage if request_stage
204
+ @page.command("Fetch.enable", patterns: [pattern], handleAuthRequests: handle_auth_requests)
195
205
  end
196
206
 
197
207
  #
@@ -323,13 +333,23 @@ module Ferrum
323
333
  #
324
334
  # @example
325
335
  # browser.network.offline_mode
326
- # browser.go_to("https://github.com/") # => Ferrum::StatusError (Request to https://github.com/ failed to reach
327
- # server, check DNS and server status)
336
+ # browser.go_to("https://github.com/")
337
+ # # => Request to https://github.com/ failed (net::ERR_INTERNET_DISCONNECTED) (Ferrum::StatusError)
328
338
  #
329
339
  def offline_mode
330
340
  emulate_network_conditions(offline: true, latency: 0, download_throughput: 0, upload_throughput: 0)
331
341
  end
332
342
 
343
+ #
344
+ # Toggles ignoring cache for each request. If true, cache will not be used.
345
+ #
346
+ # @example
347
+ # browser.network.cache(disable: true)
348
+ #
349
+ def cache(disable:)
350
+ @page.command("Network.setCacheDisabled", cacheDisabled: disable)
351
+ end
352
+
333
353
  private
334
354
 
335
355
  def subscribe_request_will_be_sent
@@ -352,6 +372,7 @@ module Ferrum
352
372
  if params["redirectResponse"]
353
373
  previous_exchange = select(request.id)[-2]
354
374
  response = Network::Response.new(@page, params)
375
+ response.loaded = true
355
376
  previous_exchange.response = response
356
377
  end
357
378
 
@@ -374,8 +395,12 @@ module Ferrum
374
395
 
375
396
  def subscribe_loading_finished
376
397
  @page.on("Network.loadingFinished") do |params|
377
- exchange = select(params["requestId"]).last
378
- exchange.response.body_size = params["encodedDataLength"] if exchange&.response
398
+ response = select(params["requestId"]).last&.response
399
+
400
+ if response
401
+ response.loaded = true
402
+ response.body_size = params["encodedDataLength"]
403
+ end
379
404
  end
380
405
  end
381
406
 
data/lib/ferrum/node.rb CHANGED
@@ -88,6 +88,26 @@ module Ferrum
88
88
  raise NotImplementedError
89
89
  end
90
90
 
91
+ def scroll_into_view
92
+ tap { page.command("DOM.scrollIntoViewIfNeeded", nodeId: node_id) }
93
+ end
94
+
95
+ def in_viewport?(of: nil)
96
+ function = <<~JS
97
+ function(element, scope) {
98
+ const rect = element.getBoundingClientRect();
99
+ const [height, width] = scope
100
+ ? [scope.offsetHeight, scope.offsetWidth]
101
+ : [window.innerHeight, window.innerWidth];
102
+ return rect.top >= 0 &&
103
+ rect.left >= 0 &&
104
+ rect.bottom <= height &&
105
+ rect.right <= width;
106
+ }
107
+ JS
108
+ page.evaluate_func(function, self, of)
109
+ end
110
+
91
111
  def select_file(value)
92
112
  page.command("DOM.setFileInputFiles", slowmoable: true, nodeId: node_id, files: Array(value))
93
113
  end
@@ -208,7 +228,7 @@ module Ferrum
208
228
 
209
229
  def content_quads
210
230
  quads = page.command("DOM.getContentQuads", nodeId: node_id)["quads"]
211
- raise CoordinatesNotFoundError, "Node is either not visible or not an HTMLElement" if quads.size.zero?
231
+ raise CoordinatesNotFoundError, "Node is either not visible or not an HTMLElement" if quads.empty?
212
232
 
213
233
  quads
214
234
  end
@@ -147,6 +147,12 @@ module Ferrum
147
147
  JS
148
148
  end
149
149
 
150
+ def device_pixel_ratio
151
+ evaluate <<~JS
152
+ window.devicePixelRatio
153
+ JS
154
+ end
155
+
150
156
  def document_size
151
157
  evaluate <<~JS
152
158
  [document.documentElement.scrollWidth,
data/lib/ferrum/page.rb CHANGED
@@ -114,14 +114,8 @@ module Ferrum
114
114
  options = { url: combine_url!(url) }
115
115
  options.merge!(referrer: referrer) if referrer
116
116
  response = command("Page.navigate", wait: GOTO_WAIT, **options)
117
- # https://cs.chromium.org/chromium/src/net/base/net_error_list.h
118
- if %w[net::ERR_NAME_NOT_RESOLVED
119
- net::ERR_NAME_RESOLUTION_FAILED
120
- net::ERR_INTERNET_DISCONNECTED
121
- net::ERR_CONNECTION_TIMED_OUT
122
- net::ERR_FILE_NOT_FOUND].include?(response["errorText"])
123
- raise StatusError, options[:url]
124
- end
117
+ error_text = response["errorText"]
118
+ raise StatusError.new(options[:url], "Request to #{options[:url]} failed (#{error_text})") if error_text
125
119
 
126
120
  response["frameId"]
127
121
  rescue TimeoutError
@@ -151,9 +145,8 @@ module Ferrum
151
145
  command("Emulation.setDeviceMetricsOverride", slowmoable: true,
152
146
  width: width,
153
147
  height: height,
154
- deviceScaleFactor: 1,
155
- mobile: false,
156
- fitWindow: false)
148
+ deviceScaleFactor: 0,
149
+ mobile: false)
157
150
  end
158
151
 
159
152
  #
@@ -325,6 +318,10 @@ module Ferrum
325
318
  use_proxy? && @proxy_user && @proxy_password
326
319
  end
327
320
 
321
+ def document_node_id
322
+ command("DOM.getDocument", depth: 0).dig("root", "nodeId")
323
+ end
324
+
328
325
  private
329
326
 
330
327
  def subscribe
@@ -350,7 +347,7 @@ module Ferrum
350
347
  on(:dialog) do |dialog, _index, total|
351
348
  if total == 1
352
349
  warn "Dialog was shown but you didn't provide `on(:dialog)` callback, accepting it by default. " \
353
- "Please take a look at https://github.com/rubycdp/ferrum#dialog"
350
+ "Please take a look at https://github.com/rubycdp/ferrum#dialogs"
354
351
  dialog.accept
355
352
  end
356
353
  end
@@ -393,7 +390,8 @@ module Ferrum
393
390
  resize(width: width, height: height)
394
391
 
395
392
  response = command("Page.getNavigationHistory")
396
- return unless response.dig("entries", 0, "transitionType") != "typed"
393
+ transition_type = response.dig("entries", 0, "transitionType")
394
+ return if transition_type == "auto_toplevel"
397
395
 
398
396
  # If we create page by clicking links, submitting forms and so on it
399
397
  # opens a new window for which `frameStoppedLoading` event never
@@ -441,10 +439,6 @@ module Ferrum
441
439
  (nil_or_relative ? @browser.base_url.join(url.to_s) : url).to_s
442
440
  end
443
441
 
444
- def document_node_id
445
- command("DOM.getDocument", depth: 0).dig("root", "nodeId")
446
- end
447
-
448
442
  def ws_url
449
443
  "ws://#{@browser.process.host}:#{@browser.process.port}/devtools/page/#{@target_id}"
450
444
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ferrum
4
- VERSION = "0.13"
4
+ VERSION = "0.14"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ferrum
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.13'
4
+ version: '0.14'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Vorotilin
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-11-12 00:00:00.000000000 Z
11
+ date: 2023-09-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: addressable
@@ -72,104 +72,6 @@ dependencies:
72
72
  - - "<"
73
73
  - !ruby/object:Gem::Version
74
74
  version: '0.8'
75
- - !ruby/object:Gem::Dependency
76
- name: chunky_png
77
- requirement: !ruby/object:Gem::Requirement
78
- requirements:
79
- - - "~>"
80
- - !ruby/object:Gem::Version
81
- version: '1.3'
82
- type: :development
83
- prerelease: false
84
- version_requirements: !ruby/object:Gem::Requirement
85
- requirements:
86
- - - "~>"
87
- - !ruby/object:Gem::Version
88
- version: '1.3'
89
- - !ruby/object:Gem::Dependency
90
- name: image_size
91
- requirement: !ruby/object:Gem::Requirement
92
- requirements:
93
- - - "~>"
94
- - !ruby/object:Gem::Version
95
- version: '2.0'
96
- type: :development
97
- prerelease: false
98
- version_requirements: !ruby/object:Gem::Requirement
99
- requirements:
100
- - - "~>"
101
- - !ruby/object:Gem::Version
102
- version: '2.0'
103
- - !ruby/object:Gem::Dependency
104
- name: pdf-reader
105
- requirement: !ruby/object:Gem::Requirement
106
- requirements:
107
- - - "~>"
108
- - !ruby/object:Gem::Version
109
- version: '2.2'
110
- type: :development
111
- prerelease: false
112
- version_requirements: !ruby/object:Gem::Requirement
113
- requirements:
114
- - - "~>"
115
- - !ruby/object:Gem::Version
116
- version: '2.2'
117
- - !ruby/object:Gem::Dependency
118
- name: puma
119
- requirement: !ruby/object:Gem::Requirement
120
- requirements:
121
- - - "~>"
122
- - !ruby/object:Gem::Version
123
- version: '4.1'
124
- type: :development
125
- prerelease: false
126
- version_requirements: !ruby/object:Gem::Requirement
127
- requirements:
128
- - - "~>"
129
- - !ruby/object:Gem::Version
130
- version: '4.1'
131
- - !ruby/object:Gem::Dependency
132
- name: rake
133
- requirement: !ruby/object:Gem::Requirement
134
- requirements:
135
- - - "~>"
136
- - !ruby/object:Gem::Version
137
- version: '13.0'
138
- type: :development
139
- prerelease: false
140
- version_requirements: !ruby/object:Gem::Requirement
141
- requirements:
142
- - - "~>"
143
- - !ruby/object:Gem::Version
144
- version: '13.0'
145
- - !ruby/object:Gem::Dependency
146
- name: rspec
147
- requirement: !ruby/object:Gem::Requirement
148
- requirements:
149
- - - "~>"
150
- - !ruby/object:Gem::Version
151
- version: '3.8'
152
- type: :development
153
- prerelease: false
154
- version_requirements: !ruby/object:Gem::Requirement
155
- requirements:
156
- - - "~>"
157
- - !ruby/object:Gem::Version
158
- version: '3.8'
159
- - !ruby/object:Gem::Dependency
160
- name: sinatra
161
- requirement: !ruby/object:Gem::Requirement
162
- requirements:
163
- - - "~>"
164
- - !ruby/object:Gem::Version
165
- version: '2.0'
166
- type: :development
167
- prerelease: false
168
- version_requirements: !ruby/object:Gem::Requirement
169
- requirements:
170
- - - "~>"
171
- - !ruby/object:Gem::Version
172
- version: '2.0'
173
75
  description: Ferrum allows you to control headless Chrome browser
174
76
  email:
175
77
  - d.vorotilin@gmail.com
@@ -212,6 +114,7 @@ files:
212
114
  - lib/ferrum/network/exchange.rb
213
115
  - lib/ferrum/network/intercepted_request.rb
214
116
  - lib/ferrum/network/request.rb
117
+ - lib/ferrum/network/request_params.rb
215
118
  - lib/ferrum/network/response.rb
216
119
  - lib/ferrum/node.rb
217
120
  - lib/ferrum/page.rb
@@ -252,7 +155,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
252
155
  - !ruby/object:Gem::Version
253
156
  version: '0'
254
157
  requirements: []
255
- rubygems_version: 3.3.7
158
+ rubygems_version: 3.4.13
256
159
  signing_key:
257
160
  specification_version: 4
258
161
  summary: Ruby headless Chrome driver