class Bauxite::Context

The Main test context. This class includes state and helper functions used by clients execute tests and by actions and selectors to interact with the test engine (i.e. Selenium WebDriver).

Context variables

Context variables are a key/value pairs scoped to the a test context.

Variables can be set using different actions. For example:

Variables can be expanded in every Action argument (e.g. selectors, texts, expressions, etc.). To obtain the value of a variable through variable expansion the following syntax must be used:

${var_name}

For example:

set field "greeting"
set name "John"
write "${field}_textbox" "Hi, my name is ${name}!"
click "${field}_button"

Variable scope

When the main test starts (via the start method), the test is bound to the global scope. The variables defined in the global scope are available to every test Action.

The global scope can have nested variable scopes created by special actions. The variables defined in a scope A are only available to that scope and scopes nested within A.

Every time an Action loads a file, a new nested scope is created. File-loading actions include:

A nested scope can bubble variables to its parent scope with the special action:

Built-in variable

Bauxite has a series of built-in variables that provide information of the current test context and allow dynamic constomizations of the test behavior.

The built-in variables are:

__FILE__

The file where the current action is defined.

__DIR__

The directory where __FILE__ is.

__SELECTOR__

The default selector used when the selector specified does not contain an = character.

__DEBUG__

Set to true if the current action is being executed by the debug console.

__RETURN__

Used internally by Bauxite::Action#return_action to indicate which variables should be returned to the parent scope.

In general, variables surrounded by double underscores and variables whose names are only numbers are reserved for Bauxite and should not be used as part of a functional test. The obvious exception is when trying to change the test behavior by changing the built-in variables.

Attributes

driver[R]

Test engine driver instance (Selenium WebDriver).

logger[R]

Logger instance.

options[R]

Test options.

tests[RW]

Test containers.

variables[RW]

Context variables.

Public Class Methods

new(options) click to toggle source

Constructs a new test context instance.

options is a hash with the following values:

:driver

selenium driver symbol (defaults to :firefox)

:timeout

selector timeout in seconds (defaults to 10s)

:logger

logger implementation name without the 'Logger' suffix (defaults to 'null' for Loggers::NullLogger).

:verbose

if true, show verbose error information (e.g. backtraces) if an error occurs (defaults to false)

:debug

if true, break into the debug console if an error occurs (defaults to false)

:wait

if true, call ::wait before stopping the test engine with stop (defaults to false)

:extensions

an array of directories that contain extensions to be loaded

# File lib/bauxite/core/context.rb, line 140
def initialize(options)
        @options = options
        @driver_name = (options[:driver] || :firefox).to_sym
        @variables = {
                '__TIMEOUT__'  => (options[:timeout] || 10).to_i,
                '__DEBUG__'    => false,
                '__SELECTOR__' => options[:selector] || 'sid'
        }
        @aliases = {}
        @tests = []
        
        client = Selenium::WebDriver::Remote::Http::Default.new
        client.timeout = (@options[:open_timeout] || 60).to_i
        @options[:driver_opt] = {} unless @options[:driver_opt]
        @options[:driver_opt][:http_client] = client

        _load_extensions(options[:extensions] || [])
        
        handle_errors do
                @logger = Context::load_logger(options[:logger], options[:logger_opt])
        end
        
        @parser = Parser.new(self)
end

Public Instance Methods

debug() click to toggle source

Breaks into the debug console.

For example:

ctx.debug
# => this breaks into the debug console
# File lib/bauxite/core/context.rb, line 253
def debug
        exec_action('debug', false)
end
find(selector) { |element| ... } click to toggle source

Finds an element by selector.

The element found is yielded to the given block (if any) and returned.

Note that the recommeneded way to call this method is by passing a block. This is because the method ensures that the element context is maintained for the duration of the block but it makes no guarantees after the block completes (the same applies if no block was given).

For example:

ctx.find('css=.my_button') { |element| element.click }
ctx.find('css=.my_button').click

For example (where using a block is mandatory):

