Class: Gless::Session

Inherits:
Object
  • Object
show all
Includes:
RSpec::Matchers
Defined in:
lib/gless/session.rb

Overview

Provides an abstraction layer between the individual pages of an website and the high-level application layer, so that the application layer doesn’t have to know about what page it’s on or similar.

For details, see the README.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(browser, config, logger, application) ⇒ Session

Sets up the session object. As the core abstraction layer that sits in the middle of everything, this requires a number of arguments. :)

Parameters:



59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/gless/session.rb', line 59

def initialize( browser, config, logger, application )
  @logger = logger

  log.debug "Session: Initializing with #{browser.inspect}"

  @browser = browser
  @application = application
  @pages = Hash.new
  @timeout = 30
  @acceptable_pages = nil
  @config = config

  @@page_classes.each do |sc|
    @pages[sc] = sc.new( @browser, self, @application )
  end

  log.debug "Session: Final pages table: #{@pages.keys.map { |x| x.name }}"

  return self
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(m, *args, &block) ⇒ Object

Anything that we don’t otherwise recognize is passed on to the current underlying page object (i.e. descendant of Gless::BasePage).

This gets complicated because of the state checking: we test extensively that we’re on the page that we think we should be on before passing things on to the page object.



104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/gless/session.rb', line 104

def method_missing(m, *args, &block)
  # Do some logging.
  if m.inspect =~ /(password|login)/i or args.inspect =~ /(password|login)/i
    log.debug "Session: Doing something with passwords, redacted."
  else
    log.debug "Session: method_missing for #{m} with arguments #{args.inspect}"
  end

  log.debug "Session: check if we've changed pages: #{@browser.title}, #{@browser.url}, #{@previous_url}, #{@current_page}, #{@acceptable_pages}"

  # Changed URL means we've changed pages, probably by surprise
  # since desired page changes happen in Gless::WrapWatir#click
  if @browser.url == @previous_url
    log.debug "Session: doesn't look like we've moved."
  else
    # See if we're on one of the acceptable pages.  We do no
    # significant waiting because Gless::WrapWatir#click should
    # have handeled that.
    good_page=false
    new_page=nil
    if @acceptable_pages.nil?
      # If we haven't gone anywhere yet, anything is good
      good_page = true
      new_page = @pages[@current_page]
    else
      @acceptable_pages.each do |page|
        log.debug "Session: Checking our current url, #{@browser.url}, for a match in #{page.name}: #{@pages[page].match_url(@browser.url)}"
        if @pages[page].match_url(@browser.url)
          clear_cache
          good_page    = true
          @current_page = page
          new_page = @pages[page]
          log.debug "Session: we seem to be on #{page.name} at #{@browser.url}"
        end
      end
    end

    good_page.should be_true, "Current URL is #{@browser.url}, which doesn't match any of the acceptable pages: #{@acceptable_pages}"

    # While this is very thorough, it slows things down quite a
    # bit, and should mostly be covered by
    # Gless::WrapWatir#click ; leaving here in case we decide we
    # need it later.
    #
    # log.debug "Session: checking for arrival at #{new_page.class.name}"
    # new_page.arrived?.should be_true

    url=@browser.url
    log.debug "Session: refreshed browser URL: #{url}"
    new_page.match_url(url).should be_true

    log.info "Session: We are currently on page #{new_page.class.name}, as we should be"

    @previous_url = url
  end

  # End of page checking code.

  cpage = @pages[@current_page]

  if m.inspect =~ /(password|login)/i or args.inspect =~ /(password|login)/i
    log.debug "Session: dispatching method #{m} with args [redacted; password maybe] to #{cpage}"
  else
    log.debug "Session: dispatching method #{m} with args #{args.inspect} to #{cpage}"
  end
  retval = cpage.send(m, *args, &block)
  log.debug "Session: method returned #{retval}"

  retval
end

Instance Attribute Details

#acceptable_pagesObject

A list of page classes of pages that it’s OK for us to be on. Usually just one, but some site workflows might have more than one thing that can happen when you click a button or whatever.

