Class: Bauxite::Context

Inherits:
Object
  • Object
show all
Defined in:
lib/bauxite/core/context.rb

Overview

to change the test behavior by changing the built-in variables.

Defined Under Namespace

Classes: StringIOProxy, StringProxy

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options) ⇒ Context

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



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# 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

Instance Attribute Details

#loggerObject (readonly)

Logger instance.



111
112
113
# File 'lib/bauxite/core/context.rb', line 111

def logger
  @logger
end

#optionsObject (readonly)

Test options.



114
115
116
# File 'lib/bauxite/core/context.rb', line 114

def options
  @options
end

#testsObject

Test containers.



120
121
122
# File 'lib/bauxite/core/context.rb', line 120

def tests
  @tests
end

#variablesObject

Context variables.



117
118
119
# File 'lib/bauxite/core/context.rb', line 117

def variables
  @variables
end

Class Method Details

.action_args(action) ⇒ Object

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

For example:

Context::action_args 'assert'
# => [ "selector", "text" ]


616
617
618
619
# 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

.actionsObject

Returns an array with the names of every action available.

For example:

Context::actions
# => [ "assert", "break", ... ]


606
607
608
# File 'lib/bauxite/core/context.rb', line 606

def self.actions
	_action_methods.map { |m| m.sub(/_action$/, '') }
end

.load_logger(loggers, options) ⇒ Object

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



432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
# 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

.loggersObject

Returns an array with the names of every logger available.

For example:

Context::loggers
# => [ "null", "bash", ... ]


645
646
647
# File 'lib/bauxite/core/context.rb', line 645

def self.loggers
	Loggers.constants.map { |l| l.to_s.downcase.sub(/logger$/, '') }
end

.max_action_name_sizeObject

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


669
670
671
# 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

.parse_action_default(text, file = '<unknown>', line = 0) ⇒ Object

Default action parsing strategy.



460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
# 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

.parsersObject

Returns an array with the names of every parser available.

For example:

Context::parsers
# => [ "default", "html", ... ]


655
656
657
658
659
# 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

.selectors(include_standard_selectors = true) ⇒ Object

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", ... ]


631
632
633
634
635
636
637
# 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

.waitObject

Prompts the user to press ENTER before resuming execution.

For example:

Context::wait
# => echoes "Press ENTER to continue" and waits for user input


425
426
427
# File 'lib/bauxite/core/context.rb', line 425

def self.wait
	Readline.readline("Press ENTER to continue\n")
end

Instance Method Details

#add_alias(name, action, args) ⇒ Object

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



454
455
456
# File 'lib/bauxite/core/context.rb', line 454

def add_alias(name, action, args)
	@aliases[name] = { :action => action, :args => args }
end

#debugObject

Breaks into the debug console.

For example:

ctx.debug
# => this breaks into the debug console


269
270
271
# File 'lib/bauxite/core/context.rb', line 269

def debug
	exec_parsed_action('debug', [], false)
end

#driverObject

Test engine driver instance (Selenium WebDriver).



259
260
261
262
# File 'lib/bauxite/core/context.rb', line 259

def driver
	_load_driver unless @driver
	@driver
end

#exec_action(text) ⇒ Object

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


309
310
311
312
# 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

#exec_action_object(action) ⇒ Object

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


540
541
542
# File 'lib/bauxite/core/context.rb', line 540

def exec_action_object(action)
	action.execute
end

#exec_file(file) ⇒ Object

Executes the specified file.

For example:

ctx.exec_file('file')
# => executes every action defined in 'file'


320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
# 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

#exec_parsed_action(action, args, log = true, text = nil) ⇒ Object

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


350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
# 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

#expand(s) ⇒ Object

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'


690
691
692
693
694
695
696
# 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

#find(selector, &block) ⇒ Object

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)


252
253
254
255
256
# 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

#get_action(action, args, text = nil) ⇒ Object

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.



500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
# 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

#get_value(element) ⇒ Object

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!'


288
289
290
291
292
293
294
# 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

#output_path(path) ⇒ Object

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'


585
586
587
588
589
590
591
592
593
594
# 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


555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
# 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

#reset_driverObject

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


200
201
202
203
# File 'lib/bauxite/core/context.rb', line 200

def reset_driver
	@driver.quit if @driver
	@driver = nil
end

#start(actions = []) ⇒ Object

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.


177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# 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

#stopObject

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


220
221
222
223
224
225
226
227
228
229
230
# 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

#with_driver_timeout(timeout) ⇒ Object

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


410
411
412
413
414
415
416
417
# 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

#with_timeout(*error_types) ⇒ Object

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


382
383
384
385
386
387
388
389
390
391
392
393
394
395
# File 'lib/bauxite/core/context.rb', line 382

def with_timeout(*error_types)
	stime = Time.new
	timeout ||= stime + @variables['__TIMEOUT__'].to_i
	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

#with_vars(vars) ⇒ Object

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"}


713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
# 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