ctx.find('frame=|myframe|css=.my_button') { |element| element.click }
# => .my_button clicked

ctx.find('frame=|myframe|css=.my_button').click
# => error, cannot click .my_button (no longer in myframe scope)
# File lib/bauxite/core/context.rb, line 242
def find(selector, &block) # yields: element
        with_timeout Selenium::WebDriver::Error::NoSuchElementError do
                Selector.new(self, @variables['__SELECTOR__']).find(selector, &block)
        end
end
get_value(element) click to toggle source

Returns the value of the specified element.

This method takes into account the type of element and selectively returns the inner text or the value of the value attribute.

For example:

# assuming <input type='text' value='Hello' />
#          <span id='label'>World!</span>

ctx.get_value(ctx.find('css=input[type=text]'))
# => returns 'Hello'

ctx.get_value(ctx.find('label'))
# => returns 'World!'
# File lib/bauxite/core/context.rb, line 272
def get_value(element)
        if ['input','select','textarea'].include? element.tag_name.downcase
                element.attribute('value') 
        else
                element.text
        end
end
reset_driver() click to toggle source

Stops the test engine and starts a new engine with the same provider.

For example:

ctx.reset_driver
=> closes the browser and opens a new one
# File lib/bauxite/core/context.rb, line 197
def reset_driver
        @driver.quit
        _load_driver
end
start(actions = []) click to toggle source

Starts the test engine and executes the actions specified. If no action was specified, returns without stopping the test engine (see stop).

For example:

lines = [
    'open "http://www.ruby-lang.org"',
    'write "name=q" "ljust"',
    'click "name=sa"',
    'break'
]
ctx.start(lines)
# => navigates to www.ruby-lang.org, types ljust in the search box
#    and clicks the "Search" button.
# File lib/bauxite/core/context.rb, line 179
def start(actions = [])
        _load_driver
        return unless actions.size > 0
        begin
                actions.each do |action|
                        exec_action(action)
                end
        ensure
                stop
        end
end
stop() click to toggle source

Stops the test engine.

Calling this method at the end of the test is mandatory if start was called without actions.

Note that the recommeneded way of executing tests is by passing a list of actions to start instead of using the start / stop pattern.

For example:

ctx.start(:firefox) # => opens firefox

# test stuff goes here

ctx.stop            # => closes firefox
# File lib/bauxite/core/context.rb, line 217
def stop
        Context::wait if @options[:wait]
        @driver.quit
end

Advanced Helpers

↑ top

Public Class Methods

load_logger(name, options) click to toggle source

Constructs a Logger instance using name as a hint for the logger type.

# File lib/bauxite/core/context.rb, line 448
def self.load_logger(name, options)
        log_name = (name || 'null').downcase
        
        class_name = "#{log_name.capitalize}Logger"
        
        unless Loggers.const_defined? class_name.to_sym
                raise NameError,
                        "Invalid logger '#{log_name}'"
        end
        
        Loggers.const_get(class_name).new(options)
end
parse_action_default(text, file = '', line = 0) click to toggle source

Default action parsing strategy.

# File lib/bauxite/core/context.rb, line 470
def self.parse_action_default(text, file = '<unknown>', line = 0)
        data = text.split(' ', 2)
        begin
                args_text = data[1] ? data[1].strip : ''
                args = []
                
                unless args_text == ''
                        # col_sep must be a regex because String.split has a
                        # special case for a single space char (' ') that produced
                        # unexpected results (i.e. if line is '"a      b"' the
                        # resulting array contains ["a b"]).
                        #
                        # ...but... 
                        #
                        # CSV expects col_sep to be a string so we need to work
                        # some dark magic here. Basically we proxy the StringIO
                        # received by CSV to returns strings for which the split
                        # method does not fold the whitespaces.
                        #
                        args = CSV.new(StringIOProxy.new(args_text), { :col_sep => ' ' })
                        .shift
                        .select { |a| a != nil } || []
                end
                
                {
                        :action => data[0].strip.downcase,
                        :args   => args
                }
        rescue StandardError => e
                raise "#{file} (line #{line+1}): #{e.message}"
        end
end
wait() click to toggle source

Prompts the user to press ENTER before resuming execution.

For example:

Context::wait
# => echoes "Press ENTER to continue" and waits for user input
# File lib/bauxite/core/context.rb, line 441
def self.wait
        Readline.readline("Press ENTER to continue\n")
end

Public Instance Methods

add_alias(name, action, args) click to toggle source

Adds an alias named name to the specified action with the arguments specified in args.

# File lib/bauxite/core/context.rb, line 464
def add_alias(name, action, args)
        @aliases[name] = { :action => action, :args => args }
end
exec_action(text, log = true, file = '', line = 0) click to toggle source

Executes the specified action string handling errors, logging and debug history.

If log is true, log the action execution (default behavior).

For example:

ctx.exec_action 'open "http://www.ruby-lang.org"'
# => navigates to www.ruby-lang.org
# File lib/bauxite/core/context.rb, line 293
def exec_action(text, log = true, file = '<unknown>', line = 0)
        data = Context::parse_action_default(text, file, line)
        exec_parsed_action(data[:action], data[:args], log, text, file, line)
end
exec_action_object(action) click to toggle source

Executes the specified action object injecting built-in variables. Note that the result returned by this method might be a lambda. If this is the case, a further call method must be issued.

This method if part of the action execution chain and is intended for advanced use (e.g. in complex actions). To execute an Action directly, the exec_action method is preferred.

For example:

action = ctx.get_action("echo", ['Hi!'], 'echo "Hi!"')
ret = ctx.exec_action_object(action)
ret.call if ret.respond_to? :call
# File lib/bauxite/core/context.rb, line 550
def exec_action_object(action)
        # Inject built-in variables
        file = action.file
        dir  = (File.exists? file) ? File.dirname(file) : Dir.pwd
        @variables['__FILE__'] = file
        @variables['__DIR__'] = File.absolute_path(dir)
        action.execute
end
exec_file(file) click to toggle source

Executes the specified file.

For example:

ctx.exec_file('file')
# => executes every action defined in 'file'
# File lib/bauxite/core/context.rb, line 304
def exec_file(file)
        @parser.parse(file) do |action, args, text, file, line|
                exec_parsed_action(action, args, true, text, file, line)
        end
end
exec_parsed_action(action, args, log = true, text = nil, file = nil, line = nil) click to toggle source

Executes the specified action handling errors, logging and debug history.

This method if part of the action execution chain and is intended for advanced use (e.g. in complex actions). To execute an Action directly, the exec_action method is preferred.

If log is true, log the action execution (default behavior).

For example:

ctx.exec_action 'open "http://www.ruby-lang.org"'
# => navigates to www.ruby-lang.org
# File lib/bauxite/core/context.rb, line 323
def exec_parsed_action(action, args, log = true, text = nil, file = nil, line = nil)
        ret = handle_errors(true) do
                
                action =  get_action(action, args, text, file, line)
                
                if log
                        @logger.log_cmd(action) do
                                Readline::HISTORY << action.text
                                exec_action_object(action)
                        end
                else
                        exec_action_object(action)
                end
        end
        handle_errors(true) do
                ret.call if ret.respond_to? :call # delayed actions (after log_cmd)
        end
end
get_action(action, args, text = nil, file = nil, line = nil) click to toggle source

Returns an executable Action object constructed from the specified arguments resolving action aliases.

This method if part of the action execution chain and is intended for advanced use (e.g. in complex actions). To execute an Action directly, the exec_action method is preferred.

