cuprite 0.14.1 → 0.14.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ba6b2d59f81358541ceedf4d5897c776ec4e36f2749894ec989b7e00d239f5dc
4
- data.tar.gz: 8c387e1473c47a21ae3f21641f9a9d63084c4ed042b94c07fcc9376f568f28d6
3
+ metadata.gz: 4146607eaf82561d128b2f4501f688b3104fae6ce4f70ec6350b24518a85ad23
4
+ data.tar.gz: 71c489599d22691f4270f1f87a85885a7bddd15bc0bfe0baa3acc2e8b13d8ce0
5
5
  SHA512:
6
- metadata.gz: 736a95aefb32b8704be3c44f3b0e22ebae5ea7f17b61a235bf9093618093c2edb3693c831be11d7d58d1f16429a1472ea73d6e7a8228ec395b7b808471ad410b
7
- data.tar.gz: f3ecd0e01f4cb54c9e1623a30c50c369c526e19b57d4157a84a6368ae9db722000323fdd011d3375186fdac7e9307727c5a15203b10b9c8a5ae29f4e18755dc0
6
+ metadata.gz: 503980e052b9a720b1e65d714d1b3ddfaa7f3c4d821c3044f4dbe397f4b0b6e18058b620b43c2095e04520f8f41eb23694f61d4a22f31a8a3617773a886552f0
7
+ data.tar.gz: ce6d5ec7d83c8e9c4036971e3d97075b199042825317d725a0f1ba9b5b4b6d77296d7c18299f8d437e25bd246c429467723bdecb8ec21ac866a6dc394ee6bb97
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018-2022 Dmitry Vorotilin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,212 @@
1
+ # Cuprite - Headless Chrome driver for Capybara
2
+
3
+ Cuprite is a pure Ruby driver (read as _no_ Selenium/WebDriver/ChromeDriver
4
+ dependency) for [Capybara](https://github.com/teamcapybara/capybara). It allows
5
+ you to run Capybara tests on a headless Chrome or Chromium. Under the hood it
6
+ uses [Ferrum](https://github.com/rubycdp/ferrum#index) which is high-level API
7
+ to the browser by CDP protocol. The design of the driver is as close to
8
+ [Poltergeist](https://github.com/teampoltergeist/poltergeist) as possible though
9
+ it's not a goal.
10
+
11
+
12
+ ## Install
13
+
14
+ Add this to your `Gemfile` and run `bundle install`.
15
+
16
+ ``` ruby
17
+ group :test do
18
+ gem "cuprite"
19
+ end
20
+ ```
21
+
22
+ In your test setup add:
23
+
24
+ ``` ruby
25
+ require "capybara/cuprite"
26
+ Capybara.javascript_driver = :cuprite
27
+ Capybara.register_driver(:cuprite) do |app|
28
+ Capybara::Cuprite::Driver.new(app, window_size: [1200, 800])
29
+ end
30
+ ```
31
+
32
+ if you use `Docker` don't forget to pass `no-sandbox` option:
33
+
34
+ ```ruby
35
+ Capybara::Cuprite::Driver.new(app, browser_options: { 'no-sandbox': nil })
36
+ ```
37
+
38
+ Since Cuprite uses [Ferrum](https://github.com/rubycdp/ferrum#examples) there
39
+ are many useful methods you can call even using this driver:
40
+
41
+ ```ruby
42
+ browser = page.driver.browser
43
+ browser.mouse.move(x: 123, y: 456).down.up
44
+ ```
45
+
46
+ If you already have tests on Poltergeist then it should simply work, for
47
+ Selenium you better check your code for `manage` calls because it works
48
+ differently in Cuprite, see the documentation below.
49
+
50
+
51
+ ## Customization
52
+
53
+ See the full list of options for
54
+ [Ferrum](https://github.com/rubycdp/ferrum#customization).
55
+
56
+ You can pass options with the following code in your test setup:
57
+
58
+ ``` ruby
59
+ Capybara.register_driver(:cuprite) do |app|
60
+ Capybara::Cuprite::Driver.new(app, options)
61
+ end
62
+ ```
63
+
64
+ `Cuprite`-specific options are:
65
+
66
+ * options `Hash`
67
+ * `:url_blacklist` (Array) - array of strings to match against requested URLs
68
+ * `:url_whitelist` (Array) - array of strings to match against requested URLs
69
+
70
+
71
+ ## Debugging
72
+
73
+ If you pass `inspector` option, remote debugging will be enabled if you run
74
+ tests with `INSPECTOR=true`. Then you can put `page.driver.debug` or
75
+ `page.driver.debug(binding)` in your test to pause it. This will launch the
76
+ browser where you can inspect the content.
77
+
78
+ ```ruby
79
+ Capybara.register_driver :cuprite do |app|
80
+ Capybara::Cuprite::Driver.new(app, inspector: ENV['INSPECTOR'])
81
+ end
82
+ ```
83
+
84
+ then somewhere in the test:
85
+
86
+ ```ruby
87
+ it "does something useful" do
88
+ visit root_path
89
+
90
+ fill_in "field", with: "value"
91
+ page.driver.debug(binding)
92
+
93
+ expect(page).to have_content("value")
94
+ end
95
+ ```
96
+
97
+ In the middle of the execution Chrome will open a new tab where you can inspect
98
+ the content and also if you passed `binding` an `irb` or `pry` console will be
99
+ opened where you can further experiment with the test.
100
+
101
+
102
+ ## Clicking/Scrolling
103
+
104
+ * `page.driver.click(x, y)` Click a very specific area of the screen.
105
+ * `page.driver.scroll_to(left, top)` Scroll to a given position.
106
+ * `element.send_keys(*keys)` Send keys to a given node.
107
+
108
+
109
+ ## Request headers
110
+
111
+ Manipulate HTTP request headers like a boss:
112
+
113
+ ``` ruby
114
+ page.driver.headers # => {}
115
+ page.driver.headers = { "User-Agent" => "Cuprite" }
116
+ page.driver.add_headers("Referer" => "https://example.com")
117
+ page.driver.headers # => { "User-Agent" => "Cuprite", "Referer" => "https://example.com" }
118
+ ```
119
+
120
+ Notice that `headers=` will overwrite already set headers. You should use
121
+ `add_headers` if you want to add a few more. These headers will apply to all
122
+ subsequent HTTP requests (including requests for assets, AJAX, etc). They will
123
+ be automatically cleared at the end of the test.
124
+
125
+
126
+ ## Network traffic
127
+
128
+ * `page.driver.network_traffic` Inspect network traffic (loaded resources) on
129
+ the current page. This returns an array of request objects.
130
+
131
+ ```ruby
132
+ page.driver.network_traffic # => [Request, ...]
133
+ request = page.driver.network_traffic.first
134
+ request.response
135
+ ```
136
+
137
+ * `page.driver.wait_for_network_idle` Natively waits for network idle and if
138
+ there are no active connections returns or raises `TimeoutError` error. Accepts
139
+ the same options as
140
+ [`wait_for_idle`](https://github.com/rubycdp/ferrum#wait_for_idleoptions)
141
+
142
+ ```ruby
143
+ page.driver.wait_for_network_idle
144
+ page.driver.refresh
145
+ ```
146
+
147
+ Please note that network traffic is not cleared when you visit new page. You can
148
+ manually clear the network traffic by calling `page.driver.clear_network_traffic`
149
+ or `page.driver.reset`
150
+
151
+ * `page.driver.wait_for_reload` unlike `wait_for_network_idle` will wait until
152
+ the whole page is reloaded or raise a timeout error. It's useful when you know
153
+ that for example after clicking autocomplete suggestion you expect page to be
154
+ reloaded, you have a few choices - put sleep or wait for network idle, but both
155
+ are bad. Sleep makes you wait longer or less than needed, network idle can
156
+ return earlier even before the whole page is started to reload. Here's the
157
+ rescue.
158
+
159
+
160
+ ## Manipulating cookies
161
+
162
+ The following methods are used to inspect and manipulate cookies:
163
+
164
+ * `page.driver.cookies` - a hash of cookies accessible to the current
165
+ page. The keys are cookie names. The values are `Cookie` objects, with
166
+ the following methods: `name`, `value`, `domain`, `path`, `size`, `secure?`,
167
+ `httponly?`, `session?`, `expires`.
168
+ * `page.driver.set_cookie(name, value, options = {})` - set a cookie.
169
+ The options hash can take the following keys: `:domain`, `:path`,
170
+ `:secure`, `:httponly`, `:expires`. `:expires` should be a
171
+ `Time` object.
172
+ * `page.driver.remove_cookie(name)` - remove a cookie
173
+ * `page.driver.clear_cookies` - clear all cookies
174
+
175
+
176
+ ## Screenshot
177
+
178
+ Besides capybara screenshot method you can get image as Base64:
179
+
180
+ * `page.driver.render_base64(format, options)`
181
+
182
+
183
+ ## Authorization
184
+
185
+ * `page.driver.basic_authorize(user, password)`
186
+ * `page.driver.set_proxy(ip, port, type, user, password)`
187
+
188
+
189
+ ## URL Blacklisting & Whitelisting
190
+
191
+ Cuprite supports URL blacklisting, which allows you to prevent scripts from
192
+ running on designated domains:
193
+
194
+ ```ruby
195
+ page.driver.browser.url_blacklist = ["http://www.example.com"]
196
+ ```
197
+
198
+ and also URL whitelisting, which allows scripts to only run on designated
199
+ domains:
200
+
201
+ ```ruby
202
+ page.driver.browser.url_whitelist = ["http://www.example.com"]
203
+ ```
204
+
205
+ If you are experiencing slower run times, consider creating a URL whitelist of
206
+ domains that are essential or a blacklist of domains that are not essential,
207
+ such as ad networks or analytics, to your testing environment.
208
+
209
+ ## License
210
+
211
+ The gem is available as open source under the terms of the
212
+ [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module Capybara
6
+ module Cuprite
7
+ class Browser < Ferrum::Browser
8
+ extend Forwardable
9
+
10
+ delegate %i[send_keys select set hover trigger before_click switch_to_frame
11
+ find_modal accept_confirm dismiss_confirm accept_prompt
12
+ dismiss_prompt reset_modals] => :page
13
+
14
+ attr_reader :url_blacklist, :url_whitelist
15
+
16
+ def initialize(options = nil)
17
+ options ||= {}
18
+ @client = nil
19
+ self.url_blacklist = options[:url_blacklist]
20
+ self.url_whitelist = options[:url_whitelist]
21
+
22
+ super
23
+ @page = false
24
+ end
25
+
26
+ def page
27
+ raise Ferrum::NoSuchPageError if @page.nil?
28
+
29
+ @page ||= attach_page
30
+ end
31
+
32
+ def reset
33
+ super
34
+ @page = attach_page
35
+ end
36
+
37
+ def quit
38
+ super
39
+ @page = false
40
+ end
41
+
42
+ def url_whitelist=(patterns)
43
+ @url_whitelist = prepare_wildcards(patterns)
44
+ page.network.intercept if @client && !@url_whitelist.empty?
45
+ end
46
+
47
+ def url_blacklist=(patterns)
48
+ @url_blacklist = prepare_wildcards(patterns)
49
+ page.network.intercept if @client && !@url_blacklist.empty?
50
+ end
51
+
52
+ def visit(*args)
53
+ goto(*args)
54
+ end
55
+
56
+ def status_code
57
+ network.status
58
+ end
59
+
60
+ def find(method, selector)
61
+ find_all(method, selector)
62
+ end
63
+
64
+ def property(node, name)
65
+ node.property(name)
66
+ end
67
+
68
+ def find_within(node, method, selector)
69
+ resolved = page.command("DOM.resolveNode", nodeId: node.node_id)
70
+ object_id = resolved.dig("object", "objectId")
71
+ find_all(method, selector, { "objectId" => object_id })
72
+ end
73
+
74
+ def window_handle
75
+ page.target_id
76
+ end
77
+
78
+ def window_handles
79
+ targets.keys
80
+ end
81
+
82
+ def within_window(locator = nil)
83
+ original = window_handle
84
+ raise Ferrum::NoSuchPageError unless window_handles.include?(locator)
85
+
86
+ switch_to_window(locator)
87
+ yield
88
+ ensure
89
+ switch_to_window(original)
90
+ end
91
+
92
+ def switch_to_window(target_id)
93
+ target = targets[target_id]
94
+ raise Ferrum::NoSuchPageError unless target
95
+
96
+ @page = attach_page(target.id)
97
+ end
98
+
99
+ def close_window(target_id)
100
+ target = targets[target_id]
101
+ raise Ferrum::NoSuchPageError unless target
102
+
103
+ @page = nil if @page.target_id == target.id
104
+ target.page.close
105
+ end
106
+
107
+ def browser_error
108
+ evaluate("_cuprite.browserError()")
109
+ end
110
+
111
+ def source
112
+ raise NotImplementedError
113
+ end
114
+
115
+ def drag(node, other)
116
+ x1, y1 = node.find_position
117
+ x2, y2 = other.find_position
118
+
119
+ mouse.move(x: x1, y: y1)
120
+ mouse.down
121
+ mouse.move(x: x2, y: y2)
122
+ mouse.up
123
+ end
124
+
125
+ def drag_by(node, x, y)
126
+ x1, y1 = node.find_position
127
+ x2 = x1 + x
128
+ y2 = y1 + y
129
+
130
+ mouse.move(x: x1, y: y1)
131
+ mouse.down
132
+ mouse.move(x: x2, y: y2)
133
+ mouse.up
134
+ end
135
+
136
+ def select_file(node, value)
137
+ node.select_file(value)
138
+ end
139
+
140
+ def parents(node)
141
+ evaluate_on(node: node, expression: "_cuprite.parents(this)", by_value: false)
142
+ end
143
+
144
+ def visible_text(node)
145
+ evaluate_on(node: node, expression: "_cuprite.visibleText(this)")
146
+ end
147
+
148
+ def delete_text(node)
149
+ evaluate_on(node: node, expression: "_cuprite.deleteText(this)")
150
+ end
151
+
152
+ def attributes(node)
153
+ value = evaluate_on(node: node, expression: "_cuprite.getAttributes(this)")
154
+ JSON.parse(value)
155
+ end
156
+
157
+ def attribute(node, name)
158
+ evaluate_on(node: node, expression: %(_cuprite.getAttribute(this, "#{name}")))
159
+ end
160
+
161
+ def value(node)
162
+ evaluate_on(node: node, expression: "_cuprite.value(this)")
163
+ end
164
+
165
+ def visible?(node)
166
+ evaluate_on(node: node, expression: "_cuprite.isVisible(this)")
167
+ end
168
+
169
+ def disabled?(node)
170
+ evaluate_on(node: node, expression: "_cuprite.isDisabled(this)")
171
+ end
172
+
173
+ def path(node)
174
+ evaluate_on(node: node, expression: "_cuprite.path(this)")
175
+ end
176
+
177
+ def all_text(node)
178
+ node.text
179
+ end
180
+
181
+ private
182
+
183
+ def find_all(method, selector, within = nil)
184
+ nodes = if within
185
+ evaluate("_cuprite.find(arguments[0], arguments[1], arguments[2])", method, selector, within)
186
+ else
187
+ evaluate("_cuprite.find(arguments[0], arguments[1])", method, selector)
188
+ end
189
+
190
+ nodes.select(&:node?)
191
+ rescue Ferrum::JavaScriptError => e
192
+ raise InvalidSelector.new(e.response, method, selector) if e.class_name == "InvalidSelector"
193
+
194
+ raise
195
+ end
196
+
197
+ def prepare_wildcards(wc)
198
+ Array(wc).map do |wildcard|
199
+ if wildcard.is_a?(Regexp)
200
+ wildcard
201
+ else
202
+ wildcard = wildcard.gsub("*", ".*")
203
+ Regexp.new(wildcard, Regexp::IGNORECASE)
204
+ end
205
+ end
206
+ end
207
+
208
+ def attach_page(target_id = nil)
209
+ target = targets[target_id] if target_id
210
+ target ||= default_context.default_target
211
+ return target.page if target.attached?
212
+
213
+ target.maybe_sleep_if_new_window
214
+ target.page = Page.new(target.id, self)
215
+ target.page
216
+ end
217
+ end
218
+ end
219
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara
4
+ module Cuprite
5
+ class Cookie
6
+ def initialize(attributes)
7
+ @attributes = attributes
8
+ end
9
+
10
+ def name
11
+ @attributes["name"]
12
+ end
13
+
14
+ def value
15
+ @attributes["value"]
16
+ end
17
+
18
+ def domain
19
+ @attributes["domain"]
20
+ end
21
+
22
+ def path
23
+ @attributes["path"]
24
+ end
25
+
26
+ def size
27
+ @attributes["size"]
28
+ end
29
+
30
+ def secure?
31
+ @attributes["secure"]
32
+ end
33
+
34
+ def httponly?
35
+ @attributes["httpOnly"]
36
+ end
37
+
38
+ def session?
39
+ @attributes["session"]
40
+ end
41
+
42
+ def expires
43
+ Time.at(@attributes["expires"]) if (@attributes["expires"]).positive?
44
+ end
45
+ end
46
+ end
47
+ end