ferrum 0.1.2 → 0.2

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.
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ferrum
4
+ class Mouse
5
+ VALID_BUTTONS = %w[none left middle right back forward].freeze
6
+
7
+ def initialize(page)
8
+ @page = page
9
+ @x = @y = 0
10
+ end
11
+
12
+ def click(x:, y:, delay: 0, timeout: 0, **options)
13
+ move(x: x, y: y)
14
+ down(**options)
15
+ sleep(delay)
16
+ # Potential wait because if network event is triggered then we have to wait until it's over.
17
+ up(timeout: timeout, **options)
18
+ self
19
+ end
20
+
21
+ def down(**options)
22
+ tap { mouse_event(type: "mousePressed", **options) }
23
+ end
24
+
25
+ def up(**options)
26
+ tap { mouse_event(type: "mouseReleased", **options) }
27
+ end
28
+
29
+ # FIXME: steps
30
+ def move(x:, y:, steps: 1)
31
+ @x, @y = x, y
32
+ @page.command("Input.dispatchMouseEvent", type: "mouseMoved", x: @x, y: @y)
33
+ self
34
+ end
35
+
36
+ private
37
+
38
+ def mouse_event(type:, button: :left, count: 1, modifiers: nil, timeout: 0)
39
+ button = validate_button(button)
40
+ options = { x: @x, y: @y, type: type, button: button, clickCount: count }
41
+ options.merge!(modifiers: modifiers) if modifiers
42
+ @page.command("Input.dispatchMouseEvent", timeout: timeout, **options)
43
+ end
44
+
45
+ def validate_button(button)
46
+ button = button.to_s
47
+ unless VALID_BUTTONS.include?(button)
48
+ raise "Invalid button: #{button}"
49
+ end
50
+ button
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
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"]
51
+ end
52
+ end
53
+ end
@@ -2,153 +2,99 @@
2
2
 
3
3
  module Ferrum
4
4
  class Node
5
- attr_reader :page, :target_id, :node_id, :description
5
+ attr_reader :page, :target_id, :node_id, :description, :tag_name
6
6
 
7
7
  def initialize(page, target_id, node_id, description)
8
- @page, @target_id, @node_id, @description =
9
- page, target_id, node_id, description
8
+ @page, @target_id = page, target_id
9
+ @node_id, @description = node_id, description
10
+ @tag_name = description["nodeName"].downcase
10
11
  end
11
12
 
12
13
  def node?
13
14
  description["nodeType"] == 1 # nodeType: 3, nodeName: "#text" e.g.
14
15
  end
15
16
 
16
- def page_send(name, *args)
17
- page.send(name, self, *args)
18
- rescue BrowserError => e
19
- case e.message
20
- when "No node with given id found"
21
- raise ObsoleteNode.new(self, e.response)
22
- else
23
- raise
24
- end
17
+ def focus
18
+ tap { page.command("DOM.focus", nodeId: node_id) }
25
19
  end
26
20
 
27
- def at_xpath(selector)
28
- page.at_xpath(selector, within: self)
21
+ def blur
22
+ tap { evaluate("this.blur()") }
29
23
  end
30
24
 
31
- def at_css(selector)
32
- page.at_css(selector, within: self)
25
+ def type(*keys)
26
+ tap { page.keyboard.type(*keys) }
33
27
  end
34
28
 
35
- def xpath(selector)
36
- page.xpath(selector, within: self)
37
- end
38
-
39
- def css(selector)
40
- page.css(selector, within: self)
41
- end
29
+ # mode: (:left | :right | :double)
30
+ # keys: (:alt, (:ctrl | :control), (:meta | :command), :shift)
31
+ # offset: { :x, :y }
32
+ def click(mode: :left, keys: [], offset: {})
33
+ x, y = page.find_position(self, offset[:x], offset[:y])
34
+ modifiers = page.keyboard.modifiers(keys)
42
35
 
