browsery 0.1.0 → 0.2.0
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/.ruby-version +1 -0
- data/bin/browsery +5 -0
- data/browsery.gemspec +8 -0
- data/lib/browsery.rb +22 -0
- data/lib/browsery/connector.rb +287 -0
- data/lib/browsery/console.rb +15 -0
- data/lib/browsery/init.rb +60 -0
- data/lib/browsery/logger.rb +12 -0
- data/lib/browsery/page_objects.rb +23 -0
- data/lib/browsery/page_objects/base.rb +266 -0
- data/lib/browsery/page_objects/element_container.rb +50 -0
- data/lib/browsery/page_objects/overlay/base.rb +85 -0
- data/lib/browsery/page_objects/widgets/base.rb +52 -0
- data/lib/browsery/parallel.rb +265 -0
- data/lib/browsery/runner.rb +111 -0
- data/lib/browsery/settings.rb +114 -0
- data/lib/browsery/test_case.rb +266 -0
- data/lib/browsery/test_cases.rb +7 -0
- data/lib/browsery/utils.rb +10 -0
- data/lib/browsery/utils/assertion_helper.rb +35 -0
- data/lib/browsery/utils/castable.rb +103 -0
- data/lib/browsery/utils/data_generator_helper.rb +145 -0
- data/lib/browsery/utils/loggable.rb +16 -0
- data/lib/browsery/utils/overlay_and_widget_helper.rb +78 -0
- data/lib/browsery/utils/page_object_helper.rb +263 -0
- data/lib/browsery/version.rb +1 -1
- data/lib/minitap/minitest5_browsery.rb +22 -0
- data/lib/minitest/autobot_settings_plugin.rb +83 -0
- data/lib/selenium/webdriver/common/element_browsery.rb +21 -0
- data/lib/tapout/custom_reporters/fancy_tap_reporter.rb +94 -0
- data/lib/yard/tagged_test_case_handler.rb +61 -0
- metadata +131 -5
@@ -0,0 +1,16 @@
|
|
1
|
+
|
2
|
+
module Browsery
|
3
|
+
module Utils
|
4
|
+
|
5
|
+
# Module that injects a convenience method to access the logger.
|
6
|
+
module Loggable
|
7
|
+
|
8
|
+
# Convenience instance method to access the default logger.
|
9
|
+
def logger
|
10
|
+
Browsery.logger
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Browsery
|
2
|
+
module Utils
|
3
|
+
module OverlayAndWidgetHelper
|
4
|
+
# Create widgets of type `name` from `items`, where `name` is the widget
|
5
|
+
# class name, and `items` is a single or an array of WebDriver elements.
|
6
|
+
#
|
7
|
+
# @param name [#to_s] the name of the widget, under `browserys/page_objects/widgets`
|
8
|
+
# to load.
|
9
|
+
# @param items [Enumerable<Selenium::WebDriver::Element>] WebDriver elements.
|
10
|
+
# @return [Enumerable<Browsery::PageObjects::Widgets::Base>]
|
11
|
+
# @raise NameError
|
12
|
+
def get_widgets!(name, items)
|
13
|
+
items = Array(items)
|
14
|
+
return [] if items.empty?
|
15
|
+
|
16
|
+
# Load the widget class
|
17
|
+
klass_name = "browsery/page_objects/widgets/#{name}".camelize
|
18
|
+
klass = begin
|
19
|
+
klass_name.constantize
|
20
|
+
rescue => exc
|
21
|
+
msg = ""
|
22
|
+
msg << "Cannot find widget '#{name}', "
|
23
|
+
msg << "because could not load class '#{klass_name}' "
|
24
|
+
msg << "with underlying error:\n #{exc.class}: #{exc.message}\n"
|
25
|
+
msg << exc.backtrace.map { |str| " #{str}" }.join("\n")
|
26
|
+
raise NameError, msg
|
27
|
+
end
|
28
|
+
|
29
|
+
page = self.page_object
|
30
|
+
|
31
|
+
if items.respond_to?(:map)
|
32
|
+
items.map { |item| klass.new(page, item) }
|
33
|
+
else
|
34
|
+
[klass.new(page, items)]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Create overlay of type `name`, where `name` is the overlay
|
39
|
+
# class name
|
40
|
+
#
|
41
|
+
# @param name [#to_s] the name of the overlay, under `browserys/page_objects/widgets`
|
42
|
+
# to load.
|
43
|
+
# @param items [Enumerable<Selenium::WebDriver::Element>] WebDriver elements.
|
44
|
+
# @return [Enumerable<Browsery::PageObjects::Overlay::Base>]
|
45
|
+
# @raise NameError
|
46
|
+
def get_overlay!(name)
|
47
|
+
# Load the Overlay class
|
48
|
+
klass_name = "browsery/page_objects/overlay/#{name}".camelize
|
49
|
+
klass = begin
|
50
|
+
klass_name.constantize
|
51
|
+
rescue => exc
|
52
|
+
msg = ""
|
53
|
+
msg << "Cannot find overlay '#{name}', "
|
54
|
+
msg << "because could not load class '#{klass_name}' "
|
55
|
+
msg << "with underlying error:\n #{exc.class}: #{exc.message}\n"
|
56
|
+
msg << exc.backtrace.map { |str| " #{str}" }.join("\n")
|
57
|
+
raise NameError, msg
|
58
|
+
end
|
59
|
+
page = self.page_object
|
60
|
+
instance = klass.new(page)
|
61
|
+
# Overlay is triggered to show when there's certain interaction on the page
|
62
|
+
# So validate! is necessary for loading some elements on some overlays
|
63
|
+
begin
|
64
|
+
instance.validate!
|
65
|
+
rescue Minitest::Assertion => exc
|
66
|
+
raise Browsery::PageObjects::InvalidePageState, "#{klass}: #{exc.message}"
|
67
|
+
end
|
68
|
+
instance
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
def page_object
|
73
|
+
raise NotImplementedError, "classes including OverlayAndWidgetHelper must override :page_object"
|
74
|
+
end
|
75
|
+
|
76
|
+
end #OverlayAndWidgetHelper
|
77
|
+
end #Utils
|
78
|
+
end #Browsery
|
@@ -0,0 +1,263 @@
|
|
1
|
+
module Browsery
|
2
|
+
module Utils
|
3
|
+
|
4
|
+
# Page object-related helper methods.
|
5
|
+
module PageObjectHelper
|
6
|
+
|
7
|
+
# Helper method to instantiate a new page object. This method should only
|
8
|
+
# be used when first loading; subsequent page objects are automatically
|
9
|
+
# instantiated by calling #cast on the page object.
|
10
|
+
#
|
11
|
+
# Pass optional parameter Driver, which can be initialized in test and will override the global driver here.
|
12
|
+
#
|
13
|
+
# @param name [String, Driver]
|
14
|
+
# @return [PageObject::Base]
|
15
|
+
def page(name, override_driver=nil)
|
16
|
+
# Get the fully-qualified class name
|
17
|
+
klass_name = "browsery/page_objects/#{name}".camelize
|
18
|
+
klass = begin
|
19
|
+
klass_name.constantize
|
20
|
+
rescue => exc
|
21
|
+
msg = ""
|
22
|
+
msg << "Cannot find page object '#{name}', "
|
23
|
+
msg << "because could not load class '#{klass_name}' "
|
24
|
+
msg << "with underlying error:\n #{exc.class}: #{exc.message}\n"
|
25
|
+
msg << exc.backtrace.map { |str| " #{str}" }.join("\n")
|
26
|
+
raise NameError, msg
|
27
|
+
end
|
28
|
+
|
29
|
+
# Get a default connector
|
30
|
+
@driver = Browsery::Connector.get_default if override_driver.nil?
|
31
|
+
@driver = override_driver if !override_driver.nil?
|
32
|
+
instance = klass.new(@driver)
|
33
|
+
|
34
|
+
# Set SauceLabs session(job) name to test's name if running on Saucelabs
|
35
|
+
begin
|
36
|
+
update_sauce_session_name if connector_is_saucelabs? && !@driver.nil?
|
37
|
+
rescue
|
38
|
+
self.logger.debug "Failed setting saucelabs session name for #{name()}"
|
39
|
+
end
|
40
|
+
|
41
|
+
# Before visiting the page, do any pre-processing necessary, if any,
|
42
|
+
# but only visit the page if the pre-processing succeeds
|
43
|
+
if block_given?
|
44
|
+
retval = yield instance
|
45
|
+
instance.go! if retval
|
46
|
+
else
|
47
|
+
instance.go! if override_driver.nil?
|
48
|
+
end
|
49
|
+
|
50
|
+
# similar like casting a page, necessary to validate some element on a page
|
51
|
+
begin
|
52
|
+
instance.validate!
|
53
|
+
rescue Minitest::Assertion => exc
|
54
|
+
raise Browsery::PageObjects::InvalidePageState, "#{klass}: #{exc.message}"
|
55
|
+
end
|
56
|
+
|
57
|
+
# Return the instance as-is
|
58
|
+
instance
|
59
|
+
end
|
60
|
+
|
61
|
+
# Local teardown for page objects. Any page objects that are loaded will
|
62
|
+
# be finalized upon teardown.
|
63
|
+
#
|
64
|
+
# @return [void]
|
65
|
+
def teardown
|
66
|
+
if !passed? && !skipped? && !@driver.nil?
|
67
|
+
json_save_to_ever_failed if Browsery.settings.rerun_failure
|
68
|
+
print_sauce_link if connector_is_saucelabs?
|
69
|
+
take_screenshot
|
70
|
+
end
|
71
|
+
begin
|
72
|
+
update_sauce_session_status if connector_is_saucelabs? && !@driver.nil? && !skipped?
|
73
|
+
rescue
|
74
|
+
self.logger.debug "Failed setting saucelabs session status for #{name()}"
|
75
|
+
end
|
76
|
+
|
77
|
+
Browsery::Connector.finalize!
|
78
|
+
super
|
79
|
+
end
|
80
|
+
|
81
|
+
# Take screenshot and save as png with test name as file name
|
82
|
+
def take_screenshot
|
83
|
+
@driver.save_screenshot("logs/#{name}.png")
|
84
|
+
end
|
85
|
+
|
86
|
+
# Create new/override same file ever_failed_tests.json with fail count
|
87
|
+
def json_save_to_ever_failed
|
88
|
+
ever_failed_tests = 'logs/tap_results/ever_failed_tests.json'
|
89
|
+
data_hash = {}
|
90
|
+
if File.file?(ever_failed_tests) && !File.zero?(ever_failed_tests)
|
91
|
+
data_hash = JSON.parse(File.read(ever_failed_tests))
|
92
|
+
end
|
93
|
+
|
94
|
+
if data_hash[name]
|
95
|
+
data_hash[name]["fail_count"] += 1
|
96
|
+
else
|
97
|
+
data_hash[name] = { "fail_count" => 1 }
|
98
|
+
end
|
99
|
+
begin
|
100
|
+
data_hash[name]["last_fail_on_sauce"] = "saucelabs.com/tests/#{@driver.session_id}"
|
101
|
+
rescue
|
102
|
+
self.logger.debug "Failed setting last_fail_on_sauce, driver may not be available"
|
103
|
+
end
|
104
|
+
|
105
|
+
File.open(ever_failed_tests, 'w+') do |file|
|
106
|
+
file.write JSON.pretty_generate(data_hash)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Print out a link of a saucelabs's job when a test is not passed
|
111
|
+
# Rescue to skip this step for tests like cube tracking
|
112
|
+
def print_sauce_link
|
113
|
+
begin
|
114
|
+
puts "Find test on saucelabs: https://saucelabs.com/tests/#{@driver.session_id}"
|
115
|
+
rescue
|
116
|
+
puts 'can not retrieve driver session id, no link to saucelabs'
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Update SauceLabs session(job) name and build number/name
|
121
|
+
def update_sauce_session_name
|
122
|
+
http_auth = Browsery.settings.sauce_session_http_auth(@driver)
|
123
|
+
body = { 'name' => name() }
|
124
|
+
unless (build_number = ENV['JENKINS_BUILD_NUMBER']).nil?
|
125
|
+
body['build'] = build_number
|
126
|
+
end
|
127
|
+
RestClient.put(http_auth, body.to_json, {:content_type => "application/json"})
|
128
|
+
end
|
129
|
+
|
130
|
+
# Update session(job) status if test is not skipped
|
131
|
+
def update_sauce_session_status
|
132
|
+
http_auth = Browsery.settings.sauce_session_http_auth(@driver)
|
133
|
+
body = { "passed" => passed? }
|
134
|
+
RestClient.put(http_auth, body.to_json, {:content_type => "application/json"})
|
135
|
+
end
|
136
|
+
|
137
|
+
def connector_is_saucelabs?
|
138
|
+
Browsery.settings.connector.include? 'saucelabs'
|
139
|
+
end
|
140
|
+
|
141
|
+
# Generic page object helper method to clear and send keys to a web element found by driver
|
142
|
+
# @param [Element, String]
|
143
|
+
def put_value(web_element, value)
|
144
|
+
web_element.clear
|
145
|
+
web_element.send_keys(value)
|
146
|
+
end
|
147
|
+
|
148
|
+
# Helper method for retrieving value from yml file
|
149
|
+
# todo should be moved to FileHelper.rb once we created this file in utils
|
150
|
+
# @param [String, String]
|
151
|
+
# keys, eg. "timeouts:implicit_wait"
|
152
|
+
def read_yml(file_name, keys)
|
153
|
+
data = Hash.new
|
154
|
+
begin
|
155
|
+
data = YAML.load_file "#{file_name}"
|
156
|
+
rescue
|
157
|
+
raise Exception, "File #{file_name} doesn't exist" unless File.exist?(file_name)
|
158
|
+
rescue
|
159
|
+
raise YAMLErrors, "Failed to load #{file_name}"
|
160
|
+
end
|
161
|
+
keys_array = keys.split(/:/)
|
162
|
+
value = data
|
163
|
+
keys_array.each do |key|
|
164
|
+
value = value[key]
|
165
|
+
end
|
166
|
+
value
|
167
|
+
end
|
168
|
+
|
169
|
+
# Retry a block of code for a number of times
|
170
|
+
def retry_with_count(count, &block)
|
171
|
+
try = 0
|
172
|
+
count.times do
|
173
|
+
try += 1
|
174
|
+
begin
|
175
|
+
block.call
|
176
|
+
return true
|
177
|
+
rescue Exception => e
|
178
|
+
Browsery.logger.warn "Exception: #{e}\nfrom\n#{block.source_location.join(':')}"
|
179
|
+
Browsery.logger.warn "Retrying" if try < count
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def with_url_change_wait(&block)
|
185
|
+
starting_url = @driver.current_url
|
186
|
+
block.call
|
187
|
+
wait(timeout: 15, message: 'Timeout waiting for URL to change')
|
188
|
+
.until { @driver.current_url != starting_url }
|
189
|
+
end
|
190
|
+
|
191
|
+
# Check if a web element exists on page or not, without wait
|
192
|
+
def is_element_present?(how, what, driver = nil)
|
193
|
+
element_appeared?(how, what, driver)
|
194
|
+
end
|
195
|
+
|
196
|
+
# Check if a web element exists and displayed on page or not, without wait
|
197
|
+
def is_element_present_and_displayed?(how, what, driver = nil)
|
198
|
+
element_appeared?(how, what, driver, check_display = true)
|
199
|
+
end
|
200
|
+
|
201
|
+
def wait_for_element_to_display(how, what, friendly_name = "element")
|
202
|
+
wait(timeout: 15, message: "Timeout waiting for #{friendly_name} to display")
|
203
|
+
.until {is_element_present_and_displayed?(how, what)}
|
204
|
+
end
|
205
|
+
|
206
|
+
def wait_for_element_to_be_present(how, what, friendly_name = "element")
|
207
|
+
wait(timeout: 15, message: "Timeout waiting for #{friendly_name} to be present")
|
208
|
+
.until {is_element_present?(how, what)}
|
209
|
+
end
|
210
|
+
|
211
|
+
# Useful when you want to wait for the status of an element attribute to change
|
212
|
+
# Example: the class attribute of <body> changes to include 'logged-in' when a user signs in to rent.com
|
213
|
+
# Example usage: wait_for_attribute_status_change(:css, 'body', 'class', 'logged-in', 'sign in')
|
214
|
+
def wait_for_attribute_to_have_value(how, what, attribute, value, friendly_name = "attribute")
|
215
|
+
wait(timeout: 15, message: "Timeout waiting for #{friendly_name} status to update")
|
216
|
+
.until { driver.find_element(how, what).attribute(attribute).include?(value) rescue retry }
|
217
|
+
end
|
218
|
+
|
219
|
+
def current_page(calling_page)
|
220
|
+
calling_page.class.to_s.split('::').last.downcase
|
221
|
+
end
|
222
|
+
|
223
|
+
private
|
224
|
+
|
225
|
+
# @param eg. (:css, 'button.cancel') or (*BUTTON_SUBMIT_SEARCH)
|
226
|
+
# @param also has an optional parameter-driver, which can be @element when calling this method in a widget object
|
227
|
+
# @return [boolean]
|
228
|
+
def element_appeared?(how, what, driver = nil, check_display = false)
|
229
|
+
original_timeout = read_yml("config/browsery/connectors/saucelabs.yml", "timeouts:implicit_wait")
|
230
|
+
@driver.manage.timeouts.implicit_wait = 0
|
231
|
+
result = false
|
232
|
+
parent_element = @driver if driver == nil
|
233
|
+
parent_element = driver if driver != nil
|
234
|
+
elements = parent_element.find_elements(how, what)
|
235
|
+
if check_display
|
236
|
+
begin
|
237
|
+
result = true if elements.size() > 0 && elements[0].displayed?
|
238
|
+
rescue
|
239
|
+
result = false
|
240
|
+
end
|
241
|
+
else
|
242
|
+
result = true if elements.size() > 0
|
243
|
+
end
|
244
|
+
@driver.manage.timeouts.implicit_wait = original_timeout
|
245
|
+
result
|
246
|
+
end
|
247
|
+
|
248
|
+
# Method that overrides click to send the enter key to the element if the current browser
|
249
|
+
# is internet explorer. Used when sending the enter key to the element will work
|
250
|
+
def browser_safe_click(element)
|
251
|
+
driver.browser == :internet_explorer ? element.send_keys(:enter) : element.click
|
252
|
+
end
|
253
|
+
|
254
|
+
# Method that overrides click to send the space key to the checkbox if the current browser
|
255
|
+
# is internet explorer. Used when sending the space key to the checkbox will work
|
256
|
+
def browser_safe_checkbox_click(element)
|
257
|
+
(driver.browser == :internet_explorer || driver.browser == :firefox) ? element.send_keys(:space) : element.click
|
258
|
+
end
|
259
|
+
|
260
|
+
end
|
261
|
+
|
262
|
+
end
|
263
|
+
end
|
data/lib/browsery/version.rb
CHANGED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'minitap'
|
2
|
+
|
3
|
+
module Minitest
|
4
|
+
|
5
|
+
##
|
6
|
+
# Base class for TapY and TapJ runners.
|
7
|
+
#
|
8
|
+
class Minitap
|
9
|
+
|
10
|
+
def tapout_before_case(test_case)
|
11
|
+
doc = {
|
12
|
+
'type' => 'case',
|
13
|
+
'subtype' => '',
|
14
|
+
'label' => "#{test_case}",
|
15
|
+
'level' => 0
|
16
|
+
}
|
17
|
+
return doc
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module Minitest
|
2
|
+
|
3
|
+
# Minitest plugin: browsery_settings
|
4
|
+
#
|
5
|
+
# This is where the options are propagated to +Browsery.settings+.
|
6
|
+
def self.plugin_browsery_settings_init(options)
|
7
|
+
Browsery.settings = options
|
8
|
+
|
9
|
+
Browsery.logger = Browsery::Logger.new('browsery.log', 'daily').tap do |logger|
|
10
|
+
logger.formatter = proc do |sev, ts, prog, msg|
|
11
|
+
msg = msg.inspect unless String === msg
|
12
|
+
"#{ts.strftime('%Y-%m-%dT%H:%M:%S.%6N')} #{sev}: #{String === msg ? msg : msg.inspect}\n"
|
13
|
+
end
|
14
|
+
logger.level = case Browsery.settings.verbosity_level
|
15
|
+
when 0
|
16
|
+
Logger::WARN
|
17
|
+
when 1
|
18
|
+
Logger::INFO
|
19
|
+
else
|
20
|
+
Logger::DEBUG
|
21
|
+
end
|
22
|
+
logger.info("Booting up with arguments: #{options[:args].inspect}")
|
23
|
+
at_exit { logger.info("Shutting down") }
|
24
|
+
end
|
25
|
+
|
26
|
+
Browsery::Console.bootstrap! if options[:console]
|
27
|
+
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
# Minitest plugin: browsery_settings
|
32
|
+
#
|
33
|
+
# This plugin for minitest injects browsery-specific command-line arguments, and
|
34
|
+
# passes it along to browsery.
|
35
|
+
def self.plugin_browsery_settings_options(parser, options)
|
36
|
+
options[:auto_finalize] = true
|
37
|
+
parser.on('-Q', '--no-auto-quit-driver', "Don't automatically quit the driver after a test case") do |value|
|
38
|
+
options[:auto_finalize] = value
|
39
|
+
end
|
40
|
+
|
41
|
+
options[:connector] = ENV['BROWSERY_CONNECTOR'] if ENV.has_key?('BROWSERY_CONNECTOR')
|
42
|
+
parser.on('-c', '--connector TYPE', 'Run using a specific connector profile') do |value|
|
43
|
+
options[:connector] = value
|
44
|
+
end
|
45
|
+
|
46
|
+
options[:env] = ENV['BROWSERY_ENV'] if ENV.has_key?('BROWSERY_ENV')
|
47
|
+
parser.on('-e', '--env ENV', 'Run against a specific environment, host_env') do |value|
|
48
|
+
options[:env] = value
|
49
|
+
end
|
50
|
+
|
51
|
+
options[:console] = false
|
52
|
+
parser.on('-i', '--console', 'Run an interactive session within the context of an empty test') do |value|
|
53
|
+
options[:console] = true
|
54
|
+
end
|
55
|
+
|
56
|
+
options[:reuse_driver] = false
|
57
|
+
parser.on('-r', '--reuse-driver', "Reuse driver between tests") do |value|
|
58
|
+
options[:reuse_driver] = value
|
59
|
+
end
|
60
|
+
|
61
|
+
parser.on('-t', '--tag TAGLIST', 'Run only tests matching a specific tag, tags, or tagsets') do |value|
|
62
|
+
options[:tags] ||= [ ]
|
63
|
+
options[:tags] << value.to_s.split(',').map { |t| t.to_sym }
|
64
|
+
end
|
65
|
+
|
66
|
+
options[:verbosity_level] = 0
|
67
|
+
parser.on('-v', '--verbose', 'Output verbose logs to the log file') do |value|
|
68
|
+
options[:verbosity_level] += 1
|
69
|
+
end
|
70
|
+
|
71
|
+
options[:parallel] = 0
|
72
|
+
parser.on('-P', '--parallel PARALLEL', 'Run any number of tests in parallel') do |value|
|
73
|
+
options[:parallel] = value
|
74
|
+
end
|
75
|
+
|
76
|
+
options[:rerun_failure] = false
|
77
|
+
parser.on('-R', '--rerun-failure [RERUN]', 'Rerun failing test; If enabled, can set number of times to rerun') do |value|
|
78
|
+
integer_value = value.nil? ? 1 : value.to_i
|
79
|
+
options[:rerun_failure] = integer_value
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|