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 +4 -4
- data/LICENSE +21 -0
- data/README.md +212 -0
- data/lib/capybara/cuprite/browser.rb +219 -0
- data/lib/capybara/cuprite/cookie.rb +47 -0
- data/lib/capybara/cuprite/driver.rb +424 -0
- data/lib/capybara/cuprite/errors.rb +63 -0
- data/lib/capybara/cuprite/javascripts/index.js +478 -0
- data/lib/capybara/cuprite/node.rb +285 -0
- data/lib/capybara/cuprite/page.rb +205 -0
- data/lib/capybara/cuprite/version.rb +7 -0
- data/lib/capybara/cuprite.rb +13 -0
- metadata +13 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4146607eaf82561d128b2f4501f688b3104fae6ce4f70ec6350b24518a85ad23
|
4
|
+
data.tar.gz: 71c489599d22691f4270f1f87a85885a7bddd15bc0bfe0baa3acc2e8b13d8ce0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|