43
- def text
44
- page.evaluate_on(node: self, expression: "this.textContent")
45
- end
46
-
47
- def property(name)
48
- page_send(:property, name)
49
- end
50
-
51
- def [](name)
52
- # Although the attribute matters, the property is consistent. Return that in
53
- # preference to the attribute for links and images.
54
- if ((tag_name == "img") && (name == "src")) || ((tag_name == "a") && (name == "href"))
55
- # if attribute exists get the property
56
- return page_send(:attribute, name) && page_send(:property, name)
36
+ case mode
37
+ when :right
38
+ page.mouse.move(x: x, y: y)
39
+ page.mouse.down(button: :right, modifiers: modifiers)
40
+ page.mouse.up(button: :right, modifiers: modifiers)
41
+ when :double
42
+ page.mouse.move(x: x, y: y)
43
+ page.mouse.down(modifiers: modifiers, count: 2)
44
+ page.mouse.up(modifiers: modifiers, count: 2)
45
+ when :left
46
+ page.mouse.click(x: x, y: y, modifiers: modifiers, timeout: 0.05)
57
47
  end
58
48
 
59
- value = property(name)
60
- value = page_send(:attribute, name) if value.nil? || value.is_a?(Hash)
61
-
62
- value
63
- end
64
-
65
- def attributes
66
- page_send(:attributes)
67
- end
68
-
69
- def value
70
- page.evaluate_on(node: self, expression: "this.value")
71
- end
72
-
73
- def set(value)
74
- if tag_name == "input"
75
- case self[:type]
76
- when "radio"
77
- click
78
- when "checkbox"
79
- click if value != checked?
80
- when "file"
81
- files = value.respond_to?(:to_ary) ? value.to_ary.map(&:to_s) : value.to_s
82
- page_send(:select_file, files)
83
- else
84
- page_send(:set, value.to_s)
85
- end
86
- elsif tag_name == "textarea"
87
- page_send(:set, value.to_s)
88
- elsif self[:isContentEditable]
89
- # FIXME:
90
- page_send(:delete_text)
91
- send_keys(value.to_s)
92
- end
93
- end
94
-
95
- def select_option
96
- page_send(:select, true)
49
+ self
97
50
  end
98
51
 
99
- def unselect_option
100
- raise NotImplemented
52
+ def hover
53
+ raise NotImplementedError
101
54
  end
102
55
 
103
- def tag_name
104
- @tag_name ||= description["nodeName"].downcase
56
+ def trigger(event)
57
+ raise NotImplementedError
105
58
  end
106
59
 
107
- def visible?
108
- page_send(:visible?)
60
+ def select_file(value)
61
+ page.command("DOM.setFileInputFiles", nodeId: node_id, files: Array(value))
109
62
  end
110
63
 
111
- def checked?
112
- self[:checked]
64
+ def at_xpath(selector)
65
+ page.at_xpath(selector, within: self)
113
66
  end
114
67
 
115
- def selected?
116
- !!self[:selected]
68
+ def at_css(selector)
69
+ page.at_css(selector, within: self)
117
70
  end
118
71
 
119
- def disabled?
120
- page_send(:disabled?)
72
+ def xpath(selector)
73
+ page.xpath(selector, within: self)
121
74
  end
122
75
 
123
- def click(keys = [], offset = {})
124
- page_send(:click, keys, offset)
76
+ def css(selector)
77
+ page.css(selector, within: self)
125
78
  end
126
79
 
127
- def right_click(keys = [], offset = {})
128
- page_send(:right_click, keys, offset)
80
+ def text
81
+ evaluate("this.textContent")
129
82
  end
130
83
 
131
- def double_click(keys = [], offset = {})
132
- page_send(:double_click, keys, offset)
84
+ def value
85
+ evaluate("this.value")
133
86
  end
134
87
 
135
- def hover
136
- page_send(:hover)
88
+ def property(name)
89
+ evaluate("this['#{name}']")
137
90
  end
138
91
 
139
- def trigger(event)
140
- page_send(:trigger, event)
92
+ def attribute(name)
93
+ evaluate("this.getAttribute('#{name}')")
141
94
  end
142
95
 
