insite 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,74 @@
1
+ module Insite
2
+ class UndefinedPage
3
+ attr_reader :arguments, :browser, :has_fragment, :page_attributes, :page_elements, :page_features, :page_url, :query_arguments, :required_arguments, :site, :url_template, :url_matcher
4
+
5
+ include Insite::CommonMethods
6
+
7
+ # Always returns false.
8
+ def defined?
9
+ false
10
+ end
11
+
12
+ def describe
13
+ puts <<-EOF
14
+ Page Class:\t#{name} (#{__FILE__})
15
+ URL Template:\tNA
16
+ URL Matcher:\tNA
17
+
18
+ Page class for any page that hasn't been defined (or that Insite doesn't recognize.)
19
+
20
+ Note: If there is a page class that is defined for this page that isn't getting
21
+ loaded, try the following:
22
+ - Review the pages URL template for problems. A URL template is *not* an interpolated
23
+ string -- it just looks like one. See the documentation for more details.
24
+ - If the URL template seems to be designed correctly for loading the page, but the
25
+ page's URL changes after the load, you will need to look at adding a URL matcher,
26
+ which overrides the URL template for purposes of checking whether the page object
27
+ is matching the currently displayed page after it has been loaded.
28
+
29
+ Page Elements:\tNA
30
+ EOF
31
+ end
32
+
33
+ def initialize(site)
34
+ @site = site
35
+ @browser = process_browser
36
+ @url = @site.browser.url
37
+ end
38
+
39
+ # TODO: Do the same cache check that's done for a defined page and reapply the
40
+ # method if the cache is updated and the new page DOES respond to the method.
41
+ def method_missing(mth, *args, &block)
42
+ raise NoMethodError, "Could not apply #{mth}. The current page could not be " \
43
+ "recognized. Current URL #{@browser.url}"
44
+ end
45
+
46
+ # Returns a Nokogiri object for the page HTML.
47
+ def nokogiri
48
+ @site.nokogiri
49
+ end
50
+
51
+ # Similar to the method that you can call on a page object you've defined (but always
52
+ # returns false since the Undefined page class is only returned when the current page
53
+ # doesn't match up to any page that you've actually defined.
54
+ def on_page?
55
+ false
56
+ end
57
+
58
+ # Returns an empty Array (since the page hasn't been defined.)
59
+ def page_elements
60
+ []
61
+ end
62
+ alias_method :widget_elements, :page_elements
63
+
64
+ # Returns the current URL.
65
+ def url
66
+ @browser.url
67
+ end
68
+
69
+ # Returns the page title displayed by the browser.
70
+ def title
71
+ @browser.title
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,3 @@
1
+ module Insite
2
+ VERSION = '0.0.1'
3
+ end
@@ -0,0 +1,342 @@
1
+ require_relative 'widget_methods'
2
+ require_relative '../methods/common_methods'
3
+
4
+ # Allows the page object developer to encapsulate common web application features
5
+ # into a "widget" that can be reused across multiple pages. Let's say that a
6
+ # web application has a search widget that is used in 11 of the application's pages.
7
+ # With a modern web app all of those search widgets will likely be implemented
8
+ # in a common way, with a similar or identical structure in the HTML. The widget
9
+ # would look something like this:
10
+ #
11
+ # class SearchWidget < Widget
12
+ # text_field :query, id: 'q'
13
+ # button :search_button, name: 'Search'
14
+ #
15
+ # def search(search_query)
16
+ # query.set search_query
17
+ # search_button.click
18
+ # end
19
+ #
20
+ # def clear
21
+ # query.set ''
22
+ # search_button.click
23
+ # end
24
+ # end
25
+ #
26
+ # Once the widget has been defined, it can be included in a page object definition
27
+ # like this:
28
+ #
29
+ # class SomePage < SomeSite::Page
30
+ # set_url 'some_page'
31
+ # search_widget :search_for_foo, :div, class: 'search-div'
32
+ # end
33
+ #
34
+ # The search widget can then be accessed like this when working with the site:
35
+ # site.some_page.search_for_foo 'some search term'
36
+ # site.search_for_foo.clear
37
+ #
38
+ # Widgets can be embedded in other widgets, but in that case, the arguments for
39
+ # accessing the child widget need to be RELATIVE to the parent widget. For example:
40
+ #
41
+ # # Generic link menu, you hover over it and one or more links are displayed.
42
+ # class LinkMenu < Widget
43
+ # end
44
+ #
45
+ # # Card widget that uses the link_menu widget. In this case, link_menu widget
46
+ # # arguments will be used to find a div a div with class == 'card-action-links'
47
+ # # WITHIN the card itself. This ensures that, if there are multiple cards
48
+ # # on the page that have link_menus, the CORRECT link_menu will be accessed
49
+ # # rather than one for some other card widget.
50
+ # class Card < Widget
51
+ # link_menu :card_menu, :div, class: 'card-action-links'
52
+ # end
53
+ module Insite
54
+ class Widget
55
+ attr_reader :site, :browser, :type, :args, :target
56
+
57
+ include CommonMethods
58
+ alias_method :update_widget, :update_object
59
+
60
+ class << self
61
+ attr_reader :widget_elements
62
+
63
+ include DOMMethods
64
+ include WidgetMethods
65
+
66
+ # - Don't allow the user to create a widget with a name that matches a DOM
67
+ # element.
68
+ #
69
+ # - Don't allow the user to create a widget method that references a
70
+ # collection (because this will be done automatically.)
71
+ tmp = name.to_s.underscore.to_sym
72
+ if DOM_METHODS.include?(name.to_s.underscore.to_sym)
73
+ raise "#{name} cannot be used as a widget name, as the methodized version of the class name (#{name.to_s.underscore} conflicts with a Watir DOM method.)"
74
+ elsif Watir::Browser.methods.include?(name.to_s.underscore.to_sym)
75
+ raise "#{name} cannot be used as a widget name, as the methodized version of the class name (#{name.to_s.underscore} conflicts with a Watir::Browser method.)"
76
+ end
77
+
78
+ if tmp =~ /.*s+/
79
+ raise "Invalid widget type :#{tmp}. You can create a widget for the DOM object but it must be for :#{tmp.singularize} (:#{tmp} will be created automatically.)"
80
+ end
81
+ end # Self.
82
+
83
+ extend Forwardable
84
+
85
+ def self.inherited(subclass)
86
+ name_string = subclass.name.demodulize.underscore
87
+ pluralized_name_string = name_string.pluralize
88
+
89
+ if name_string == pluralized_name_string
90
+ raise ArgumentError, "When defining a new widget, define the singular version only (Plural case will be handled automatically.)"
91
+ end
92
+
93
+ # Creates an accessor method for a new widget when one gets defined via inheritance.
94
+ # In this case the method is for a single instance of the widget. The top-level block
95
+ # defines the widget accessor in the Insite::Widget module. The methods in that module
96
+ # automatically get included in page classes and are used to define widget accessors.
97
+ #
98
+ # In this case an accessor for an individual widget is being defined.
99
+ WidgetMethods.send(:define_method, name_string) do |method_name, dom_type, *args, &block|
100
+ unless name_string == 'Widget'
101
+ @widget_elements ||= []
102
+ @widget_elements << method_name.to_sym unless @widget_elements.include?(method_name.to_sym)
103
+
104
+ define_method(method_name) do
105
+ if is_a? Widget
106
+ elem = send(dom_type, *args, &block)
107
+ else
108
+ elem = @browser.send(dom_type, *args, &block)
109
+ end
110
+
111
+ # TODO: Bandaid.
112
+ if dom_type.to_s == dom_type.to_s.pluralize
113
+ raise ArgumentError, "Individual widget method :#{method_name} cannot initialize a widget using an element collection (#{elem.class}.) Use :#{method_name.pluralize} rather than :#{method_name} if you want to define a widget collection."
114
+ else
115
+ subclass.new(self, dom_type, *args, &block)
116
+ end
117
+ end
118
+ end
119
+ end
120
+
121
+ # Creates an accessor method for a new widget when one gets defined via inheritance.
122
+ # In this case the method is for a single instance of the widget. The top-level block
123
+ # defines the widget accessor in the Insite::Widget module. The methods in that module
124
+ # automatically get included in page classes and are used to define widget accessors.
125
+ #
126
+ # In this case an accessor is being defined for a widget collection.
127
+ #
128
+ # TODO: The current implementation for widget collections isn't ideal and should be
129
+ # replaced at some point. It'd be much better to use a (lazy) custom collection for this.
130
+ WidgetMethods.send(:define_method, pluralized_name_string) do |method_name, dom_type, *args, &block|
131
+ unless name_string == 'Widget'
132
+ @widget_elements ||= []
133
+ @widget_elements << method_name.to_sym unless @widget_elements.include?(method_name.to_sym)
134
+
135
+ define_method(method_name) do
136
+ if is_a?(Widget) && present?
137
+ elem = send(dom_type, *args, &block)
138
+ elsif is_a?(Widget) && !present?
139
+ return []
140
+ else
141
+ elem = @browser.send(dom_type, *args, &block)
142
+ end
143
+
144
+ # TODO: Bandaid.
145
+ if dom_type.to_s == dom_type.to_s.singularize
146
+ raise ArgumentError, "Widget collection method :#{method_name} cannot initialize a widget collection using an individual element (#{elem.class}.) Use :#{method_name.to_s.singularize} rather than :#{method_name} if you want to define a widget for an individual element."
147
+ else
148
+ # TODO: Revisit the whole .to_a thing, need a custom collection or
149
+ # somesuch (don't bypass watir wait logic.)
150
+ t = Time.now
151
+ loop do
152
+ elem = @browser.send(dom_type, *args, &block)
153
+ break if elem.present? && elem.length > 0
154
+ break if Time.now > t + 4
155
+ end
156
+
157
+ if elem.present?
158
+ elem.to_a.map! { |x| subclass.new(self, x, [], &block) }
159
+ else
160
+ []
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end # self.
167
+
168
+ # This method gets used 2 different ways. Most of the time, dom_type and args
169
+ # will be a symbol and a set of hash arguments that will be used to locate an
170
+ # element.
171
+ #
172
+ # In some cases, dom_type can be a Watir DOM object, and in this case, the
173
+ # args are ignored and the widget is initialized using the Watir object.
174
+ #
175
+ # TODO: Needs a rewrite, lines between individual and collection are blurred
176
+ # here and that makes the code more confusing. And there should be a proper
177
+ # collection class for element collections, with possibly some AR-like accessors.
178
+ def initialize(parent, dom_type, *args)
179
+ @parent = parent
180
+ @site = parent.class.ancestors.include?(Insite) ? parent : parent.site
181
+ @browser = @site.browser
182
+ @widget_elements = self.class.widget_elements
183
+
184
+ if dom_type.is_a?(Watir::HTMLElement) || dom_type.is_a?(Watir::Element)
185
+ @dom_type = nil
186
+ @args = nil
187
+ @target = dom_type
188
+ elsif [String, Symbol].include? dom_type.class
189
+ @dom_type = dom_type
190
+ @args = args
191
+
192
+ if @parent.is_a? Widget
193
+ @target = @parent.send(dom_type, *args)
194
+ else
195
+ @target = @browser.send(dom_type, *args)
196
+ end
197
+
198
+ # New webdriver approach.
199
+ begin
200
+ @target.scroll.to
201
+ sleep 0.1
202
+ rescue => e
203
+ t = Time.now + 2
204
+ while Time.now <= t do
205
+ break if @target.present?
206
+ sleep 0.1
207
+ end
208
+ end
209
+ elsif dom_type.is_a? Watir::ElementCollection
210
+ @dom_type = nil
211
+ @args = nil
212
+ if @parent.is_a? Widget
213
+ @target = dom_type.map { |x| self.class.new(@parent, x.to_subtype) }
214
+ else
215
+ @target = dom_type.map { |x| self.class.new(@site, x.to_subtype) }
216
+ end
217
+ else
218
+ raise "Unhandled exception."
219
+ end
220
+ end
221
+
222
+ # Delegates method calls down to the widget's wrapped element if the element supports the method.
223
+ #
224
+ # Supports dynamic link methods. Examples:
225
+ # s.accounts_page account
226
+ #
227
+ # # Nav to linked page only.
228
+ # s.account_actions.edit_account_info
229
+ #
230
+ # # Update linked page after nav:
231
+ # s.account_actions.edit_account_info username: 'foo'
232
+ #
233
+ # # Link with modal (if the modal requires args they should be passed as hash keys):
234
+ # # s.hosted_pages.refresh_urls
235
+ def method_missing(mth, *args, &block)
236
+ if @target.respond_to? mth
237
+ @target.send(mth, *args, &block)
238
+ else
239
+ if args[0].is_a? Hash
240
+ page_arguments = args[0]#.with_indifferent_access
241
+ elsif args.empty?
242
+ # Do nothing.
243
+ elsif args[0].nil?
244
+ raise ArgumentError, "Optional argument for :#{mth} must be a hash. Got NilClass."
245
+ else
246
+ raise ArgumentError, "Optional argument must be a hash (got #{args[0].class}.)"
247
+ end
248
+
249
+ if present?
250
+ # If it's a widget we want to hover over it to ensure links are visible
251
+ # before trying to find them.
252
+ if self.is_a?(Widget)
253
+ t = Time.now
254
+ loop do
255
+ begin
256
+ scroll.to
257
+ hover
258
+ sleep 0.2
259
+ break
260
+ rescue => e
261
+ break if Time.now > t + 10
262
+ sleep 0.2
263
+ end
264
+ end
265
+ end
266
+
267
+ # Dynamic helper method, returns DOM object for link (no validation).
268
+ if mth.to_s =~ /_link$/
269
+ return a(text: /^#{mth.to_s.sub(/_link$/, '').gsub('_', '.*')}/i)
270
+ # Dynamic helper method, returns DOM object for button (no validation).
271
+ elsif mth.to_s =~ /_button$/
272
+ return button(value: /^#{mth.to_s.sub(/_button$/, '').gsub('_', '.*')}/i)
273
+ # Dynamic helper method for links. If a match is found, clicks on the link and performs follow up actions.
274
+ elsif elem = as.find { |x| x.text =~ /^#{mth.to_s.gsub('_', '.*')}/i } # See if there's a matching button and treat it as a method call if so.
275
+ elem.click
276
+ sleep 1
277
+
278
+ current_page = @site.page
279
+
280
+ if page_arguments.present?
281
+
282
+ if current_page.respond_to?(:submit)
283
+ current_page.submit page_arguments
284
+ elsif @browser.input(xpath: "//div[starts-with(@class,'Row') and last()]//input[@type='submit' and last()]").present?
285
+ current_page.update_page page_arguments
286
+ @browser.input(xpath: "//div[starts-with(@class,'Row') and last()]//input[@type='submit' and last()]").click
287
+ end
288
+ current_page = @site.page
289
+ end
290
+ # Dynamic helper method for buttons. If a match is found, clicks on the link and performs follow up actions.
291
+ elsif elem = buttons.find { |x| x.text =~ /^#{mth.to_s.gsub('_', '.*')}/i } # See if there's a matching button and treat it as a method call if so.
292
+ elem.click
293
+ sleep 1
294
+
295
+ if @site.modal.present?
296
+ @site.modal.continue(page_arguments)
297
+ else
298
+ current_page = @site.page
299
+
300
+ if page_arguments.present?
301
+ if current_page.respond_to?(:submit)
302
+ current_page.submit page_arguments
303
+ elsif @browser.input(xpath: "//div[starts-with(@class,'Row') and last()]//input[@type='submit' and last()]").present?
304
+ current_page.update_page page_arguments
305
+ @browser.input(xpath: "//div[starts-with(@class,'Row') and last()]//input[@type='submit' and last()]").click
306
+ end
307
+ current_page = @site.page
308
+ end
309
+ end
310
+ else
311
+ raise NoMethodError, "undefined method `#{mth}' for #{self.class}."
312
+ end
313
+ else
314
+ raise NoMethodError, "Unhandled method call `#{mth}' for #{self.class} (The widget was not present in the DOM at the point that the method was called.)"
315
+ end
316
+
317
+ page_arguments.present? ? page_arguments : current_page
318
+ end
319
+ end
320
+
321
+ def present?
322
+ sleep 0.1
323
+ begin
324
+ if @parent
325
+ if @parent.present? && @target.present?
326
+ true
327
+ else
328
+ false
329
+ end
330
+ else
331
+ if @target.present?
332
+ true
333
+ else
334
+ false
335
+ end
336
+ end
337
+ rescue => e
338
+ false
339
+ end
340
+ end
341
+ end
342
+ end
@@ -0,0 +1,4 @@
1
+ module Insite
2
+ module WidgetMethods
3
+ end
4
+ end
metadata ADDED
@@ -0,0 +1,198 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: insite
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - John Fitisoff
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-04-08 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: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: nokogiri
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: watir
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 6.0.0
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 6.0.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: watir-scroll
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: 0.2.0
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: 0.2.0
83
+ - !ruby/object:Gem::Dependency
84
+ name: coveralls
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: simplecov
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: pry
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rake
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rspec
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ description: Page object library.
154
+ email: jfitisoff@yahoo.com
155
+ executables: []
156
+ extensions: []
157
+ extra_rdoc_files: []
158
+ files:
159
+ - lib/insite.rb
160
+ - lib/insite/constants.rb
161
+ - lib/insite/element_container/element_container.rb
162
+ - lib/insite/errors.rb
163
+ - lib/insite/feature/feature.rb
164
+ - lib/insite/insite.rb
165
+ - lib/insite/methods/common_methods.rb
166
+ - lib/insite/methods/dom_methods.rb
167
+ - lib/insite/page/defined_page.rb
168
+ - lib/insite/page/undefined_page.rb
169
+ - lib/insite/version.rb
170
+ - lib/insite/widget/widget.rb
171
+ - lib/insite/widget/widget_methods.rb
172
+ homepage: http://rubygems.org/gems/insite
173
+ licenses:
174
+ - MIT
175
+ metadata: {}
176
+ post_install_message:
177
+ rdoc_options: []
178
+ require_paths:
179
+ - lib
180
+ required_ruby_version: !ruby/object:Gem::Requirement
181
+ requirements:
182
+ - - ">="
183
+ - !ruby/object:Gem::Version
184
+ version: '0'
185
+ required_rubygems_version: !ruby/object:Gem::Requirement
186
+ requirements:
187
+ - - ">="
188
+ - !ruby/object:Gem::Version
189
+ version: '0'
190
+ requirements: []
191
+ rubyforge_project:
192
+ rubygems_version: 2.5.2.1
193
+ signing_key:
194
+ specification_version: 4
195
+ summary: Wraps page objects up into a site object, which provides some introspection
196
+ and navigation capabilities that page objects don't provide. Works with Watir and
197
+ Selenium.
198
+ test_files: []