playwright-ruby-client 1.37.1 → 1.38.1

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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +13 -7
  3. data/documentation/docs/api/download.md +17 -7
  4. data/documentation/docs/api/element_handle.md +1 -14
  5. data/documentation/docs/api/frame.md +1 -6
  6. data/documentation/docs/api/keyboard.md +4 -0
  7. data/documentation/docs/api/locator.md +31 -16
  8. data/documentation/docs/api/locator_assertions.md +684 -0
  9. data/documentation/docs/api/page.md +1 -6
  10. data/documentation/docs/api/request.md +17 -0
  11. data/documentation/docs/include/api_coverage.md +44 -0
  12. data/lib/playwright/channel.rb +1 -1
  13. data/lib/playwright/channel_owners/browser_context.rb +15 -0
  14. data/lib/playwright/channel_owners/page.rb +2 -0
  15. data/lib/playwright/channel_owners/request.rb +17 -1
  16. data/lib/playwright/channel_owners/route.rb +5 -1
  17. data/lib/playwright/errors.rb +15 -2
  18. data/lib/playwright/events.rb +1 -0
  19. data/lib/playwright/javascript/value_parser.rb +8 -0
  20. data/lib/playwright/javascript/value_serializer.rb +11 -5
  21. data/lib/playwright/locator_assertions_impl.rb +417 -0
  22. data/lib/playwright/locator_impl.rb +28 -5
  23. data/lib/playwright/test.rb +68 -0
  24. data/lib/playwright/utils.rb +4 -0
  25. data/lib/playwright/version.rb +2 -2
  26. data/lib/playwright_api/download.rb +12 -3
  27. data/lib/playwright_api/element_handle.rb +1 -14
  28. data/lib/playwright_api/frame.rb +1 -6
  29. data/lib/playwright_api/keyboard.rb +4 -0
  30. data/lib/playwright_api/locator.rb +31 -16
  31. data/lib/playwright_api/locator_assertions.rb +538 -0
  32. data/lib/playwright_api/page.rb +1 -6
  33. data/lib/playwright_api/request.rb +17 -0
  34. data/lib/playwright_api/worker.rb +4 -4
  35. data/sig/playwright.rbs +44 -0
  36. metadata +7 -3
@@ -54,6 +54,20 @@ def frame
54
54
 
55
55
  Returns the [Frame](./frame) that initiated this request.
56
56
 
