ferrum 0.3 → 0.4

Sign up to get free protection for your applications and to get access to all the features.
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