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 = config.get_default( 600, :global, :browser, :timeout )
  @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
# 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_truthy, "Current URL is #{@browser.url}, which doesn't match any of the acceptable pages: #{@acceptable_pages}"

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

    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.



375
376
377
378
379
380
381
382
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
# File 'lib/gless/session.rb', line 375

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 = nil
  new_page = nil
  exceptions = {}

  # See if we're on one of the acceptable pages; wait until we
  # are for "timeout" seconds.
  start_time = Time.now.to_i
  while true
    self.log.debug "Session: change_pages: yielding to passed block."
    begin
      yield if block_given?
    rescue Watir::Exception::UnknownObjectException => e
      error_message ||= "Caught UnknownObjectExepction in the block we were passed; are the validators for #{@acceptable_pages} correct?  Are you sure that's the right list of pages?  Here's the exception: #{e.inspect}"
    end
    self.log.debug "Session: change_pages: done yielding to passed block."

    # We're *definitely* staying on the same page; don't do any
    # more work to check where we are
    if @acceptable_pages.member?( @current_page ) and @acceptable_pages.length == 1
      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)}"
        begin
          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}"

            break
          end
        rescue StandardError => e
          # Catching exceptions from "arrived?"; in this case we don't
          # care until later
          exceptions[page.name] = e.inspect
        end
      end

      if good_page == true
        break
      else
        sleep 1
      end
    end

    if (Time.now.to_i - start_time) > @timeout
      break
    end
  end

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

    @previous_url = url
  else
    # Timed out.
    error_message ||= "Session: change_pages: attempt to change pages to #{click_destination} timed out after #{@timeout} seconds, more or less.  If the clicked element exists, are the validators for #{@acceptable_pages} correct?  Here are the exceptions from each page we tried: #{YAML.dump(exceptions)}"
  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:



343
344
345
346
347
348
349
350
351
352
353
# File 'lib/gless/session.rb', line 343

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.



332
333
334
# File 'lib/gless/session.rb', line 332

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



176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/gless/session.rb', line 176

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



301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'lib/gless/session.rb', line 301

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.



218
219
220
221
222
223
224
225
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
# File 'lib/gless/session.rb', line 218

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