# File lib/bauxite/core/context.rb, line 510
def get_action(action, args, text = nil, file = nil, line = nil)
        while (alias_action = @aliases[action])
                action = alias_action[:action]
                args   = alias_action[:args].map do |a|
                        a.gsub(/\$\{(\d+)(\*q?)?\}/) do |match|
                                # expand ${1} to args[0], ${2} to args[1], etc.
                                # expand ${4*} to "#{args[4]} #{args[5]} ..."
                                # expand ${4*q} to "\"#{args[4]}\" \"#{args[5]}\" ..."
                                idx = $1.to_i-1
                                if $2 == nil
                                        args[idx] || ''
                                else
                                        range = args[idx..-1]
                                        range = range.map { |arg| '"'+arg.gsub('"', '""')+'"' } if $2 == '*q'
                                        range.join(' ')
                                end
                        end
                end
        end
        
        text = ([action] + args.map { |a| '"'+a.gsub('"', '""')+'"' }).join(' ') unless text
        file = @variables['__FILE__'] unless file
        line = @variables['__LINE__'] unless line
        
        Action.new(self, action, args, text, file, line)
end
handle_errors(break_into_debug = false, exit_on_error = true) { || ... } click to toggle source

Executes the block inside a rescue block applying standard criteria of error handling.

The default behavior is to print the exception message and exit.

If the :verbose option is set, the exception backtrace will also be printed.

If the break_into_debug argument is true and the :debug option is set, the handler will break into the debug console instead of exiting.

If the exit_on_error argument is false the handler will not exit after printing the error message.

For example:

ctx = Context.new({ :debug => true })
ctx.handle_errors(true) { raise 'break into debug now!' }
# => this breaks into the debug console
# File lib/bauxite/core/context.rb, line 362
def handle_errors(break_into_debug = false, exit_on_error = true)
        yield
rescue StandardError => e
        if @logger
                @logger.log "#{e.message}\n", :error
        else
                puts e.message
        end
        if @options[:verbose]
                p e
                puts e.backtrace
        end
        unless @variables['__DEBUG__']
                if break_into_debug and @options[:debug]
                        debug
                elsif exit_on_error
                        if @variables['__RAISE_ERROR__']
                                raise
                        else
                                exit false
                        end
                end
        end
end
try_exec_action(action, args) click to toggle source

Executes the specified action and returns true if the action succeeds and false otherwise.

This method is intended to simplify conditional actions that execute different code depending on the outcome of an action execution.

For example:

if ctx.try_exec_action(action, args)
    # => when action succeeds...
else
    # => when action fails...
end
# File lib/bauxite/core/context.rb, line 572
def try_exec_action(action, args)
        action = get_action(action, args)
        
        with_timeout Bauxite::Errors::AssertionError do
                with_vars({ '__TIMEOUT__' => 0}) do
                        begin
                                ret = exec_action_object(action)
                                ret.call if ret.respond_to? :call
                                true
                        rescue Bauxite::Errors::AssertionError
                                false
                        end
                end
        end
end
with_driver_timeout(timeout) { || ... } click to toggle source

Executes the given block using the specified driver timeout.

Note that the driver timeout is the time (in seconds) Selenium will wait for a specific element to appear in the page (using any of the available Selector strategies).

For example

ctx.with_driver_timeout 0.5 do
    ctx.find ('find_me_quickly') do |e|
        # do something with e
    end
end
# File lib/bauxite/core/context.rb, line 426
def with_driver_timeout(timeout)
        current = @driver_timeout
        @driver.manage.timeouts.implicit_wait = timeout
        yield
ensure
        @driver_timeout = current
        @driver.manage.timeouts.implicit_wait = current
end
with_timeout(*error_types) { || ... } click to toggle source

Executes the given block retrying for at most ${__TIMEOUT__} seconds. Note that this method does not take into account the time it takes to execute the block itself.

For example

ctx.with_timeout StandardError do
    ctx.find ('element_with_delay') do |e|
        # do something with e
    end
end
# File lib/bauxite/core/context.rb, line 398
def with_timeout(*error_types)
        stime = Time.new
        timeout ||= stime + @variables['__TIMEOUT__']
        yield
rescue *error_types => e
        t = Time.new
        rem = timeout - t
        raise if rem < 0

        @logger.progress(rem.round)

        sleep(1.0/10.0) if (t - stime).to_i < 1
        retry
