gless 1.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,319 @@
1
+ require 'rspec'
2
+
3
+ module Gless
4
+
5
+ # Provides an abstraction layer between the individual pages of an
6
+ # website and the high-level application layer, so that the
7
+ # application layer doesn't have to know about what page it's on
8
+ # or similar.
9
+ #
10
+ # For details, see the README.
11
+ class Gless::Session
12
+ include RSpec::Matchers
13
+
14
+ # The page class for the page the session thinks we're currently
15
+ # on.
16
+ attr_reader :current_page
17
+
18
+ # A list of page classes of pages that it's OK for us to be on.
19
+ # Usually just one, but some site workflows might have more than
20
+ # one thing that can happen when you click a button or whatever.
21
+ #
22
+ # When you assign a value here, a fair bit of processing is
23
+ # done. Most of the actual work is in check_acceptable_pages
24
+ #
25
+ # The user can give us a class, a symbol, or a list of those; no
26
+ # matter what, we return a list. That list is of possible pages
27
+ # that, if we turn out to be on one of them, that's OK, and if
28
+ # not we freak out.
29
+ #
30
+ # @param [Class, Symbol, Array] newpages A page class, or a
31
+ # symbol naming a page class, or an array of those, for which
32
+ # pages are acceptable.
33
+ attr_reader :acceptable_pages
34
+
35
+ # See docs for :acceptable_pages
36
+ def acceptable_pages= newpage
37
+ log.debug "Session: changing acceptable pages list to #{newpage}"
38
+ @acceptable_pages = (check_acceptable_pages newpage).flatten
39
+ log.info "Session: acceptable pages list has been changed to: #{@acceptable_pages}"
40
+ end
41
+
42
+ # This exists only to be called by +inherited+ on
43
+ # Gless::BasePage; see documentation there.
44
+ def self.add_page_class( klass )
45
+ @@page_classes ||= []
46
+ @@page_classes << klass
47
+ end
48
+
49
+
50
+ # Sets up the session object. As the core abstraction layer that
51
+ # sits in the middle of everything, this requires a number of
52
+ # arguments. :)
53
+ #
54
+ # @param [Gless::Browser] browser
55
+ # @param [Gless::EnvConfig] config
56
+ # @param [Gless::Logger] logger
57
+ # @param [Object] application See the README for a description
58
+ # of the stuff the application object is expected to have.
59
+ def initialize( browser, config, logger, application )
60
+ @logger = logger
61
+
62
+ log.debug "Session: Initializing with #{browser.inspect}"
63
+
64
+ @browser = browser
65
+ @application = application
66
+ @pages = Hash.new
67
+ @timeout = 30
68
+ @acceptable_pages = nil
69
+ @config = config
70
+
71
+ @@page_classes.each do |sc|
72
+ @pages[sc] = sc.new( @browser, self, @application )
73
+ end
74
+
75
+ log.debug "Session: Final pages table: #{@pages.keys.map { |x| x.name }}"
76
+
77
+ return self
78
+ end
79
+
80
+ # Just passes through to the Gless::EnvConfig component's +get+
81
+ # method.
82
+ def get_config(*args)
83
+ @config.get(*args)
84
+ end
85
+
86
+ # Just a shortcut to get to the Gless::Logger object.
87
+ def log
88
+ @logger
89
+ end
90
+
91
+ # Anything that we don't otherwise recognize is passed on to the
92
+ # current underlying page object (i.e. descendant of
93
+ # Gless::BasePage).
94
+ #
95
+ # This gets complicated because of the state checking: we test
96
+ # extensively that we're on the page that we think we should be
97
+ # on before passing things on to the page object.
98
+ def method_missing(m, *args, &block)
99
+ # Do some logging.
100
+ if m.inspect =~ /(password|login)/i or args.inspect =~ /(password|login)/i
101
+ log.debug "Session: Doing something with passwords, redacted."
102
+ else
103
+ log.debug "Session: method_missing for #{m} with arguments #{args.inspect}"
104
+ end
105
+
106
+ log.debug "Session: check if we've changed pages: #{@browser.title}, #{@browser.url}, #{@previous_url}, #{@current_page}, #{@acceptable_pages}"
107
+
108
+ # Changed URL means we've changed pages. Our current page no
109
+ # longer being in the acceptable pages list means we *should*
110
+ # have changed pages. So we check both.
111
+ if @browser.url == @previous_url && @acceptable_pages.member?( @current_page )
112
+ log.debug "Session: doesn't look like we've moved."
113
+ else
114
+ # See if we're on one of the acceptable pages; wait until we
115
+ # are for "timeout" seconds.
116
+ good_page=false
117
+ new_page=nil
118
+ @timeout.times do
119
+ if @acceptable_pages.nil?
120
+ # If we haven't gone anywhere yet, anything is good
121
+ good_page = true
122
+ new_page = @pages[@current_page]
123
+ break
124
+ end
125
+
126
+ @acceptable_pages.each do |page|
127
+ log.debug "Session: Checking our current url, #{@browser.url}, for a match in #{page.name}: #{@pages[page].match_url(@browser.url)}"
128
+ if @pages[page].match_url(@browser.url)
129
+ good_page = true
130
+ @current_page = page
131
+ new_page = @pages[page]
132
+ log.debug "Session: we seem to be on #{page.name} at #{@browser.url}"
133
+ break
134
+ end
135
+ end
136
+
137
+ if good_page
138
+ break
139
+ end
140
+ sleep 1
141
+ end
142
+
143
+ good_page.should be_true, "Current URL is #{@browser.url}, which doesn't match any of the acceptable pages: #{@acceptable_pages}"
144
+
145
+ log.debug "Session: checking for arrival at #{new_page.class.name}"
146
+ new_page.arrived?.should be_true
147
+
148
+ url=@browser.url
149
+ log.debug "Session: refreshed browser URL: #{url}"
150
+ new_page.match_url(url).should be_true
151
+
152
+ log.info "Session: We are currently on page #{new_page.class.name}, as we should be"
153
+
154
+ @previous_url = url
155
+ end
156
+
157
+ cpage = @pages[@current_page]
158
+
159
+ if m.inspect =~ /(password|login)/i or args.inspect =~ /(password|login)/i
160
+ log.debug "Session: dispatching method #{m} with args [redacted; password maybe] to #{cpage}"
161
+ else
162
+ log.debug "Session: dispatching method #{m} with args #{args.inspect} to #{cpage}"
163
+ end
164
+ retval = cpage.send(m, *args, &block)
165
+ log.debug "Session: method returned #{retval}"
166
+
167
+ retval
168
+ end
169
+
170
+ # This function is used to go to an intitial entry point for a
171
+ # website. The page in question must have had set_entry_url run
172
+ # in its class definition, to define how to do this. This setup
173
+ # exists because explaining to the session that we really should
174
+ # be on that page is a bit tricky.
175
+ #
176
+ # @param [Class] pklas The class for the page object that has a
177
+ # set_entry_url that we are using.
178
+ def enter(pklas)
179
+ log.info "Session: Entering the site directly using the entry point for the #{pklas.name} page class"
180
+ @current_page = pklas
181
+ @pages[pklas].enter
182
+ # Needs to run through our custom acceptable_pages= method
183
+ self.acceptable_pages = pklas
184
+ end
185
+
186
+ # Wait for long-term AJAX-style processing, i.e. watch the page
187
+ # for extended amounts of time until particular events have
188
+ # occured.
189
+ #
190
+ # @param [String] message The text to print to the user each
191
+ # time the page is not completely loaded.
192
+ # @param [Hash] opts Various named options.
193
+ #
194
+ # @option opts [Integer] numtimes The number of times to test the page.
195
+ # @option opts [Integer] interval The number of seconds to delay
196
+ # between each check.
197
+ # @option opts [Array] any_elements Watir page elements, if any
198
+ # of them are present, the page load is considered complete.
199
+ # @option opts all_elements Watir page elements, if all of them
200
+ # are present, the page load is considered complete.
201
+ #
202
+ # @yieldreturn [Boolean] An optional Proc/code block; if
203
+ # present, it is run before each page check. This is so
204
+ # simple interactions can occur without waiting for the
205
+ # timeout, and so the whole process can be short-circuited.
206
+ # If the block returns true, the long_wait ends successfully.
207
+ #
208
+ # @example
209
+ #
210
+ # @session.long_wait "Cloud Application: Still waiting for the environment to be deleted.", :any_elements => [ @session.no_environments, @session.environment_deleted ]
211
+ #
212
+ # @return [Boolean] Returns true if, on any page test, the
213
+ # element conditions were met or the block returned true (at
214
+ # which point it exits immediately), false otherwise.
215
+ def long_wait message, opts = {}
216
+ # Merge in the defaults
217
+ opts = { :numtimes => 120, :interval => 30, :any_elements => nil, :all_elements => nil }.merge(opts)
218
+
219
+ begin
220
+ opts[:numtimes].times do |count|
221
+ # Run a code block if given; might do other checks, or
222
+ # click things we need to finish, or whatever
223
+ if block_given?
224
+ self.log.debug "Session: long_wait: yielding to passed block."
225
+ blockout = yield
226
+ if blockout == true
227
+ return true
228
+ end
229
+ end
230
+
231
+ # If any of these are present, we're done.
232
+ if opts[:any_elements]
233
+ opts[:any_elements].each do |elem|
234
+ self.log.debug "Session: long_wait: in any_elements, looking for #{elem}"
235
+ if elem.present?
236
+ self.log.debug "Session: long_wait: completed due to the presence of #{elem}"
237
+ return true
238
+ end
239
+ end
240
+ end
241
+ # If all of these are present, we're done.
242
+ if opts[:all_elements]
243
+ all_elems=true
244
+ opts[:all_elements].each do |elem|
245
+ self.log.debug "Session: long_wait: in all_elements, looking for #{elem}"
246
+ if ! elem.present?
247
+ all_elems=false
248
+ end
249
+ end
250
+ if all_elems == true
251
+ self.log.debug "Session: long_wait: completed due to the presence of all off #{opts[:all_elements]}"
252
+ return true
253
+ end
254
+ end
255
+
256
+ # We're still here, let the user know
257
+ self.log.info message
258
+
259
+ if (((count + 1) % 20) == 0) && (self.get_config :global, :debug)
260
+ self.log.debug "Session: long_wait: We've waited a multiple of 20 times, so giving you a debugger; 'c' to continue."
261
+ debugger
262
+ end
263
+
264
+ sleep opts[:interval]
265
+ end
266
+ rescue Exception => e
267
+ self.log.debug "Session: long_wait: Had an exception #{e}"
268
+ if self.get_config :global, :debug
269
+ self.log.debug "Session: long_wait: Had an exception in debug mode: #{e.inspect}"
270
+ self.log.debug "Session: long_wait: Had an exception in debug mode: #{e.message}"
271
+ self.log.debug "Session: long_wait: Had an exception in debug mode: #{e.backtrace.join("\n")}"
272
+
273
+ self.log.debug "Session: long_wait: Had an exception, and you're in debug mode, so giving you a debugger."
274
+ debugger
275
+ end
276
+ end
277
+
278
+ return false
279
+ end
280
+
281
+ # Deals with popup alerts in the browser (i.e. the javascript
282
+ # alert() function). Always clicks "ok" or equivalent.
283
+ #
284
+ # FIXME: Check the text of the alert to see that it's the one
285
+ # we want.
286
+ #
287
+ # Note that we're using @browser because things can be a bit
288
+ # wonky during an alert; we don't want to run session's "are we
289
+ # on the right page?" tests, or even talk to the page object.
290
+ def handle_alert
291
+ @browser.alert.wait_until_present
292
+
293
+ if @browser.alert.exists?
294
+ @browser.alert.ok
295
+ end
296
+ end
297
+
298
+ # Does the heavy lifting, such as it is, for +acceptable_pages=+
299
+ #
300
+ # @param [Class, Symbol, Array] newpage A page class, or a
301
+ # symbol naming a page class, or an array of those, for which
302
+ # pages are acceptable.
303
+ #
304
+ # @return [Array<Gless::BasePage>]
305
+ def check_acceptable_pages newpage
306
+ if newpage.kind_of? Class
307
+ return [ newpage ]
308
+ elsif newpage.kind_of? Symbol
309
+ return [ @pages.keys.find { |x| x.name =~ /#{newpage.to_s}$/ } ]
310
+ elsif newpage.kind_of? Array
311
+ return newpage.map { |p| check_acceptable_pages p }
312
+ else
313
+ raise "You set the acceptable_pages to #{newpage.class.name}; unhandled"
314
+ end
315
+ end
316
+
317
+ end
318
+
319
+ end
@@ -0,0 +1,164 @@
1
+
2
+ module Gless
3
+
4
+ # This class, as its name sort of implies, is used to wrap Watir
5
+ # elements. Every element on a Gless page (i.e. any descentant of
6
+ # Gless::BasePage that uses the "element" class mothed) is not
7
+ # actually a Watir element but rather a Gless::WrapWatir instead.
8
+ #
9
+ # Most things are passed through to the underlying Watir element,
10
+ # but extensive logging occurs (in fact, if you have debugging on,
11
+ # this is where screenshots occur), and various extremely
12
+ # low-level checks are done to try to work around potential
13
+ # Selenium problems. For example, all text entry is checked at
14
+ # this level and retried until it works, since Selenium/WebDriver
15
+ # tends to be flaky about that (and it's even worse if the browser
16
+ # window gets focus during the text entry).
17
+ #
18
+ # This shouldn't ever need to be used by a user; it's done
19
+ # automatically by the +element+ class method.
20
+ class Gless::WrapWatir
21
+ include RSpec::Matchers
22
+
23
+ # Sets up the wrapping.
24
+ #
25
+ # @param [Gless::Browser] browser
26
+ # @param [Gless::Session] session
27
+ # @param [Symbol] orig_type The type of the element; normally
28
+ # with watir you'd do something like
29
+ #
30
+ # watir.button :value, 'Submit'
31
+ #
32
+ # In that expression, "button" is the orig_type.
33
+ # @param [Hash] orig_selector_args In the example
34
+ # above,
35
+ #
36
+ # { :value => 'Submit' }
37
+ #
38
+ # is the selector arguments.
39
+ # @param [Gless::BasePage, Array<Gless::BasePage>] click_destination Optional. A list of pages that are OK places to end up after we click on this element
40
+ def initialize(browser, session, orig_type, orig_selector_args, click_destination)
41
+ @browser = browser
42
+ @session = session
43
+ @orig_type = orig_type
44
+ @orig_selector_args = orig_selector_args
45
+ @elem = @browser.send(@orig_type, @orig_selector_args)
46
+ @num_retries = 3
47
+ @wait_time = 30
48
+ @click_destination = click_destination
49
+ end
50
+
51
+ # Passes everything through to the underlying Watir object, but
52
+ # with logging.
53
+ def method_missing(m, *args, &block)
54
+ wrapper_logging(m, args)
55
+ @elem.send(m, *args, &block)
56
+ end
57
+
58
+ # Used to log all pass through behaviours. In debug mode,
59
+ # displays details about what method was passed through, and the
60
+ # nature of the element in question.
61
+ def wrapper_logging(m, args)
62
+ if @orig_selector_args.inspect =~ /password/i
63
+ @session.log.debug "WrapWatir: Doing something with passwords, redacted."
64
+ else
65
+ if @session.get_config :global, :debug
66
+ @session.log.add_to_replay_log( @browser, @session )
67
+ end
68
+
69
+ @session.log.debug "WrapWatir: Calling #{m} with arguments #{args.inspect} on a #{@elem.class.name} element identified by: #{@orig_selector_args.inspect}"
70
+
71
+ if @elem.present? && @elem.class.name == 'Watir::HTMLElement'
72
+ @session.log.warn "FIXME: You have been lazy and said that something is of type 'element'; its actual type is #{@elem.to_subtype.class.name}; the element is identified by #{@orig_selector_args.inspect}"
73
+ end
74
+ end
75
+ end
76
+
77
+ # A wrapper around Watir's click; handles the changing of
78
+ # acceptable pages (i.e. click_destination processing, see
79
+ # {Gless::BasePage} and {Gless::Session} for more details).
80
+ def click
81
+ if @click_destination
82
+ @session.log.debug "WrapWatir: A #{@elem.class.name} element identified by: #{@orig_selector_args.inspect} has a special destination when clicked, #{@click_destination}"
83
+ @session.acceptable_pages = @click_destination
84
+ end
85
+ wrapper_logging('click', nil)
86
+ @session.log.debug "WrapWatir: Calling click on a #{@elem.class.name} element identified by: #{@orig_selector_args.inspect}"
87
+ @elem.click
88
+ end
89
+
90
+ # Used by +set+, see description there.
91
+ def set_retries!(retries)
92
+ @num_retries=retries
93
+
94
+ return self
95
+ end
96
+
97
+ # Used by +set+, see description there.
98
+ def set_timeout!(timeout)
99
+ @wait_time=timeout
100
+
101
+ return self
102
+ end
103
+
104
+ # A wrapper around Watir's set element that retries operations.
105
+ # In particular, text fields and radio elements are checked to
106
+ # make sure that what we intended to enter *actually* got
107
+ # entered. set_retries! and set_timeout! set the number of
108
+ # times to try to get things working and the delay between ecah
109
+ # such try.
110
+ def set(*args)
111
+ wrapper_logging('set', args)
112
+
113
+ # Double-check text fields
114
+ if @elem.class.name == 'Watir::TextField'
115
+ set_text = args[0]
116
+ @elem.set(set_text)
117
+
118
+ @num_retries.times do |x|
119
+ @session.log.debug "WrapWatir: Checking that text entry worked"
120
+ @elem = @browser.send(@orig_type, @orig_selector_args)
121
+ if @elem.value == set_text
122
+ break
123
+ else
124
+ @session.log.debug "WrapWatir: It did not; sleeping for #{@wait_time} seconds"
125
+ sleep @wait_time
126
+ @session.log.debug "WrapWatir: Retrying."
127
+ wrapper_logging('set', set_text)
128
+ @elem.set(set_text)
129
+ end
130
+ end
131
+ @elem.value.should == set_text
132
+ @session.log.debug "WrapWatir: The text entry worked"
133
+
134
+ return self
135
+
136
+ # Double-check radio buttons
137
+ elsif @elem.class.name == 'Watir::Radio'
138
+ wrapper_logging('set', [])
139
+ @elem.set
140
+
141
+ @num_retries.times do |x|
142
+ @session.log.debug "WrapWatir: Checking that the radio selection worked"
143
+ @elem = @browser.send(@orig_type, @orig_selector_args)
144
+ if @elem.set? == true
145
+ break
146
+ else
147
+ @session.log.debug "WrapWatir: It did not; sleeping for #{@wait_time} seconds"
148
+ sleep @wait_time
149
+ @session.log.debug "WrapWatir: Retrying."
150
+ wrapper_logging('set', [])
151
+ @elem.set
152
+ end
153
+ end
154
+ @elem.set?.should be_true
155
+ @session.log.debug "WrapWatir: The radio set worked"
156
+
157
+ return self
158
+
159
+ else
160
+ @elem.set(*args)
161
+ end
162
+ end
163
+ end
164
+ end
data/lib/gless.rb ADDED
@@ -0,0 +1,54 @@
1
+
2
+ # The Gless module itself only defines a setup method; all the meat
3
+ # is in the other classes, especially {Gless::Session}. See the
4
+ # README for a general overview; it lives at
5
+ # https://github.com/rlpowell/gless , which is also the home of this
6
+ # project.
7
+ module Gless
8
+ # The current version number.
9
+ VERSION = '1.0.1'
10
+
11
+ # Sets up the config, logger and browser instances, the ordering
12
+ # of which is slightly tricky.
13
+ #
14
+ # @return [Gless::Logger, Gless::EnvConfig, Gless::Browser] logger, config, browser (in that order)
15
+ def self.setup( tag )
16
+ logger = Gless::Logger.new( tag )
17
+
18
+ # Create the config reading/storage object
19
+ config = Gless::EnvConfig.new( )
20
+
21
+ # Get the whole backtrace, please.
22
+ if config.get :global, :debug
23
+ ::Cucumber.use_full_backtrace = true
24
+
25
+ ::RSpec.configure do |config|
26
+ # RSpec automatically cleans stuff out of backtraces;
27
+ # sometimes this is annoying when trying to debug something e.g. a gem
28
+ config.backtrace_clean_patterns = []
29
+ end
30
+ end
31
+
32
+ # Turn on verbose (info) level logging.
33
+ if config.get :global, :verbose
34
+ logger.normal_log.level = ::Logger::INFO
35
+ logger.replay_log.level = ::Logger::INFO
36
+ logger.debug "Verbose/info level logging enabled."
37
+ end
38
+
39
+ # Turn on debug level logging.
40
+ if config.get :global, :debug
41
+ logger.normal_log.level = ::Logger::DEBUG
42
+ logger.replay_log.level = ::Logger::DEBUG
43
+ logger.debug "Debug level logging enabled."
44
+ end
45
+
46
+ # Create the browser.
47
+ browser = Gless::Browser.new( config, logger )
48
+ browser.cookies.clear
49
+
50
+ return logger, config, browser
51
+ end
52
+ end
53
+
54
+ Dir["#{File.dirname(__FILE__)}/gless/*.rb"].each {|r| load r }