57
+ **Usage**
58
+
59
+ ```ruby
60
+ frame_url = request.frame.url
61
+ ```
62
+
63
+ **Details**
64
+
65
+ Note that in some cases the frame is not available, and this method will throw.
66
+ - When request originates in the Service Worker. You can use `request.serviceWorker()` to check that.
67
+ - When navigation request is issued before the corresponding frame is created. You can use [Request#navigation_request?](./request#navigation_request?) to check that.
68
+
69
+ Here is an example that handles all the cases:
70
+
57
71
  ## headers
58
72
 
59
73
  ```
@@ -93,6 +107,9 @@ def navigation_request?
93
107
 
94
108
  Whether this request is driving frame's navigation.
95
109
 
110
+ Some navigation requests are issued before the corresponding frame is created, and therefore
111
+ do not have [Request#frame](./request#frame) available.
112
+
96
113
  ## method
97
114
 
98
115
  ```
@@ -470,6 +470,7 @@
470
470
  * or
471
471
  * page
472
472
  * press
473
+ * press_sequentially
473
474
  * screenshot
474
475
  * scroll_into_view_if_needed
475
476
  * select_option
@@ -526,6 +527,49 @@
526
527
 
527
528
  * ~~new_context~~
528
529
 
530
+ ## LocatorAssertions
531
+
532
+ * not_to_be_attached
533
+ * not_to_be_checked
534
+ * not_to_be_disabled
535
+ * not_to_be_editable
536
+ * not_to_be_empty
537
+ * not_to_be_enabled
538
+ * not_to_be_focused
539
+ * not_to_be_hidden
540
+ * not_to_be_in_viewport
541
+ * not_to_be_visible
542
+ * not_to_contain_text
543
+ * not_to_have_attribute
544
+ * not_to_have_class
545
+ * not_to_have_count
546
+ * not_to_have_css
547
+ * not_to_have_id
548
+ * not_to_have_js_property
549
+ * not_to_have_text
550
+ * not_to_have_value
551
+ * not_to_have_values
552
+ * to_be_attached
553
+ * to_be_checked
554
+ * to_be_disabled
555
+ * to_be_editable
556
+ * to_be_empty
557
+ * to_be_enabled
558
+ * to_be_focused
559
+ * to_be_hidden
560
+ * to_be_in_viewport
561
+ * to_be_visible
562
+ * to_contain_text
563
+ * to_have_attribute
564
+ * to_have_class
565
+ * to_have_count
566
+ * to_have_css
567
+ * to_have_id
568
+ * to_have_js_property
569
+ * to_have_text
570
+ * to_have_value
571
+ * to_have_values
572
+
529
573
  ## Android
530
574
 
531
575
  * ~~connect~~
@@ -46,7 +46,7 @@ module Playwright
46
46
 
47
47
  private def with_logging(&block)
48
48
  locations = caller_locations
49
- first_api_call_location_idx = locations.index { |loc| loc.absolute_path.include?('playwright_api') }
49
+ first_api_call_location_idx = locations.index { |loc| loc.absolute_path&.include?('playwright_api') }
50
50
  unless first_api_call_location_idx
51
51
  return block.call(nil)
52
52
  end
@@ -38,6 +38,12 @@ module Playwright
38
38
  @channel.on('console', ->(params) {
39
39
  on_console_message(ChannelOwners::ConsoleMessage.from(params['message']))
40
40
  })
41
+ @channel.on('pageError', ->(params) {
42
+ on_page_error(
43
+ Error.parse(params['error']['error']),
44
+ ChannelOwners::Page.from_nullable(params['page']),
45
+ )
46
+ })
41
47
  @channel.on('dialog', ->(params) {
42
48
  on_dialog(ChannelOwners::Dialog.from(params['dialog']))
43
49
  })
@@ -97,6 +103,8 @@ module Playwright
97
103
  end
98
104
 
99
105
  private def on_route(route)
106
+ route.send(:update_context, self)
107
+
100
108
  # It is not desired to use PlaywrightApi.wrap directly.
101
109
  # However it is a little difficult to define wrapper for `handler` parameter in generate_api.
102
110
  # Just a workaround...
@@ -177,6 +185,13 @@ module Playwright
177
185
  end
178
186
  end
179
187
 
188
+ private def on_page_error(error, page)
189
+ emit(Events::BrowserContext::WebError, WebError.new(error, page))
190
+ if page
191
+ page.emit(Events::Page::PageError, error)
192
+ end
193
+ end
194
+
180
195
  private def on_request(request, page)
181
196
  emit(Events::BrowserContext::Request, request)
182
197
  page&.emit(Events::Page::Request, request)
@@ -96,6 +96,8 @@ module Playwright
96
96
  end
97
97
 
98
98
  private def on_route(route)
99
+ route.send(:update_context, self)
100
+
99
101
  # It is not desired to use PlaywrightApi.wrap directly.
100
102
  # However it is a little difficult to define wrapper for `handler` parameter in generate_api.
101
103
  # Just a workaround...
@@ -86,8 +86,24 @@ module Playwright
86
86
  ChannelOwners::Response.from_nullable(resp)
87
87
  end
88
88
 
89
+ class FramePageNotReadyError < StandardError
90
+ MESSAGE = [
91
+ 'Frame for this navigation request is not available, because the request',
92
+ 'was issued before the frame is created. You can check whether the request',
93
+ 'is a navigation request by calling isNavigationRequest() method.',
94
+ ].join('\n').freeze
95
+
96
+ def initialize
97
+ super(MESSAGE)
98
+ end
99
+ end
100
+
89
101
  def frame
90
- ChannelOwners::Frame.from(@initializer['frame'])
102
+ ChannelOwners::Frame.from(@initializer['frame']).tap do |result|
103
+ unless result.page
104
+ raise FramePageNotReadyError.new
105
+ end
106
+ end
91
107
  end
92
108
 
93
109
  def navigation_request?
@@ -111,7 +111,7 @@ module Playwright
111
111
  end
112
112
 
113
113
  def fetch(headers: nil, method: nil, postData: nil, url: nil, maxRedirects: nil, timeout: nil)
114
- api_request_context = request.frame.page.context.request
114
+ api_request_context = @context.request
115
115
  api_request_context.send(:_inner_fetch,
116
116
  request,
117
117
  url,
@@ -172,5 +172,9 @@ module Playwright
172
172
  mime_types = MIME::Types.type_for(filepath)
173
173
  mime_types.first.to_s || 'application/octet-stream'
174
174
  end
175
+
176
+ private def update_context(context)
177
+ @context = context
178
+ end
175
179
  end
176
180
  end
@@ -5,13 +5,13 @@ module Playwright
5
5
  if error_payload['name'] == 'TimeoutError'
6
6
  TimeoutError.new(
7
7
  message: error_payload['message'],
8
- stack: error_payload['stack'].split("\n"),
8
+ stack: error_payload['stack'],
9
9
  )
10
10
  else
11
11
  new(
12
12
  name: error_payload['name'],
13
13
  message: error_payload['message'],
14
- stack: error_payload['stack'].split("\n"),
14
+ stack: error_payload['stack'],
15
15
  )
16
16
  end
17
17
  end
@@ -25,6 +25,8 @@ module Playwright
25
25
  @message = message
26
26
  @stack = stack
27
27
  end
28
+
29
+ attr_reader :name, :message, :stack
28
30
  end
29
31
 
30
32
  class DriverCrashedError < StandardError
@@ -38,4 +40,15 @@ module Playwright
38
40
  super(name: 'TimeoutError', message: message, stack: stack)
39
41
  end
40
42
  end
43
+
44
+ class WebError
45
+ def initialize(error, page)
46
+ @error = error
47
+ @page = PlaywrightApi.wrap(page)
48
+ end
49
+
50
+ attr_reader :error, :page
51
+ end
52
+
53
+ class AssertionError < StandardError; end
41
54
  end
@@ -29,6 +29,7 @@ end
29
29
  Console: 'console',
30
30
  Dialog: 'dialog',
31
31
  Page: 'page',
32
+ WebError: 'weberror',
32
33
  ServiceWorker: 'serviceworker',
33
34
  Request: 'request',
34
35
  Response: 'response',
@@ -56,6 +56,14 @@ module Playwright
56
56
  return hash['bi'].to_i
57
57
  end
58
58
 
59
+ if hash.key?('m')
60
+ return parse_hash(hash['m']).to_h
61
+ end
62
+
63
+ if hash.key?('se')
64
+ return Set.new(parse_hash(hash['se']))
65
+ end
66
+
59
67
  if hash.key?('r')
60
68
  # @see https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/RegExp
61
69
  # @see https://docs.ruby-lang.org/ja/latest/class/Regexp.html
@@ -24,7 +24,7 @@ module Playwright
24
24
  @handles << value.channel
25
25
  { h: index }
26
26
  when nil
27
- { v: 'undefined' }
27
+ { v: 'null' }
28
28
  when Float::NAN
29
29
  { v: 'NaN'}
30
30
  when Float::INFINITY
@@ -56,11 +56,17 @@ module Playwright
56
56
  result = []
57
57
  value.each { |v| result << serialize_value(v) }
58
58
  { a: result, id: id }
59
+ when Set
60
+ { se: serialize_value(value.to_a) }
59
61
  when Hash
60
- id = @visited.log(value)
61
- result = []
62
- value.each { |key, v| result << { k: key, v: serialize_value(v) } }
63
- { o: result, id: id }
62
+ if value.any? { |k, v| !k.is_a?(String) && !k.is_a?(Symbol) } # Map
63
+ { m: serialize_value(value.to_a) }
64
+ else
65
+ id = @visited.log(value)
66
+ result = []
67
+ value.each { |key, v| result << { k: key, v: serialize_value(v) } }
68
+ { o: result, id: id }
69
+ end
64
70
  else
65
71
  raise ArgumentError.new("Unexpected value: #{value}")
66
72
  end
@@ -0,0 +1,417 @@
1
+ module Playwright
2
+ # ref: https://github.com/microsoft/playwright-python/blob/main/playwright/_impl/_assertions.py
3
+ define_api_implementation :LocatorAssertionsImpl do
4
+ def self._define_negation(method_name)
5
+ define_method("not_#{method_name}") do |*args, **kwargs|
6
+ if kwargs.empty? # for Ruby < 2.7
7
+ _not.public_send(method_name, *args)
8
+ else
9
+ _not.public_send(method_name, *args, **kwargs)
10
+ end
11
+ end
12
+ end
13
+
14
+ def initialize(locator, timeout, is_not, message)
15
+ @locator = locator
16
+ @timeout = timeout
17
+ @is_not = is_not
18
+ @custom_message = message
19
+ end
20
+
21
+ private def expect_impl(expression, expect_options, expected, message)
22
+ expect_options[:timeout] ||= 5000
23
+ expect_options[:isNot] = @is_not
24
+ message.gsub!("expected to", "not expected to") if @is_not
25
+ expect_options.delete(:useInnerText) if expect_options.key?(:useInnerText) && expect_options[:useInnerText].nil?
26
+
27
+ result = @locator.expect(expression, expect_options)
28
+
29
+ if result["matches"] == @is_not
30
+ actual = result["received"]
31
+
32
+ log =
33
+ if result.key?("log")
34
+ log_contents = result["log"].join("\n").strip
35
+
36
+ "\nCall log:\n #{log_contents}"
37
+ else
38
+ ""
39
+ end
40
+
41
+ out_message =
42
+ if @custom_message && expected
43
+ "\nExpected value: '#{expected}'"
44
+ elsif @custom_message
45
+ @custom_message
46
+ elsif message != "" && expected
47
+ "\n#{message} '#{expected}'"
48
+ else
49
+ "\n#{message}"
50
+ end
51
+
52
+ out = "#{out_message}\nActual value #{actual} #{log}"
53
+ raise AssertionError.new(out)
54
+ else
55
+ true
56
+ end
57
+ end
58
+
59
+ private def _not # "not" is reserved in Ruby
60
+ LocatorAssertionsImpl.new(
61
+ @locator,
62
+ @timeout,
63
+ !@is_not,
64
+ @message
65
+ )
66
+ end
67
+
68
+ private def expected_regex(pattern, match_substring, normalize_white_space, ignore_case)
69
+ regex = JavaScript::Regex.new(pattern)
70
+ expected = {
71
+ regexSource: regex.source,
72
+ regexFlags: regex.flag,
73
+ matchSubstring: match_substring,
74
+ normalizeWhiteSpace: normalize_white_space,
75
+ ignoreCase: ignore_case
76
+ }
77
+ expected.delete(:ignoreCase) if ignore_case.nil?
78
+
79
+ expected
80
+ end
81
+
82
+ private def to_expected_text_values(items, match_substring = false, normalize_white_space = false, ignore_case = false)
83
+ return [] unless items.respond_to?(:each)
84
+
85
+ items.each.with_object([]) do |item, out|
86
+ out <<
87
+ if item.is_a?(String) && ignore_case
88
+ {
89
+ string: item,
90
+ matchSubstring: match_substring,
91
+ normalizeWhiteSpace: normalize_white_space,
92
+ ignoreCase: ignore_case,
93
+ }
94
+ elsif item.is_a?(String)
95
+ {
96
+ string: item,
97
+ matchSubstring: match_substring,
98
+ normalizeWhiteSpace: normalize_white_space,
99
+ }
100
+ elsif item.is_a?(Regexp)
101
+ expected_regex(item, match_substring, normalize_white_space, ignore_case)
102
+ end
103
+ end
104
+ end
105
+
106
+ def to_contain_text(expected, ignoreCase: nil, timeout: nil, useInnerText: nil)
107
+ useInnerText = false if useInnerText.nil?
108
+
109
+ if expected.respond_to?(:each)
110
+ expected_text = to_expected_text_values(
111
+ expected,
112
+ true,
113
+ true,
114
+ ignoreCase,
115
+ )
116
+
117
+ expect_impl(
118
+ "to.contain.text.array",
119
+ {
120
+ expectedText: expected_text,
121
+ useInnerText: useInnerText,
122
+ timeout: timeout,
123
+ },
124
+ expected,
125
+ "Locator expected to contain text"
126
+ )
127
+ else
128
+ expected_text = to_expected_text_values(
129
+ [expected],
130
+ true,
131
+ true,
132
+ ignoreCase
133
+ )
134
+
135
+ expect_impl(
136
+ "to.have.text",
137
+ {
138
+ expectedText: expected_text,
139
+ useInnerText: useInnerText,
140
+ timeout: timeout,
141
+ },
142
+ expected,
143
+ "Locator expected to contain text"
144
+ )
145
+ end
146
+ end
147
+ _define_negation :to_contain_text
148
+
149
+ def to_have_attribute(name, value, timeout: nil)
150
+ expected_text = to_expected_text_values([value])
151
+ expect_impl(
152
+ "to.have.attribute.value",
153
+ {
154
+ expressionArg: name,
155
+ expectedText: expected_text,
156
+ timeout: timeout,
157
+ },
158
+ value,
159
+ "Locator expected to have attribute"
160
+ )
161
+ end
162
+ _define_negation :to_have_attribute
163
+
164
+ def to_have_class(expected, timeout: nil)
165
+ if expected.respond_to?(:each)
166
+ expected_text = to_expected_text_values(expected)
167
+ expect_impl(
168
+ "to.have.class.array",
169
+ {
170
+ expectedText: expected_text,
171
+ timeout: timeout,
172
+ },
173
+ expected,
174
+ "Locator expected to have class"
175
+ )
176
+ else
177
+ expected_text = to_expected_text_values([expected])
178
+ expect_impl(
179
+ "to.have.class",
180
+ {
181
+ expectedText: expected_text,
182
+ timeout: timeout,
183
+ },
184
+ expected,
185
+ "Locator expected to have class"
186
+ )
187
+ end
188
+ end
189
+ _define_negation :to_have_class
190
+
191
+ def to_have_count(count, timeout: nil)
192
+ expect_impl(
193
+ "to.have.count",
194
+ {
195
+ expectedNumber: count,
196
+ timeout: timeout,
197
+ },
198
+ count,
199
+ "Locator expected to have count"
200
+ )
201
+ end
202
+ _define_negation :to_have_count
203
+
204
+ def to_have_css(name, value, timeout: nil)
205
+ expected_text = to_expected_text_values([value])
206
+ expect_impl(
207
+ "to.have.css",
208
+ {
209
+ expressionArg: name,
210
+ expectedText: expected_text,
211
+ timeout: timeout,
212
+ },
213
+ value,
214
+ "Locator expected to have CSS"
215
+ )
216
+ end
217
+ _define_negation :to_have_css
218
+
219
+ def to_have_id(id, timeout: nil)
220
+ expected_text = to_expected_text_values([id])
221
+ expect_impl(
222
+ "to.have.id",
223
+ {
224
+ expectedText: expected_text,
225
+ timeout: timeout,
226
+ },
227
+ id,
228
+ "Locator expected to have ID"
229
+ )
230
+ end
231
+ _define_negation :to_have_id
232
+
233
+ def to_have_js_property(name, value, timeout: nil)
234
+ expect_impl(
235
+ "to.have.property",
236
+ {
237
+ expressionArg: name,
238
+ expectedValue: value,
239
+ timeout: timeout,
240
+ },
241
+ value,
242
+ "Locator expected to have JS Property"
243
+ )
244
+ end
245
+ _define_negation :to_have_js_property
246
+
247
+ def to_have_value(value, timeout: nil)
248
+ expected_text = to_expected_text_values([value])
249
+
250
+ expect_impl(
251
+ "to.have.value",
252
+ {
253
+ expectedText: expected_text,
254
+ timeout: timeout,
255
+ },
256
+ value,
257
+ "Locator expected to have Value"
258
+ )
259
+ end
260
+ _define_negation :to_have_value
261
+
262
+ def to_have_values(values, timeout: nil)
263
+ expected_text = to_expected_text_values(values)
264
+
265
+ expect_impl(
266
+ "to.have.values",
267
+ {
268
+ expectedText: expected_text,
269
+ timeout: timeout,
270
+ },
271
+ values,
272
+ "Locator expected to have Values"
273
+ )
274
+ end
275
+ _define_negation :to_have_values
276
+
277
+ def to_have_text(expected, ignoreCase: nil, timeout: nil, useInnerText: nil)
278
+ if expected.respond_to?(:each)
279
+ expected_text = to_expected_text_values(
280
+ expected,
281
+ true,
282
+ true,
283
+ ignoreCase,
284
+ )
285
+ expect_impl(
286
+ "to.have.text.array",
287
+ {
288
+ expectedText: expected_text,
289
+ useInnerText: useInnerText,
290
+ timeout: timeout,
291
+ },
292
+ expected,
293
+ "Locator expected to have text"
294
+ )
295
+ else
296
+ expected_text = to_expected_text_values(
297
+ [expected],
298
+ true,
299
+ true,
300
+ ignoreCase,
301
+ )
302
+ expect_impl(
303
+ "to.have.text",
304
+ {
305
+ expectedText: expected_text,
306
+ useInnerText: useInnerText,
307
+ timeout: timeout,
308
+ },
309
+ expected,
310
+ "Locator expected to have text"
311
+ )
312
+ end
313
+ end
314
+ _define_negation :to_have_text
315
+
316
+ def to_be_attached(attached: nil, timeout: nil)
317
+ expect_impl(
318
+ (attached || attached.nil?) ? "to.be.attached" : "to.be.detached",
319
+ { timeout: timeout },
320
+ nil,
321
+ "Locator expected to be attached"
322
+ )
323
+ end
324
+ _define_negation :to_be_attached
325
+
326
+ def to_be_checked(checked: nil, timeout: nil)
327
+ expect_impl(
328
+ (checked || checked.nil?) ? "to.be.checked" : "to.be.unchecked",
329
+ { timeout: timeout },
330
+ nil,
331
+ "Locator expected to be checked"
332
+ )
333
+ end
334
+ _define_negation :to_be_checked
335
+
336
+ def to_be_disabled(timeout: nil)
337
+ expect_impl(
338
+ "to.be.disabled",
339
+ { timeout: timeout },
340
+ nil,
341
+ "Locator expected to be disabled"
342
+ )
343
+ end
344
+ _define_negation :to_be_disabled
345
+
346
+ def to_be_editable(editable: nil, timeout: nil)
347
+ expect_impl(
348
+ (editable || editable.nil?) ? "to.be.editable" : "to.be.readonly",
349
+ { timeout: timeout },
350
+ nil,
351
+ "Locator expected to be editable"
352
+ )
353
+ end
354
+ _define_negation :to_be_editable
355
+
356
+ def to_be_empty(timeout: nil)
357
+ expect_impl(
358
+ "to.be.empty",
359
+ { timeout: timeout },
360
+ nil,
361
+ "Locator expected to be empty"
362
+ )
363
+ end
364
+ _define_negation :to_be_empty
365
+
366
+ def to_be_enabled(enabled: nil, timeout: nil)
367
+ expect_impl(
368
+ (enabled || enabled.nil?) ? "to.be.enabled" : "to.be.disabled",
369
+ { timeout: timeout },
370
+ nil,
371
+ "Locator expected to be enabled"
372
+ )
373
+ end
374
+ _define_negation :to_be_enabled
375
+
376
+ def to_be_hidden(timeout: nil)
377
+ expect_impl(
378
+ "to.be.hidden",
379
+ { timeout: timeout },
380
+ nil,
381
+ "Locator expected to be hidden"
382
+ )
383
+ end
384
+ _define_negation :to_be_hidden
385
+
386
+ def to_be_visible(timeout: nil, visible: nil)
387
+ expect_impl(
388
+ (visible || visible.nil?) ? "to.be.visible" : "to.be.hidden",
389
+ { timeout: timeout },
390
+ nil,
391
+ "Locator expected to be visible"
392
+ )
393
+ end
394
+ _define_negation :to_be_visible
395
+
396
+ def to_be_focused(timeout: nil)
397
+ expect_impl(
398
+ "to.be.focused",
399
+ { timeout: timeout },
400
+ nil,
401
+ "Locator expected to be focused"
402
+ )
403
+ end
404
+ _define_negation :to_be_focused
405
+
406
+ def to_be_in_viewport(ratio: nil, timeout: nil)
407
+ expect_impl(
408
+ "to.be.in.viewport",
409
+ { timeout: timeout, expectedNumber: ratio },
410
+ nil,
411
+ "Locator expected to be in viewport"
412
+ )
413
+ end
414
+ _define_negation :to_be_in_viewport
415
+
416
+ end
417
+ end