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.
- checksums.yaml +7 -0
- data/lib/insite.rb +29 -0
- data/lib/insite/constants.rb +32 -0
- data/lib/insite/element_container/element_container.rb +42 -0
- data/lib/insite/errors.rb +16 -0
- data/lib/insite/feature/feature.rb +30 -0
- data/lib/insite/insite.rb +294 -0
- data/lib/insite/methods/common_methods.rb +216 -0
- data/lib/insite/methods/dom_methods.rb +60 -0
- data/lib/insite/page/defined_page.rb +518 -0
- data/lib/insite/page/undefined_page.rb +74 -0
- data/lib/insite/version.rb +3 -0
- data/lib/insite/widget/widget.rb +342 -0
- data/lib/insite/widget/widget_methods.rb +4 -0
- metadata +198 -0
@@ -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
|