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