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 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
@@ -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,12 @@
1
+ class ElementContainer
2
+ attr_accessor :element
3
+
4
+ def initialize(element)
5
+ @element = element
6
+ end
7
+
8
+ def method_missing(sym, *args, &block)
9
+ @element.send(sym, *args, &block)
10
+ end
11
+
12
+ end
@@ -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
@@ -0,0 +1,3 @@
1
+ module SiteObject
2
+ VERSION = '0.1.0'
3
+ 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: