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,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: []