cuprite 0.18 → 0.20

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: ed2641aa002e810fd4dfedc8b3128f38e5f2df0f747d59bc1cf7a81aa1295076
4
- data.tar.gz: 7e967cb1a6a11ad9f446290cd5035d2d5d3e9c6724ba6f4df4c31e761b29545d
3
+ metadata.gz: 159c7e6289accaf81577a8549e3e1447ea90e0f99694b91547704fef206b8c56
4
+ data.tar.gz: 1b70f9e72fbdd0a089d45229ecb12604d655a782ca2a10b358220b66865e5d09
5
5
  SHA512:
6
- metadata.gz: dcee409e8ee24caf139c3a0c29318ccea5ddefb0759f228f0f24db76aef28864c73d46e02ecc820160e82d6f366c61648ad1dc20dd930b5d9f300c8853d17003
7
- data.tar.gz: 5168b1c18e81936795a02154829100dc3a616c2d2c3db71c134c8e64f5f13d93fd1098e8c26aea8d84a26840bf547a80b263b63b293420dc9f18cc18a5d29632
6
+ metadata.gz: dac1cb17193f518ce4e276c2580d02118e9a1cce86737c8e0b501c2747a438b89dfda2ed2eb5b66b23e4a92fa7d30c53cbb991ae172358fbbb12531fd441a777
7
+ data.tar.gz: f9758427a804d2dc3ecda09610598fa2a22389f520789800fc5b71bf60f5cef9be01a48a464610a1200914ab4f005ca71634557f9b113586c594097a0c592884
data/README.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Cuprite - Headless Chrome driver for Capybara
2
2
 
