ferrum 0.3 → 0.4

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: 5c762e928c31b548af3ecfc9e2ca3cd90e7315c34c313387a0bde299bc201e31
4
- data.tar.gz: c46c348b3631700428723ca110bb4a40e0106cfca9b84ab0f743bf4356f348e9
3
+ metadata.gz: 6f524fb695add05ab04355a4f694710f0cda6ce317048f234cd6d597a6db5b54
4
+ data.tar.gz: 4f755ea3ce87efa0a92b2689188ec8ce78979b716e8053e9687fe4f9e97baf2e
5
5
  SHA512:
6
- metadata.gz: 854dd57cdd66bfd68535d88f087b145e4f700afdb078c3f0776e374d1fd9c919010c32282cef9b5ac4bb1c2cff21ee7307c7d5b3e3e58bc354cf4477206b8cd9
7
- data.tar.gz: 207738d98111f3d67322ab926f022e184672b73ae8246acb4a01260d12db07d602f10c29b6612ca1830926091444dae51aeaf0896c5e33d8f202fe9c791dc2a7
6
+ metadata.gz: f52f33ea6d6292ab9f829f9e4870ba7be5fe0f9c045f4b4fe1e540b5363440027ae8890987e92e95612b91e1d82d28a19ff3d204cabeac0bebde96943d399baa
7
+ data.tar.gz: 3156ae9cc12403dd2b497119371b895d7f37865318ffa2d424676df91346403d85386615abe2db020dffe77b055de72b5e44ccf69137d9bd1fa01a9ae75ffa0f
data/README.md CHANGED
@@ -159,15 +159,6 @@ browser.goto("https://github.com/")
159
159
  browser.refresh
160
160
  ```
161
161
 
162
- #### status : `Integer`
163
-
164
- Contains the status code of the response (e.g., 200 for a success).
165
-
166
- ```ruby
167
- browser.goto("https://github.com/")
168
- browser.status # => 200
169
- ```
170
-
171
162
 
172
163
  ## Finders
173
164
 
@@ -261,8 +252,8 @@ browser.body # => '<html itemscope="" itemtype="http://schema.org/WebPage" lang=
261
252
  Saves screenshot on a disk or returns it as base64.
262
253
 
263
254
  * options `Hash`
264
- * :path `String` to save a screenshot on the disk. If passed `:encoding` is
265
- set to `:binary` automatically
255
+ * :path `String` to save a screenshot on the disk. `:encoding` will be set to
256
+ `:binary` automatically
266
257
  * :encoding `Symbol` `:base64` | `:binary` you can set it to return image as
267
258
  Base64
268
259
  * :format `String` "jpeg" | "png"
@@ -286,8 +277,8 @@ browser.screenshot(full: true, quality: 60) # "iVBORw0KGgoAAAANSUhEUgAABAAAAAMAC
286
277
  Saves PDF on a disk or returns it as base64.
287
278
 
288
279
  * options `Hash`
289
- * :path `String` to save a screenshot on the disk. If passed `:encoding` is
290
- set to `:binary` automatically
280
+ * :path `String` to save a pdf on the disk. `:encoding` will be set to
281
+ `:binary` automatically
291
282
  * :encoding `Symbol` `:base64` | `:binary` you can set it to return pdf as
292
283
  Base64
293
284
  * :landscape `Boolean` paper orientation. Defaults to false.
@@ -305,17 +296,98 @@ browser.pdf(path: "google.pdf", paper_width: 1.0, paper_height: 1.0) # => 14983
305
296
 
306
297
  ## Network
307
298
 
308
- #### network_traffic : `Array<Network::Request>`
299
+ browser.network
300
+
301
+ #### traffic `Array<Network::Exchange>`
302
+
303
+ Returns all information about network traffic as `Network::Exchange` instance
304
+ which in general is a wrapper around `request`, `response` and `error`.
305
+
306
+ ```ruby
307
+ browser.goto("https://github.com/")
308
+ browser.network.traffic # => [#<Ferrum::Network::Exchange, ...]
309
+ ```
310
+
311
+ #### request : `Network::Request`
312
+
313
+ Page request of the main frame.
314
+
315
+ ```ruby
316
+ browser.goto("https://github.com/")
317
+ browser.network.request # => #<Ferrum::Network::Request...
318
+ ```
319
+
320
+ #### response : `Network::Response`
321
+
322
+ Page response of the main frame.
323
+
324
+ ```ruby
325
+ browser.goto("https://github.com/")
326
+ browser.network.response # => #<Ferrum::Network::Response...
327
+ ```
309
328
 
310
- Returns all information about network traffic as a request/response array.
329
+ #### status : `Integer`
330
+
331
+ Contains the status code of the main page response (e.g., 200 for a
332
+ success). This is just a shortcut for `response.status`.
333
+
334
+ ```ruby
335
+ browser.goto("https://github.com/")
336
+ browser.network.status # => 200
337
+ ```
338
+
339
+ #### clear(type)
340
+
341
+ Clear browser's cache or collected traffic.
342
+
343
+ * type `Symbol` it is either `:traffic` or `:cache`
344
+
345
+ ```ruby
346
+ traffic = browser.network.traffic # => []
347
+ browser.goto("https://github.com/")
348
+ traffic.size # => 51
349
+ browser.network.clear(:traffic)
350
+ traffic.size # => 0
351
+ ```
352
+
353
+ #### intercept(\*\*options)
354
+
355
+ Set request interception for given options. This method is only sets request
356
+ interception, you should use `on` callback to catch requests and abort or
357
+ continue them.
358
+
359
+ * options `Hash`
360
+ * :pattern `String` \* by default
361
+ * :resource_type `Symbol` one of the [resource types](https://chromedevtools.github.io/devtools-protocol/tot/Network#type-ResourceType)
362
+
363
+ ```ruby
364
+ browser = Ferrum::Browser.new
365
+ browser.network.intercept
366
+ browser.on(:request) do |request|
367
+ if request.match?(/bla-bla/)
368
+ request.abort
369
+ else
370
+ request.continue
371
+ end
372
+ end
373
+ browser.goto("https://google.com")
374
+ ```
311
375
 
312
- #### clear_network_traffic
376
+ #### authorize(\*\*options)
313
377
 
314
- Cleans up collected data.
378
+ If site uses authorization you can provide credentials using this method.
315
379
 
316
- #### response_headers : `Hash`
380
+ * options `Hash`
381
+ * :type `Symbol` `:server` | `:proxy` site or proxy authorization
382
+ * :user `String`
383
+ * :password `String`
317
384
 
318
- Returns all headers for a given request in `goto` method.
385
+ ```ruby
386
+ browser.network.authorize(user: "login", password: "pass")
387
+ browser.goto("http://example.com/authenticated")
388
+ puts browser.network.status # => 200
389
+ puts browser.body # => Welcome, authenticated client
390
+ ```
319
391
 
320
392
 
321
393
  ### Mouse
@@ -540,23 +612,12 @@ Play around inside given frame
540
612
  browser.goto("https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe")
541
613
  frame = browser.at_xpath("//iframe")
542
614
  browser.within_frame(frame) do
615
+ puts browser.frame_title # => HTML Demo: <iframe>
543
616
  puts browser.frame_url # => https://interactive-examples.mdn.mozilla.net/pages/tabbed/iframe.html
544
617
  end
545
618
  ```