end

Metadata

↑ top

Public Class Methods

action_args(action) click to toggle source

Returns an array with the names of the arguments of the specified action.

For example:

Context::action_args 'assert'
# => [ "selector", "text" ]
# File lib/bauxite/core/context.rb, line 609
def self.action_args(action)
        action += '_action' unless _action_methods.include? action
        Action.public_instance_method(action).parameters.map { |att, name| name.to_s }
end
actions() click to toggle source

Returns an array with the names of every action available.

For example:

Context::actions
# => [ "assert", "break", ... ]
# File lib/bauxite/core/context.rb, line 599
def self.actions
        _action_methods.map { |m| m.sub(/_action$/, '') }
end
loggers() click to toggle source

Returns an array with the names of every logger available.

For example:

Context::loggers
# => [ "null", "bash", ... ]
# File lib/bauxite/core/context.rb, line 638
def self.loggers
        Loggers.constants.map { |l| l.to_s.downcase.sub(/logger$/, '') }
end
max_action_name_size() click to toggle source

Returns the maximum size in characters of an action name.

This method is useful to pretty print lists of actions

For example:

# assuming actions = [ "echo", "assert", "tryload" ]
Context::max_action_name_size
# => 7
# File lib/bauxite/core/context.rb, line 662
def self.max_action_name_size
        actions.inject(0) { |s,a| a.size > s ? a.size : s }
end
parsers() click to toggle source

Returns an array with the names of every parser available.

For example:

Context::parsers
# => [ "default", "html", ... ]
# File lib/bauxite/core/context.rb, line 648
def self.parsers
        (Parser.public_instance_methods(false)                           - ParserModule.public_instance_methods(false))
        .map { |p| p.to_s }
end
selectors(include_standard_selectors = true) click to toggle source

Returns an array with the names of every selector available.

If include_standard_selectors is true (default behavior) both standard and custom selector are returned, otherwise only custom selectors are returned.

For example:

Context::selectors
# => [ "class", "id", ... ]
# File lib/bauxite/core/context.rb, line 624
def self.selectors(include_standard_selectors = true)
        ret = Selector.public_instance_methods(false).map { |a| a.to_s.sub(/_selector$/, '') }
        if include_standard_selectors
                ret += Selenium::WebDriver::SearchContext::FINDERS.map { |k,v| k.to_s }
        end
        ret
end

Variable manipulation methods

↑ top

Public Instance Methods

expand(s) click to toggle source

Recursively replaces occurencies of variable expansions in s with the corresponding variable value.

The variable expansion expression format is:

'${variable_name}'

For example:

ctx.variables = { 'a' => '1', 'b' => '2', 'c' => 'a' }
ctx.expand '${a}'    # => '1'
ctx.expand '${b}'    # => '2'
ctx.expand '${c}'    # => 'a'
ctx.expand '${${c}}' # => '1'
# File lib/bauxite/core/context.rb, line 683
def expand(s)
        result = @variables.inject(s) do |s,kv|
                s = s.gsub(/\$\{#{kv[0]}\}/, kv[1].to_s)
        end
        result = expand(result) if result != s
        result
end
with_vars(vars) { || ... } click to toggle source

Temporarily alter the value of context variables.

This method alters the value of the variables specified in the vars hash for the duration of the given block. When the block completes, the original value of the context variables is restored.

For example:

ctx.variables = { 'a' => '1', 'b' => '2', c => 'a' }
ctx.with_vars({ 'a' => '10', d => '20' }) do
   p ctx.variables
   # => {"a"=>"10", "b"=>"2", "c"=>"a", "d"=>"20"}
end
p ctx.variables
# => {"a"=>"1", "b"=>"2", "c"=>"a"}
# File lib/bauxite/core/context.rb, line 706
def with_vars(vars)
        current = @variables
        @variables = @variables.merge(vars)
        yield
ensure
        @variables = current
end