3
+ ## [Documentation](https://docs.rubycdp.com/docs/cuprite/introduction)
4
+
5
+ <img align="right"
6
+ width="160" height="160"
7
+ alt="Cuprite logo"
8
+ src="https://raw.githubusercontent.com/rubycdp/cuprite/main/logo.svg?sanitize=true">
9
+
3
10
  Cuprite is a pure Ruby driver (read as _no_ Selenium/WebDriver/ChromeDriver
4
11
  dependency) for [Capybara](https://github.com/teamcapybara/capybara). It allows
5
12
  you to run Capybara tests on a headless Chrome or Chromium. Under the hood it
@@ -7,6 +14,8 @@ uses [Ferrum](https://github.com/rubycdp/ferrum#index) which is high-level API
7
14
  to the browser by CDP protocol.
8
15
 
9
16
 
17
+ <br clear="right">
18
+
10
19
  ## Install
11
20
 
12
21
  Add this to your `Gemfile` and run `bundle install`.
@@ -49,7 +58,8 @@ because Docker is involved. Prefer running the browser as a non-root user with
49
58
  a working sandbox when possible.
50
59
 
51
60
  Since Cuprite uses [Ferrum](https://github.com/rubycdp/ferrum#examples) there
52
- are many useful methods you can call even using this driver:
61
+ are many useful methods you can call even using this driver.
62
+ `page.driver.browser` is a `Ferrum::Browser`:
53
63
 
54
64
  ```ruby
55
65
  browser = page.driver.browser
@@ -84,7 +94,18 @@ end
84
94
  timer) only surfaces on whatever command runs next.
85
95
  * `:url_blacklist` (Array) - array of regexes to match against requested URLs
86
96
  * `:url_whitelist` (Array) - array of regexes to match against requested URLs
97
+ * `:emulate` (Symbol) - device from Chrome DevTools' list to emulate, e.g. `:iphone_16`, `:pixel_9`, `:ipad_mini`
98
+ * `:mobile` (Boolean) - emulate a mobile device: viewport meta tag and touch events
99
+ * `:scale_factor` (Numeric) - device scale factor to emulate
87
100
 
101
+ `emulate:` sets the device's `:window_size`, `:scale_factor`, `:mobile` and user agent, options passed explicitly
102
+ take precedence:
103
+
104
+ ```ruby
105
+ Capybara.register_driver(:cuprite) do |app|
106
+ Capybara::Cuprite::Driver.new(app, emulate: :iphone_14)
107
+ end
108
+ ```
88
109
 
89
110
  ## Debugging
90
111
 
@@ -109,35 +130,35 @@ opened where you can further experiment with the test.
109
130
 
110
131
  ## Clicking/Scrolling
111
132
 
112
- * `page.driver.click(x, y)` Click a very specific area of the screen.
113
- * `page.driver.scroll_to(left, top)` Scroll to a given position.
133
+ * `page.driver.browser.mouse.click(x:, y:)` Click a very specific area of the screen.
134
+ * `page.scroll_to(left, top)` Scroll to a given position.
114
135
  * `element.send_keys(*keys)` Send keys to a given node.
115
136
 
116
137
 
117
138
  ## Request headers
118
139
 
119
- Manipulate HTTP request headers like a boss:
140
+ Manipulate HTTP request headers with Ferrum:
120
141
 
121
142
  ``` ruby
122
- page.driver.headers # => {}
123
- page.driver.headers = { "User-Agent" => "Cuprite" }
124
- page.driver.add_headers("Referer" => "https://example.com")
125
- page.driver.headers # => { "User-Agent" => "Cuprite", "Referer" => "https://example.com" }
143
+ page.driver.browser.headers.get # => {}
144
+ page.driver.browser.headers.set("User-Agent" => "Cuprite")
145
+ page.driver.browser.headers.add({ "Referer" => "https://example.com" })
146
+ page.driver.browser.headers.get # => { "User-Agent" => "Cuprite", "Referer" => "https://example.com" }
126
147
  ```
127
148
 
128
- Notice that `headers=` will overwrite already set headers. You should use
129
- `add_headers` if you want to add a few more. These headers will apply to all
130
- subsequent HTTP requests (including requests for assets, AJAX, etc). They will
131
- be automatically cleared at the end of the test.
149
+ Notice that `set` will overwrite already set headers. You should use `add` if
150
+ you want to add a few more. These headers will apply to all subsequent HTTP
151
+ requests (including requests for assets, AJAX, etc). They will be automatically
152
+ cleared at the end of the test.
132
153
 
133
154
 
134
155
  ## Network traffic
135
156
 
136
- * `page.driver.network_traffic` allows you to inspect network traffic (i.e., loaded resources) on the current page. It returns an array of `Ferrum::Network::Exchange` objects, each representing a network request/response exchange. You can query both the request and response details of each exchange.
157
+ * `page.driver.browser.network.traffic` allows you to inspect network traffic (i.e., loaded resources) on the current page. It returns an array of `Ferrum::Network::Exchange` objects, each representing a network request/response exchange. You can query both the request and response details of each exchange.
137
158
 
138
159
  ```ruby
139
160
  # Retrieve all network exchanges
140
- network_traffic = page.driver.network_traffic
161
+ network_traffic = page.driver.browser.network.traffic
141
162
 
142
163
  # Access the first exchange
143
164
  first_exchange = network_traffic.first
@@ -146,22 +167,22 @@ first_exchange = network_traffic.first
146
167
  response = first_exchange.response
147
168
  ```
148
169
 
149
- * `page.driver.wait_for_network_idle` Natively waits for network idle and if
150
- there are no active connections returns or raises `TimeoutError` error. Accepts
151
- the same options as
170
+ * `page.driver.browser.network.wait_for_idle` Natively waits for network idle
171
+ and returns `false` if there are still active connections after timeout.
172
+ Accepts the same options as
152
173
  [`wait_for_idle`](https://github.com/rubycdp/ferrum#wait_for_idleoptions)
153
174
 
154
175
  ```ruby
155
- page.driver.wait_for_network_idle
176
+ page.driver.browser.network.wait_for_idle
156
177
  page.driver.refresh
157
178
  ```
158
179
 
159
180
  Please note that network traffic is not cleared when you visit new page. You can
160
- manually clear the network traffic by calling `page.driver.clear_network_traffic`
161
- or `page.driver.reset`
181
+ manually clear the network traffic by calling
182
+ `page.driver.browser.network.clear(:traffic)` or `page.driver.reset!`
162
183
 
163
- * `page.driver.wait_for_reload` unlike `wait_for_network_idle` will wait until
164
- the whole page is reloaded or raise a timeout error. It's useful when you know
184
+ * `page.driver.browser.wait_for_reload` unlike `network.wait_for_idle` will wait until
185
+ the whole page is reloaded or the timeout passes. It's useful when you know
165
186
  that for example after clicking autocomplete suggestion you expect page to be
166
187
  reloaded, you have a few choices - put sleep or wait for network idle, but both
167
188
  are bad. Sleep makes you wait longer or less than needed, network idle can
@@ -171,34 +192,45 @@ rescue.
171
192
 
172
193
  ## Manipulating cookies
173
194
 
174
- The following methods are used to inspect and manipulate cookies:
195
+ Use Ferrum's cookies to inspect and manipulate cookies:
175
196
 
176
- * `page.driver.cookies` - a hash of cookies accessible to the current
177
- page. The keys are cookie names. The values are `Cookie` objects, with
178
- the following methods: `name`, `value`, `domain`, `path`, `size`, `secure?`,
179
- `httponly?`, `session?`, `expires`.
180
- * `page.driver.set_cookie(name, value, options = {})` - set a cookie.
181
- The options hash can take the following keys: `:domain`, `:path`,
182
- `:secure`, `:httponly`, `:expires`. `:expires` should be a
183
- `Time` object.
184
- * `page.driver.remove_cookie(name)` - remove a cookie
185
- * `page.driver.clear_cookies` - clear all cookies
197
+ ```ruby
198
+ cookies = page.driver.browser.cookies
199
+ cookies.set(name: "stealth", value: "omg")
200
+ cookies["stealth"].value # => "omg"
201
+ cookies.all # => { "stealth" => #<Ferrum::Cookies::Cookie ...> }
202
+ cookies.remove(name: "stealth")
203
+ cookies.clear
204
+ ```
186
205
 
187
206
 
188
207
  ## Screenshot
189
208
 
190
- Besides capybara screenshot method you can get image as Base64:
209
+ Besides capybara screenshot method you can get image or PDF as Base64 with Ferrum:
191
210
 
192
- * `page.driver.render_base64(format, options)`
211
+ ```ruby
212
+ page.driver.browser.screenshot(format: :jpeg, full: true)
213
+ page.driver.browser.pdf(paper_width: 6, paper_height: 12)
214
+ ```
193
215
 
194
216
 
195
217
  ## Authorization
196
218
 
197
- * `page.driver.basic_authorize(user, password)`
219
+ * `page.driver.browser.network.authorize(user:, password:) { |request, _index, _total| request.continue }`
198
220
 
199
221
  ## Proxy
200
222
 
201
- * `page.driver.set_proxy(ip, port, user, password)`
223
+ ```ruby
224
+ Capybara::Cuprite::Driver.new(app, proxy: { host: "127.0.0.1", port: 8080, user: "user", password: "password" })
225
+ ```
226
+
227
+ The proxy is passed to Chrome as a command-line flag when the browser starts.
228
+
229
+ When connecting to a remote browser with the `:url` option (browserless, Selenoid, etc.), Cuprite doesn't launch
230
+ Chrome and the proxy flag is never applied, so configure the proxy on the remote browser itself. The `user` and
231
+ `password` are still used to answer the proxy's authentication challenge.
232
+
233
+ The `bypass` option is a list of hosts that skip the proxy, so don't pass the host you want to go through it.
202
234
 
203
235
 
204
236
  ## URL Blocklisting & Allowlisting
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "capybara/cuprite/deprecation"
4
+
5
+ module Capybara
6
+ module Cuprite
7
+ class Browser < Ferrum::Browser
8
+ module Deprecations
9
+ def visit(*args)
10
+ Cuprite.deprecation_warning("#{self.class}#visit", "use #goto")
11
+ goto(*args)
12
+ end
13
+
14
+ def status_code
15
+ Cuprite.deprecation_warning("#{self.class}#status_code", "use #network.status")
16
+ network.status
17
+ end
18
+
19
+ def window_handle
20
+ Cuprite.deprecation_warning("#{self.class}#window_handle", "use #current_window_handle")
21
+ current_window_handle
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara
4
+ module Cuprite
5
+ class Browser < Ferrum::Browser
6
+ module Extensions
7
+ def raise_on_unhandled_modal
8
+ @options.raise_on_unhandled_modal
9
+ end
10
+
11
+ def raise_on_unhandled_modal=(value)
12
+ @options.raise_on_unhandled_modal = value
13
+ end
14
+
15
+ def url_whitelist
16
+ @options.url_whitelist
17
+ end
18
+ alias url_allowlist url_whitelist
19
+
20
+ def url_whitelist=(patterns)
21
+ @options.url_whitelist = prepare_wildcards(patterns)
22
+ page.network.whitelist = @options.url_whitelist if @client && @options.url_whitelist.any?
23
+ end
24
+ alias url_allowlist= url_whitelist=
25
+
26
+ def url_blacklist
27
+ @options.url_blacklist
28
+ end
29
+ alias url_blocklist url_blacklist
30
+
31
+ def url_blacklist=(patterns)
32
+ @options.url_blacklist = prepare_wildcards(patterns)
33
+ page.network.blacklist = @options.url_blacklist if @client && @options.url_blacklist.any?
34
+ end
35
+ alias url_blocklist= url_blacklist=
36
+
37
+ private
38
+
39
+ def prepare_wildcards(patterns)
40
+ string_passed = false
41
+
42
+ Array(patterns).map do |pattern|
43
+ if pattern.is_a?(Regexp)
44
+ pattern
45
+ else
46
+ string_passed = true
47
+ pattern = pattern.gsub("*", ".*")
48
+ Regexp.new(pattern, Regexp::IGNORECASE)
49
+ end
50
+ end
51
+ ensure
52
+ if string_passed
53
+ warn "Passing strings to blacklist/whitelist is deprecated, pass regexp at #{caller(4..4).first}"
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -1,19 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "forwardable"
3
+ require "capybara/cuprite/browser/deprecations"
4
+ require "capybara/cuprite/browser/extensions"
4
5
 
5
6
  module Capybara
6
7
  module Cuprite
7
8
  class Browser < Ferrum::Browser
8
- extend Forwardable
9
-
10
- DRAG_HTML5_JS = File.read(File.expand_path("javascripts/drag.js", __dir__))
11
-
12
- delegate %i[send_keys select set hover trigger before_click switch_to_frame
13
- find_modal accept_confirm dismiss_confirm accept_prompt
14
- dismiss_prompt reset_modals] => :page
9
+ include Deprecations
10
+ include Extensions
15
11
 
16
12
  def initialize(options = nil)
13
+ device = options&.dig(:emulate)
14
+ options = Devices.fetch(device).except(:user_agent).merge(options) if device
17
15
  super
18
16
 
19
17
  @options.raise_on_unhandled_modal = options&.dig(:raise_on_unhandled_modal)
@@ -31,20 +29,21 @@ module Capybara
31
29
  end
32
30
 
33
31
  def page
34
- raise Ferrum::NoSuchPageError if @page&.closed?
32
+ raise Ferrum::NoSuchPageError if @page_closed
35
33
 
36
34
  @page ||= attach_page
37
35
  end
38
36
 
39
37
  def reset
40
38
  super
39
+ ensure
41
40
  @options.reset_window_size
42
- @page = nil
41
+ forget_page
43
42
  end
44
43
 
45
44
  def quit
46
45
  super
47
- @page = nil
46
+ forget_page
48
47
  end
49
48
 
50
49
  def resize(**options)
@@ -52,249 +51,47 @@ module Capybara
52
51
  super
53
52
  end
54
53
 
55
- def raise_on_unhandled_modal
56
- @options.raise_on_unhandled_modal
57
- end
58
-
59
- def raise_on_unhandled_modal=(value)
60
- @options.raise_on_unhandled_modal = value
61
- end
62
-
63
- def url_whitelist
64
- @options.url_whitelist
65
- end
66
- alias url_allowlist url_whitelist
67
-
68
- def url_whitelist=(patterns)
69
- @options.url_whitelist = prepare_wildcards(patterns)
70
- page.network.whitelist = @options.url_whitelist if @client && @options.url_whitelist.any?
71
- end
72
- alias url_allowlist= url_whitelist=
73
-
74
- def url_blacklist
75
- @options.url_blacklist
76
- end
77
- alias url_blocklist url_blacklist
78
-
79
- def url_blacklist=(patterns)
80
- @options.url_blacklist = prepare_wildcards(patterns)
81
- page.network.blacklist = @options.url_blacklist if @client && @options.url_blacklist.any?
82
- end
83
- alias url_blocklist= url_blacklist=
84
-
85
- def visit(*args)
86
- goto(*args)
87
- end
88
-
89
- def status_code
90
- network.status
91
- end
92
-
93
- def find(method, selector)
94
- find_all(method, selector)
95
- end
96
-
97
- def property(node, name)
98
- node.property(name)
99
- end
100
-
101
- def find_within(node, method, selector)
102
- resolved = page.command("DOM.resolveNode", nodeId: node.node_id)
103
- object_id = resolved.dig("object", "objectId")
104
- find_all(method, selector, { "objectId" => object_id })
105
- end
106
-
107
- def window_handle
54
+ def current_window_handle
108
55
  page.target_id
109
56
  end
110
57
 
111
58
  def window_handles
112
- targets.keys
59
+ targets.values.select(&:page?).map(&:id)
113
60
  end
114
61
 
115
- def within_window(locator = nil)
116
- original = window_handle
117
- raise Ferrum::NoSuchPageError unless window_handles.include?(locator)
118
-
119
- switch_to_window(locator)
120
- yield
121
- ensure
122
- switch_to_window(original)
62
+ def open_new_window
63
+ attach_page(default_context.create_target.id)
123
64
  end
124
65
 
125
66
  def switch_to_window(target_id)
126
- target = targets[target_id]
127
- raise Ferrum::NoSuchPageError unless target
128
-
67
+ target = window_target(target_id)
68
+ forget_page
129
69
  @page = attach_page(target.id)
130
70
  @page.activate
131
71
  end
132
72
 
133
73
  def close_window(target_id)
134
- target = targets[target_id]
135
- raise Ferrum::NoSuchPageError unless target
136
-
137
- @page = ClosedPage.new if @page.target_id == target.id
74
+ target = window_target(target_id)
75
+ if @page&.target_id == target.id
76
+ forget_page
77
+ @page_closed = true
78
+ end
138
79
  target.page.close
139
80
  targets.delete(target_id) # page.close is async, delete target asap
140
81
  end
141
82
 
142
- def active_element
143
- evaluate("document.activeElement")
144
- end
145
-
146
- def browser_error
147
- evaluate("_cuprite.browserError()")
148
- end
149
-
150
- def source
151
- raise NotImplementedError
152
- end
153
-
154
- def drag(node, other, drop_modifiers, options = {})
155
- steps = options.fetch(:steps, 1)
156
- delay = options[:delay]
157
- scroll = options.fetch(:scroll, true)
158
- html5 = options[:html5]
159
-
160
- execute("_cuprite.dragMousedownTracker()")
161
-
162
- node.scroll_into_view if scroll
163
- x1, y1 = node.find_position
164
-
165
- mouse.move(x: x1, y: y1)
166
- mouse.down
167
- sleep delay if delay
168
-
169
- html5 = !evaluate_on(node: node, expression: "_cuprite.legacyDragCheck(this)") if html5.nil?
170
-
171
- if html5
172
- drag_html5(node, other, drop_modifiers, delay)
173
- else
174
- modifiers = keyboard.modifiers(drop_modifiers)
175
-
176
- other.scroll_into_view if scroll
177
- x2, y2 = other.find_position
178
- mouse.move(x: x2, y: y2, steps: steps)
179
-
180
- mouse.up(modifiers: modifiers)
181
- end
182
- end
183
-
184
- def drag_html5(node, other, drop_modifiers, delay)
185
- keys = drop_modifiers.map(&:to_s)
186
- evaluate_async(DRAG_HTML5_JS, timeout, node, other, (delay || 0.05) * 1000, keys)
187
- mouse.up
188
- end
189
-
190
- def drag_by(node, dx, dy, steps, delay = nil, scroll = true)
191
- x1, y1 = node.find_position
192
-
193
- mouse.move(x: x1, y: y1)
194
- mouse.down
195
-
196
- sleep delay if delay
197
-
198
- evaluate("window.scrollBy(#{dx}, #{dy})") if scroll # should be extracted to Mouse#scroll_by in ferrum
199
-
200
- x2, y2 = node.find_position
201
- mouse.move(x: x2 + dx, y: y2 + dy, steps: steps)
202
- mouse.up
203
- end
204
-
205
- def drop(node, *args)
206
- if args[0].is_a?(String)
207
- execute("_cuprite.attachDropInput()")
208
- input = find(:css, "#_cuprite_drop_file").first
209
- select_file(input, args)
210
- evaluate_on(node: node, expression: "_cuprite.dropFile(this)")
211
- else
212
- strings = args.flat_map { |arg| arg.map { |type, data| { "type" => type, "data" => data } } }
213
- evaluate_on(node: node, expression: "_cuprite.dropString(#{strings.to_json}, this)")
214
- end
215
- end
216
-
217
- def select_file(node, value)
218
- node.select_file(value)
219
- end
220
-
221
- def parents(node)
222
- evaluate_on(node: node, expression: "_cuprite.parents(this)", by_value: false)
223
- end
224
-
225
- def visible_text(node)
226
- evaluate_on(node: node, expression: "_cuprite.visibleText(this)")
227
- end
228
-
229
- def delete_text(node)
230
- evaluate_on(node: node, expression: "_cuprite.deleteText(this)")
231
- end
232
-
233
- def attributes(node)
234
- value = evaluate_on(node: node, expression: "_cuprite.getAttributes(this)")
235
- JSON.parse(value)
236
- end
237
-
238
- def attribute(node, name)
239
- evaluate_on(node: node, expression: %(_cuprite.getAttribute(this, "#{name}")))
240
- end
241
-
242
- def value(node)
243
- evaluate_on(node: node, expression: "_cuprite.value(this)")
244
- end
245
-
246
- def visible?(node)
247
- evaluate_on(node: node, expression: "_cuprite.isVisible(this)")
248
- end
249
-
250
- def disabled?(node)
251
- evaluate_on(node: node, expression: "_cuprite.isDisabled(this)")
252
- end
253
-
254
- def path(node)
255
- evaluate_on(node: node, expression: "_cuprite.path(this)")
256
- end
257
-
258
- def obscured?(node)
259
- evaluate_on(node: node, expression: "_cuprite.isObscured(this)")
260
- end
261
-
262
- def all_text(node)
263
- node.text
264
- end
265
-
266
83
  private
267
84
 
268
- def find_all(method, selector, within = nil)
269
- nodes = if within
270
- evaluate("_cuprite.find(arguments[0], arguments[1], arguments[2])", method, selector, within)
271
- else
272
- evaluate("_cuprite.find(arguments[0], arguments[1])", method, selector)
273
- end
274
-
275
- nodes.select(&:node?)
276
- rescue Ferrum::JavaScriptError => e
277
- raise InvalidSelector.new(e.response, method, selector) if e.class_name == "InvalidSelector"
278
-
279
- raise
85
+ def forget_page
86
+ @page = nil
87
+ @page_closed = false
280
88
  end
281
89
 
282
- def prepare_wildcards(patterns)
283
- string_passed = false
90
+ def window_target(target_id)
91
+ target = targets[target_id]
92
+ raise Ferrum::NoSuchPageError unless target&.page?
284
93
 
285
- Array(patterns).map do |pattern|
286
- if pattern.is_a?(Regexp)
287
- pattern
288
- else
289
- string_passed = true
290
- pattern = pattern.gsub("*", ".*")
291
- Regexp.new(pattern, Regexp::IGNORECASE)
292
- end
293
- end
294
- ensure
295
- if string_passed
296
- warn "Passing strings to blacklist/whitelist is deprecated, pass regexp at #{caller(4..4).first}"
297
- end
94
+ target
298
95
  end
299
96
 
300
97
  def attach_page(target_id = nil)
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara
4
+ module Cuprite
5
+ REMOVE_DEPRECATED_IN = "0.21"
6
+
7
+ def self.deprecation_warning(method, advice = nil)
8
+ message = "#{method} is deprecated and will be removed in #{REMOVE_DEPRECATED_IN}"
9
+ message += ", #{advice} instead" if advice
10
+ warn message, uplevel: 2
11
+ end
12
+ end
13
+ end