546
619
 
547
620
 
548
- ## Authorization
549
-
550
- #### authorize(\*\*options)
551
-
552
- If site uses authorization you can provide credentials using this method.
553
-
554
- * options `Hash`
555
- * :type `Symbol` `:server` | `:proxy` site or proxy authorization
556
- * :user `String`
557
- * :password `String`
558
-
559
-
560
621
  ## Dialog
561
622
 
562
623
  #### accept(text)
@@ -580,34 +641,3 @@ browser.on(:dialog) do |dialog|
580
641
  end
581
642
  browser.goto("https://google.com")
582
643
  ```
583
-
584
-
585
- ## Interception
586
-
587
- #### intercept_request(\*\*options)
588
-
589
- Set request interception for given options. This method is only sets request
590
- interception, you should use `on` callback to catch it.
591
-
592
- * options `Hash`
593
- * :pattern `String` \* by default
594
- * :resource_type `Symbol` one of the [resource types](https://chromedevtools.github.io/devtools-protocol/tot/Network#type-ResourceType)
595
-
596
- #### on(event)
597
-
598
- Set callback for given event.
599
-
600
- * event `Symbol`
601
-
602
- ```ruby
603
- browser = Ferrum::Browser.new
604
- browser.intercept_request
605
- browser.on(:request_intercepted) do |request|
606
- if request.match?(/bla-bla/)
607
- request.abort
608
- else
609
- request.continue
610
- end
611
- end
612
- browser.goto("https://google.com")
613
- ```
@@ -16,15 +16,13 @@ module Ferrum
16
16
  extend Forwardable
17
17
  delegate %i[window_handle window_handles switch_to_window
18
18
  open_new_window close_window within_window page] => :targets
19
- delegate %i[goto back forward refresh status
19
+ delegate %i[goto back forward refresh
20
20
  at_css at_xpath css xpath current_url title body
21
- headers cookies network_traffic clear_network_traffic response_headers
22
- intercept_request continue_request abort_request
21
+ headers cookies network
23
22
  mouse keyboard
24
23
  screenshot pdf
25
24
  evaluate evaluate_on evaluate_async execute
26
25
  frame_url frame_title within_frame
27
- authorize
28
26
  on] => :page
29
27
 
30
28
  attr_reader :client, :process, :logger, :js_errors, :slowmo, :base_url,
@@ -82,10 +80,6 @@ module Ferrum
82
80
  raise
83
81
  end
84
82
 
85
- def clear_memory_cache
86
- page.command("Network.clearBrowserCache")
87
- end
88
-
89
83
  def reset
90
84
  @window_size = @original_window_size
91
85
  targets.reset
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ferrum/network/exchange"
4
+ require "ferrum/network/intercepted_request"
5
+
6
+ module Ferrum
7
+ class Network
8
+ CLEAR_TYPE = %i[traffic cache].freeze
9
+ AUTHORIZE_TYPE = %i[server proxy].freeze
10
+ RESOURCE_TYPES = %w[Document Stylesheet Image Media Font Script TextTrack
11
+ XHR Fetch EventSource WebSocket Manifest
12
+ SignedExchange Ping CSPViolationReport Other].freeze
13
+
14
+ attr_reader :traffic
15
+
16
+ def initialize(page)
17
+ @page = page
18
+ @traffic = []
19
+ @exchange = nil
20
+ end
21
+
22
+ def request
23
+ @exchange&.request
24
+ end
25
+
26
+ def response
27
+ @exchange&.response
28
+ end
29
+
30
+ def status
31
+ response&.status
32
+ end
33
+
34
+ def clear(type)
35
+ unless CLEAR_TYPE.include?(type)
36
+ raise ArgumentError, ":type should be in #{CLEAR_TYPE}"
37
+ end
38
+
39
+ if type == :traffic
40
+ @traffic.clear
41
+ else
42
+ @page.command("Network.clearBrowserCache")
43
+ end
44
+
45
+ true
46
+ end
47
+
48
+ def intercept(pattern: "*", resource_type: nil)
49
+ pattern = { urlPattern: pattern }
50
+ if resource_type && RESOURCE_TYPES.include?(resource_type.to_s)
51
+ pattern[:resourceType] = resource_type
52
+ end
53
+
54
+ @page.command("Network.setRequestInterception", patterns: [pattern])
55
+ end
56
+
57
+ def authorize(user:, password:, type: :server)
58
+ unless AUTHORIZE_TYPE.include?(type)
59
+ raise ArgumentError, ":type should be in #{AUTHORIZE_TYPE}"
60
+ end
61
+
62
+ @authorized_ids ||= {}
63
+ @authorized_ids[type] ||= []
64
+
65
+ intercept
66
+
67
+ @page.on(:request) do |request, index, total|
68
+ if request.auth_challenge?(type)
69
+ response = authorized_response(@authorized_ids[type],
70
+ request.interception_id,
71
+ user, password)
72
+
73
+ @authorized_ids[type] << request.interception_id
74
+ request.continue(authChallengeResponse: response)
75
+ elsif index + 1 < total
76
+ next # There are other callbacks that can handle this, skip
77
+ else
78
+ request.continue
79
+ end
80
+ end
81
+ end
82
+
83
+ def subscribe
84
+ @page.on("Network.requestWillBeSent") do |params|
85
+ # On redirects Chrome doesn't change `requestId` and there's no
86
+ # `Network.responseReceived` event for such request. If there's already
87
+ # exchange object with this id then we got redirected and params has
88
+ # `redirectResponse` key which contains the response.
89
+ if exchange = first_by(params["requestId"])
90
+ exchange.build_response(params)
91
+ end
92
+
93
+ exchange = Network::Exchange.new(params)
94
+ @exchange = exchange if exchange.navigation_request?(@page.frame_id)
95
+ @traffic << exchange
96
+ end
97
+
98
+ @page.on("Network.responseReceived") do |params|
99
+ if exchange = last_by(params["requestId"])
100
+ exchange.build_response(params)
101
+ end
102
+ end
103
+
104
+ @page.on("Network.loadingFinished") do |params|
105
+ exchange = last_by(params["requestId"])
106
+ if exchange && exchange.response
107
+ exchange.response.body_size = params["encodedDataLength"]
108
+ end
109
+ end
110
+
111
+ @page.on("Log.entryAdded") do |params|
112
+ entry = params["entry"] || {}
113
+ if entry["source"] == "network" &&
114
+ entry["level"] == "error" &&
115
+ exchange = last_by(entry["networkRequestId"])
116
+ exchange.build_error(entry)
117
+ end
118
+ end
119
+ end
120
+
121
+ def authorized_response(ids, interception_id, username, password)
122
+ if ids.include?(interception_id)
123
+ { response: "CancelAuth" }
124
+ elsif username && password
125
+ { response: "ProvideCredentials",
126
+ username: username,
127
+ password: password }
128
+ else
129
+ { response: "CancelAuth" }
130
+ end
131
+ end
132
+
133
+ def first_by(request_id)
134
+ @traffic.find { |e| e.request.id == request_id }
135
+ end
136
+
137
+ def last_by(request_id)
138
+ @traffic.select { |e| e.request.id == request_id }.last
139
+ end
140
+ end
141
+ end
@@ -1,25 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Ferrum::Network
4
- class Error
5
- def initialize(data)
6
- @data = data
7
- end
3
+ module Ferrum
4
+ class Network
5
+ class Error
6
+ def initialize(data)
7
+ @data = data
8
+ end
8
9
 
9
- def id
10
- @data["networkRequestId"]
11
- end
10
+ def id
11
+ @data["networkRequestId"]
12
+ end
12
13
 
13
- def url
14
- @data["url"]
15
- end
14
+ def url
15
+ @data["url"]
16
+ end
16
17
 
17
- def description
18
- @data["text"]
19
- end
18
+ def description
19
+ @data["text"]
20
+ end
20
21
 
21
- def time
22
- @time ||= Time.strptime(@data["timestamp"].to_s, "%s")
22
+ def time
23
+ @time ||= Time.strptime(@data["timestamp"].to_s, "%s")
24
+ end
23
25
  end
24
26
  end
25
27
  end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ferrum/network/error"
4
+ require "ferrum/network/request"
5
+ require "ferrum/network/response"
6
+
7
+ module Ferrum
8
+ class Network
9
+ class Exchange
10
+ attr_reader :request, :response, :error
11
+
12
+ def initialize(params)
13
+ @response = @error = nil
14
+ build_request(params)
15
+ end
16
+
17
+ def build_request(params)
18
+ @request = Network::Request.new(params)
19
+ end
20
+
21
+ def build_response(params)
22
+ @response = Network::Response.new(params)
23
+ end
24
+
25
+ def build_error(params)
26
+ @error = Network::Error.new(params)
27
+ end
28
+
29
+ def navigation_request?(frame_id)
30
+ request.type?(:document) &&
31
+ request.frame_id == frame_id
32
+ end
33
+
34
+ def blocked?
35
+ response.nil?
36
+ end
37
+
38
+ def to_a
39
+ [request, response, error]
40
+ end
41
+ end
42
+ end
43
+ end
@@ -1,53 +1,62 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Ferrum::Network
4
- class InterceptedRequest
5
- attr_accessor :interception_id, :frame_id, :resource_type,
6
- :is_navigation_request
7
-
8
- def initialize(page, params)
9
- @page, @params = page, params
10
- @interception_id = params["interceptionId"]
11
- @frame_id = params["frameId"]
12
- @resource_type = params["resourceType"]
13
- @is_navigation_request = params["isNavigationRequest"]
14
- @request = params.dig("request")
15
- end
16
-
17
- def auth_challenge?(source)
18
- @params.dig("authChallenge", "source")&.downcase&.to_s == source.to_s
19
- end
20
-
21
- def match?(regexp)
22
- !!url.match(regexp)
23
- end
24
-
25
- def abort
26
- @page.abort_request(interception_id)
27
- end
28
-
29
- def continue(**options)
30
- @page.continue_request(interception_id, **options)
31
- end
32
-
33
- def url
34
- @request["url"]
35
- end
36
-
37
- def method
38
- @request["method"]
39
- end
40
-
41
- def headers
42
- @request["headers"]
43
- end
44
-
45
- def initial_priority
46
- @request["initialPriority"]
47
- end
48
-
49
- def referrer_policy
50
- @request["referrerPolicy"]
3
+ module Ferrum
4
+ class Network
5
+ class InterceptedRequest
6
+ attr_accessor :interception_id, :frame_id, :resource_type
7
+
8
+ def initialize(page, params)
9
+ @page, @params = page, params
10
+ @interception_id = params["interceptionId"]
11
+ @frame_id = params["frameId"]
12
+ @resource_type = params["resourceType"]
13
+ @request = params["request"]
14
+ end
15
+
16
+ def navigation_request?
17
+ @params["isNavigationRequest"]
18
+ end
19
+
20
+ def auth_challenge?(source)
21
+ @params.dig("authChallenge", "source")&.downcase&.to_s == source.to_s
22
+ end
23
+
24
+ def match?(regexp)
25
+ !!url.match(regexp)
26
+ end
27
+
28
+ def continue(**options)
29
+ options = options.merge(interceptionId: interception_id)
30
+ @page.command("Network.continueInterceptedRequest", **options)
31
+ end
32
+
33
+ def abort
34
+ continue(errorReason: "Aborted")
35
+ end
36
+
37
+ def url
38
+ @request["url"]
39
+ end
40
+
41
+ def method
42
+ @request["method"]
43
+ end
44
+
45
+ def headers
46
+ @request["headers"]
47
+ end
48
+
49
+ def initial_priority
50
+ @request["initialPriority"]
51
+ end
52
+
53
+ def referrer_policy
54
+ @request["referrerPolicy"]
55
+ end
56
+
57
+ def inspect
58
+ %(#<#{self.class} @interception_id=#{@interception_id.inspect} @frame_id=#{@frame_id.inspect} @resource_type=#{@resource_type.inspect} @request=#{@request.inspect}>)
59
+ end
51
60
  end
52
61
  end
53
62
  end
@@ -2,32 +2,45 @@
2
2
 
3
3
  require "time"
4
4
 
5
- module Ferrum::Network
6
- class Request
7
- attr_accessor :response, :error
8
-
9
- def initialize(data)
10
- @data = data
11
- end
12
-
13
- def id
14
- @data["id"]
15
- end
16
-
17
- def url
18
- @data["url"]
19
- end
20
-
21
- def method
22
- @data["method"]
23
- end
24
-
25
- def headers
26
- @data["headers"]
27
- end
28
-
29
- def time
30
- @time ||= Time.strptime(@data["time"].to_s, "%s")
5
+ module Ferrum
6
+ class Network
7
+ class Request
8
+ def initialize(params)
9
+ @params = params
10
+ @request = @params["request"]
11
+ end
12
+
13
+ def id
14
+ @params["requestId"]
15
+ end
16
+
17
+ def type
18
+ @params["type"]
19
+ end
20
+
21
+ def type?(value)
22
+ type.downcase == value.to_s.downcase
23
+ end
24
+
25
+ def frame_id
26
+ @params["frameId"]
27
+ end
28
+
29
+ def url
30
+ @request["url"]
31
+ end
32
+
33
+ def method
34
+ @request["method"]
35
+ end
36
+
37
+ def headers
38
+ @request["headers"]
39
+ end
40
+
41
+ def time
42
+ @time ||= Time.strptime(@params["wallTime"].to_s, "%s")
43
+ end
31
44
  end
32
45
  end
33
46
  end
@@ -1,44 +1,51 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Ferrum::Network
4
- class Response
5
- attr_accessor :body_size
6
-
7
- def initialize(data)
8
- @data = data
9
- end
10
-
11
- def id
12
- @data["id"]
13
- end
14
-
15
- def url
16
- @data["url"]
17
- end
18
-
19
- def status
20
- @data["status"]
21
- end
22
-
23
- def status_text
24
- @data["statusText"]
25
- end
26
-
27
- def headers
28
- @data["headers"]
29
- end
30
-
31
- def headers_size
32
- @data["encodedDataLength"]
33
- end
34
-
35
- # FIXME: didn't check if we have it on redirect response
36
- def redirect_url
37
- @data["redirectURL"]
38
- end
39
-
40
- def content_type
41
- @content_type ||= @data.dig("headers", "contentType").sub(/;.*\z/, "")
3
+ module Ferrum
4
+ class Network
5
+ class Response
6
+ attr_reader :body_size
7
+
8
+ def initialize(params)
9
+ @params = params
10
+ @response = params["response"] || @params["redirectResponse"]
11
+ end
12
+
13
+ def id
14
+ @params["requestId"]
15
+ end
16
+
17
+ def url
18
+ @response["url"]
19
+ end
20
+
21
+ def status
22
+ @response["status"]
23
+ end
24
+
25
+ def status_text
26
+ @response["statusText"]
27
+ end
28
+
29
+ def headers
30
+ @response["headers"]
31
+ end
32
+
33
+ def headers_size
34
+ @response["encodedDataLength"]
35
+ end
36
+
37
+ def content_type
38
+ @content_type ||= @response.dig("headers", "contentType")&.sub(/;.*\z/, "")
39
+ end
40
+
41
+ # See https://crbug.com/883475
42
+ # Sometimes we never get the Network.responseReceived event.
43
+ # See https://crbug.com/764946
44
+ # `Network.loadingFinished` encodedDataLength contains both body and
45
+ # headers sizes received by wire.
46
+ def body_size=(size)
47
+ @body_size = size - headers_size
48
+ end
42
49
  end
43
50
  end
44
51
  end
@@ -5,34 +5,13 @@ require "ferrum/keyboard"
5
5
  require "ferrum/headers"
6
6
  require "ferrum/cookies"
7
7
  require "ferrum/dialog"
8
+ require "ferrum/network"
8
9
  require "ferrum/page/dom"
9
10
  require "ferrum/page/runtime"
10
11
  require "ferrum/page/frame"
11
- require "ferrum/page/net"
12
12
  require "ferrum/page/screenshot"
13
13
  require "ferrum/browser/client"
14
- require "ferrum/network/error"
15
- require "ferrum/network/request"
16
- require "ferrum/network/response"
17
- require "ferrum/network/intercepted_request"
18
-
19
- # RemoteObjectId is from a JavaScript world, and corresponds to any JavaScript
20
- # object, including JS wrappers for DOM nodes. There is a way to convert between
21
- # node ids and remote object ids (DOM.requestNode and DOM.resolveNode).
22
- #
23
- # NodeId is used for inspection, when backend tracks the node and sends updates to
24
- # the frontend. If you somehow got NodeId over protocol, backend should have
25
- # pushed to the frontend all of it's ancestors up to the Document node via
26
- # DOM.setChildNodes. After that, frontend is always kept up-to-date about anything
27
- # happening to the node.
28
- #
29
- # BackendNodeId is just a unique identifier for a node. Obtaining it does not send
30
- # any updates, for example, the node may be destroyed without any notification.
31
- # This is a way to keep a reference to the Node, when you don't necessarily want
32
- # to keep track of it. One example would be linking to the node from performance
33
- # data (e.g. relayout root node). BackendNodeId may be either resolved to
34
- # inspected node (DOM.pushNodesByBackendIdsToFrontend) or described in more
35
- # details (DOM.describeNode).
14
+
36
15
  module Ferrum
37
16
  class Page
38
17
  NEW_WINDOW_WAIT = ENV.fetch("FERRUM_NEW_WINDOW_WAIT", 0.3).to_f
@@ -51,17 +30,15 @@ module Ferrum
51
30
  end
52
31
  end
53
32
 
54
- include DOM, Runtime, Frame, Net, Screenshot
33
+ include DOM, Runtime, Frame, Screenshot
55
34
 
56
35
  attr_accessor :referrer
57
- attr_reader :target_id, :status,
58
- :headers, :cookies, :response_headers,
59
- :mouse, :keyboard,
60
- :browser
36
+ attr_reader :target_id, :browser,
37
+ :headers, :cookies, :network,
38
+ :mouse, :keyboard
61
39
 
62
40
  def initialize(target_id, browser, new_window = false)
63
41
  @target_id, @browser = target_id, browser
64
- @network_traffic = []
65
42
  @event = Event.new.tap(&:set)
66
43
 
67
44
  @frames = {}
@@ -80,6 +57,7 @@ module Ferrum
80
57
 
81
58
  @mouse, @keyboard = Mouse.new(self), Keyboard.new(self)
82
59
  @headers, @cookies = Headers.new(self), Cookies.new(self)
60
+ @network = Network.new(self)
83
61
 
84
62
  subscribe
85
63
  prepare_page
@@ -131,21 +109,6 @@ module Ferrum
131
109
  command("Page.reload", wait: timeout)
132
110
  end
133
111
 
134
- def network_traffic(type = nil)
135
- case type.to_s
136
- when "all"
137
- @network_traffic
138
- when "blocked"
139
- @network_traffic.select { |r| r.response.nil? } # when request blocked
140
- else
141
- @network_traffic.select { |r| r.response } # when request isn't blocked
142
- end
143
- end
144
-
145
- def clear_network_traffic
146
- @network_traffic = []
147
- end
148
-
149
112
  def back
150
113
  history_navigate(delta: -1)
151
114
  end
@@ -171,7 +134,7 @@ module Ferrum
171
134
  dialog = Dialog.new(self, params)
172
135
  block.call(dialog, index, total)
173
136
  end
174
- when :request_intercepted
137
+ when :request
175
138
  @client.on("Network.requestIntercepted") do |params, index, total|
176
139
  request = Network::InterceptedRequest.new(self, params)
177
140
  block.call(request, index, total)
@@ -186,87 +149,45 @@ module Ferrum
186
149
  def subscribe
187
150
  super
188
151
 
152
+ network.subscribe
153
+
154
+ on("Network.loadingFailed") do |params|
155
+ id, canceled = params.values_at("requestId", "canceled")
156
+ # Set event as we aborted main request we are waiting for
157
+ if network.request&.id == id && canceled == true
158
+ @event.set
159
+ @document_id = get_document_id
160
+ end
161
+ end
162
+
189
163
  if @browser.logger
190
- @client.on("Runtime.consoleAPICalled") do |params|
164
+ on("Runtime.consoleAPICalled") do |params|
191
165
  params["args"].each { |r| @browser.logger.puts(r["value"]) }
192
166
  end
193
167
  end
194
168
 
195
169
  if @browser.js_errors
196
- @client.on("Runtime.exceptionThrown") do |params|
170
+ on("Runtime.exceptionThrown") do |params|
197
171
  # FIXME https://jvns.ca/blog/2015/11/27/why-rubys-timeout-is-dangerous-and-thread-dot-raise-is-terrifying/
198
172
  Thread.main.raise JavaScriptError.new(params.dig("exceptionDetails", "exception"))
199
173
  end
200
174
  end
201
175
 
202
- @client.on("Page.windowOpen") do
176
+ on("Page.windowOpen") do
203
177
  @browser.targets.refresh
204
178
  end
205
179
 
206
- @client.on("Page.navigatedWithinDocument") do
180
+ on("Page.navigatedWithinDocument") do
207
181
  @event.set if @waiting_frames.empty?
208
182
  end
209
183
 
210
- @client.on("Page.domContentEventFired") do |params|
184
+ on("Page.domContentEventFired") do |params|
211
185
  # `frameStoppedLoading` doesn't occur if status isn't success
212
- if @status != 200
186
+ if network.status != 200
213
187
  @event.set
214
188
  @document_id = get_document_id
215
189
  end
216
190
  end
217
-
218
- @client.on("Network.requestWillBeSent") do |params|
219
- if params["frameId"] == @frame_id
220
- # Possible types:
221
- # Document, Stylesheet, Image, Media, Font, Script, TextTrack, XHR,
222
- # Fetch, EventSource, WebSocket, Manifest, SignedExchange, Ping,
223
- # CSPViolationReport, Other
224
- if params["type"] == "Document"
225
- @event.reset
226
- @request_id = params["requestId"]
227
- end
228
- end
229
-
230
- id, time = params.values_at("requestId", "wallTime")
231
- params = params["request"].merge("id" => id, "time" => time)
232
- @network_traffic << Network::Request.new(params)
233
- end
234
-
235
- @client.on("Network.responseReceived") do |params|
236
- if params["requestId"] == @request_id
237
- @response_headers = params.dig("response", "headers")
238
- @status = params.dig("response", "status")
239
- end
240
-
241
- if request = @network_traffic.find { |r| r.id == params["requestId"] }
242
- params = params["response"].merge("id" => params["requestId"])
243
- request.response = Network::Response.new(params)
244
- end
245
- end
246
-
247
- @client.on("Network.loadingFinished") do |params|
248
- if request = @network_traffic.find { |r| r.id == params["requestId"] }
249
- # Sometimes we never get the Network.responseReceived event.
250
- # See https://crbug.com/883475
251
- #
252
- # Network.loadingFinished's encodedDataLength contains both body and headers
253
- # sizes received by wire. See https://crbug.com/764946
254
- if response = request.response
255
- response.body_size = params["encodedDataLength"] - response.headers_size
256
- end
257
- end
258
- end
259
-
260
- @client.on("Log.entryAdded") do |params|
261
- source = params.dig("entry", "source")
262
- level = params.dig("entry", "level")
263
- if source == "network" && level == "error"
264
- id = params.dig("entry", "networkRequestId")
265
- if request = @network_traffic.find { |r| r.id == id }
266
- request.error = Network::Error.new(params["entry"])
267
- end
268
- end
269
- end
270
191
  end
271
192
 
272
193
  def prepare_page
@@ -1,5 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # RemoteObjectId is from a JavaScript world, and corresponds to any JavaScript
4
+ # object, including JS wrappers for DOM nodes. There is a way to convert between
5
+ # node ids and remote object ids (DOM.requestNode and DOM.resolveNode).
6
+ #
7
+ # NodeId is used for inspection, when backend tracks the node and sends updates to
8
+ # the frontend. If you somehow got NodeId over protocol, backend should have
9
+ # pushed to the frontend all of it's ancestors up to the Document node via
10
+ # DOM.setChildNodes. After that, frontend is always kept up-to-date about anything
11
+ # happening to the node.
12
+ #
13
+ # BackendNodeId is just a unique identifier for a node. Obtaining it does not send
14
+ # any updates, for example, the node may be destroyed without any notification.
15
+ # This is a way to keep a reference to the Node, when you don't necessarily want
16
+ # to keep track of it. One example would be linking to the node from performance
17
+ # data (e.g. relayout root node). BackendNodeId may be either resolved to
18
+ # inspected node (DOM.pushNodesByBackendIdsToFrontend) or described in more
19
+ # details (DOM.describeNode).
3
20
  module Ferrum
4
21
  class Page
5
22
  module DOM
@@ -3,6 +3,8 @@
3
3
  module Ferrum
4
4
  class Page
5
5
  module Frame
6
+ attr_reader :frame_id
7
+
6
8
  def execution_context_id
7
9
  context_id = current_execution_context_id
8
10
  raise NoExecutionContextError unless context_id
@@ -42,28 +44,28 @@ module Ferrum
42
44
  def subscribe
43
45
  super if defined?(super)
44
46
 
45
- @client.on("Page.frameAttached") do |params|
47
+ on("Page.frameAttached") do |params|
46
48
  @frames[params["frameId"]] = { "parent_id" => params["parentFrameId"] }
47
49
  end
48
50
 
49
- @client.on("Page.frameStartedLoading") do |params|
51
+ on("Page.frameStartedLoading") do |params|
50
52
  @waiting_frames << params["frameId"]
51
53
  @event.reset
52
54
  end
53
55
 
54
- @client.on("Page.frameNavigated") do |params|
56
+ on("Page.frameNavigated") do |params|
55
57
  id = params["frame"]["id"]
56
58
  if frame = @frames[id]
57
59
  frame.merge!(params["frame"].select { |k, _| k == "name" || k == "url" })
58
60
  end
59
61
  end
60
62
 
61
- @client.on("Page.frameScheduledNavigation") do |params|
63
+ on("Page.frameScheduledNavigation") do |params|
62
64
  @waiting_frames << params["frameId"]
63
65
  @event.reset
64
66
  end
65
67
 
66
- @client.on("Page.frameStoppedLoading") do |params|
68
+ on("Page.frameStoppedLoading") do |params|
67
69
  # `DOM.performSearch` doesn't work without getting #document node first.
68
70
  # It returns node with nodeId 1 and nodeType 9 from which descend the
69
71
  # tree and we save it in a variable because if we call that again root
@@ -79,7 +81,17 @@ module Ferrum
79
81
  end
80
82
  end
81
83
 
82
- @client.on("Runtime.executionContextCreated") do |params|
84
+ on("Network.requestWillBeSent") do |params|
85
+ if params["frameId"] == @frame_id
86
+ # Possible types:
87
+ # Document, Stylesheet, Image, Media, Font, Script, TextTrack, XHR,
88
+ # Fetch, EventSource, WebSocket, Manifest, SignedExchange, Ping,
89
+ # CSPViolationReport, Other
90
+ @event.reset if params["type"] == "Document"
91
+ end
92
+ end
93
+
94
+ on("Runtime.executionContextCreated") do |params|
83
95
  context_id = params.dig("context", "id")
84
96
  @execution_context_id ||= context_id
85
97
 
@@ -93,7 +105,7 @@ module Ferrum
93
105
  end
94
106
  end
95
107
 
96
- @client.on("Runtime.executionContextDestroyed") do |params|
108
+ on("Runtime.executionContextDestroyed") do |params|
97
109
  context_id = params["executionContextId"]
98
110
 
99
111
  if @execution_context_id == context_id
@@ -104,7 +116,7 @@ module Ferrum
104
116
  frame["execution_context_id"] = nil if frame
105
117
  end
106
118
 
107
- @client.on("Runtime.executionContextsCleared") do
119
+ on("Runtime.executionContextsCleared") do
108
120
  # If we didn't have time to set context id at the beginning we have
109
121
  # to set lock and release it when we set something.
110
122
  @execution_context_id = nil
@@ -23,17 +23,13 @@ module Ferrum
23
23
 
24
24
  def save_file(path, data)
25
25
  bin = Base64.decode64(data)
26
+ return bin unless path
26
27
  File.open(path.to_s, "wb") { |f| f.write(bin) }
27
28
  end
28
29
 
29
30
  def common_options(encoding: :base64, path: nil, **_)
30
31
  encoding = encoding.to_sym
31
32
  encoding = :binary if path
32
-
33
- if encoding == :binary && !path
34
- raise "You have to provide `:path` for `:binary` encoding"
35
- end
36
-
37
33
  [path, encoding]
38
34
  end
39
35
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ferrum
4
- VERSION = "0.3"
4
+ VERSION = "0.4"
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.3'
4
+ version: '0.4'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Vorotilin
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-09-12 00:00:00.000000000 Z
11
+ date: 2019-09-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: websocket-driver
@@ -205,7 +205,9 @@ files:
205
205
  - lib/ferrum/keyboard.json
206
206
  - lib/ferrum/keyboard.rb
207
207
  - lib/ferrum/mouse.rb
208
+ - lib/ferrum/network.rb
208
209
  - lib/ferrum/network/error.rb
210
+ - lib/ferrum/network/exchange.rb
209
211
  - lib/ferrum/network/intercepted_request.rb
210
212
  - lib/ferrum/network/request.rb
211
213
  - lib/ferrum/network/response.rb
@@ -213,7 +215,6 @@ files:
213
215
  - lib/ferrum/page.rb
214
216
  - lib/ferrum/page/dom.rb
215
217
  - lib/ferrum/page/frame.rb
216
- - lib/ferrum/page/net.rb
217
218
  - lib/ferrum/page/runtime.rb
218
219
  - lib/ferrum/page/screenshot.rb
219
220
  - lib/ferrum/targets.rb
@@ -1,83 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ferrum
4
- class Page
5
- module Net
6
- AUTHORIZE_TYPE = %i[server proxy]
7
- RESOURCE_TYPES = %w[Document Stylesheet Image Media Font Script TextTrack
8
- XHR Fetch EventSource WebSocket Manifest
9
- SignedExchange Ping CSPViolationReport Other]
10
-
11
- def authorize(user:, password:, type: :server)
12
- unless AUTHORIZE_TYPE.include?(type)
13
- raise ArgumentError, ":type should be in #{AUTHORIZE_TYPE}"
14
- end
15
-
16
- @authorized_ids ||= {}
17
- @authorized_ids[type] ||= []
18
-
19
- intercept_request
20
-
21
- on(:request_intercepted) do |request, index, total|
22
- if request.auth_challenge?(type)
23
- response = authorized_response(@authorized_ids[type],
24
- request.interception_id,
25
- user, password)
26
-
27
- @authorized_ids[type] << request.interception_id
28
- request.continue(authChallengeResponse: response)
29
- elsif index + 1 < total
30
- next # There are other callbacks that can handle this, skip
31
- else
32
- request.continue
33
- end
34
- end
35
- end
36
-
37
- def intercept_request(pattern: "*", resource_type: nil)
38
- pattern = { urlPattern: pattern }
39
- if resource_type && RESOURCE_TYPES.include?(resource_type.to_s)
40
- pattern[:resourceType] = resource_type
41
- end
42
-
43
- command("Network.setRequestInterception", patterns: [pattern])
44
- end
45
-
46
- def continue_request(interception_id, options = nil)
47
- options ||= {}
48
- options = options.merge(interceptionId: interception_id)
49
- command("Network.continueInterceptedRequest", **options)
50
- end
51
-
52
- def abort_request(interception_id)
53
- continue_request(interception_id, errorReason: "Aborted")
54
- end
55
-
56
- private
57
-
58
- def subscribe
59
- super if defined?(super)
60
-
61
- @client.on("Network.loadingFailed") do |params|
62
- # Free mutex as we aborted main request we are waiting for
63
- if params["requestId"] == @request_id && params["canceled"] == true
64
- @event.set
65
- @document_id = get_document_id
66
- end
67
- end
68
- end
69
-
70
- def authorized_response(ids, interception_id, username, password)
71
- if ids.include?(interception_id)
72
- { response: "CancelAuth" }
73
- elsif username && password
74
- { response: "ProvideCredentials",
75
- username: username,
76
- password: password }
77
- else
78
- { response: "CancelAuth" }
79
- end
80
- end
81
- end
82
- end
83
- end