When you assign a value here, a fair bit of processing is done. Most of the actual work is in check_acceptable_pages

The user can give us a class, a symbol, or a list of those; no matter what, we return a list. That list is of possible pages that, if we turn out to be on one of them, that’s OK, and if not we freak out.

Parameters:

  • newpages (Class, Symbol, Array)

    A page class, or a symbol naming a page class, or an array of those, for which pages are acceptable.



33
34
35
# File 'lib/gless/session.rb', line 33

def acceptable_pages
  @acceptable_pages
end

#current_pageObject (readonly)

The page class for the page the session thinks we’re currently on.



16
17
18
# File 'lib/gless/session.rb', line 16

def current_page
  @current_page
end

Class Method Details

.add_page_class(klass) ⇒ Object

This exists only to be called by inherited on Gless::BasePage; see documentation there.



44
45
46
47
# File 'lib/gless/session.rb', line 44

def self.add_page_class( klass )
  @@page_classes ||= []
  @@page_classes << klass
end

Instance Method Details

#change_pages(click_destination) { ... } ⇒ Boolean, String

Does the heavy lifting of moving between pages when an element has a new page destination. Mostly used by Gless::WrapWatir

Note that this attempts to click on the button (or do whatever else the passed block does) many times in an attempt to get to the right page. If multiple attempts are a problem, you should circumvent this method; WrapWatir#click_once exists for this purpose.

Parameters:

  • newpage (Class, Symbol, Array)

    The page(s) that we could be moving to; same idea as #acceptable_pages=

Yields:

  • A required Proc/code block that contains the action to take to attempt to change pages (i.e. clicking on a button or whatever). May be run multiple times, as the whole point here is to keep trying until it works.

Returns:

  • (Boolean, String)

    Returns both whether it managed to get to the page in question and, if not, what sort of errors were seen.



383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
# File 'lib/gless/session.rb', line 383

def change_pages click_destination
  self.acceptable_pages = click_destination

  log.debug "Session: change_pages: checking to see if we have changed pages: #{@browser.title}, #{@current_page}, #{@acceptable_pages}"

  good_page = false
  error_message = ''
  new_page = nil

  # See if we're on one of the acceptable pages; wait until we
  # are for "timeout" seconds.
  @timeout.times do
    self.log.debug "Session: change_pages: yielding to passed block."
    begin
      yield if block_given?
    rescue Watir::Exception::UnknownObjectException => e
      error_message = "Caught UnknownObjectExepction; are the validators for #{@acceptable_pages} correct?  #{e.inspect}"
      log.warn "Session#change_pages: #{error_message}"
    end
    self.log.debug "Session: change_pages: done yielding to passed block."

    if @acceptable_pages.member?( @current_page )
      good_page = true
      new_page = @current_page
      break
    else
      new_page = nil

      if @acceptable_pages.nil?
        # If we haven't gone anywhere yet, anything is good
        log.debug "Session: change_pages: no acceptable pages, so accepting the current page."
        good_page    = true
        new_page = @pages[@current_page]
        break
      end

      url=@browser.url
      log.debug "Session: change_pages: refreshed browser URL: #{url}"

      @acceptable_pages.each do |page|
        log.debug "Session: change_pages: Checking our current url, #{url}, for a match in #{page.name}: #{@pages[page].match_url(url)}"
        if @pages[page].match_url(url) and @pages[page].arrived? == true
          clear_cache
          good_page    = true
          @current_page = page
          new_page = @pages[page]
          log.debug "Session: change_pages: we seem to be on #{page.name} at #{url}"
        end
      end

      if new_page
        if not new_page.match_url(url)
          good_page = false
          error_message = "Current URL is #{url}, which doesn't match that expected for any of the acceptable pages: #{@acceptable_pages}"
          next
        end

        log.debug "Session: change_pages: checking for arrival at #{new_page.class.name}"
        if not new_page.arrived?
          good_page = false
          error_message = "The current page, at #{url}, doesn't have all of the elements for any of the acceptable pages: #{@acceptable_pages}"
          next
        end
      end

      if good_page == true
        break
      else
        sleep 1
      end
    end
  end

  if good_page
    log.info "Session: change_pages: We have successfully moved to page #{new_page.class.name}"

    @previous_url = url
  else
    # Timed out.
    error_message = "Session: change_pages: attempt to change pages to #{click_destination} timed out after #{@timeout} tries.  If the clicked element exists, are the validators for #{@acceptable_pages} correct?"
  end

  return good_page, error_message
end

#check_acceptable_pages(newpage) ⇒ Array<Gless::BasePage>

Does the heavy lifting, such as it is, for acceptable_pages=

Parameters:

  • newpage (Class, Symbol, Array)

    A page class, or a symbol naming a page class, or an array of those, for which pages are acceptable.

Returns:



351
352
353
354
355
356
357
358
359
360
361
# File 'lib/gless/session.rb', line 351

def check_acceptable_pages newpage
  if newpage.kind_of? Class
    return [ newpage ]
  elsif newpage.kind_of? Symbol
    return [ @pages.keys.find { |x| x.name =~ /(^|::)#{newpage.to_s}$/ } ]
  elsif newpage.kind_of? Array
    return newpage.map { |p| check_acceptable_pages p }
  else
    raise "You set the acceptable_pages to #{newpage.class.name}; unhandled"
  end
end

#clear_cache(page_class = nil) ⇒ Object

Clears the cached elements. Used before each page change.

Parameters:

  • page_class (Class) (defaults to: nil)

    The page class of the page whose cached elements are to be cleared; defaults to the current page.



340
341
342
# File 'lib/gless/session.rb', line 340

def clear_cache page_class = nil
  @pages[page_class || current_page].cached_elements = Hash.new
end

#enter(pklas, always = true) ⇒ Object

This function is used to go to an intitial entry point for a website. The page in question must have had set_entry_url run in its class definition, to define how to do this. This setup exists because explaining to the session that we really should be on that page is a bit tricky.

Parameters:

  • pklas (Class)

    The class for the page object that has a set_entry_url that we are using.

  • always (Boolean) (defaults to: true)

    (true) Whether to enter the given page even



184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/gless/session.rb', line 184

def enter(pklas, always = true)
  log.info "Session: Entering the site directly using the entry point for the #{pklas.name} page class"

  if always || pklas != @current_page
    @current_page = pklas
    @pages[pklas].enter
    # Needs to run through our custom acceptable_pages= method
    self.acceptable_pages = pklas
  else
    log.debug "Session: Already on page"
  end
end

#get_config(*args) ⇒ Object

Just passes through to the Gless::EnvConfig component’s get method.



82
83
84
# File 'lib/gless/session.rb', line 82

def get_config(*args)
  @config.get(*args)
end

#get_config_default(*args) ⇒ Object

Just passes through to the Gless::EnvConfig component’s get_default method.



88
89
90
# File 'lib/gless/session.rb', line 88

def get_config_default(*args)
  @config.get_default(*args)
end

#handle_alert(wait_for_alert = true, expected_text = nil) ⇒ Object

Deals with popup alerts in the browser (i.e. the javascript alert() function). Always clicks “ok” or equivalent.

Note that we’re using @browser because things can be a bit wonky during an alert; we don’t want to run session’s “are we on the right page?” tests, or even talk to the page object.

is present, failing if the request times out, before processing it; otherwise, handle any alerts if there are any currently present.

pop-up alert is checked against this parameter; if it differs, an exception will be raised.

Parameters:

  • wait_for_alert (Boolean) (defaults to: true)

    (true) Whether to wait until an alert

  • expected_text (String, Regexp) (defaults to: nil)

    (nil) If not nil, the text of the



309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
# File 'lib/gless/session.rb', line 309

def handle_alert wait_for_alert = true, expected_text = nil
  @browser.alert.wait_until_present if wait_for_alert

  if @browser.alert.exists?
    begin
      if expected_text
        current_text = @browser.alert.text
        if (expected_text.kind_of? Regexp) ? expected_text !~ current_text : expected_text != current_text
          msg = "The actual alert text differs from what was expected.  current_text: #{current_text}; expected_text: #{expected_text}"
          @logger.error msg
          raise msg
        end
      end

      @browser.alert.ok
    rescue Selenium::WebDriver::Error::NoAlertPresentError => e
      msg = "Alert no longer exists; likely closed by user: #{e.message}"
      if wait_for_alert
        @logger.warn msg
        raise
      else
        @logger.info msg
      end
    end
  end
end

#logObject

Just a shortcut to get to the Gless::Logger object.



93
94
95
# File 'lib/gless/session.rb', line 93

def log
  @logger
end

#long_wait(message, opts = {}) ⇒ Boolean

Wait for long-term AJAX-style processing, i.e. watch the page for extended amounts of time until particular events have occured.

Examples:


@session.long_wait "Cloud Application: Still waiting for the environment to be deleted.", :any_elements => [ @session.no_environments, @session.environment_deleted ]

Parameters:

  • message (String)

    The text to print to the user each time the page is not completely loaded.

  • opts (Hash) (defaults to: {})

    Various named options.

Options Hash (opts):

  • numtimes (Integer)

    The number of times to test the page.

  • interval (Integer)

    The number of seconds to delay between each check.

  • any_elements (Array)

    Watir page elements, if any of them are present, the page load is considered complete.

  • all_elements (Object)

    Watir page elements, if all of them are present, the page load is considered complete.

Yield Returns:

  • (Boolean)

    An optional Proc/code block; if present, it is run before each page check. This is so simple interactions can occur without waiting for the timeout, and so the whole process can be short-circuited. If the block returns true, the long_wait ends successfully.

Returns:

  • (Boolean)

    Returns true if, on any page test, the element conditions were met or the block returned true (at which point it exits immediately), false otherwise.



226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
# File 'lib/gless/session.rb', line 226

def long_wait message, opts = {}
  # Merge in the defaults
  opts = { :numtimes => 120, :interval => 30, :any_elements => nil, :all_elements => nil }.merge(opts)

  begin
    opts[:numtimes].times do |count|
      # Run a code block if given; might do other checks, or
      # click things we need to finish, or whatever
      if block_given?
        self.log.debug "Session: long_wait: yielding to passed block."
        blockout = yield
        if blockout == true
          return true
        end
      end

      # If any of these are present, we're done.
      if opts[:any_elements]
        opts[:any_elements].each do |elem|
          self.log.debug "Session: long_wait: in any_elements, looking for #{elem}"
          if elem.present?
            self.log.debug "Session: long_wait: completed due to the presence of #{elem}"
            return true
          end
        end
      end
      # If all of these are present, we're done.
      if opts[:all_elements]
        all_elems=true
        opts[:all_elements].each do |elem|
          self.log.debug "Session: long_wait: in all_elements, looking for #{elem}"
          if ! elem.present?
            all_elems=false
          end
        end
        if all_elems == true
          self.log.debug "Session: long_wait: completed due to the presence of all off #{opts[:all_elements]}"
          return true
        end
      end

      # We're still here, let the user know
      self.log.info message

      if (((count + 1) % 20) == 0) && (self.get_config :global, :debug)
        self.log.debug "Session: long_wait: We've waited a multiple of 20 times, so giving you a debugger; 'c' to continue."
        debugger
      end

      sleep opts[:interval]
    end
  rescue Exception => e
    self.log.warn "Session: long_wait: Had an exception #{e}"
    if self.get_config :global, :debug
      self.log.debug "Session: long_wait: Had an exception in debug mode: #{e.inspect}"
      self.log.debug "Session: long_wait: Had an exception in debug mode: #{e.message}"
      self.log.debug "Session: long_wait: Had an exception in debug mode: #{e.backtrace.join("\n")}"

      self.log.debug "Session: long_wait: Had an exception, and you're in debug mode, so giving you a debugger.  Use 'continue' to proceed."
      debugger
    end

    self.log.debug "Session: long_wait: Retrying after exception."
    retry
  end

  return false
end