insite 0.0.1

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.
@@ -0,0 +1,216 @@
1
+ # TODO: A lot of this should be handled via delegation.
2
+ module Insite
3
+ module CommonMethods
4
+ # Returns a Watir::Browser object.
5
+ def browser
6
+ @browser
7
+ end
8
+
9
+ # Returns a Selenium::WebDriver::Driver object.
10
+ def driver
11
+ @browser.driver
12
+ end
13
+
14
+ # Don't override the default if it's already there.
15
+ unless defined? :html
16
+ # Returns current HTML for the object.
17
+ def html
18
+ @browser.html
19
+ end
20
+ end
21
+
22
+ # Returns a string representation of the page.
23
+ def inspect
24
+ "#<#{self.class.name}:#{object_id} @url=#{@browser.url}>"
25
+ end
26
+
27
+ private
28
+ def process_browser
29
+ if @site.browser.is_a?(Watir::Browser)
30
+ begin
31
+ if @site.browser.exists?
32
+ return @site.browser
33
+ else
34
+ raise(
35
+ Insite::Errors::BrowserClosedError,
36
+ "Browser check failed. The browser is no longer present.\n\n"
37
+ )
38
+ end
39
+ rescue(Insite::Errors::BrowserNotOpenError) => e
40
+ raise e
41
+ rescue => e
42
+ raise(
43
+ Insite::Errors::BrowserResponseError,
44
+ <<~eos
45
+ Browser check failed. The browser returned an #{e.class} (#{e}) when it was queried.
46
+ Backtrace for the error:
47
+ #{e.backtrace.join("\n")}
48
+
49
+ eos
50
+ )
51
+ end
52
+ elsif @site.browser.nil?
53
+ raise(
54
+ Insite::Errors::BrowserNotOpenError,
55
+ "A browser has not been defined for the site. Try using Site#open to " \
56
+ "start a browser.\n\n"
57
+ )
58
+ else
59
+ raise(
60
+ Insite::Errors::BrowserNotValidError,
61
+ "Expected: Watir::Browser. Actual: #{@site.browser.class}.\n\n"
62
+ )
63
+ end
64
+
65
+ end
66
+ public
67
+
68
+ def update_object(hash_args = {})
69
+ rescues = [
70
+ Watir::Exception::ObjectDisabledException,
71
+ Watir::Exception::UnknownObjectException,
72
+ Selenium::WebDriver::Error::ElementNotVisibleError,
73
+ Selenium::WebDriver::Error::UnknownError
74
+ ]
75
+ failed = []
76
+ hash_args.each do |k, v|
77
+ begin
78
+ k = k.to_sym
79
+ if @page_elements.include?(k)
80
+ elem = public_send(k)
81
+
82
+ if [Watir::Alert, Watir::FileField, Watir::TextField, Watir::TextArea].include? elem.class
83
+ elem.set v
84
+ elsif [Watir::Select].include? elem.class
85
+ elem.select v
86
+ elsif [Watir::Anchor, Watir::Button].include? elem.class
87
+ case v
88
+ when Symbol
89
+ elem.public_send v
90
+ when TrueClass
91
+ elem.click
92
+ when FalseClass
93
+ # Do nothing here.
94
+ else
95
+ raise ArgumentError, "Unsupported argument for #{elem.class}: '#{v}'"
96
+ end
97
+ elsif elem.is_a?(Watir::Radio)
98
+ case v
99
+ when Symbol
100
+ elem.public_send v
101
+ when TrueClass
102
+ 3.times do
103
+ elem.set
104
+ break if elem.set?
105
+ sleep 0.5
106
+ end
107
+ when FalseClass
108
+ raise ArgumentError, "Unsupported argument for #{elem.class}: '#{v}' \
109
+ (You can only set a radio button, so false is not a valid argument.)"
110
+ else
111
+ raise ArgumentError, "Unsupported argument for #{elem.class}: '#{v}'"
112
+ end
113
+ elsif elem.is_a?(Watir::CheckBox)
114
+ case v
115
+ when Symbol
116
+ elem.public_send v
117
+ when TrueClass
118
+ 3.times do
119
+ elem.set
120
+ break if elem.set?
121
+ sleep 0.5
122
+ end
123
+ when FalseClass
124
+ 3.times do
125
+ elem.clear
126
+ break if !elem.set?
127
+ sleep 0.5
128
+ end
129
+ elem.clear
130
+ else
131
+ raise ArgumentError, "Unsupported argument for #{elem.class}: '#{v}'"
132
+ end
133
+ elsif elem.is_a?(Watir::RadioCollection)
134
+ # TODO: Remove, not appropriate as a general use case.
135
+ rb = elem.to_a.find do |r|
136
+ r.text =~ /#{Regexp.escape(v)}/i || r.parent.text =~ /#{Regexp.escape(v)}/i
137
+ end
138
+
139
+ if rb
140
+ rb.click
141
+ else
142
+ raise "No matching radio button could be detected for '#{val}' for #{elem}."
143
+ end
144
+ else
145
+ case v
146
+ when Symbol
147
+ elem.public_send v
148
+ when TrueClass
149
+ elem.set
150
+ when FalseClass
151
+ elem.clear
152
+ else
153
+ raise ArgumentError, "Unsupported argument for #{elem.class}: '#{v}'"
154
+ end
155
+ end
156
+ elsif @widget_elements.include?(k)
157
+ w = public_send(k)
158
+
159
+ begin
160
+ w.update(v)
161
+ rescue => e
162
+ begin
163
+ if v.is_a?(Array)
164
+ public_send(k).update(v)
165
+ else
166
+ public_send(k).update(*v)
167
+ end
168
+ rescue => e2
169
+ raise ArgumentError, "Dynamic method call failed for #{k}.", e2.backtrace.join("\n")
170
+ end
171
+ end
172
+ else
173
+ begin
174
+ if v.is_a?(Array)
175
+ public_send(k, *v)
176
+ else
177
+ public_send(k, v)
178
+ end
179
+ rescue => e
180
+ begin
181
+ if v.is_a?(Array)
182
+ public_send(k).update(v)
183
+ else
184
+ public_send(k).update(*v)
185
+ end
186
+ rescue => e2
187
+ raise ArgumentError, "Dynamic method call failed for #{k}.", e2.backtrace
188
+ end
189
+ end
190
+ end
191
+ rescue => e
192
+ if rescues.any? { |err| e.is_a?(err) }
193
+ unless failed.include?(k)
194
+ puts "Rescued #{e.class} when trying to update #{k}. Sleeping 5 seconds and then trying again."
195
+ failed << k
196
+ sleep 5
197
+ redo
198
+ end
199
+ else
200
+ raise e, "Failure trying to update #{k} with #{v.class}: #{v}:\n" + e.backtrace.join("\n")
201
+ end
202
+ end
203
+ sleep 0.2
204
+ end
205
+ sleep 1
206
+ hash_args
207
+ end
208
+
209
+ # Returns a Nokogiri document for the object ONLY. So no need to specify a
210
+ # relative path.
211
+ def nokogiri
212
+ Nokogiri::HTML(html)
213
+ end
214
+ alias document nokogiri
215
+ end
216
+ end
@@ -0,0 +1,60 @@
1
+ module Insite
2
+ module DOMMethods
3
+ DOM_METHODS.each do |mth|
4
+ define_method(mth) do |name=nil, *args, &block|
5
+ if block
6
+ element_container(name, mth, *args, &block)
7
+ else
8
+ el(name) { |b| b.send(mth, parse_args(args.flatten)) }
9
+ end
10
+ end
11
+ end
12
+
13
+ # TODO: (More context when this happens.)
14
+ # ArgumentError: wrong number of arguments (given 1, expected 0)
15
+ # from /Users/john/.rbenv/versions/2.3.5/lib/ruby/gems/2.3.0/gems/insite-0.5.1/lib/insite/methods/dom_methods.rb:17:in `block in el'
16
+ def el(name, &block)
17
+ @page_elements ||= []
18
+ @page_elements << name.to_sym
19
+
20
+ define_method(name) do
21
+ begin
22
+ elem = block.call(@browser)
23
+ begin
24
+ elem.dup.scroll.to
25
+ rescue => e
26
+ end
27
+ rescue(Watir::Exception::UnknownObjectException) => e
28
+ tmp = page
29
+
30
+ if tmp == @most_recent_page
31
+ raise e
32
+ else
33
+ @most_recent_page = tmp
34
+ elem = block.call(@browser)
35
+ begin
36
+ elem.dup.scroll.to
37
+ rescue => e
38
+ end
39
+ end
40
+ end
41
+ elem
42
+ end
43
+ end
44
+
45
+ # Duplicates Watir DOM element argument parsing for element methods.
46
+ private
47
+ def parse_args(args)
48
+ case args.length
49
+ when 2
50
+ return { args[0] => args[1] }
51
+ when 1
52
+ obj = args.first
53
+ return obj if obj.kind_of? Hash
54
+ when 0
55
+ return {}
56
+ end
57
+ end
58
+ public
59
+ end
60
+ end
@@ -0,0 +1,518 @@
1
+ # TODO: Title matcher
2
+ # TODO: Add page query methods.
3
+ module Insite
4
+ class DefinedPage
5
+ attr_reader :arguments, :browser, :has_fragment, :page_attributes, :page_elements, :page_features, :page_url, :query_arguments, :required_arguments, :site, :url_template, :url_matcher, :widget_elements
6
+
7
+ include Insite::CommonMethods
8
+ alias_method :update_page, :update_object
9
+
10
+ class << self
11
+ attr_reader :has_fragment, :page_attributes, :page_elements, :page_features, :page_url, :url_matcher, :url_template
12
+ attr_accessor :widget_elements
13
+
14
+ include Insite::DOMMethods
15
+ include Insite::WidgetMethods
16
+
17
+ def describe
18
+ puts <<-EOF
19
+ Page Class:\t#{name} (#{__FILE__})
20
+ URL Template:\t#{@url_template.pattern}"
21
+ URL Matcher:\t#{@url_matcher || 'Not specified.'}
22
+
23
+ Contains user-defined logic for a single page.
24
+
25
+ Page Elements:\n#{@page_elements.sort.map { |x| " #{x} #{x.class.to_s.methodize}\n" }.join }
26
+
27
+ Widgets:\n#{@widget_elements.sort.map { |x| " #{x} #{x.class.to_s.methodize}\n" }.join }
28
+
29
+ Features:\n#{@widget_elements.sort.map { |x| " #{x} #{x.class.to_s.methodize}\n" }.join }
30
+
31
+ EOF
32
+
33
+ end
34
+
35
+ private
36
+ def element_container(name, type, *args, &block)
37
+ tmpklass = Class.new(ElementContainer) do
38
+ self.class_eval(&block) if block_given?
39
+ end
40
+ cname = name.to_s.camelcase + 'Container'
41
+ const_set(cname, tmpklass) unless const_defined? cname
42
+
43
+ @page_elements ||= []
44
+ @page_elements << name.to_sym
45
+
46
+ define_method(name) do
47
+ self.class.const_get(cname).send(:new, @site, @browser.send(type, *args))
48
+ end
49
+ end
50
+ public
51
+
52
+ # Creates a section within the page. TODO: section is a DOM element, rename this.
53
+ def feature(fname, klass = Insite::Feature, &block)
54
+ tmpklass = Class.new(klass) do
55
+ self.class_eval(&block) if block_given?
56
+ end
57
+
58
+ const_set(fname.to_s.camelcase, tmpklass) unless const_defined? fname.to_s.camelcase
59
+ @page_features ||= []
60
+ @page_features << fname.to_s.underscore.to_sym
61
+ define_method(fname.to_s.underscore) do
62
+ tmpklass.new(page = self)
63
+ end
64
+ end
65
+
66
+ # Allows you to set special page attributes that affect page behavior. The two page
67
+ # attributes currently supported are :navigation_disabled and :page_template:
68
+ #
69
+ # * When :navigation_disabled is specified as a page attribute, all automatic and
70
+ # manual browser navigation is disabled. If you call the page's page methods
71
+ # automatic navigation is turned off -- it won't automatically load the page for
72
+ # you. And it the method will raise a Insite::Errors::PageNavigationNotAllowedError if you call
73
+ # the page's accessor method while you aren't actually on the page. And finally,
74
+ # the page's visit method is disabled. This attribute is useful only when you
75
+ # have a page that can't be automatically navigated to, in which case all of
76
+ # the navigation features described above wouldn't work anyway.
77
+ #
78
+ # * When :page_template is specified as a page attribute, the site object won't
79
+ # create an accessor method for the page when initializing and also won't include
80
+ # the page when calling the site object's pages method. This allows you to define
81
+ # a page object for inheritance purposes only. The idea behind this is to put common
82
+ # features one or more of these templates, which won't get used directly. Then your
83
+ # other page objects that you actually do want to use can inherit from one of the
84
+ # templates, gaining all of its features. For example, you can put things like a
85
+ # logout link or common menus into a template and then have all of the page objects
86
+ # that need those features inherit from the template and get those features
87
+ # automatically.
88
+ #
89
+ # If an unsupported attribute is specified a Insite::Errors::PageConfigError will be raised.
90
+ #
91
+ # Usage:
92
+ # set_attributes :attr1, :attr2
93
+ def set_attributes(*args)
94
+ @page_attributes ||= []
95
+ args.each do |arg|
96
+ case arg
97
+ when :navigation_disabled
98
+ @navigation_disabled = true
99
+ when :page_template
100
+ @page_template = true
101
+ else
102
+ raise(
103
+ Insite::Errors::PageConfigError,
104
+ "Unsupported page attribute argument: #{arg} for #{self} page definition. " \
105
+ "Argument class: #{arg.class}. Arguments must be one or more of the following " \
106
+ "symbols: :navigation_disabled, :template."
107
+ )
108
+ end
109
+ end
110
+
111
+ @page_attributes = args
112
+ end
113
+
114
+ def page_template?
115
+ @page_attributes ||= []
116
+ @page_attributes.include? :page_template
117
+ end
118
+
119
+ # Returns an array of symbols representing the required arguments for the page's page URL.
120
+ def required_arguments
121
+ @arguments ||= @url_template.keys.map { |k| k.to_sym }
122
+ end
123
+
124
+ def query_arguments
125
+ required_arguments.find { |x| @url_template.pattern =~ /\?.*#{x}=*/ }
126
+ end
127
+
128
+ # Used to define the full or relative URL to the page. Typically, you will *almost* *always* want to use
129
+ # this method when defining a page object (but see notes below.) The URL can be defined in a number
130
+ # of different ways. Here are some examples using Google News:
131
+ #
132
+ # *Relative* *URL*
133
+ #
134
+ # set_url "/nwshp?hl=en"
135
+ #
136
+ # Relative URLs are most commonly used when defining page objects. The idea here is that you can
137
+ # change the base_url when calling the site object, which allows you to use the same code across
138
+ # multiple test environments by changing the base_url as you initialize a site object.
139
+ #
140
+ # *Relative* *URL* *with* *URL* *Templating*
141
+ # set_url "/nwshp?hl={language}"
142
+ #
143
+ # This takes the relative URL example one step further, allowing you to set the page's parameters.
144
+ # Note that the the language specified in the first relative URL example ('en') was replaced by
145
+ # '{language}' in this one. Insite uses the Addressable library, which supports this kind of
146
+ # templating. When you template a value in the URL, the page object will allow you to specify the
147
+ # templated value when it's being initialized. Here's an example of how this works using a news site.
148
+ # Here's the base site class:
149
+ #
150
+ # class NewsSite
151
+ # include Insite
152
+ # end
153
+ #
154
+ # Here's a page object for the news page, templating the language value in the URL:
155
+ #
156
+ # class NewsPage < NewsSite::Page
157
+ # set_url "/news?l={language}"
158
+ # end
159
+ #
160
+ # After you've initialized the site object you can load the Spanish or French versions of the
161
+ # page by changing the hash argument used to call the page from the site object:
162
+ #
163
+ # site = NewsSite.new(base_url: "http://news.somesite.com")
164
+ # site.news_page(language: 'es')
165
+ # site.news_page(language: 'fr')
166
+ #
167
+ # In addition to providing a hash of templated values when initializing a page you can also use
168
+ # an object, as long as that object responds to all of the templated arguments in the page's
169
+ # URL definition. Here's a simple class that has a language method that we can use for the news
170
+ # page described above:
171
+ #
172
+ # class Country
173
+ # attr_reader :language
174
+ #
175
+ # def initialize(lang)
176
+ # @language = lang
177
+ # end
178
+ # end
179
+ #
180
+ # In the example below, the Country class is used to create a new new country object called 'c'.
181
+ # This object has been initialized with a Spanish language code and the news page
182
+ # will load the spanish version of the page when it's called with the country object.
183
+ #
184
+ # site = NewsSite.new(base_url: "http://news.somesite.com")
185
+ # c = Country.new('es')
186
+ # => <Country:0x007fcb0dc67f98 @language="es">
187
+ # c.language
188
+ # => 'es'
189
+ # site.news_page(c)
190
+ # => <NewsPage:0x003434546566>
191
+ #
192
+ # If one or more URL parameters are missing when the page is getting initialized then the page
193
+ # will look at the hash arguments used to initialize the site. If the argument the page needs is
194
+ # defined in the site's initialization arguments it will use that. For example, if the site
195
+ # is initialized with a port, subdomain, or any other argument you can use those values
196
+ # when defining a page URL. Example:
197
+ #
198
+ # class ConfigPage < MySite::Page
199
+ # set_url "/foo/{subdomain}/config"
200
+ # end
201
+ #
202
+ # site = MySite.new(subdomain: 'foo')
203
+ # => <MySite:0x005434546511>
204
+ # site.configuration_page # No need to provide a subdomain here as long as the site has it.
205
+ # => <ConfigPage:0x705434546541>
206
+ #
207
+ # *Full* *URL*
208
+ # set_url "http://news.google.com/nwshp?hl=en"
209
+ #
210
+ # Every once in a while you may not want to use a base URL that has been defined. This allows you
211
+ # to do that. Just define a complete URL for that page object and that's what will get used; the
212
+ # base_url will be ignored.
213
+ #
214
+ # *No* *URL*
215
+ #
216
+ # The set_url method is not mandatory. when defining a page. If you don't use set_url in the page
217
+ # definition then the page will defined the base_url as the page's URL.
218
+ def set_url(url)
219
+ url ? @page_url = url : nil
220
+ end
221
+
222
+ def set_url_template(base_url)
223
+ case @page_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
+ @has_fragment = @url_template.pattern =~ /#/
230
+ end
231
+
232
+ # Optional. Allows you to specify a fallback mechanism for checking to see if the correct page is
233
+ # being displayed. This only gets used in cases where the primary mechanism for checking a page
234
+ # (the URL template defined by Page#set_url) fails to match the current browser URL. When that
235
+ # happens the regular expression defined here will be applied and the navigation check will pass
236
+ # if the regular expression matches the current browser URL.
237
+ #
238
+ # In most cases, you won't need to define a URL matcher and should just rely on the default page
239
+ # matching that uses the page's URL template. The default matching should work fine for most cases.
240
+ def set_url_matcher(regexp)
241
+ regexp ? @url_matcher = regexp : nil
242
+ end
243
+
244
+ # Used to import page features for use within the page. Example:
245
+ #
246
+ # class ConfigPage < MySite::Page
247
+ # use_features :footer, :sidebar
248
+ # end
249
+ #
250
+ # Then, once the page object has been initialized:
251
+ #
252
+ # site.config_page.footer.about.click
253
+ #
254
+ # Use the PageFeature class to define page features.
255
+ def use_features(*args)
256
+ if @page_features
257
+ @page_feature += args
258
+ else
259
+ @page_features = args
260
+ end
261
+ end
262
+
263
+ def widget_method(method_name, widget_symbol, widget_method, target_element)
264
+ @widget_methods ||= []
265
+ @widget_methods << method_name.to_sym unless @widget_methods.include?(method_name.to_sym)
266
+
267
+ define_method(method_name) do |*args, &block|
268
+ self.class.const_get(widget_symbol.to_s.camelize)
269
+ .new(@site, @site.send(target_element))
270
+ .send(widget_method, *args, &block)
271
+ end
272
+ end
273
+ end # Self.
274
+
275
+ def describe
276
+ self.class.describe
277
+ end
278
+
279
+ def defined?
280
+ true
281
+ end
282
+
283
+ # Initializes a new page object. There's no need to ever call this method directly.
284
+ # Your site class (the one that includes the Insite module) will handle this for
285
+ # you
286
+ def initialize(site, args = nil)
287
+ @site = site
288
+ @browser = process_browser
289
+
290
+ @widget_elements = self.class.widget_elements ||= []
291
+ @browser = @site.browser
292
+ @page_attributes = self.class.page_attributes
293
+ @page_url = self.class.page_url
294
+ @page_elements = self.class.page_elements
295
+ # TODO: Clean this up
296
+ @page_features = self.class.instance_variable_get(:@page_features)
297
+ @required_arguments = self.class.required_arguments
298
+ @url_matcher = self.class.url_matcher
299
+ @url_template = self.class.url_template
300
+ @query_arguments = self.class.query_arguments
301
+ @has_fragment = self.class.has_fragment
302
+
303
+ # Try to expand the URL template if the URL has parameters.
304
+ @arguments = {}.with_indifferent_access # Stores the param list that will expand the url_template after examining the arguments used to initialize the page.
305
+ match = @url_template.match(@browser.url)
306
+
307
+ if @required_arguments.present? && !args
308
+ @required_arguments.each do |arg|
309
+ if match && match.keys.include?(arg.to_s)
310
+ @arguments[arg] = match[arg.to_s]
311
+ elsif @site.arguments[arg]
312
+ @arguments[arg] = @site.arguments[arg]
313
+ elsif @site.respond_to?(arg) && @site.public_send(arg)
314
+ @arguments[arg] = @site.public_send(arg)
315
+ else
316
+ raise(
317
+ Insite::Errors::PageInitError,
318
+ "No arguments provided when attempting to initialize #{self.class.name}. " \
319
+ "This page object requires the following arguments for initialization: "\
320
+ ":#{@required_arguments.join(', :')}.\n\n#{caller.join("\n")}"
321
+ )
322
+ end
323
+ end
324
+ elsif @required_arguments.present?
325
+ @required_arguments.each do |arg| # Try to extract each URL argument from the hash or object provided, OR from the site object.
326
+ if args.is_a?(Hash) && args.present?
327
+ args = args.with_indifferent_access
328
+
329
+ if args[arg] #The hash has the required argument.
330
+ @arguments[arg]= args[arg]
331
+ elsif match.keys.include?(arg.to_s)
332
+ @arguments[arg] = match[arg.to_s]
333
+ elsif @site.respond_to?(arg)
334
+ @arguments[arg] = site.public_send(arg)
335
+ else
336
+ raise(
337
+ Insite::Errors::PageInitError,
338
+ "A required page argument is missing. #{args.class} was provided, " \
339
+ "but this object did not respond to :#{arg}, which is necessary to " \
340
+ "build a URL for the #{self.class.name} page.\n\n#{caller.join("\n")}"
341
+ )
342
+ end
343
+ elsif args # Some non-hash object was provided.
344
+ if args.respond_to?(arg) #The hash has the required argument.
345
+ @arguments[arg]= args.public_send(arg)
346
+ elsif @site.respond_to?(arg)
347
+ @arguments[arg]= site.public_send(arg)
348
+ else
349
+ raise(
350
+ Insite::Errors::PageInitError,
351
+ "A required page argument is missing. #{args.class} was provided, but " \
352
+ "this object did not respond to :#{arg}, which is necessary to build " \
353
+ "a URL for the #{self.class.name} page.\n\n#{caller.join("\n")}"
354
+ )
355
+ end
356
+ else
357
+ # Do nothing here yet.
358
+ end
359
+ end
360
+ elsif @required_arguments.empty? && args # If there are no required arguments then nothing should be provided.
361
+ raise(
362
+ Insite::Errors::PageInitError,
363
+ "#{args.class} was provided as a #{self.class.name} initialization argument, " \
364
+ "but the page URL doesn't require any arguments.\n\n#{caller.join("\n")}"
365
+ )
366
+ else
367
+ # Do nothing here yet.
368
+ end
369
+
370
+ @url = @url_template.expand(@arguments).to_s
371
+ @page_features ||= []
372
+ @page_features.each do |fname|
373
+ begin
374
+ klass = fname.to_s.camelize.constantize
375
+ rescue NameError => e
376
+ klass = self.class.const_get fname.to_s.camelize
377
+ end
378
+
379
+ self.class_eval do
380
+ #klass = fname.to_s.camelize.constantize
381
+ if klass.alias
382
+ define_method(klass.alias) do
383
+ klass.new(self)
384
+ end
385
+ else
386
+ define_method(fname) do
387
+ klass.new(self)
388
+ end
389
+ end
390
+ end
391
+ end
392
+
393
+ @site.most_recent_page = self
394
+ unless on_page?
395
+ if navigation_disabled?
396
+ raise(
397
+ Insite::Errors::PageNavigationNotAllowedError,
398
+ "Navigation is intentionally disabled for the #{self.class.name} page. " \
399
+ "You can only call the accessor method for this page when it's already " \
400
+ "being displayed in the browser.\n\nCurrent URL:" \
401
+ "\n------------\n#{@site.browser.url}\n\n#{caller.join("\n")}"
402
+ )
403
+ end
404
+
405
+ visit
406
+ end
407
+ end
408
+
409
+ # Custom inspect method so that console output doesn't get in the way when debugging.
410
+ def inspect
411
+ "#<#{self.class.name}:#{object_id} @url_template=#{@url_template.inspect}>"
412
+ end
413
+
414
+ def navigation_disabled?
415
+ @page_attributes.include? :navigation_disabled
416
+ end
417
+
418
+ def on_page?
419
+ url = @browser.url
420
+
421
+ if @url_matcher
422
+ if @url_matcher =~ url
423
+ return true
424
+ else
425
+ return false
426
+ end
427
+ elsif @url_template.match(url)
428
+ if @arguments.empty?
429
+ return true
430
+ else
431
+ if pargs = @url_template.extract(Addressable::URI.parse(url))
432
+ pargs = pargs.with_indifferent_access
433
+ @required_arguments.all? do |k|
434
+ pargs[k] == @arguments[k] ||
435
+ pargs[k] == @arguments[k].to_s ||
436
+ !@arguments[k] && pargs[k] # Don't complain if arg is not explicit.
437
+ end
438
+ end
439
+ end
440
+ elsif @url_template.match(url.split(/(\?|#|\/$)/)[0])
441
+ if @arguments.empty?
442
+ return true
443
+ else
444
+ if pargs = @url_template.extract(Addressable::URI.parse(url))
445
+ pargs = pargs.with_indifferent_access
446
+ @required_arguments.all? { |k| pargs[k] == @arguments[k].to_s }
447
+ end
448
+ end
449
+ else
450
+ false
451
+ end
452
+ end
453
+
454
+ # Refreshes the page.
455
+ def refresh
456
+ @browser.refresh
457
+ self
458
+ end
459
+
460
+ # Returns the page title displayed by the browser.
461
+ def title
462
+ @browser.title
463
+ end
464
+
465
+ # Reloads the page (No need to call this method for initial navigation, which
466
+ # happens automatically when the page is first initialized.)
467
+ #
468
+ # Raises an Insite::Errors::PageNavigationNotAllowedError when
469
+ # navigation has been disabled for the page.
470
+ #
471
+ # Raises an Insite::Errors::WrongPageError if the specified page
472
+ # isn't getting displayed after navigation.
473
+ def visit
474
+ if navigation_disabled?
475
+ raise(
476
+ Insite::Errors::PageNavigationNotAllowedError,
477
+ "Navigation has been disabled for the #{self.class.name} page. This was " \
478
+ "done when defining the page's class and usually means that the page can't " \
479
+ "be reached directly through a URL and requires some additional work to access."
480
+ )
481
+ end
482
+
483
+ 2.times do
484
+ begin
485
+ @browser.goto(@url)
486
+ break
487
+ rescue Net::ReadTimeout => e
488
+ sleep 3
489
+ end
490
+ end
491
+
492
+ if @url_matcher
493
+ unless on_page?
494
+ raise(
495
+ Insite::Errors::WrongPageError,
496
+ "Navigation check failed after attempting to access the #{self.class.name} page. " \
497
+ "This page has a URL matcher (a regular expression) defined for it. When a URL " \
498
+ "matcher is defined it is used in place of the URL template that is normally used " \
499
+ " to check for page display (URL template was still used for navigation.)" \
500
+ "\n\nURL after navigation:\n#{@browser.url}" \
501
+ "\n\nPage URL matcher that failed: #{@url_matcher}"
502
+ )
503
+ end
504
+ else
505
+ unless on_page?
506
+ raise(
507
+ Insite::Errors::WrongPageError,
508
+ "Navigation check failed after attempting to access the #{self.class.name} page. " \
509
+ "Current URL #{@browser.url} did not match #{@url_template.pattern}"
510
+ )
511
+ end
512
+ end
513
+
514
+ @site.most_recent_page = self
515
+ self
516
+ end
517
+ end
518
+ end