rudra 1.0.16 → 1.1.3
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/lib/rudra.rb +229 -33
- metadata +42 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d1094036482a36f5196cd490c2594b66aabf2eebb1aa6c5adfb2b49c426d03e3
|
4
|
+
data.tar.gz: 4fd39aae44a3cf44d263014147cba3eb50a3c0622445fb0452dfda3f438120a1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8c1fca4fe84b87a53c2681df286f0862fa88fec448af16a4a2752c34dca6c9c0e3686b4a4f4db05fd222d539a08586e6750a524801033d127fda974345f79025
|
7
|
+
data.tar.gz: 813860ba0fa303826bcc1ea916592b73b0b42130c2ecb92082562b576be71a26a445cc3128964cc6b2342d1837d37b0b3855867bf66259b5c772cbe8d1702099
|
data/lib/rudra.rb
CHANGED
@@ -1,10 +1,9 @@
|
|
1
1
|
require 'selenium-webdriver'
|
2
|
-
require 'webdrivers
|
3
|
-
require '
|
4
|
-
require '
|
5
|
-
|
6
|
-
|
7
|
-
# Selenium::WebDriver::Firefox::Service.driver_path = './webdrivers/geckodriver'
|
2
|
+
require 'webdrivers'
|
3
|
+
require 'zip'
|
4
|
+
require 'base64'
|
5
|
+
require 'json'
|
6
|
+
require 'stringio'
|
8
7
|
|
9
8
|
# Selenium IDE-like WebDriver based upon Ruby binding
|
10
9
|
# @author Aaron Chen
|
@@ -14,10 +13,14 @@ require 'webdrivers/iedriver'
|
|
14
13
|
# @attr_reader [String] install_dir The install directory of WebDrivers
|
15
14
|
# @attr_reader [String] locale The browser locale
|
16
15
|
# @attr_reader [Boolean] headless Headless mode for Google Chrome
|
16
|
+
# @attr_reader [String] window_size Chrome window size when headless
|
17
17
|
# @attr_reader [String] screen_dir The screenshot directory of save_screenshot
|
18
|
-
# @attr_reader [String] log_prefix Prefix for logging
|
18
|
+
# @attr_reader [String] log_prefix Prefix for logging descriptions and methods
|
19
19
|
# @attr_reader [Integer] timeout The driver timeout
|
20
|
-
# @attr_reader [Boolean] verbose Verbose mode
|
20
|
+
# @attr_reader [Boolean] verbose Turn on/off Verbose mode
|
21
|
+
# @attr_reader [Boolean] silent Turn off Turn on/off descriptions
|
22
|
+
# @attr_reader [String] chrome_auth_username Chrome Basic Auth Extension - username
|
23
|
+
# @attr_reader [String] chrome_auth_password Chrome Basic Auth Extension - password
|
21
24
|
class Rudra
|
22
25
|
# Supported Browsers
|
23
26
|
BROWSERS = %i[chrome firefox ie safari].freeze
|
@@ -31,13 +34,15 @@ class Rudra
|
|
31
34
|
# Attributes
|
32
35
|
ATTRIBUTES = %i[
|
33
36
|
browser driver install_dir locale
|
34
|
-
headless screen_dir
|
35
|
-
timeout verbose
|
37
|
+
headless window_size screen_dir
|
38
|
+
log_prefix timeout verbose silent
|
39
|
+
chrome_auth_username chrome_auth_password
|
36
40
|
].freeze
|
37
41
|
|
38
42
|
attr_reader :browser, :driver, :install_dir, :locale,
|
39
|
-
:headless, :
|
40
|
-
:timeout, :verbose
|
43
|
+
:headless, :window_size, :screen_dir,
|
44
|
+
:log_prefix, :timeout, :verbose, :silent,
|
45
|
+
:chrome_auth_username, :chrome_auth_password
|
41
46
|
|
42
47
|
# Initialize an instance of Rudra
|
43
48
|
# @param [Hash] options the options to initialize Rudra
|
@@ -47,18 +52,26 @@ class Rudra
|
|
47
52
|
# directory of WebDrivers
|
48
53
|
# @option options [Symbol] :locale (:en) the browser locale
|
49
54
|
# @option options [Boolean] :headless (false) headless mode
|
55
|
+
# @option options [String] :window_size ('1280,720') window size when headless
|
50
56
|
# @option options [String] :screen_dir ('./screens/') the location of screenshots
|
51
|
-
# @option options [String] :log_prefix (' - ') prefix for logging
|
57
|
+
# @option options [String] :log_prefix (' - ') prefix for logging descriptions and methods
|
52
58
|
# @option options [Integer] :timeout (30) implicit_wait timeout
|
53
|
-
# @option options [Boolean] :verbose (
|
59
|
+
# @option options [Boolean] :verbose (false) Turn on/off verbose mode
|
60
|
+
# @option options [Boolean] :silent (false) Turn on/off descriptions
|
61
|
+
# @option options [String] :chrome_auth_username ('') username for Chrome Basic Auth extension
|
62
|
+
# @option options [String] :chrome_auth_password ('') password for Chrome Basic Auth extension
|
54
63
|
def initialize(options = {})
|
55
64
|
self.browser = options.fetch(:browser, :chrome)
|
56
65
|
self.install_dir = options.fetch(:install_dir, './webdrivers/')
|
57
66
|
self.locale = options.fetch(:locale, :en)
|
58
67
|
self.headless = options.fetch(:headless, false)
|
68
|
+
self.window_size = options.fetch(:window_size, '1280,720')
|
59
69
|
self.screen_dir = options.fetch(:screen_dir, './screens/')
|
60
70
|
self.log_prefix = options.fetch(:log_prefix, ' - ')
|
61
|
-
self.verbose = options.fetch(:verbose,
|
71
|
+
self.verbose = options.fetch(:verbose, false)
|
72
|
+
self.silent = options.fetch(:silent, false)
|
73
|
+
self.chrome_auth_username = options.fetch(:chrome_auth_username, '')
|
74
|
+
self.chrome_auth_password = options.fetch(:chrome_auth_password, '')
|
62
75
|
self.main_label = caller_locations(2, 1).first.label
|
63
76
|
|
64
77
|
initialize_driver
|
@@ -104,6 +117,7 @@ class Rudra
|
|
104
117
|
end
|
105
118
|
|
106
119
|
# Send keys to an alert
|
120
|
+
# @param [String] keys keystrokes to send
|
107
121
|
def alert_send_keys(keys)
|
108
122
|
switch_to_alert.send_keys(keys)
|
109
123
|
end
|
@@ -147,6 +161,26 @@ class Rudra
|
|
147
161
|
driver.manage.delete_cookie(name)
|
148
162
|
end
|
149
163
|
|
164
|
+
# Check if an element is found
|
165
|
+
# @param [String, Selenium::WebDriver::Element] locator the locator to
|
166
|
+
# identify the element or Selenium::WebDriver::Element
|
167
|
+
# @param [Integer] seconds seconds before timed out
|
168
|
+
def element_found?(locator, seconds = 1)
|
169
|
+
how, what = parse_locator(locator)
|
170
|
+
|
171
|
+
implicit_wait(seconds)
|
172
|
+
|
173
|
+
begin
|
174
|
+
wait_for(seconds) { driver.find_element(how, what).displayed? }
|
175
|
+
rescue Selenium::WebDriver::Error::TimeoutError
|
176
|
+
false
|
177
|
+
rescue Net::ReadTimeout
|
178
|
+
false
|
179
|
+
ensure
|
180
|
+
implicit_wait(timeout)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
150
184
|
# Execute the given JavaScript
|
151
185
|
# @param [String] script JavaScript source to execute
|
152
186
|
# @param [Selenium::WebDriver::Element, Integer, Float, Boolean, NilClass,
|
@@ -175,7 +209,7 @@ class Rudra
|
|
175
209
|
|
176
210
|
element ||= driver.find_element(how, what)
|
177
211
|
|
178
|
-
|
212
|
+
raise Selenium::WebDriver::Error::NoSuchElementError, "Failed to find element: #{locator}" unless element
|
179
213
|
|
180
214
|
wait_for { element.displayed? }
|
181
215
|
|
@@ -187,8 +221,11 @@ class Rudra
|
|
187
221
|
# @return [Array<Selenium::WebDriver::Element>] the elements found
|
188
222
|
def find_elements(locator)
|
189
223
|
how, what = parse_locator(locator)
|
190
|
-
driver.find_elements(how, what)
|
191
|
-
|
224
|
+
elements = driver.find_elements(how, what)
|
225
|
+
|
226
|
+
raise Selenium::WebDriver::Error::NoSuchElementError, "Failed to find elements: #{locator}" if elements.empty?
|
227
|
+
|
228
|
+
elements
|
192
229
|
end
|
193
230
|
|
194
231
|
# Move forward a single entry in the browser's history
|
@@ -208,7 +245,7 @@ class Rudra
|
|
208
245
|
|
209
246
|
# Maximize the current window
|
210
247
|
def maximize
|
211
|
-
driver.manage.window.maximize
|
248
|
+
driver.manage.window.maximize unless headless
|
212
249
|
end
|
213
250
|
|
214
251
|
# Maximize the current window to the size of the screen
|
@@ -266,6 +303,12 @@ class Rudra
|
|
266
303
|
driver.page_source
|
267
304
|
end
|
268
305
|
|
306
|
+
# Print description in the console
|
307
|
+
# @param [String] description description to show
|
308
|
+
def puts(description)
|
309
|
+
$stdout.puts "#{log_prefix}#{description.chomp}" unless silent
|
310
|
+
end
|
311
|
+
|
269
312
|
# Refresh the current pagef
|
270
313
|
def refresh
|
271
314
|
driver.navigate.refresh
|
@@ -281,13 +324,16 @@ class Rudra
|
|
281
324
|
# Save a PNG screenshot to file
|
282
325
|
# @param [String] filename the filename of PNG screenshot
|
283
326
|
def save_screenshot(filename)
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
@screen_dir,
|
288
|
-
filename.end_with?('.png') ? filename : "#{filename}.png"
|
289
|
-
)
|
327
|
+
file = File.join(
|
328
|
+
@screen_dir,
|
329
|
+
sanitize(filename.end_with?('.png') ? filename : "#{filename}.png")
|
290
330
|
)
|
331
|
+
|
332
|
+
dir = File.dirname(file)
|
333
|
+
|
334
|
+
mkdir(dir) unless Dir.exist?(dir)
|
335
|
+
|
336
|
+
driver.save_screenshot(file)
|
291
337
|
end
|
292
338
|
|
293
339
|
# Switch to the currently active modal dialog
|
@@ -302,6 +348,7 @@ class Rudra
|
|
302
348
|
end
|
303
349
|
|
304
350
|
# Switch to the frame with the given id
|
351
|
+
# @param [String] id the frame id
|
305
352
|
def switch_to_frame(id)
|
306
353
|
driver.switch_to.frame(id)
|
307
354
|
end
|
@@ -337,6 +384,50 @@ class Rudra
|
|
337
384
|
wait_for { find_element(locator).enabled? }
|
338
385
|
end
|
339
386
|
|
387
|
+
# Wait until the element, identified by locator, is found in frame
|
388
|
+
# @param [String] frame_id the frame id
|
389
|
+
# @param [String] locator the locator to identify the element
|
390
|
+
def wait_for_element_found_in_frame(frame_id, locator)
|
391
|
+
switch_to_frame frame_id
|
392
|
+
|
393
|
+
how, what = parse_locator(locator)
|
394
|
+
|
395
|
+
wait_for do
|
396
|
+
begin
|
397
|
+
driver.find_element(how, what)
|
398
|
+
rescue Selenium::WebDriver::Error::NoSuchWindowError
|
399
|
+
false
|
400
|
+
end
|
401
|
+
end
|
402
|
+
end
|
403
|
+
|
404
|
+
# Wait (in seconds) until the element is not displayed
|
405
|
+
# @param [String, Selenium::WebDriver::Element] locator the locator to
|
406
|
+
# identify the element or Selenium::WebDriver::Element
|
407
|
+
# @param [Integer] seconds seconds before timed out
|
408
|
+
def wait_for_not_visible(locator, seconds = 3)
|
409
|
+
how, what = parse_locator(locator)
|
410
|
+
|
411
|
+
implicit_wait(seconds)
|
412
|
+
|
413
|
+
begin
|
414
|
+
wait_for(seconds) do
|
415
|
+
begin
|
416
|
+
elements = driver.find_elements(how, what)
|
417
|
+
elements.empty? || elements.map(&:displayed?).none?
|
418
|
+
rescue Selenium::WebDriver::Error::StaleElementReferenceError
|
419
|
+
false
|
420
|
+
end
|
421
|
+
end
|
422
|
+
rescue Selenium::WebDriver::Error::TimeoutError
|
423
|
+
true
|
424
|
+
rescue Net::ReadTimeout
|
425
|
+
true
|
426
|
+
ensure
|
427
|
+
implicit_wait(timeout)
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
340
431
|
# Wait until the title of the page including the given string
|
341
432
|
# @param [String] string the string to compare
|
342
433
|
def wait_for_title(string)
|
@@ -385,7 +476,7 @@ class Rudra
|
|
385
476
|
# @param [String] attribute the name of the attribute
|
386
477
|
# @return [String, nil] attribute value
|
387
478
|
def attribute(locator, attribute)
|
388
|
-
find_element(locator).
|
479
|
+
find_element(locator).attribute(attribute)
|
389
480
|
end
|
390
481
|
|
391
482
|
# If the element, identified by locator, has the given attribute
|
@@ -420,7 +511,14 @@ class Rudra
|
|
420
511
|
# @param [String, Selenium::WebDriver::Element] locator the locator to
|
421
512
|
# identify the element or Selenium::WebDriver::Element
|
422
513
|
def click(locator)
|
423
|
-
|
514
|
+
wait_for do
|
515
|
+
begin
|
516
|
+
element = find_element(locator)
|
517
|
+
element.enabled? && element.click.nil?
|
518
|
+
rescue Selenium::WebDriver::Error::ElementClickInterceptedError
|
519
|
+
false
|
520
|
+
end
|
521
|
+
end
|
424
522
|
end
|
425
523
|
|
426
524
|
# Click the given element, identified by locator, with an offset
|
@@ -733,6 +831,38 @@ class Rudra
|
|
733
831
|
), find_element(locator), event)
|
734
832
|
end
|
735
833
|
|
834
|
+
# Wait until the element, identified by locator, attribute has value
|
835
|
+
# @param [String, Selenium::WebDriver::Element] locator the locator to identify the element
|
836
|
+
# @param [String] attribute the name of the attribute
|
837
|
+
# @param [String] value the value of the attribute
|
838
|
+
def wait_for_attribute_to_include(locator, attribute, value)
|
839
|
+
how, what = parse_locator(locator)
|
840
|
+
|
841
|
+
wait_for do
|
842
|
+
begin
|
843
|
+
driver.find_element(how, what)&.attribute(attribute)&.downcase&.include?(value.downcase)
|
844
|
+
rescue Selenium::WebDriver::Error::StaleElementReferenceError
|
845
|
+
false
|
846
|
+
end
|
847
|
+
end
|
848
|
+
end
|
849
|
+
|
850
|
+
# Wait until the element, identified by locator, excluding string in text
|
851
|
+
# @param [String, Selenium::WebDriver::Element] locator the locator to
|
852
|
+
# identify the element or Selenium::WebDriver::Element
|
853
|
+
# @param [String] string the string to exclude
|
854
|
+
def wait_for_text_to_exclude(locator, string)
|
855
|
+
wait_for { text(locator).exclude?(string) }
|
856
|
+
end
|
857
|
+
|
858
|
+
# Wait until the element, identified by locator, including string in text
|
859
|
+
# @param [String, Selenium::WebDriver::Element] locator the locator to
|
860
|
+
# identify the element or Selenium::WebDriver::Element
|
861
|
+
# @param [String] string the string to compare
|
862
|
+
def wait_for_text_to_include(locator, string)
|
863
|
+
wait_for { text(locator).include?(string) }
|
864
|
+
end
|
865
|
+
|
736
866
|
#
|
737
867
|
# Tool Functions
|
738
868
|
#
|
@@ -1070,7 +1200,7 @@ class Rudra
|
|
1070
1200
|
end
|
1071
1201
|
|
1072
1202
|
(instance_methods - superclass.instance_methods).map do |method_name|
|
1073
|
-
next if private_method_defined?(method_name) || ATTRIBUTES.include?(method_name)
|
1203
|
+
next if private_method_defined?(method_name) || ATTRIBUTES.include?(method_name) || method_name == :puts
|
1074
1204
|
|
1075
1205
|
original_method = instance_method(method_name)
|
1076
1206
|
|
@@ -1082,7 +1212,8 @@ class Rudra
|
|
1082
1212
|
|
1083
1213
|
private
|
1084
1214
|
|
1085
|
-
|
1215
|
+
attr_accessor :main_label
|
1216
|
+
attr_writer :silent, :window_size, :chrome_auth_username, :chrome_auth_password
|
1086
1217
|
|
1087
1218
|
def browser=(brw)
|
1088
1219
|
unless BROWSERS.include?(brw)
|
@@ -1135,6 +1266,8 @@ class Rudra
|
|
1135
1266
|
def initialize_driver
|
1136
1267
|
@driver = if browser == :chrome
|
1137
1268
|
Selenium::WebDriver.for(:chrome, options: chrome_options)
|
1269
|
+
# elsif browser == :edge
|
1270
|
+
# Selenium::WebDriver.for(:edge, options: edge_options)
|
1138
1271
|
elsif browser == :firefox
|
1139
1272
|
Selenium::WebDriver.for(:firefox, options: firefox_options)
|
1140
1273
|
elsif browser == :ie
|
@@ -1147,7 +1280,14 @@ class Rudra
|
|
1147
1280
|
def chrome_options
|
1148
1281
|
options = Selenium::WebDriver::Chrome::Options.new
|
1149
1282
|
options.add_argument('--disable-notifications')
|
1150
|
-
|
1283
|
+
if headless
|
1284
|
+
options.add_argument('--headless')
|
1285
|
+
options.add_argument("--window-size=#{window_size}")
|
1286
|
+
end
|
1287
|
+
if chrome_auth_username && chrome_auth_password
|
1288
|
+
encoded = chrome_basic_auth_extension(chrome_auth_username, chrome_auth_password)
|
1289
|
+
options.add_encoded_extension(encoded)
|
1290
|
+
end
|
1151
1291
|
options.add_option(
|
1152
1292
|
'excludeSwitches',
|
1153
1293
|
%w[enable-automation enable-logging]
|
@@ -1156,6 +1296,10 @@ class Rudra
|
|
1156
1296
|
options
|
1157
1297
|
end
|
1158
1298
|
|
1299
|
+
# def edge_options
|
1300
|
+
# Selenium::WebDriver::Edge::Options.new
|
1301
|
+
# end
|
1302
|
+
|
1159
1303
|
def firefox_options
|
1160
1304
|
options = Selenium::WebDriver::Firefox::Options.new
|
1161
1305
|
options.add_preference('intl.accept_languages', locale)
|
@@ -1188,24 +1332,76 @@ class Rudra
|
|
1188
1332
|
how.to_sym
|
1189
1333
|
end
|
1190
1334
|
|
1191
|
-
|
1335
|
+
raise Selenium::WebDriver::Error::InvalidSelectorError, "Cannot parse locator: #{locator}" unless HOWS.include?(how)
|
1192
1336
|
|
1193
1337
|
[how, what]
|
1194
1338
|
end
|
1195
1339
|
|
1196
1340
|
def log(method_name, *args)
|
1197
|
-
return unless
|
1341
|
+
return unless verbose && caller_locations(2, 1).first.label == main_label
|
1198
1342
|
|
1199
1343
|
arguments = args.map(&:to_s).join(', ')
|
1200
1344
|
|
1201
|
-
puts
|
1345
|
+
puts log_prefix + (
|
1202
1346
|
arguments.empty? ? method_name.to_s : "#{method_name}(#{arguments})"
|
1203
1347
|
)
|
1204
1348
|
end
|
1205
1349
|
|
1350
|
+
def sanitize(filename)
|
1351
|
+
invalid_characters = ['/', '\\', '?', '%', '*', ':', '|', '"', '<', '>']
|
1352
|
+
invalid_characters.each do |character|
|
1353
|
+
filename.gsub!(character, '')
|
1354
|
+
end
|
1355
|
+
filename
|
1356
|
+
end
|
1357
|
+
|
1206
1358
|
def random_id(length = 8)
|
1207
1359
|
charset = [(0..9), ('a'..'z')].flat_map(&:to_a)
|
1208
1360
|
id = Array.new(length) { charset.sample }.join
|
1209
1361
|
"rudra_#{id}"
|
1210
1362
|
end
|
1363
|
+
|
1364
|
+
def chrome_basic_auth_extension(username, password)
|
1365
|
+
manifest = {
|
1366
|
+
"manifest_version": 2,
|
1367
|
+
"name": 'Rudra Basic Auth Extension',
|
1368
|
+
"version": '1.0.0',
|
1369
|
+
"permissions": ['*://*/*', 'webRequest', 'webRequestBlocking'],
|
1370
|
+
"background": {
|
1371
|
+
"scripts": ['background.js']
|
1372
|
+
}
|
1373
|
+
}
|
1374
|
+
|
1375
|
+
background = <<~JSCRIPT
|
1376
|
+
var username = '#{username}';
|
1377
|
+
var password = '#{password}';
|
1378
|
+
|
1379
|
+
chrome.webRequest.onAuthRequired.addListener(
|
1380
|
+
function handler(details) {
|
1381
|
+
if (username == null) {
|
1382
|
+
return { cancel: true };
|
1383
|
+
}
|
1384
|
+
|
1385
|
+
var authCredentials = { username: username, password: username };
|
1386
|
+
username = password = null;
|
1387
|
+
|
1388
|
+
return { authCredentials: authCredentials };
|
1389
|
+
},
|
1390
|
+
{ urls: ['<all_urls>'] },
|
1391
|
+
['blocking']
|
1392
|
+
);
|
1393
|
+
JSCRIPT
|
1394
|
+
|
1395
|
+
stringio = Zip::OutputStream.write_buffer do |zos|
|
1396
|
+
zos.put_next_entry('manifest.json')
|
1397
|
+
zos.write manifest.to_json
|
1398
|
+
zos.put_next_entry('background.js')
|
1399
|
+
zos.write background
|
1400
|
+
end
|
1401
|
+
# File.open('basic_auth.crx', 'wb') do |f|
|
1402
|
+
# f << stringio.string
|
1403
|
+
# end
|
1404
|
+
|
1405
|
+
Base64.strict_encode64(stringio.string)
|
1406
|
+
end
|
1211
1407
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rudra
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Aaron Chen
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-06-
|
11
|
+
date: 2020-06-18 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: yard
|
@@ -24,6 +24,46 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: 0.9.25
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 3.9.0
|
34
|
+
- - "~>"
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: '3.9'
|
37
|
+
type: :runtime
|
38
|
+
prerelease: false
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 3.9.0
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '3.9'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rubyzip
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 2.3.0
|
54
|
+
- - "~>"
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '2.3'
|
57
|
+
type: :runtime
|
58
|
+
prerelease: false
|
59
|
+
version_requirements: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: 2.3.0
|
64
|
+
- - "~>"
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '2.3'
|
27
67
|
- !ruby/object:Gem::Dependency
|
28
68
|
name: selenium-webdriver
|
29
69
|
requirement: !ruby/object:Gem::Requirement
|