insite 0.0.1

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