143
- def scroll_to(element, location, position = nil)
144
- if element.is_a?(Node)
145
- scroll_element_to_location(element, location)
146
- elsif location.is_a?(Symbol)
147
- scroll_to_location(location)
148
- else
149
- scroll_to_coords(*position)
150
- end
151
- self
96
+ def evaluate(expression)
97
+ page.evaluate_on(node: self, expression: expression)
152
98
  end
153
99
 
154
100
  def ==(other)
@@ -159,15 +105,6 @@ module Ferrum
159
105
  target_id == other.target_id && description["backendNodeId"] == other.description["backendNodeId"]
160
106
  end
161
107
 
162
- def send_keys(*keys)
163
- page_send(:send_keys, keys)
164
- end
165
- alias_method :send_key, :send_keys
166
-
167
- def path
168
- page_send(:path)
169
- end
170
-
171
108
  def inspect
172
109
  %(#<#{self.class} @target_id=#{@target_id.inspect} @node_id=#{@node_id} @description=#{@description.inspect}>)
173
110
  end
@@ -1,14 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "ferrum/mouse"
4
+ require "ferrum/keyboard"
5
+ require "ferrum/headers"
6
+ require "ferrum/cookies"
3
7
  require "ferrum/page/dom"
4
8
  require "ferrum/page/input"
5
9
  require "ferrum/page/runtime"
6
10
  require "ferrum/page/frame"
7
11
  require "ferrum/page/net"
12
+ require "ferrum/page/screenshot"
8
13
  require "ferrum/browser/client"
9
14
  require "ferrum/network/error"
10
15
  require "ferrum/network/request"
11
16
  require "ferrum/network/response"
17
+ require "ferrum/network/intercepted_request"
12
18
 
13
19
  # RemoteObjectId is from a JavaScript world, and corresponds to any JavaScript
14
20
  # object, including JS wrappers for DOM nodes. There is a way to convert between
@@ -31,10 +37,13 @@ module Ferrum
31
37
  class Page
32
38
  NEW_WINDOW_BUG_SLEEP = 0.3
33
39
 
34
- include Input, DOM, Runtime, Frame, Net
40
+ include Input, DOM, Runtime, Frame, Net, Screenshot
35
41
 
36
42
  attr_accessor :referrer
37
- attr_reader :target_id, :status, :response_headers
43
+ attr_reader :target_id, :status,
44
+ :headers, :cookies, :response_headers,
45
+ :mouse, :keyboard,
46
+ :browser
38
47
 
39
48
  def initialize(target_id, browser, new_window = false)
40
49
  @target_id, @browser = target_id, browser
@@ -50,22 +59,16 @@ module Ferrum
50
59
  # Dirty hack because new window doesn't have events at all
51
60
  sleep(NEW_WINDOW_BUG_SLEEP) if new_window
52
61
 
53
- begin
54
- @session_id = @browser.command("Target.attachToTarget", targetId: @target_id)["sessionId"]
55
- rescue BrowserError => e
56
- case e.message
57
- when "No target with given id found"
58
- raise NoSuchWindowError
59
- else
60
- raise
61
- end
62
- end
62
+ @session_id = @browser.command("Target.attachToTarget", targetId: @target_id)["sessionId"]
63
63
 
64
64
  host = @browser.process.host
65
65
  port = @browser.process.port
66
66
  ws_url = "ws://#{host}:#{port}/devtools/page/#{@target_id}"
67
67
  @client = Browser::Client.new(browser, ws_url, 1000)
68
68
 
69
+ @mouse, @keyboard = Mouse.new(self), Keyboard.new(self)
70
+ @headers, @cookies = Headers.new(self), Cookies.new(self)
71
+
69
72
  subscribe
70
73
  prepare_page
71
74
  end
@@ -83,12 +86,13 @@ module Ferrum
83
86
  net::ERR_NAME_RESOLUTION_FAILED
84
87
  net::ERR_INTERNET_DISCONNECTED
85
88
  net::ERR_CONNECTION_TIMED_OUT].include?(response["errorText"])
