gless 1.0.1

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