browsery 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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