site-object 0.1.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 +7 -0
- data/lib/site-object.rb +9 -0
- data/lib/site-object/element_container.rb +12 -0
- data/lib/site-object/exceptions.rb +19 -0
- data/lib/site-object/page.rb +435 -0
- data/lib/site-object/page_feature.rb +80 -0
- data/lib/site-object/site.rb +216 -0
- data/lib/site-object/version.rb +3 -0
- metadata +90 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 560ad57af8dc4f42986d83000bc3ebae19c8b49e
|
4
|
+
data.tar.gz: db9a6825e5a7590eddd5f52b69d31cc07f45fa2b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 792d37179ae06be6cf700e01e3764185bdc01743541b5f34dc04952f249a99484c2114b5a894f998eb95db018445d9879c953bc225de2f84dbd7694d8766ea01
|
7
|
+
data.tar.gz: 8ed7700e22002050e9e6bd5453441e2a8e847ccf95f32b46e3423611fbb8ded78ce0b006ebc225e62ca913e795fb4a8e4800a20af74de088ecb1139443305d6b
|
data/lib/site-object.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
require 'active_support/all'
|
2
|
+
require 'addressable/template'
|
3
|
+
|
4
|
+
require "site-object/element_container"
|
5
|
+
require "site-object/exceptions"
|
6
|
+
require "site-object/page"
|
7
|
+
require "site-object/page_feature"
|
8
|
+
require "site-object/site"
|
9
|
+
require "site-object/version"
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module SiteObjectExceptions
|
2
|
+
class BrowserLibraryNotSupportedError < RuntimeError
|
3
|
+
end
|
4
|
+
|
5
|
+
class PageInitError < RuntimeError
|
6
|
+
end
|
7
|
+
|
8
|
+
class PageNavigationError < RuntimeError
|
9
|
+
end
|
10
|
+
|
11
|
+
class PageNavigationNotAllowedError < RuntimeError
|
12
|
+
end
|
13
|
+
|
14
|
+
class SiteInitError < RuntimeError
|
15
|
+
end
|
16
|
+
|
17
|
+
class WrongPageError < RuntimeError
|
18
|
+
end
|
19
|
+
end # SiteObjectExceptions
|
@@ -0,0 +1,435 @@
|
|
1
|
+
# Page objects are containers for all of the functionality of a page that you want to expose for testing
|
2
|
+
# purposes. When you create a page object you define a URL to access it, elements for all of the page
|
3
|
+
# elements that you want to work with as well as higher level methods that use those elements to perform
|
4
|
+
# page operations.
|
5
|
+
#
|
6
|
+
# Here's a very simple account edit page example that has two fields and one button and assumes
|
7
|
+
# that you've defined a site object called 'ExampleSite.'
|
8
|
+
#
|
9
|
+
# class AccountDetailsEditPage < ExampleSite::Page
|
10
|
+
# set_url "/accounts/{account_code}/edit" # Parameterized URL.
|
11
|
+
#
|
12
|
+
# element(:first_name) {|b| b.text_field(:id, 'fname') } # text_field is a Watir method.
|
13
|
+
# element(:last_name) {|b| b.text_field(:id, 'fname') } # text_field is a Watir method.
|
14
|
+
# element(:save) {|b| b.button(:id, 'fname') } # text_field is a Watir method.
|
15
|
+
#
|
16
|
+
# def update(fname, lname) # Very simple method that uses the page elements defined above.
|
17
|
+
# first_name.set fname
|
18
|
+
# last_name.set lname
|
19
|
+
# save.click
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# The URL defined in the example above is "parameterized" ({account_code} is a placeholder.)
|
24
|
+
# You don't need to specify parameters for a URL, but if you do you need to call the page with a hash
|
25
|
+
# argument. To use the page after initializing an instance of the site object:
|
26
|
+
#
|
27
|
+
# site.account_details_edit_page(account_code: 12345)
|
28
|
+
#
|
29
|
+
# Pages only take arguments if the URL is parameterized.
|
30
|
+
#
|
31
|
+
# Note that in the example above that there's no explicit navigation call. This is because the site will
|
32
|
+
#look at its current URL and automatically navigate to the page if it's not already on it.
|
33
|
+
#
|
34
|
+
# Here's a simple page object for the rubygems.org search page. Note that no page URL is
|
35
|
+
# defined using the PageObject#set_url method. This is because the page URL for the landing page is
|
36
|
+
# the same as the base URL for the site. When a page URL isn't explicitly defined the base URL is used
|
37
|
+
# in its place:
|
38
|
+
#
|
39
|
+
# class LandingPage < RubyGems::Page
|
40
|
+
# element(:search_field) { |b| b.browser.text_field(:id, 'home_query') }
|
41
|
+
# element(:search_submit) { |b| b.browser.input(:id, 'search_submit') }
|
42
|
+
#
|
43
|
+
# def search(criteria)
|
44
|
+
# search_field.set('rails')
|
45
|
+
# search_submit.click
|
46
|
+
# expect_page(SearchResultsPage)
|
47
|
+
# end
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# Page objects aren't initialized outside of the context of a site object. When a site object is initialized
|
51
|
+
# it creates accessor methods for each page object that inherits from the site's page class. In the
|
52
|
+
# example above, the LandingPage class inherits from the RubyGems site object's page class so you'd
|
53
|
+
# be able to use it once you've initialized a RubyGems site:
|
54
|
+
#
|
55
|
+
# site.landing_page.search("rails") # Returns an instance of the landing page after performing a search.
|
56
|
+
#
|
57
|
+
# Because the site object has accessor methods for all of its pages and page navigation is automatic
|
58
|
+
# it's not always necessary to specify a page object directly. But you can get one if need one:
|
59
|
+
#
|
60
|
+
# page = site.some_page
|
61
|
+
# =><SomePage>
|
62
|
+
module PageObject
|
63
|
+
|
64
|
+
module PageClassMethods
|
65
|
+
attr_reader :arguments, :browser, :navigation_disabled, :page_elements, :page_features, :page_url, :site, :unique_methods, :url_template, :url_matcher
|
66
|
+
|
67
|
+
# Looks up all of the descendants of the current class. Used to figure out which page object classes
|
68
|
+
# belong to the site.
|
69
|
+
def descendants
|
70
|
+
ObjectSpace.each_object(Class).select { |klass| klass < self }
|
71
|
+
end
|
72
|
+
|
73
|
+
# This method can be used to disable page navigation when defining a page class (it sets an
|
74
|
+
# instance variable called @navigation during initialization.) The use case for this is a page
|
75
|
+
# that can't be accessed directly and requires some level of browser interaction to reach.
|
76
|
+
# To disable navigation:
|
77
|
+
#
|
78
|
+
# class SomePage < SomeSite::Page
|
79
|
+
# disable_automatic_navigation true
|
80
|
+
# end
|
81
|
+
#
|
82
|
+
# When navigation is disabled there will be no automatic navigation when the page is called.
|
83
|
+
# If the current page is not the page that you want a SiteObject::WrongPageError will
|
84
|
+
# be raised.
|
85
|
+
# If the visit method is called on the page a SiteObject::PageNavigationNotAllowedError
|
86
|
+
# will be raised.
|
87
|
+
def disable_automatic_navigation
|
88
|
+
@navigation_disabled = true
|
89
|
+
end
|
90
|
+
|
91
|
+
# Used to define access to a single HTML element on a page. This method takes two arguments:
|
92
|
+
# * A symbol representing the element you are defining. This symbol is used to create an accessor
|
93
|
+
# method on the page object.
|
94
|
+
# * A block where access to the HTML element gets defined.
|
95
|
+
#
|
96
|
+
# Example: The page you are working with has a "First Name" field:
|
97
|
+
#
|
98
|
+
# element(:first_name) { |b| b.text_field(:id 'signup-first-name') }
|
99
|
+
#
|
100
|
+
# In the example above, the block argument 'b' is the browser object that will get passed down from
|
101
|
+
# the site to the page and used when the page needs to access the element. You can actually use any
|
102
|
+
# label for the block argument but it's recommended that you use something like 'b' or 'browser'
|
103
|
+
# consistently here because it's always going to be some sort of browser object.
|
104
|
+
#
|
105
|
+
# When page objects get initialized they'll create an accessor method for the element and you can
|
106
|
+
# then work with the element in the same way you'd work with it in Watir or Selenium.
|
107
|
+
#
|
108
|
+
# The element method is aliased to 'el' and using this alias is recommended as it saves space:
|
109
|
+
#
|
110
|
+
# el(:first_name) { |b| b.text_field(:id 'signup-first-name') }
|
111
|
+
def element(name, &block)
|
112
|
+
@page_elements ||= []
|
113
|
+
@page_elements << name.to_sym
|
114
|
+
define_method(name) do
|
115
|
+
block.call(@browser)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
alias :el :element
|
119
|
+
|
120
|
+
# Returns an array of symbols representing the required arguments for the page's page URL.
|
121
|
+
def required_arguments
|
122
|
+
@arguments ||= @url_template.keys.map { |k| k.to_sym }
|
123
|
+
end
|
124
|
+
|
125
|
+
# Used to define the full or relative URL to the page. Typically, you will *almost* *always* want to use
|
126
|
+
# this method when defining a page object (but see notes below.) The URL can be defined in a number
|
127
|
+
# of different ways. Here are some examples using Google News:
|
128
|
+
#
|
129
|
+
# *Relative* *URL*
|
130
|
+
#
|
131
|
+
# set_url "/nwshp?hl=en"
|
132
|
+
#
|
133
|
+
# Relative URLs are most commonly used when defining page objects. The idea here is that you can
|
134
|
+
# change the base_url when calling the site object, which allows you to use the same code across
|
135
|
+
# multiple test environments by changing the base_url as you initialize a site object.
|
136
|
+
#
|
137
|
+
# *Relative* *URL* *with* *URL* *Templating*
|
138
|
+
# set_url "/nwshp?hl={language}"
|
139
|
+
#
|
140
|
+
# This takes the relative URL example one step further, allowing you to set the page's parameters.
|
141
|
+
# Note that the the language specified in the first relative URL example ('en') was replaced by
|
142
|
+
# '{language}' in this one. Siteobject uses the Addressable library, which supports this kind of
|
143
|
+
# templating. When you template a value in the URL, the page object will allow you to specify the
|
144
|
+
# templated value when it's being initialized. Here's an example of how this works using a news site.
|
145
|
+
# Here's the base site object class:
|
146
|
+
#
|
147
|
+
# class NewsSite
|
148
|
+
# include SiteObject
|
149
|
+
# end
|
150
|
+
#
|
151
|
+
# Here's a page object for the news page, templating the language value in the URL:
|
152
|
+
#
|
153
|
+
# class NewsPage < NewsSite::Page
|
154
|
+
# set_url "/news?l={language}"
|
155
|
+
# end
|
156
|
+
#
|
157
|
+
# After you've initialized the site object you can load the Spanish or French versions of the
|
158
|
+
# page by changing the hash argument used to call the page from the site object:
|
159
|
+
#
|
160
|
+
# site = NewsSite.new(base_url: "http://news.somesite.com")
|
161
|
+
# site.news_page(language: 'es')
|
162
|
+
# site.news_page(language: 'fr')
|
163
|
+
#
|
164
|
+
# In addition to providing a hash of templated values when initializing a page you can also use
|
165
|
+
# an object, as long as that object responds to all of the templated arguments in the page's
|
166
|
+
# URL definition. Here's a simple class that has a language method that we can use for the news
|
167
|
+
# page described above:
|
168
|
+
#
|
169
|
+
# class Country
|
170
|
+
# attr_reader :language
|
171
|
+
#
|
172
|
+
# def initialize(lang)
|
173
|
+
# @language = lang
|
174
|
+
# end
|
175
|
+
# end
|
176
|
+
#
|
177
|
+
# In the example below, the Country class is used to create a new new country object called 'c'.
|
178
|
+
# This object has been initialized with a Spanish language code and the news page
|
179
|
+
# will load the spanish version of the page when it's called with the country object.
|
180
|
+
#
|
181
|
+
# site = NewsSite.new(base_url: "http://news.somesite.com")
|
182
|
+
# c = Country.new('es')
|
183
|
+
# => <Country:0x007fcb0dc67f98 @language="es">
|
184
|
+
# c.language
|
185
|
+
# => 'es'
|
186
|
+
# site.news_page(c)
|
187
|
+
# => <NewsPage:0x003434546566>
|
188
|
+
#
|
189
|
+
# If one or more URL parameters are missing when the page is getting initialized then the page
|
190
|
+
# will look at the hash arguments used to initialize the site. If the argument the page needs is
|
191
|
+
# defined in the site's initialization arguments it will use that. For example, if the site
|
192
|
+
# object is initialized with a port, subdomain, or any other argument you can use those values
|
193
|
+
# when defining a page URL. Example:
|
194
|
+
#
|
195
|
+
# class ConfigPage < MySite::Page
|
196
|
+
# set_url "/foo/{subdomain}/config"
|
197
|
+
# end
|
198
|
+
#
|
199
|
+
# site = MySite.new(subdomain: 'foo')
|
200
|
+
# => <MySite:0x005434546511>
|
201
|
+
# site.configuration_page # No need to provide a subdomain here as long as the site object has it.
|
202
|
+
# => <ConfigPage:0x705434546541>
|
203
|
+
#
|
204
|
+
# *Full* *URL*
|
205
|
+
# set_url "http://news.google.com/nwshp?hl=en"
|
206
|
+
#
|
207
|
+
# Every once in a while you may not want to use a base URL that has been defined. This allows you
|
208
|
+
# to do that. Just define a complete URL for that page object and that's what will get used; the
|
209
|
+
# base_url will be ignored.
|
210
|
+
#
|
211
|
+
# *No* *URL*
|
212
|
+
#
|
213
|
+
# The set_url method is not mandatory. when defining a page. If you don't use set_url in the page
|
214
|
+
# definition then the page will defined the base_url as the page's URL.
|
215
|
+
def set_url(url)
|
216
|
+
url ? @page_url = url : nil
|
217
|
+
end
|
218
|
+
|
219
|
+
def set_url_template(base_url)
|
220
|
+
begin
|
221
|
+
case @page_url.to_s
|
222
|
+
when '' # There's no page URL so just assume the base URL
|
223
|
+
@url_template = Addressable::Template.new(base_url)
|
224
|
+
when /(http:\/\/|https:\/\/)/i
|
225
|
+
@url_template = Addressable::Template.new(@page_url)
|
226
|
+
else
|
227
|
+
@url_template = Addressable::Template.new(Addressable::URI.parse("#{base_url}#{@page_url}"))
|
228
|
+
end
|
229
|
+
rescue Addressable::URI::InvalidURIError => e
|
230
|
+
raise SiteObject::PageInitError, "Unable to initialize #{self.class} because there's no base_url defined for the site and the page object URL that was defined was a URL fragment (#{@page_url})\n\n#{caller.join("\n")}"
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
# Optional. Allows you to specify a fallback mechanism for checking to see if the correct page is
|
235
|
+
# being displayed. This only gets used in cases where the primary mechanism for checking a page
|
236
|
+
# (the URL template defined by Page#set_url) fails to match the current browser URL. When that
|
237
|
+
# happens the regular expression defined here will be applied and the navigation check will pass
|
238
|
+
# if the regular expression matches the current browser URL.
|
239
|
+
#
|
240
|
+
# In most cases, you won't need to define a URL matcher and should just rely on the default page
|
241
|
+
# matching that uses the page's URL template. The default matching should work fine for most cases.
|
242
|
+
def set_url_matcher(regexp)
|
243
|
+
regexp ? @url_matcher = regexp : nil
|
244
|
+
end
|
245
|
+
|
246
|
+
# Used to import page features for use within the page. Example:
|
247
|
+
#
|
248
|
+
# class ConfigPage < MySite::Page
|
249
|
+
# use_features :footer, :sidebar
|
250
|
+
# end
|
251
|
+
#
|
252
|
+
# Then, once the page object has been initialized:
|
253
|
+
#
|
254
|
+
# site.config_page.footer.about.click
|
255
|
+
#
|
256
|
+
# Use the PageFeature class to define page features.
|
257
|
+
def use_features(*args)
|
258
|
+
@page_features = args
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
module PageInstanceMethods
|
263
|
+
attr_reader :arguments, :browser, :navigation_disabled, :page_elements, :page_features, :page_url, :required_arguments, :site, :url_template, :url_matcher
|
264
|
+
|
265
|
+
# Takes the name of a page class. If the current page is of that class then it returns a page
|
266
|
+
# object for the page. Raises a SiteObject::WrongPageError if that's not the case.
|
267
|
+
# It's generally not a good idea to put error checking inside a page object. This should only be
|
268
|
+
# used in cases where there is a page transition and that transition is always expected to work.
|
269
|
+
def expect_page(page)
|
270
|
+
@site.expect_page(page)
|
271
|
+
end
|
272
|
+
|
273
|
+
# There's no need to ever call this directly. Initializes a page object within the context of a
|
274
|
+
# site object. Takes a site object and a hash of configuration arguments. The site object will
|
275
|
+
# handle all of this for you.
|
276
|
+
def initialize(site, args={})
|
277
|
+
@browser = site.browser
|
278
|
+
@navigation_disabled = self.class.navigation_disabled
|
279
|
+
@page_url = self.class.page_url
|
280
|
+
@page_elements = self.class.page_elements
|
281
|
+
@page_features = self.class.page_features
|
282
|
+
@required_arguments = self.class.required_arguments
|
283
|
+
@site = site
|
284
|
+
@url_matcher = self.class.url_matcher
|
285
|
+
@url_template = self.class.url_template
|
286
|
+
|
287
|
+
# Try to expand the URL template if the URL has parameters.
|
288
|
+
@arguments = {}.with_indifferent_access # Stores the param list that will expand the url_template after examining the arguments used to initialize the page.
|
289
|
+
if @required_arguments.length > 0
|
290
|
+
@required_arguments.each do |arg| # Try to extract each URL argument from the hash or object provided, OR from the site object.
|
291
|
+
if args.is_a?(Hash) && !args.empty?
|
292
|
+
if args.with_indifferent_access[arg] #The hash has the required argument.
|
293
|
+
@arguments[arg]= args.with_indifferent_access[arg]
|
294
|
+
elsif @site.respond_to?(arg)
|
295
|
+
@arguments[arg]= site.send(arg)
|
296
|
+
else
|
297
|
+
raise SiteObject::PageInitError, "#{args.class} was provided, but this object did not respond to :#{arg}, which is necessary to build an URL for the #{self.class.name} page.\n\n#{caller.join("\n")}"
|
298
|
+
end
|
299
|
+
# elsif args.is_a?(Hash) && args.empty?
|
300
|
+
# raise SiteObject::PageInitError, "An attempt to initialize the #{self.class.name} page object failed because no page arguments were provided. This page object requires the following arguments for initialization: :#{@required_arguments.join(", :")}\n\n#{caller.join("\n")}."
|
301
|
+
elsif args # Some non-hash object was provided.
|
302
|
+
if args.respond_to?(arg) #The hash has the required argument.
|
303
|
+
@arguments[arg]= args.send(arg)
|
304
|
+
elsif @site.respond_to?(arg)
|
305
|
+
@arguments[arg]= site.send(arg)
|
306
|
+
else
|
307
|
+
raise SiteObject::PageInitError, "#{args.class} was provided, but this object did not respond to :#{arg}, which is necessary to build an URL for the #{self.class.name} page.\n\n#{caller.join("\n")}"
|
308
|
+
end
|
309
|
+
else
|
310
|
+
# Do nothing here.
|
311
|
+
end
|
312
|
+
end
|
313
|
+
elsif @required_arguments.length > 0 && !args
|
314
|
+
raise SiteObject::PageInitError, "No object was provided when attempting to initialize #{self.class.name}. This page object requires the following arguments for initialization: :#{@required_arguments.join(', :')}.\n\n#{caller.join("\n")}"
|
315
|
+
elsif @required_arguments.length == 0 && !args
|
316
|
+
unless @args.is_a?(Hash) && args.empty?
|
317
|
+
raise SiteObject::PageInitError, "#{args.class} was provided as a #{self.class.name} initialization argument, but the page URL doesn't require any arguments.\n\n#{caller.join("\n")}"
|
318
|
+
end
|
319
|
+
else
|
320
|
+
# Do nothing here.
|
321
|
+
end
|
322
|
+
@url = @url_template.expand(@arguments).to_s
|
323
|
+
|
324
|
+
@page_features ||= []
|
325
|
+
@page_features.each do |arg|
|
326
|
+
self.class_eval do
|
327
|
+
klass = eval("#{arg.to_s.camelize}")
|
328
|
+
if klass.alias
|
329
|
+
define_method(klass.alias) do
|
330
|
+
klass.new(@browser, args)
|
331
|
+
end
|
332
|
+
else
|
333
|
+
define_method(arg) do
|
334
|
+
klass.new(@browser, args)
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
@site.most_recent_page = self
|
341
|
+
unless on_page?
|
342
|
+
if @navigation_disabled
|
343
|
+
if page = @site.page
|
344
|
+
raise SiteObject::PageNavigationNotAllowedError, "The #{self.class.name} page could not be accessed. Navigation is intentionally disabled for this page and the browser was displaying the #{@site.page.class.name} page when you tried to access it.\n\nPAGE URL:\n------------\n#{@site.browser.url}\n\n#{caller.join("\n")}"
|
345
|
+
else
|
346
|
+
raise SiteObject::PageNavigationNotAllowedError, "The #{self.class.name} page could not be accessed. Navigation is intentionally disabled for this page and the page that the browser was displaying could not be recognized.\n\nPAGE URL:\n------------\n#{@site.browser.url}\n\nPAGE TEXT:\n------------\n#{@site.browser.text}\n\n#{caller.join("\n")}"
|
347
|
+
end
|
348
|
+
end
|
349
|
+
visit
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
# Custom inspect method so that console output doesn't get in the way when debugging.
|
354
|
+
def inspect
|
355
|
+
"#<#{self.class.name}:#{object_id} @url_template=#{@url_template.inspect}>"
|
356
|
+
end
|
357
|
+
|
358
|
+
# Returns true if the page defined by the page object is currently being displayed in the browser,
|
359
|
+
# false if not. It does this in two different ways, which are described below.
|
360
|
+
#
|
361
|
+
# A page always has a URL defined for it. This is typically done by using the Page.set_url method
|
362
|
+
# to specify a URL when defining the page. You can skip using the set_url method but in that case
|
363
|
+
# the page URL defaults to the base URL defined for the site object.
|
364
|
+
#
|
365
|
+
# The default approach for determining if the page is being displayed relies on the URL defined for
|
366
|
+
# the page. This method first does a general match against the current browser URL page's URL template.
|
367
|
+
# If a match occurs here, and there are no required arguments for the page the method returns true.
|
368
|
+
# If the page's URL template does require arguments the method performs an additional check to
|
369
|
+
# verify that each of the arguments defined for the page match what's in the current browser URL.
|
370
|
+
# If all of the arguments match then the method will return true.
|
371
|
+
#
|
372
|
+
# This should work for most cases but may not always be enough. For example, there may be a redirect
|
373
|
+
# and the URL used to navigate to the page may not be the final page URL. There's a fallback
|
374
|
+
# mechanism for these sorts of situations. You can use the Page.set_url_matcher method to define a
|
375
|
+
# regular expression that the method will use in place of the URL template. If the regular expression
|
376
|
+
# matches, then the method will return true even if the URL wouldn't match the URL template.
|
377
|
+
#
|
378
|
+
# It's better to use the default URL matching if possible. But if for some reason it's not feasible
|
379
|
+
# you can use the alternate method to specify how to match the page.
|
380
|
+
def on_page?
|
381
|
+
url = @browser.url
|
382
|
+
|
383
|
+
if @url_matcher && @url_matcher =~ url
|
384
|
+
return true
|
385
|
+
elsif @url_template.match(url)
|
386
|
+
if @arguments.empty?
|
387
|
+
return true
|
388
|
+
else
|
389
|
+
if page_args = @url_template.extract(Addressable::URI.parse(url))
|
390
|
+
page_args = page_args.with_indifferent_access
|
391
|
+
return true if @arguments.all? { |k, v| page_args[k] == v.to_s }
|
392
|
+
end
|
393
|
+
end
|
394
|
+
end
|
395
|
+
false
|
396
|
+
end
|
397
|
+
|
398
|
+
# Refreshes the page.
|
399
|
+
def refresh # TODO: Isolate browser library-specific code so that the adding new browser
|
400
|
+
if @browser.is_a?(Watir::Browser)
|
401
|
+
@browser.refresh
|
402
|
+
elsif @browser.is_a?(Selenium::WebDriver::Driver)
|
403
|
+
@browser.navigate.refresh
|
404
|
+
else
|
405
|
+
raise SiteObject::BrowserLibraryNotSupportedError, "Only Watir-Webdriver and Selenium Webdriver are currently supported. Class of browser object: #{@browser.class.name}"
|
406
|
+
end
|
407
|
+
self
|
408
|
+
end
|
409
|
+
|
410
|
+
# Navigates to the page that it's called on. Raises a SiteObject::PageNavigationNotAllowedError when
|
411
|
+
# navigation has been disabled for the page. Raises a SiteObject::WrongPageError if the
|
412
|
+
# specified page isn't getting displayed after navigation.
|
413
|
+
def visit
|
414
|
+
if @navigation_disabled
|
415
|
+
raise SiteObject::PageNavigationNotAllowedError, "Navigation has been disabled for the #{self.class.name} page. This was done when defining the page class and usually means that the page can't be reached directly through a URL and requires some additional work to access."
|
416
|
+
end
|
417
|
+
if @browser.is_a?(Watir::Browser)
|
418
|
+
@browser.goto(@url)
|
419
|
+
elsif @browser.is_a?(Selenium::WebDriver::Driver)
|
420
|
+
@browser.get(@url)
|
421
|
+
else
|
422
|
+
raise SiteObject::BrowserLibraryNotSupportedError, "Only Watir-Webdriver and Selenium Webdriver are currently supported. Class of browser object: #{@browser.class.name}"
|
423
|
+
end
|
424
|
+
|
425
|
+
if @url_matcher
|
426
|
+
raise SiteObject::WrongPageError, "Navigation check failed after attempting to access the #{self.class.name} page. Current URL #{@browser.url} did not match #{@url_template.pattern}. A URL matcher was also defined for the page and the secondary check against the URL matcher also failed. URL matcher: #{@url_matcher}" unless on_page?
|
427
|
+
else
|
428
|
+
raise SiteObject::WrongPageError, "Navigation check failed after attempting to access the #{self.class.name} page. Current URL #{@browser.url} did not match #{@url_template.pattern}" unless on_page?
|
429
|
+
end
|
430
|
+
|
431
|
+
@site.most_recent_page = self
|
432
|
+
self
|
433
|
+
end
|
434
|
+
end
|
435
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'active_support/inflector'
|
2
|
+
require 'addressable/template'
|
3
|
+
|
4
|
+
# Creates a reusable piece of functionality that can be applied to multiple pages. For example, maybe
|
5
|
+
# you have a footer that's common to all pages displayed after the user logs into the app:
|
6
|
+
#
|
7
|
+
# class Footer < PageFeature
|
8
|
+
# element(:news) { |b| b.link(:text, 'News') }
|
9
|
+
# element(:support) { |b| b.link(:text, 'Support') }
|
10
|
+
# element(:contact_us) { |b| b.link(:text, 'Contact Us') }
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# class TestPage < MySite::Page
|
14
|
+
# set_url('/blah')
|
15
|
+
# use_features :footer
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# The PageObject.use_features method is then used to add the page feature to the classes that you want
|
19
|
+
# to add it to. Note that the name of the page feature class (Footer) gets added to the class as :footer.
|
20
|
+
# Once added to the page definition, the footer page feature can then be accessed from the page using the
|
21
|
+
# 'footer' method.
|
22
|
+
#
|
23
|
+
# mysite.test_page.footer.contact_us.present? # present? is a method supported by Watir.
|
24
|
+
# =>true
|
25
|
+
#
|
26
|
+
# There may be some cases where you don't want the feature name to be the same as the class name. In these
|
27
|
+
# sorts of situations you can use the PageFeature.feature_name method to override this behavior.
|
28
|
+
class PageFeature
|
29
|
+
class << self
|
30
|
+
attr_accessor :alias
|
31
|
+
|
32
|
+
# Used to define access to a single HTML element on a page. This method takes two arguments:
|
33
|
+
#
|
34
|
+
# * A symbol representing the element you are defining. This symbol is used to create an accessor method on the
|
35
|
+
# page object.
|
36
|
+
# * A block where access to the HTML element gets defined.
|
37
|
+
#
|
38
|
+
# Example: The page you are working with has a "First Name" field.
|
39
|
+
#
|
40
|
+
# element(:first_name) { |b| b.text_field(:id 'signup-first-name') }
|
41
|
+
#
|
42
|
+
# In this example, 'b' is the browser object that will get passed down from the site to the page and then used
|
43
|
+
# when the page element needs to be accessed.
|
44
|
+
#
|
45
|
+
# The 'element' method is aliased to 'el' and using the alias is recommended as it saves space:
|
46
|
+
#
|
47
|
+
# el(:first_name) { |b| b.text_field(:id 'signup-first-name') }
|
48
|
+
#
|
49
|
+
def element(name, &block)
|
50
|
+
define_method(name) do
|
51
|
+
block.call(@browser)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
alias :el :element
|
55
|
+
|
56
|
+
# By default, the feature gets imported using the class name of the PageFeature class. For example if you
|
57
|
+
# import a PageFeature named Footer, the page will have a 'footer' method that provides access to the News,
|
58
|
+
# Support and Contact Us links (see example above.) You can use this method to overwrite the default naming
|
59
|
+
# scheme if you need to. In the example below, the footer feature defined here would have a page object
|
60
|
+
# accessor method called special_footer:
|
61
|
+
#
|
62
|
+
# class Footer < PageFeature
|
63
|
+
# feature_name :special_footer
|
64
|
+
#
|
65
|
+
# element(:news) { |b| b.link(:text, 'News') }
|
66
|
+
# element(:support) { |b| b.link(:text, 'Support') }
|
67
|
+
# element(:contact_us) { |b| b.link(:text, 'Contact Us') }
|
68
|
+
# end
|
69
|
+
def feature_name(name)
|
70
|
+
@alias = name
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
# Not meant to be accessed directly. Use PageObject.use_features to add a PageFeature to a PageObject.
|
76
|
+
def initialize(browser, args={})
|
77
|
+
@args = args
|
78
|
+
@browser = browser
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,216 @@
|
|
1
|
+
# Usage:
|
2
|
+
# require 'site-object'
|
3
|
+
# class MySite
|
4
|
+
# include SiteObject
|
5
|
+
# end
|
6
|
+
module SiteObject
|
7
|
+
attr_reader :base_url, :unique_methods
|
8
|
+
attr_accessor :pages, :browser, :arguments, :most_recent_page
|
9
|
+
|
10
|
+
include SiteObjectExceptions
|
11
|
+
|
12
|
+
# Sets up a Page class when the SiteObject module is included in the class you're using to model
|
13
|
+
# your site.
|
14
|
+
def self.included(base)
|
15
|
+
klass = Class.new
|
16
|
+
base.const_set('Page', klass)
|
17
|
+
base::Page.send(:extend, PageObject::PageClassMethods)
|
18
|
+
base::Page.send(:include, PageObject::PageInstanceMethods)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Closes the site object's browser
|
22
|
+
def close_browser
|
23
|
+
@browser.close # Same for watir-webdriver and selenium-webdriver.
|
24
|
+
end
|
25
|
+
|
26
|
+
# Helper method designed to assist with complex workflows. Basically, if you're navigating and
|
27
|
+
# expect a certain page to be displayed you can use this method to confirm that the page you want is
|
28
|
+
# displayed and then get a page object for it.
|
29
|
+
#
|
30
|
+
# This method just checks to see if the right class of page is being displayed. If you have defined
|
31
|
+
# a templated value in the URL of the page object getting checked it doesn't check the values of
|
32
|
+
# the arguments. It only confirms whether or not the arguments are present.
|
33
|
+
def expect_page(page_arg)
|
34
|
+
p = page
|
35
|
+
|
36
|
+
if p
|
37
|
+
if p.class.name == page_arg.class.name # Do this if it looks like an instance of a page.
|
38
|
+
return p
|
39
|
+
elsif p.class == page_arg # Do this if it looks like a page class name.
|
40
|
+
return p
|
41
|
+
elsif page_arg.is_a?(Symbol) && p.class.name.underscore.to_sym == page_arg
|
42
|
+
return p
|
43
|
+
else
|
44
|
+
raise SiteObject::WrongPageError, "Expected #{page_arg} page to be displayed but the URL doesn't look right. \n\n#{caller.join("\n")}"
|
45
|
+
end
|
46
|
+
else
|
47
|
+
raise SiteObject::WrongPageError, "Expected #{page_arg} page to be displayed but the URL doesn't appear to match the URL template of any known page. \n\n#{caller.join("\n")}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Creates a site object, which will have accessor methods for all pages that have been defined for
|
52
|
+
# the site. This object takes a hash argument. There is only one required value (the base_url for
|
53
|
+
# the site.) Example:
|
54
|
+
#
|
55
|
+
# class MySite
|
56
|
+
# include SiteObject
|
57
|
+
# end
|
58
|
+
#
|
59
|
+
# site = MySite.new(base_url: "http://foo.com")
|
60
|
+
#
|
61
|
+
# You can also specify any other arguments that you want for later use:
|
62
|
+
#
|
63
|
+
# site = MySite.new(
|
64
|
+
# base_url: "http://foo.com",
|
65
|
+
# foo: true
|
66
|
+
# bar: 1
|
67
|
+
# )
|
68
|
+
# site.foo
|
69
|
+
# => true
|
70
|
+
# site.bar
|
71
|
+
# => 1
|
72
|
+
def initialize(args={})
|
73
|
+
unless args.is_a?(Hash)
|
74
|
+
unless
|
75
|
+
raise SiteObject::SiteInitError, "You must provide hash arguments when initializing a site object. At a minimum you must specify a base_url. Example:\ns = SiteObject.new(base_url: 'http://foo.com')"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
@arguments = args.with_indifferent_access
|
80
|
+
@base_url = @arguments[:base_url]
|
81
|
+
@browser = @arguments[:browser]
|
82
|
+
@pages = self.class::Page.descendants
|
83
|
+
|
84
|
+
# Set up accessor methods for each page and defines the URL template.
|
85
|
+
@pages.each do |current_page|
|
86
|
+
current_page.set_url_template(@base_url)
|
87
|
+
|
88
|
+
self.class.class_eval do
|
89
|
+
define_method(current_page.to_s.underscore) do |args={}, block=nil|
|
90
|
+
current_page.new(self, args)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
visited = Set.new
|
96
|
+
tmp = @pages.map {|p| p.instance_methods }.flatten
|
97
|
+
tmp.each do |element|
|
98
|
+
if visited.include?(element)
|
99
|
+
else
|
100
|
+
visited << element
|
101
|
+
end
|
102
|
+
end
|
103
|
+
@unique_methods = visited
|
104
|
+
end
|
105
|
+
|
106
|
+
# Custom inspect method so that console output doesn't get in the way when debugging.
|
107
|
+
def inspect
|
108
|
+
"#<#{self.class.name}:0x#{object_id}\n @base_url=\"#{@base_url}\"\n @most_recent_page=#{@most_recent_page}>"
|
109
|
+
end
|
110
|
+
|
111
|
+
# In cases where the site object doesn't recognize a method it will try to delegate the method call
|
112
|
+
# to the page that's currently being displayed in the browser, assuming that the site object recognizes
|
113
|
+
# the page by its URL. If the page is the last visited page and method is unique, (i.e., doesn't belong
|
114
|
+
# to any other page object) then the site object won't attempt to regenerate the page when calling
|
115
|
+
# the method.
|
116
|
+
def method_missing(sym, *args, &block)
|
117
|
+
if @unique_methods.include?(sym) && @most_recent_page.respond_to?(sym)
|
118
|
+
if args && block
|
119
|
+
@most_recent_page.send(sym, *args, &block)
|
120
|
+
elsif args
|
121
|
+
@most_recent_page.send(sym, *args)
|
122
|
+
elsif block
|
123
|
+
@most_recent_page.send(sym, &block)
|
124
|
+
else
|
125
|
+
@most_recent_page.send sym
|
126
|
+
end
|
127
|
+
elsif p = page
|
128
|
+
if p.respond_to?(sym)
|
129
|
+
if args && block
|
130
|
+
p.send(sym, *args, &block)
|
131
|
+
elsif args
|
132
|
+
p.send(sym, *args)
|
133
|
+
elsif block
|
134
|
+
p.send(sym, &block)
|
135
|
+
else
|
136
|
+
p.send sym
|
137
|
+
end
|
138
|
+
else
|
139
|
+
super
|
140
|
+
end
|
141
|
+
else
|
142
|
+
super
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Returns true or false depending on whether the specified page is displayed. You can use a page
|
147
|
+
# object or a PageObject class name to identify the page you are looking for. Examples:
|
148
|
+
#
|
149
|
+
# page = site.account_summary_page
|
150
|
+
# =>#<AccountSummaryPage:70341126478080 ...>
|
151
|
+
# site.on_page? page
|
152
|
+
# =>true
|
153
|
+
#
|
154
|
+
# site.on_page? AccountSummaryPage
|
155
|
+
# =>true
|
156
|
+
def on_page?(page_arg)
|
157
|
+
url = @browser.url
|
158
|
+
|
159
|
+
if page_arg.url_matcher && page_arg.url_matcher =~ url
|
160
|
+
return true
|
161
|
+
elsif page_arg.url_template.match url
|
162
|
+
return true
|
163
|
+
else
|
164
|
+
return false
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# Can be used to open a browser for the site object if the user did not pass one in when it was
|
169
|
+
# initialized. The arguments used here get passed down to Watir when starting the browser. Example:
|
170
|
+
# s = SomeSite.new(hash)
|
171
|
+
# s.open_browser :watir, :firefox
|
172
|
+
def open_browser(platform, browser_type, args={})
|
173
|
+
# puts "howdy"
|
174
|
+
# binding.pry
|
175
|
+
case platform
|
176
|
+
when :watir
|
177
|
+
# binding.pry
|
178
|
+
@browser = Watir::Browser.new(browser_type, args)
|
179
|
+
when :selenium
|
180
|
+
@browser = Selenium::WebDriver::Driver.for(browser_type, args)
|
181
|
+
else
|
182
|
+
raise ArgumentError "Platform argument must be either :watir or :selenium."
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
# Looks at the page currently being displayed in the browser and tries to return a page object for
|
187
|
+
# it. Does this by looking at the currently displayed URL in the browser. The first page that gets
|
188
|
+
# checked is the page that was most recently accessed. After that it will cycle through all available
|
189
|
+
# pages looking for a match. Returns nil if it can't find a matching page object.
|
190
|
+
def page
|
191
|
+
return @most_recent_page if @most_recent_page && @most_recent_page.on_page?
|
192
|
+
|
193
|
+
url = @browser.url
|
194
|
+
found_page = nil
|
195
|
+
|
196
|
+
@pages.each do |p|
|
197
|
+
if p.url_template.match url
|
198
|
+
found_page = p
|
199
|
+
elsif p.url_matcher && p.url_matcher =~ url
|
200
|
+
found_page = p
|
201
|
+
end
|
202
|
+
|
203
|
+
break if found_page
|
204
|
+
end
|
205
|
+
|
206
|
+
if found_page && !found_page.required_arguments.empty?
|
207
|
+
return found_page.new(self, found_page.url_template.extract(url))
|
208
|
+
elsif found_page
|
209
|
+
return found_page.new(self)
|
210
|
+
else
|
211
|
+
return nil
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
at_exit {@browser.close if @browser}
|
216
|
+
end
|
metadata
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: site-object
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- John Fitisoff
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-05-25 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: addressable
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.3'
|
34
|
+
- - ">="
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: 2.3.8
|
37
|
+
type: :runtime
|
38
|
+
prerelease: false
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - "~>"
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '2.3'
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 2.3.8
|
47
|
+
description: Wraps page objects up into a site object, which provides some introspection
|
48
|
+
and navigation capabilities that page objects don't provide. Works with Watir and
|
49
|
+
Selenium.
|
50
|
+
email:
|
51
|
+
- jfitisoff@yahoo.com
|
52
|
+
executables: []
|
53
|
+
extensions: []
|
54
|
+
extra_rdoc_files: []
|
55
|
+
files:
|
56
|
+
- lib/site-object.rb
|
57
|
+
- lib/site-object/element_container.rb
|
58
|
+
- lib/site-object/exceptions.rb
|
59
|
+
- lib/site-object/page.rb
|
60
|
+
- lib/site-object/page_feature.rb
|
61
|
+
- lib/site-object/site.rb
|
62
|
+
- lib/site-object/version.rb
|
63
|
+
homepage: https://github.com/jfitisoff/site-object
|
64
|
+
licenses:
|
65
|
+
- MIT
|
66
|
+
metadata: {}
|
67
|
+
post_install_message:
|
68
|
+
rdoc_options: []
|
69
|
+
require_paths:
|
70
|
+
- lib
|
71
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - ">="
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '0'
|
81
|
+
requirements: []
|
82
|
+
rubyforge_project: site-object
|
83
|
+
rubygems_version: 2.2.2
|
84
|
+
signing_key:
|
85
|
+
specification_version: 4
|
86
|
+
summary: Wraps page objects up into a site object, which provides some introspection
|
87
|
+
and navigation capabilities that page objects don't provide. Works with Watir and
|
88
|
+
Selenium.
|
89
|
+
test_files: []
|
90
|
+
has_rdoc:
|