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.
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Browsery
2
- VERSION = '0.1.0'
2
+ VERSION = '0.2.0'
3
3
  end
@@ -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