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:
-
Bauxite::Action#set sets a variable to a literal string.
-
Bauxite::Action#store sets a variable to the value of an element in the page.
-
Bauxite::Action#exec sets a variable to the output of an external command (i.e. stdout).
-
Bauxite::Action#js sets a variable to the result of Javascript command.
-
Bauxite::Action#replace sets a variable to the result of doing a find-and-replace operation on a literal.
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
Logger instance.
Test options.
Test containers.
Context variables.
Public Class Methods
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 tofalse
) - :debug
-
if
true
, break into the debug console if an error occurs (defaults tofalse
) - :wait
-
if
true
, call ::wait before stopping the test engine with stop (defaults tofalse
) - :extensions
-
an array of directories that contain extensions to be loaded
# File lib/bauxite/core/context.rb, line 138 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', '__OUTPUT__' => options[:output], '__DIR__' => File.absolute_path(Dir.pwd) } @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] || []) @logger = Context::load_logger(options[:logger], options[:logger_opt]) @parser = Parser.new(self) end
Public Instance Methods
Breaks into the debug console.
For example:
ctx.debug # => this breaks into the debug console
# File lib/bauxite/core/context.rb, line 269 def debug exec_parsed_action('debug', [], false) end
Test engine driver instance (Selenium WebDriver).
# File lib/bauxite/core/context.rb, line 259 def driver _load_driver unless @driver @driver end
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 252 def find(selector, &block) # yields: element with_timeout Selenium::WebDriver::Error::NoSuchElementError do Selector.new(self, @variables['__SELECTOR__']).find(selector, &block) end end
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 288 def get_value(element) if ['input','select','textarea'].include? element.tag_name.downcase element.attribute('value') else element.text end end
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 200 def reset_driver @driver.quit if @driver @driver = nil end
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 177 def start(actions = []) return unless actions.size > 0 begin actions.each do |action| begin break if exec_action(action) == :break rescue StandardError => e print_error(e) raise unless @options[:debug] debug end end ensure stop end end
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 220 def stop Context::wait if @options[:wait] begin @logger.finalize(self) rescue StandardError => e print_error(e) raise ensure @driver.quit if @driver end end
Advanced Helpers
↑ topPublic Class Methods
Constructs a Logger instance using name
as a hint for the
logger type.
# File lib/bauxite/core/context.rb, line 432 def self.load_logger(loggers, options) if loggers.is_a? Array return Loggers::CompositeLogger.new(options, loggers) end name = loggers 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
Default action parsing strategy.
# File lib/bauxite/core/context.rb, line 460 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
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 425 def self.wait Readline.readline("Press ENTER to continue\n") end
Public Instance Methods
Adds an alias named name
to the specified action
with the arguments specified in args
.
# File lib/bauxite/core/context.rb, line 454 def add_alias(name, action, args) @aliases[name] = { :action => action, :args => args } end
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 309 def exec_action(text) data = Context::parse_action_default(text, '<unknown>', 0) exec_parsed_action(data[:action], data[:args], true, text) end
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 540 def exec_action_object(action) action.execute end
Executes the specified file
.
For example:
ctx.exec_file('file') # => executes every action defined in 'file'
# File lib/bauxite/core/context.rb, line 320 def exec_file(file) current_dir = @variables['__DIR__' ] current_file = @variables['__FILE__'] current_line = @variables['__LINE__'] @parser.parse(file) do |action, args, text, file, line| @variables['__DIR__'] = File.absolute_path(File.dirname(file)) @variables['__FILE__'] = file @variables['__LINE__'] = line break if exec_parsed_action(action, args, true, text) == :break end @variables['__DIR__' ] = current_dir @variables['__FILE__'] = current_file @variables['__LINE__'] = current_line end
Executes the specified action handling errors, logging and debug history.
If log
is true
, log the action execution (default
behavior).
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:
ctx.exec_action 'open "http://www.ruby-lang.org"' # => navigates to www.ruby-lang.org
# File lib/bauxite/core/context.rb, line 350 def exec_parsed_action(action, args, log = true, text = nil) action = get_action(action, args, text) ret = nil if log @logger.log_cmd(action) do Readline::HISTORY << action.text ret = exec_action_object(action) end else ret = exec_action_object(action) end if ret.respond_to? :call # delayed actions (after log_cmd) ret.call else ret end rescue Selenium::WebDriver::Error::UnhandledAlertError raise Bauxite::Errors::AssertionError, "Unexpected modal present" end
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 500 def get_action(action, args, text = 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__'] line = @variables['__LINE__'] Action.new(self, action, args, text, file, line) end
Returns the output path for path
accounting for the
__OUTPUT__
variable.
For example:
# assuming --output /mnt/disk ctx.output_path '/tmp/myfile.txt' # => returns '/tmp/myfile.txt' ctx.output_path 'myfile.txt' # => returns '/mnt/disk/myfile.txt'
# File lib/bauxite/core/context.rb, line 585 def output_path(path) unless Pathname.new(path).absolute? output = @variables['__OUTPUT__'] if output Dir.mkdir output unless Dir.exists? output path = File.join(output, path) end end path end
Prints the specified error
using the Logger configured and
handling the verbose option.
For example:
begin # => some code here rescue StandardError => e @ctx.print_error e # => additional error handling code here end
# File lib/bauxite/core/context.rb, line 555 def print_error(e, capture = true) if @logger @logger.log "#{e.message}\n", :error else puts e.message end if @options[:verbose] p e puts e.backtrace end if capture and @options[:capture] with_vars(e.variables) do exec_parsed_action('capture', [] , false) e.variables['__CAPTURE__'] = @variables['__CAPTURE__'] end end end
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 410 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
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 382 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
↑ topPublic Class Methods
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 616 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
Returns an array with the names of every action available.
For example:
Context::actions # => [ "assert", "break", ... ]
# File lib/bauxite/core/context.rb, line 606 def self.actions _action_methods.map { |m| m.sub(/_action$/, '') } end
Returns an array with the names of every logger available.
For example:
Context::loggers # => [ "null", "bash", ... ]
# File lib/bauxite/core/context.rb, line 645 def self.loggers Loggers.constants.map { |l| l.to_s.downcase.sub(/logger$/, '') } end
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 669 def self.max_action_name_size actions.inject(0) { |s,a| a.size > s ? a.size : s } end
Returns an array with the names of every parser available.
For example:
Context::parsers # => [ "default", "html", ... ]
# File lib/bauxite/core/context.rb, line 655 def self.parsers (Parser.public_instance_methods(false) - ParserModule.public_instance_methods(false)) .map { |p| p.to_s } end
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 631 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
↑ topPublic Instance Methods
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 690 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
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 713 def with_vars(vars) current = @variables @variables = @variables.merge(vars) ret_vars = nil ret = yield returned = @variables['__RETURN__'] if returned == ['*'] ret_vars = @variables.clone ret_vars.delete '__RETURN__' elsif returned != nil ret_vars = @variables.select { |k,v| returned.include? k } end rescue StandardError => e e.instance_variable_set "@variables", @variables def e.variables @variables end raise ensure @variables = current @variables.merge!(ret_vars) if ret_vars ret end