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,266 @@
|
|
1
|
+
require 'minitest/assertions'
|
2
|
+
|
3
|
+
module Browsery
|
4
|
+
module PageObjects
|
5
|
+
|
6
|
+
# The base page object. All page objects should be a subclass of this.
|
7
|
+
# Every subclass must implement the following class methods:
|
8
|
+
#
|
9
|
+
# expected_path
|
10
|
+
#
|
11
|
+
# All methods added here will be available to all subclasses, so do so
|
12
|
+
# sparingly. This class has access to assertions, which should only be
|
13
|
+
# used to validate the page.
|
14
|
+
class Base
|
15
|
+
include Minitest::Assertions
|
16
|
+
include Utils::Castable
|
17
|
+
include Utils::Loggable
|
18
|
+
include Utils::PageObjectHelper
|
19
|
+
include Utils::OverlayAndWidgetHelper
|
20
|
+
extend ElementContainer
|
21
|
+
|
22
|
+
attr_accessor :assertions
|
23
|
+
attr_accessor :failures
|
24
|
+
attr_reader :driver
|
25
|
+
|
26
|
+
# Given a set of arguments (no arguments by default), return the expected
|
27
|
+
# path to the page, which must only have file path and query-string.
|
28
|
+
#
|
29
|
+
# @param args [String] one or more arguments to be used in calculating
|
30
|
+
# the expected path, if any.
|
31
|
+
# @return [String] the expected path.
|
32
|
+
def self.expected_path(*args)
|
33
|
+
raise NotImplementedError, "expected_path is not defined for #{self}"
|
34
|
+
end
|
35
|
+
|
36
|
+
# Initializes a new page object from the driver. When a page is initialized,
|
37
|
+
# no validation occurs. As such, do not call this method directly. Rather,
|
38
|
+
# use PageObjectHelper#page in a test case, or #cast in another page object.
|
39
|
+
#
|
40
|
+
# @param driver [Selenium::WebDriver] The WebDriver instance.
|
41
|
+
def initialize(driver)
|
42
|
+
@driver = driver
|
43
|
+
|
44
|
+
@assertions = 0
|
45
|
+
@failures = []
|
46
|
+
end
|
47
|
+
|
48
|
+
def find_first(how, what)
|
49
|
+
driver.find_element(how, what)
|
50
|
+
end
|
51
|
+
|
52
|
+
def find_all(how, what)
|
53
|
+
driver.all(how, what)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns the current path loaded in the driver.
|
57
|
+
#
|
58
|
+
# @return [String] The current path, without hostname.
|
59
|
+
def current_path
|
60
|
+
current_url.path
|
61
|
+
end
|
62
|
+
|
63
|
+
# Returns the current URL loaded in the driver.
|
64
|
+
#
|
65
|
+
# @return [String] The current URL, including hostname.
|
66
|
+
def current_url
|
67
|
+
URI.parse(driver.current_url)
|
68
|
+
end
|
69
|
+
|
70
|
+
## interface for Overlay And Widget Helper version of get_widgets! and get_overlay!
|
71
|
+
def page_object
|
72
|
+
self
|
73
|
+
end
|
74
|
+
|
75
|
+
# Instructs the driver to visit the {expected_path}.
|
76
|
+
#
|
77
|
+
# @param args [*Object] optional parameters to pass into {expected_path}.
|
78
|
+
def go!(*args)
|
79
|
+
driver.get(driver.url_for(self.class.expected_path(*args)))
|
80
|
+
end
|
81
|
+
|
82
|
+
# Check that the page includes a certain string.
|
83
|
+
#
|
84
|
+
# @param value [String] the string to search
|
85
|
+
# @return [Boolean]
|
86
|
+
def include?(value)
|
87
|
+
driver.page_source.include?(value)
|
88
|
+
end
|
89
|
+
|
90
|
+
# Retrieves all META tags with a `name` attribute on the current page.
|
91
|
+
def meta
|
92
|
+
tags = driver.all(:css, 'meta[name]')
|
93
|
+
tags.inject(Hash.new) do |vals, tag|
|
94
|
+
vals[tag.attribute(:name)] = tag.attribute(:content) if tag.attribute(:name)
|
95
|
+
vals
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def headline
|
100
|
+
driver.find_element(:css, 'body div.site-content h1').text
|
101
|
+
end
|
102
|
+
|
103
|
+
# Get page title from any page
|
104
|
+
def title
|
105
|
+
driver.title
|
106
|
+
end
|
107
|
+
|
108
|
+
# By default, any driver state is accepted for any page. This method
|
109
|
+
# should be overridden in subclasses.
|
110
|
+
def validate!
|
111
|
+
true
|
112
|
+
end
|
113
|
+
|
114
|
+
# Wait for all dom events to load
|
115
|
+
def wait_for_dom(timeout = 15)
|
116
|
+
uuid = SecureRandom.uuid
|
117
|
+
# make sure body is loaded before appending anything to it
|
118
|
+
wait(timeout: timeout, msg: "Timeout after waiting #{timeout} for body to load").until do
|
119
|
+
is_element_present?(:css, 'body')
|
120
|
+
end
|
121
|
+
driver.execute_script <<-EOS
|
122
|
+
_.defer(function() {
|
123
|
+
$('body').append("<div id='#{uuid}'></div>");
|
124
|
+
});
|
125
|
+
EOS
|
126
|
+
wait(timeout: timeout, msg: "Timeout after waiting #{timeout} for all dom events to finish").until do
|
127
|
+
is_element_present?(:css, "div[id='#{uuid}']")
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Wait on all AJAX requests to finish
|
132
|
+
def wait_for_ajax(timeout = 15)
|
133
|
+
wait(timeout: timeout, msg: "Timeout after waiting #{timeout} for all ajax requests to finish").until do
|
134
|
+
driver.execute_script 'return window.jQuery != undefined && jQuery.active == 0'
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Explicitly wait for a certain condition to be true:
|
139
|
+
# wait.until { driver.find_element(:css, 'body.tmpl-srp') }
|
140
|
+
# when timeout is not specified, default timeout 5 sec will be used
|
141
|
+
# when timeout is larger than 15, max timeout 15 sec will be used
|
142
|
+
def wait(opts = {})
|
143
|
+
if !opts[:timeout].nil? && opts[:timeout] > 15
|
144
|
+
puts "WARNING: #{opts[:timeout]} sec timeout is NOT supported by wait method,
|
145
|
+
max timeout 15 sec will be used instead"
|
146
|
+
opts[:timeout] = 15
|
147
|
+
end
|
148
|
+
Selenium::WebDriver::Wait.new(opts)
|
149
|
+
end
|
150
|
+
|
151
|
+
# Wrap blocks acting on Selenium elements and catch errors they
|
152
|
+
# raise. This probably qualifies as a Dumb LISPer Trick. If there's a
|
153
|
+
# better Ruby-ish way to do this, I welcome it. [~jacord]
|
154
|
+
def with_rescue(lbl, &blk)
|
155
|
+
yield ## run the block
|
156
|
+
## rescue errors. Rerunning may help, but we can also test for specific
|
157
|
+
## problems.
|
158
|
+
rescue Selenium::WebDriver::Error::ElementNotVisibleError => e
|
159
|
+
## The element is in the DOM but e.visible? is 'false'. Retry may help.
|
160
|
+
logger.debug "Retrying #{lbl}: #{e.class}"
|
161
|
+
yield
|
162
|
+
rescue Selenium::WebDriver::Error::StaleElementReferenceError => e
|
163
|
+
## The page has changed and invalidated your element. Retry may help.
|
164
|
+
logger.debug "Retrying #{lbl}: #{e.class}"
|
165
|
+
yield
|
166
|
+
rescue Selenium::WebDriver::Error::NoSuchElementError => e
|
167
|
+
## Raised by get_element(s). Retry MAY help, but check first for HTTP
|
168
|
+
## 500, which may be best handled higher up the stack.
|
169
|
+
logger.debug "Recovering from NoSuchElementError during #{lbl}"
|
170
|
+
raise_on_error_page
|
171
|
+
## If we got past the above, retry the block.
|
172
|
+
logger.debug "Retrying #{lbl}: #{e.class}"
|
173
|
+
yield
|
174
|
+
end
|
175
|
+
|
176
|
+
## Wrap an action, wait for page title change. This function eliminates
|
177
|
+
## some error-prone boilerplate around fetching page titles
|
178
|
+
def with_page_title_wait(&blk)
|
179
|
+
title = driver.title
|
180
|
+
yield
|
181
|
+
wait_for_title_change(title)
|
182
|
+
end
|
183
|
+
|
184
|
+
# returns the all the page source of a page, useful for debugging
|
185
|
+
#
|
186
|
+
def page_source
|
187
|
+
driver.page_source
|
188
|
+
end
|
189
|
+
|
190
|
+
## PageObject validate! helper. Raises RuntimeError if one of our error
|
191
|
+
## pages is displaying. This can prevent a test from taking the entire
|
192
|
+
## implicit_wait before announcing error. [~jacord]
|
193
|
+
def raise_on_error_page
|
194
|
+
logger.debug "raise_on_error_page"
|
195
|
+
title = ''
|
196
|
+
begin
|
197
|
+
title = driver.title
|
198
|
+
rescue ReadTimeout
|
199
|
+
logger.debug 'ReadTimeout exception was thrown while trying to execute driver.title'
|
200
|
+
logger.debug 'ignore exception and proceed'
|
201
|
+
end
|
202
|
+
title = driver.title
|
203
|
+
logger.debug "Page Title: '#{title}'"
|
204
|
+
raise "HTTP 500 Error" if %r/Internal Server Error/ =~ title
|
205
|
+
raise "HTTP 503 Error" if %r/503 Service Temporarily Unavailable/ =~ title
|
206
|
+
raise "HTTP 404 Error" if %r/Error 404: Page Not Found/ =~ title
|
207
|
+
|
208
|
+
header = driver.find_element('body h1') rescue nil
|
209
|
+
|
210
|
+
unless header.nil?
|
211
|
+
raise "HTTP 500 Error" if header.text == 'Internal Server Error'
|
212
|
+
end
|
213
|
+
|
214
|
+
end
|
215
|
+
|
216
|
+
# click on a link on any page, cast a new page and return it
|
217
|
+
def click_on_link!(link_text, page_name)
|
218
|
+
driver.find_element(:link, link_text).location_once_scrolled_into_view
|
219
|
+
driver.find_element(:link, link_text).click
|
220
|
+
# check user angent, if it's on IE, wait 2sec for the title change
|
221
|
+
sleep 2 if driver.browser == :ie # todo remove this if every page has wait for title change in validate!
|
222
|
+
#sleep 5 #wait for 5 secs
|
223
|
+
logger.debug "click_on_link '#{link_text}'"
|
224
|
+
cast(page_name)
|
225
|
+
end
|
226
|
+
|
227
|
+
def wait_for_title_change(title)
|
228
|
+
title = driver.title if title.nil?
|
229
|
+
logger.debug("Waiting for title change from '#{title}'")
|
230
|
+
wait(timeout: 15, message: "Waited 15 sec for page transition")
|
231
|
+
.until { driver.title != title }
|
232
|
+
logger.debug("Arrived at #{driver.title}")
|
233
|
+
end
|
234
|
+
|
235
|
+
def wait_for_link(link_text)
|
236
|
+
message = "waited 15 sec, can't find link #{link_text} on page"
|
237
|
+
wait(timeout: 15, message: message).until{ driver.find_element(:link, link_text) }
|
238
|
+
|
239
|
+
unless driver.find_element(:link, link_text).displayed?
|
240
|
+
driver.navigate.refresh
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
# example usage:
|
245
|
+
# original_url = driver.current_url
|
246
|
+
# driver.find_element(*LINK_REGISTER).click # do some action that should cause url to change
|
247
|
+
# wait_for_url_change(original_url)
|
248
|
+
def wait_for_url_change(original_url)
|
249
|
+
time = 15
|
250
|
+
message = "waited #{time} sec, url is still #{original_url}, was expecting it to change"
|
251
|
+
wait(timeout: time, message: message).until { driver.current_url != original_url }
|
252
|
+
end
|
253
|
+
|
254
|
+
def go_to_page!(url, page_type = :base)
|
255
|
+
driver.navigate.to(url)
|
256
|
+
cast(page_type)
|
257
|
+
end
|
258
|
+
|
259
|
+
def go_to_subpage!(url_path, page_type = :base)
|
260
|
+
driver.navigate.to(driver.url_for(url_path))
|
261
|
+
cast(page_type)
|
262
|
+
end
|
263
|
+
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Browsery
|
2
|
+
module PageObjects
|
3
|
+
module ElementContainer
|
4
|
+
|
5
|
+
def element(element_name, *find_args)
|
6
|
+
build element_name, *find_args do |how, what|
|
7
|
+
define_method element_name.to_s do
|
8
|
+
find_first(how, what)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def elements(collection_name, *find_args)
|
14
|
+
build collection_name, *find_args do |how, what|
|
15
|
+
define_method collection_name.to_s do
|
16
|
+
find_all(how, what)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
alias_method :collection, :elements
|
21
|
+
|
22
|
+
def add_to_mapped_items(item)
|
23
|
+
@mapped_items ||= []
|
24
|
+
@mapped_items << item.to_s
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def build(name, *find_args)
|
30
|
+
if find_args.empty?
|
31
|
+
create_no_selector name
|
32
|
+
else
|
33
|
+
add_to_mapped_items name
|
34
|
+
if find_args.size == 1
|
35
|
+
yield(:css, *find_args)
|
36
|
+
else
|
37
|
+
yield(*find_args)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def create_no_selector(method_name)
|
43
|
+
define_method method_name do
|
44
|
+
fail Browsery::NoSelectorForElement.new, "#{self.class.name} => :#{method_name} needs a selector"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module Browsery
|
2
|
+
module PageObjects
|
3
|
+
module Overlay
|
4
|
+
|
5
|
+
# A Overlay represents a portion (an element) of a page that is repeated
|
6
|
+
# or reproduced multiple times, either on the same page, or across multiple
|
7
|
+
# page objects or page modules.
|
8
|
+
class Base
|
9
|
+
include Utils::Castable
|
10
|
+
include Utils::PageObjectHelper
|
11
|
+
include Utils::OverlayAndWidgetHelper
|
12
|
+
extend ElementContainer
|
13
|
+
|
14
|
+
attr_reader :driver
|
15
|
+
|
16
|
+
def initialize(page)
|
17
|
+
@driver = page.driver
|
18
|
+
@page = page
|
19
|
+
|
20
|
+
# works here but not in initialize of base of page objects
|
21
|
+
# because a page instance is already present when opening an overlay
|
22
|
+
end
|
23
|
+
|
24
|
+
## for overlay that include Utils::OverlayAndWidgetHelper
|
25
|
+
def page_object
|
26
|
+
@page
|
27
|
+
end
|
28
|
+
|
29
|
+
def find_first(how, what)
|
30
|
+
driver.find_element(how, what)
|
31
|
+
end
|
32
|
+
|
33
|
+
def find_all(how, what)
|
34
|
+
driver.all(how, what)
|
35
|
+
end
|
36
|
+
|
37
|
+
# By default, any driver state is accepted for any page. This method
|
38
|
+
# should be overridden in subclasses.
|
39
|
+
def validate!
|
40
|
+
true
|
41
|
+
end
|
42
|
+
|
43
|
+
# Wait for all dom events to load
|
44
|
+
def wait_for_dom(timeout = 15)
|
45
|
+
uuid = SecureRandom.uuid
|
46
|
+
# make sure body is loaded before appending anything to it
|
47
|
+
wait(timeout: timeout, msg: "Timeout after waiting #{timeout} for body to load").until do
|
48
|
+
is_element_present?(:css, 'body')
|
49
|
+
end
|
50
|
+
driver.execute_script <<-EOS
|
51
|
+
_.defer(function() {
|
52
|
+
$('body').append("<div id='#{uuid}'></div>");
|
53
|
+
});
|
54
|
+
EOS
|
55
|
+
wait(timeout: timeout, msg: "Timeout after waiting #{timeout} for all dom events to finish").until do
|
56
|
+
is_element_present?(:css, "div[id='#{uuid}']")
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Wait on all AJAX requests to finish
|
61
|
+
def wait_for_ajax(timeout = 15)
|
62
|
+
wait(timeout: timeout, msg: "Timeout after waiting #{timeout} for all ajax requests to finish").until do
|
63
|
+
driver.execute_script 'return window.jQuery != undefined && jQuery.active == 0'
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Explicitly wait for a certain condition to be true:
|
68
|
+
# wait.until { driver.find_element(:css, 'body.tmpl-srp') }
|
69
|
+
# when timeout is not specified, default timeout 5 sec will be used
|
70
|
+
# when timeout is larger than 15, max timeout 15 sec will be used
|
71
|
+
def wait(opts = {})
|
72
|
+
if !opts[:timeout].nil? && opts[:timeout] > 15
|
73
|
+
puts "WARNING: #{opts[:timeout]} sec timeout is NOT supported by wait method,
|
74
|
+
max timeout 15 sec will be used instead"
|
75
|
+
opts[:timeout] = 15
|
76
|
+
end
|
77
|
+
Selenium::WebDriver::Wait.new(opts)
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Browsery
|
2
|
+
module PageObjects
|
3
|
+
module Widgets
|
4
|
+
|
5
|
+
# A widget represents a portion (an element) of a page that is repeated
|
6
|
+
# or reproduced multiple times, either on the same page, or across multiple
|
7
|
+
# page objects or page modules.
|
8
|
+
class Base
|
9
|
+
include Utils::Castable
|
10
|
+
include Utils::PageObjectHelper
|
11
|
+
include Utils::OverlayAndWidgetHelper
|
12
|
+
extend ElementContainer
|
13
|
+
|
14
|
+
attr_reader :driver, :element, :page
|
15
|
+
|
16
|
+
def initialize(page, element)
|
17
|
+
@driver = page.driver
|
18
|
+
@page = page
|
19
|
+
@element = element
|
20
|
+
end
|
21
|
+
|
22
|
+
## for widgets that include Utils::OverlayAndWidgetHelper
|
23
|
+
def page_object
|
24
|
+
@page
|
25
|
+
end
|
26
|
+
|
27
|
+
def find_first(how, what)
|
28
|
+
element.find_element(how, what)
|
29
|
+
end
|
30
|
+
|
31
|
+
def find_all(how, what)
|
32
|
+
element.all(how, what)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Explicitly wait for a certain condition to be true:
|
36
|
+
# wait.until { driver.find_element(:css, 'body.tmpl-srp') }
|
37
|
+
# when timeout is not specified, default timeout 5 sec will be used
|
38
|
+
# when timeout is larger than 15, max timeout 15 sec will be used
|
39
|
+
def wait(opts = {})
|
40
|
+
if !opts[:timeout].nil? && opts[:timeout] > 15
|
41
|
+
puts "WARNING: #{opts[:timeout]} sec timeout is NOT supported by wait method,
|
42
|
+
max timeout 15 sec will be used instead"
|
43
|
+
opts[:timeout] = 15
|
44
|
+
end
|
45
|
+
Selenium::WebDriver::Wait.new(opts)
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|