86
- raise StatusFailError, "url" => options[:url]
89
+ raise StatusError, options[:url]
87
90
  end
88
91
  response["frameId"]
89
92
  end
90
93
 
91
94
  def close
95
+ @headers.clear
92
96
  @browser.command("Target.detachFromTarget", sessionId: @session_id)
93
97
  @browser.command("Target.closeTarget", targetId: @target_id)
94
98
  close_connection
@@ -130,12 +134,12 @@ module Ferrum
130
134
  @network_traffic = []
131
135
  end
132
136
 
133
- def go_back
134
- go(-1)
137
+ def back
138
+ history_navigate(delta: -1)
135
139
  end
136
140
 
137
- def go_forward
138
- go(1)
141
+ def forward
142
+ history_navigate(delta: 1)
139
143
  end
140
144
 
141
145
  def accept_confirm
@@ -165,8 +169,8 @@ module Ferrum
165
169
 
166
170
  begin
167
171
  modal_text = @modal_messages.shift
168
- raise ModalNotFound if modal_text.nil? || (expect_text && !modal_text.match(expect_regexp))
169
- rescue ModalNotFound => e
172
+ raise ModalNotFoundError if modal_text.nil? || (expect_text && !modal_text.match(expect_regexp))
173
+ rescue ModalNotFoundError => e
170
174
  raise e, not_found_msg if (::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - start_time) >= timeout_sec
171
175
  sleep(0.05)
172
176
  retry
@@ -217,13 +221,13 @@ module Ferrum
217
221
  options = { accept: accept_modal }
218
222
  response = @modal_response || params["defaultPrompt"]
219
223
  options.merge!(promptText: response) if response
220
- @client.command("Page.handleJavaScriptDialog", **options)
224
+ command("Page.handleJavaScriptDialog", **options)
221
225
  else
222
226
  warn "Modal window has been opened, but you didn't wrap your code into (`accept_prompt` | `dismiss_prompt` | `accept_confirm` | `dismiss_confirm` | `accept_alert`), accepting by default"
223
227
  options = { accept: true }
224
228
  response = params["defaultPrompt"]
225
229
  options.merge!(promptText: response) if response
226
- @client.command("Page.handleJavaScriptDialog", **options)
230
+ command("Page.handleJavaScriptDialog", **options)
227
231
  end
228
232
  end
229
233
 
@@ -310,7 +314,7 @@ module Ferrum
310
314
  end
311
315
 
312
316
  @browser.extensions.each do |extension|
313
- @client.command("Page.addScriptToEvaluateOnNewDocument", source: extension)
317
+ command("Page.addScriptToEvaluateOnNewDocument", source: extension)
314
318
  end
315
319
 
316
320
  inject_extensions
@@ -318,10 +322,6 @@ module Ferrum
318
322
  width, height = @browser.window_size
319
323
  resize(width: width, height: height)
320
324
 
321
- url_whitelist = Array(@browser.url_whitelist)
322
- url_blacklist = Array(@browser.url_blacklist)
323
- intercept_request("*") if !url_whitelist.empty? || !url_blacklist.empty?
324
-
325
325
  response = command("Page.getNavigationHistory")
326
326
  if response.dig("entries", 0, "transitionType") != "typed"
327
327
  # If we create page by clicking links, submiting forms and so on it
@@ -340,13 +340,13 @@ module Ferrum
340
340
  # https://github.com/cyrus-and/chrome-remote-interface/issues/319
341
341
  # We also evaluate script just in case because
342
342
  # `Page.addScriptToEvaluateOnNewDocument` doesn't work in popups.
343
- @client.command("Runtime.evaluate", expression: extension,
344
- contextId: execution_context_id,
345
- returnByValue: true)
343
+ command("Runtime.evaluate", expression: extension,
344
+ contextId: execution_context_id,
345
+ returnByValue: true)
346
346
  end
347
347
  end
348
348
 
349
- def go(delta)
349
+ def history_navigate(delta:)
350
350
  history = command("Page.getNavigationHistory")
351
351
  index, entries = history.values_at("currentIndex", "entries")
352
352