bauxite 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/Rakefile +69 -0
- data/bin/bauxite +27 -0
- data/doc/Bauxite/Action.html +1463 -0
- data/doc/Bauxite/ActionModule.html +342 -0
- data/doc/Bauxite/Context.html +1439 -0
- data/doc/Bauxite/Errors/AssertionError.html +107 -0
- data/doc/Bauxite/Errors/FileNotFoundError.html +107 -0
- data/doc/Bauxite/Errors.html +100 -0
- data/doc/Bauxite/Loggers/CompositeLogger.html +325 -0
- data/doc/Bauxite/Loggers/EchoLogger.html +164 -0
- data/doc/Bauxite/Loggers/FileLogger.html +215 -0
- data/doc/Bauxite/Loggers/NullLogger.html +334 -0
- data/doc/Bauxite/Loggers/TerminalLogger.html +586 -0
- data/doc/Bauxite/Loggers/XtermLogger.html +287 -0
- data/doc/Bauxite/Loggers.html +103 -0
- data/doc/Bauxite/Selector.html +422 -0
- data/doc/Bauxite/SelectorModule.html +283 -0
- data/doc/Bauxite.html +98 -0
- data/doc/created.rid +37 -0
- data/doc/fonts/Lato-Light.ttf +0 -0
- data/doc/fonts/Lato-LightItalic.ttf +0 -0
- data/doc/fonts/Lato-Regular.ttf +0 -0
- data/doc/fonts/Lato-RegularItalic.ttf +0 -0
- data/doc/fonts/SourceCodePro-Bold.ttf +0 -0
- data/doc/fonts/SourceCodePro-Regular.ttf +0 -0
- data/doc/fonts.css +167 -0
- data/doc/images/add.png +0 -0
- data/doc/images/arrow_up.png +0 -0
- data/doc/images/brick.png +0 -0
- data/doc/images/brick_link.png +0 -0
- data/doc/images/bug.png +0 -0
- data/doc/images/bullet_black.png +0 -0
- data/doc/images/bullet_toggle_minus.png +0 -0
- data/doc/images/bullet_toggle_plus.png +0 -0
- data/doc/images/date.png +0 -0
- data/doc/images/delete.png +0 -0
- data/doc/images/find.png +0 -0
- data/doc/images/loadingAnimation.gif +0 -0
- data/doc/images/macFFBgHack.png +0 -0
- data/doc/images/package.png +0 -0
- data/doc/images/page_green.png +0 -0
- data/doc/images/page_white_text.png +0 -0
- data/doc/images/page_white_width.png +0 -0
- data/doc/images/plugin.png +0 -0
- data/doc/images/ruby.png +0 -0
- data/doc/images/tag_blue.png +0 -0
- data/doc/images/tag_green.png +0 -0
- data/doc/images/transparent.png +0 -0
- data/doc/images/wrench.png +0 -0
- data/doc/images/wrench_orange.png +0 -0
- data/doc/images/zoom.png +0 -0
- data/doc/index.html +111 -0
- data/doc/js/darkfish.js +140 -0
- data/doc/js/jquery.js +18 -0
- data/doc/js/navigation.js +142 -0
- data/doc/js/search.js +109 -0
- data/doc/js/search_index.js +1 -0
- data/doc/js/searcher.js +228 -0
- data/doc/rdoc.css +580 -0
- data/doc/table_of_contents.html +510 -0
- data/lib/bauxite/actions/alias.rb +51 -0
- data/lib/bauxite/actions/assert.rb +49 -0
- data/lib/bauxite/actions/assertv.rb +40 -0
- data/lib/bauxite/actions/break.rb +39 -0
- data/lib/bauxite/actions/click.rb +35 -0
- data/lib/bauxite/actions/debug.rb +99 -0
- data/lib/bauxite/actions/echo.rb +36 -0
- data/lib/bauxite/actions/exec.rb +46 -0
- data/lib/bauxite/actions/js.rb +41 -0
- data/lib/bauxite/actions/load.rb +49 -0
- data/lib/bauxite/actions/open.rb +34 -0
- data/lib/bauxite/actions/params.rb +40 -0
- data/lib/bauxite/actions/replace.rb +37 -0
- data/lib/bauxite/actions/reset.rb +37 -0
- data/lib/bauxite/actions/return.rb +62 -0
- data/lib/bauxite/actions/ruby.rb +58 -0
- data/lib/bauxite/actions/set.rb +39 -0
- data/lib/bauxite/actions/source.rb +44 -0
- data/lib/bauxite/actions/store.rb +38 -0
- data/lib/bauxite/actions/test.rb +61 -0
- data/lib/bauxite/actions/tryload.rb +79 -0
- data/lib/bauxite/actions/wait.rb +38 -0
- data/lib/bauxite/actions/write.rb +40 -0
- data/lib/bauxite/application.rb +150 -0
- data/lib/bauxite/core/Action.rb +205 -0
- data/lib/bauxite/core/Context.rb +575 -0
- data/lib/bauxite/core/Errors.rb +36 -0
- data/lib/bauxite/core/Logger.rb +86 -0
- data/lib/bauxite/core/Selector.rb +156 -0
- data/lib/bauxite/loggers/composite.rb +70 -0
- data/lib/bauxite/loggers/echo.rb +36 -0
- data/lib/bauxite/loggers/file.rb +45 -0
- data/lib/bauxite/loggers/terminal.rb +130 -0
- data/lib/bauxite/loggers/xterm.rb +79 -0
- data/lib/bauxite/selectors/attr.rb +39 -0
- data/lib/bauxite/selectors/default.rb +38 -0
- data/lib/bauxite/selectors/frame.rb +60 -0
- data/lib/bauxite.rb +29 -0
- data/test/alias.bxt +6 -0
- data/test/assertv.bxt +2 -0
- data/test/delay/page.html +5 -0
- data/test/delay.bxt +2 -0
- data/test/exec.bxt +6 -0
- data/test/format/page.html +7 -0
- data/test/format.bxt +17 -0
- data/test/frame/child_frame.html +7 -0
- data/test/frame/grandchild_frame.html +5 -0
- data/test/frame/page.html +5 -0
- data/test/frame.bxt +6 -0
- data/test/js.bxt +5 -0
- data/test/load/child.bxt +13 -0
- data/test/load.bxt +17 -0
- data/test/ruby/custom.rb +5 -0
- data/test/ruby.bxt +2 -0
- data/test/selectors/page.html +7 -0
- data/test/selectors.bxt +7 -0
- data/test/stdin.bxt +1 -0
- data/test/test/test1.bxt +2 -0
- data/test/test/test2.bxt +3 -0
- data/test/test/test3.bxt +2 -0
- data/test/test.bxt.manual +4 -0
- metadata +194 -0
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
#--
|
|
2
|
+
# Copyright (c) 2014 Patricio Zavolinsky
|
|
3
|
+
#
|
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
# in the Software without restriction, including without limitation the rights
|
|
7
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
# furnished to do so, subject to the following conditions:
|
|
10
|
+
#
|
|
11
|
+
# The above copyright notice and this permission notice shall be included in
|
|
12
|
+
# all copies or substantial portions of the Software.
|
|
13
|
+
#
|
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
# SOFTWARE.
|
|
21
|
+
#++
|
|
22
|
+
|
|
23
|
+
require 'selenium-webdriver'
|
|
24
|
+
require 'readline'
|
|
25
|
+
require 'csv'
|
|
26
|
+
|
|
27
|
+
# Load dependencies and extensions without leaking dir into the global scope
|
|
28
|
+
lambda do
|
|
29
|
+
dir = File.expand_path(File.dirname(__FILE__))
|
|
30
|
+
Dir[File.join(dir, '*.rb')].each { |file| require file }
|
|
31
|
+
Dir[File.join(dir, '..', 'actions' , '*.rb')].each { |file| require file }
|
|
32
|
+
Dir[File.join(dir, '..', 'selectors', '*.rb')].each { |file| require file }
|
|
33
|
+
Dir[File.join(dir, '..', 'loggers' , '*.rb')].each { |file| require file }
|
|
34
|
+
end.call
|
|
35
|
+
|
|
36
|
+
module Bauxite
|
|
37
|
+
# The Main test context. This class includes state and helper functions
|
|
38
|
+
# used by clients execute tests and by actions and selectors to interact
|
|
39
|
+
# with the test engine (i.e. Selenium WebDriver).
|
|
40
|
+
class Context
|
|
41
|
+
# Test engine driver instance (Selenium WebDriver).
|
|
42
|
+
attr_reader :driver
|
|
43
|
+
|
|
44
|
+
# Logger instance.
|
|
45
|
+
attr_reader :logger
|
|
46
|
+
|
|
47
|
+
# Test options.
|
|
48
|
+
attr_reader :options
|
|
49
|
+
|
|
50
|
+
# Context variables.
|
|
51
|
+
attr_accessor :variables
|
|
52
|
+
|
|
53
|
+
# Action aliases.
|
|
54
|
+
attr_accessor :aliases
|
|
55
|
+
|
|
56
|
+
# Test containers.
|
|
57
|
+
attr_accessor :tests
|
|
58
|
+
|
|
59
|
+
# Constructs a new test context instance.
|
|
60
|
+
#
|
|
61
|
+
# +options+ is a hash with the following values:
|
|
62
|
+
# [:driver] selenium driver symbol (defaults to +:firefox+)
|
|
63
|
+
# [:timeout] selector timeout in seconds (defaults to +10s+)
|
|
64
|
+
# [:logger] logger implementation name without the 'Logger' suffix (defaults to 'null' for Loggers::NullLogger).
|
|
65
|
+
# [:verbose] if +true+, show verbose error information (e.g. backtraces) if an error occurs (defaults to +false+)
|
|
66
|
+
# [:debug] if +true+, break into the #debug console if an error occurs (defaults to +false+)
|
|
67
|
+
# [:wait] if +true+, call ::wait before stopping the test engine with #stop (defaults to +false+)
|
|
68
|
+
#
|
|
69
|
+
def initialize(options)
|
|
70
|
+
@options = options
|
|
71
|
+
@driver_name = (options[:driver] || :firefox).to_sym
|
|
72
|
+
@variables = {
|
|
73
|
+
'__TIMEOUT__' => (options[:timeout] || 10).to_i
|
|
74
|
+
}
|
|
75
|
+
@aliases = {}
|
|
76
|
+
@tests = []
|
|
77
|
+
|
|
78
|
+
handle_errors do
|
|
79
|
+
@logger = Context::load_logger(options[:logger], options[:logger_opt])
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Starts the test engine and executes the actions specified. If no action
|
|
84
|
+
# was specified, returns without stopping the test engine (see #stop).
|
|
85
|
+
#
|
|
86
|
+
# For example:
|
|
87
|
+
# lines = [
|
|
88
|
+
# 'open "http://www.ruby-lang.org"',
|
|
89
|
+
# 'write "name=q" "ljust"',
|
|
90
|
+
# 'click "name=sa"',
|
|
91
|
+
# 'break'
|
|
92
|
+
# ]
|
|
93
|
+
# ctx.start(lines.map { |l| ctx.parse_action(l) })
|
|
94
|
+
# # => navigates to www.ruby-lang.org, types ljust in the search box
|
|
95
|
+
# # and clicks the "Search" button.
|
|
96
|
+
#
|
|
97
|
+
def start(actions = [])
|
|
98
|
+
_load_driver
|
|
99
|
+
return unless actions.size > 0
|
|
100
|
+
begin
|
|
101
|
+
actions.each do |action|
|
|
102
|
+
exec_action(action)
|
|
103
|
+
end
|
|
104
|
+
ensure
|
|
105
|
+
stop
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Stops the test engine and starts a new engine with the same provider.
|
|
110
|
+
#
|
|
111
|
+
# For example:
|
|
112
|
+
# ctx.reset_driver
|
|
113
|
+
# => closes the browser and opens a new one
|
|
114
|
+
#
|
|
115
|
+
def reset_driver
|
|
116
|
+
@driver.quit
|
|
117
|
+
_load_driver
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Stops the test engine.
|
|
121
|
+
#
|
|
122
|
+
# Calling this method at the end of the test is mandatory if #start was
|
|
123
|
+
# called without +actions+.
|
|
124
|
+
#
|
|
125
|
+
# Note that the recommeneded way of executing tests is by passing a list
|
|
126
|
+
# of +actions+ to #start instead of using the #start / #stop pattern.
|
|
127
|
+
#
|
|
128
|
+
# For example:
|
|
129
|
+
# ctx.start(:firefox) # => opens firefox
|
|
130
|
+
#
|
|
131
|
+
# # test stuff goes here
|
|
132
|
+
#
|
|
133
|
+
# ctx.stop # => closes firefox
|
|
134
|
+
#
|
|
135
|
+
def stop
|
|
136
|
+
Context::wait if @options[:wait]
|
|
137
|
+
@driver.quit
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Finds an element by +selector+.
|
|
141
|
+
#
|
|
142
|
+
# The element found is yielded to the given +block+ (if any) and returned.
|
|
143
|
+
#
|
|
144
|
+
# Note that the recommeneded way to call this method is by passing a
|
|
145
|
+
# +block+. This is because the method ensures that the element context is
|
|
146
|
+
# maintained for the duration of the +block+ but it makes no guarantees
|
|
147
|
+
# after the +block+ completes (the same applies if no +block+ was given).
|
|
148
|
+
#
|
|
149
|
+
# For example:
|
|
150
|
+
# ctx.find('css=.my_button') { |element| element.click }
|
|
151
|
+
# ctx.find('css=.my_button').click
|
|
152
|
+
#
|
|
153
|
+
# For example (where using a +block+ is mandatory):
|
|
154
|
+
# ctx.find('frame=|myframe|css=.my_button') { |element| element.click }
|
|
155
|
+
# # => .my_button clicked
|
|
156
|
+
#
|
|
157
|
+
# ctx.find('frame=|myframe|css=.my_button').click
|
|
158
|
+
# # => error, cannot click .my_button (no longer in myframe scope)
|
|
159
|
+
#
|
|
160
|
+
def find(selector, &block) # yields: element
|
|
161
|
+
with_timeout Selenium::WebDriver::Error::NoSuchElementError do
|
|
162
|
+
Selector.new(self).find(selector, &block)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Breaks into the debug console.
|
|
167
|
+
#
|
|
168
|
+
# For example:
|
|
169
|
+
# ctx.debug
|
|
170
|
+
# # => this breaks into the debug console
|
|
171
|
+
def debug
|
|
172
|
+
exec_action('debug', false)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Returns the value of the specified +element+.
|
|
176
|
+
#
|
|
177
|
+
# This method takes into account the type of element and selectively
|
|
178
|
+
# returns the inner text or the value of the +value+ attribute.
|
|
179
|
+
#
|
|
180
|
+
# For example:
|
|
181
|
+
# # assuming <input type='text' value='Hello' />
|
|
182
|
+
# # <span id='label'>World!</span>
|
|
183
|
+
#
|
|
184
|
+
# ctx.get_value(ctx.find('css=input[type=text]'))
|
|
185
|
+
# # => returns 'Hello'
|
|
186
|
+
#
|
|
187
|
+
# ctx.get_value(ctx.find('label'))
|
|
188
|
+
# # => returns 'World!'
|
|
189
|
+
#
|
|
190
|
+
def get_value(element)
|
|
191
|
+
if ['input','select','textarea'].include? element.tag_name.downcase
|
|
192
|
+
element.attribute('value')
|
|
193
|
+
else
|
|
194
|
+
element.text
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# ======================================================================= #
|
|
199
|
+
# :section: Advanced Helpers
|
|
200
|
+
# ======================================================================= #
|
|
201
|
+
|
|
202
|
+
# Executes the specified action handling errors, logging and debug history.
|
|
203
|
+
# Actions can be obtained by calling #parse_action.
|
|
204
|
+
#
|
|
205
|
+
# If +log+ is +true+, log the action execution (default behavior).
|
|
206
|
+
#
|
|
207
|
+
# For example:
|
|
208
|
+
# action = ctx.parse_action('open "http://www.ruby-lang.org"')
|
|
209
|
+
# ctx.exec_action action
|
|
210
|
+
# # => navigates to www.ruby-lang.org
|
|
211
|
+
#
|
|
212
|
+
def exec_action(action, log = true)
|
|
213
|
+
if (action.is_a? String)
|
|
214
|
+
action = { :text => action, :file => '<unknown>', :line => 0 }
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
ret = handle_errors(true) do
|
|
218
|
+
|
|
219
|
+
action = _load_action(action)
|
|
220
|
+
|
|
221
|
+
# Inject built-in variables
|
|
222
|
+
file = action.file
|
|
223
|
+
dir = (File.exists? file) ? File.dirname(file) : Dir.pwd
|
|
224
|
+
@variables['__FILE__'] = file
|
|
225
|
+
@variables['__DIR__'] = File.absolute_path(dir)
|
|
226
|
+
|
|
227
|
+
if log
|
|
228
|
+
@logger.log_cmd(action) do
|
|
229
|
+
Readline::HISTORY << action.text
|
|
230
|
+
action.execute
|
|
231
|
+
end
|
|
232
|
+
else
|
|
233
|
+
action.execute
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
ret.call if ret.respond_to? :call # delayed actions (after log_cmd)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Parses the specified text into a test action array.
|
|
240
|
+
#
|
|
241
|
+
# See #parse_action for more details.
|
|
242
|
+
#
|
|
243
|
+
# For example:
|
|
244
|
+
# ctx.parse_file('file')
|
|
245
|
+
# # => [ { :cmd => 'echo', ... } ]
|
|
246
|
+
#
|
|
247
|
+
def parse_file(file)
|
|
248
|
+
if (file == 'stdin')
|
|
249
|
+
_parse_file($stdin, file)
|
|
250
|
+
else
|
|
251
|
+
File.open(file) { |io| _parse_file(io, file) }
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Parses a line of action text into an array. The input +line+ should be a
|
|
256
|
+
# space-separated list of values, surrounded by optional quotes (").
|
|
257
|
+
#
|
|
258
|
+
# The first element in +line+ will be interpreted as an action name. Valid
|
|
259
|
+
# action names are retuned by ::actions.
|
|
260
|
+
#
|
|
261
|
+
# The other elements in +line+ will be interpreted as action arguments.
|
|
262
|
+
#
|
|
263
|
+
# For example:
|
|
264
|
+
# Context::parse_args('echo "Hello World!"')
|
|
265
|
+
# # => ' ["echo", "Hello World!"]
|
|
266
|
+
#
|
|
267
|
+
def self.parse_args(line)
|
|
268
|
+
# col_sep must be a regex because String.split has a special case for
|
|
269
|
+
# a single space char (' ') that produced unexpected results (i.e.
|
|
270
|
+
# if line is '"a b"' the resulting array contains ["a b"]).
|
|
271
|
+
#
|
|
272
|
+
# ...but...
|
|
273
|
+
#
|
|
274
|
+
# CSV expects col_sep to be a string so we need to work some dark magic
|
|
275
|
+
# here. Basically we proxy the StringIO received by CSV to returns
|
|
276
|
+
# strings for which the split method does not fold the whitespaces.
|
|
277
|
+
#
|
|
278
|
+
return [] if line.strip == ''
|
|
279
|
+
CSV.new(StringIOProxy.new(line), { :col_sep => ' ' })
|
|
280
|
+
.shift
|
|
281
|
+
.select { |a| a != nil }
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Executes the +block+ inside a rescue block applying standard criteria of
|
|
285
|
+
# error handling.
|
|
286
|
+
#
|
|
287
|
+
# The default behavior is to print the exception message and exit.
|
|
288
|
+
#
|
|
289
|
+
# If the +:verbose+ option is set, the exception backtrace will also be
|
|
290
|
+
# printed.
|
|
291
|
+
#
|
|
292
|
+
# If the +break_into_debug+ argument is +true+ and the +:debug+ option is
|
|
293
|
+
# set, the handler will break into the debug console instead of exiting.
|
|
294
|
+
#
|
|
295
|
+
# If the +exit_on_error+ argument is +false+ the handler will not exit
|
|
296
|
+
# after printing the error message.
|
|
297
|
+
#
|
|
298
|
+
# For example:
|
|
299
|
+
# ctx = Context.new({ :debug => true })
|
|
300
|
+
# ctx.handle_errors(true) { raise 'break into debug now!' }
|
|
301
|
+
# # => this breaks into the debug console
|
|
302
|
+
#
|
|
303
|
+
def handle_errors(break_into_debug = false, exit_on_error = true)
|
|
304
|
+
yield
|
|
305
|
+
rescue StandardError => e
|
|
306
|
+
if @logger
|
|
307
|
+
@logger.log "#{e.message}\n", :error
|
|
308
|
+
else
|
|
309
|
+
puts e.message
|
|
310
|
+
end
|
|
311
|
+
if @options[:verbose]
|
|
312
|
+
p e
|
|
313
|
+
puts e.backtrace
|
|
314
|
+
end
|
|
315
|
+
if break_into_debug and @options[:debug]
|
|
316
|
+
debug
|
|
317
|
+
elsif exit_on_error
|
|
318
|
+
if @variables['__RAISE_ERROR__']
|
|
319
|
+
raise
|
|
320
|
+
else
|
|
321
|
+
exit false
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# Executes the given block retrying for at most <tt>${__TIMEOUT__}</tt>
|
|
327
|
+
# seconds. Note that this method does not take into account the time it
|
|
328
|
+
# takes to execute the block itself.
|
|
329
|
+
#
|
|
330
|
+
# For example
|
|
331
|
+
# ctx.with_timeout StandardError do
|
|
332
|
+
# ctx.find ('element_with_delay') do |e|
|
|
333
|
+
# # do something with e
|
|
334
|
+
# end
|
|
335
|
+
# end
|
|
336
|
+
#
|
|
337
|
+
def with_timeout(*error_types)
|
|
338
|
+
stime = Time.new
|
|
339
|
+
timeout ||= stime + @variables['__TIMEOUT__']
|
|
340
|
+
yield
|
|
341
|
+
rescue *error_types => e
|
|
342
|
+
t = Time.new
|
|
343
|
+
rem = timeout - t
|
|
344
|
+
raise if rem < 0
|
|
345
|
+
|
|
346
|
+
@logger.progress(rem.round)
|
|
347
|
+
|
|
348
|
+
sleep(1.0/10.0) if (t - stime).to_i < 1
|
|
349
|
+
retry
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# Prompts the user to press ENTER before resuming execution.
|
|
353
|
+
#
|
|
354
|
+
# For example:
|
|
355
|
+
# Context::wait
|
|
356
|
+
# # => echoes "Press ENTER to continue" and waits for user input
|
|
357
|
+
#
|
|
358
|
+
def self.wait
|
|
359
|
+
Readline.readline("Press ENTER to continue\n")
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
# Constructs a Logger instance using +name+ as a hint for the logger
|
|
363
|
+
# type.
|
|
364
|
+
#
|
|
365
|
+
def self.load_logger(name, options)
|
|
366
|
+
log_name = (name || 'null').downcase
|
|
367
|
+
|
|
368
|
+
class_name = "#{log_name.capitalize}Logger"
|
|
369
|
+
|
|
370
|
+
unless Loggers.const_defined? class_name.to_sym
|
|
371
|
+
raise NameError,
|
|
372
|
+
"Invalid logger '#{log_name}'"
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
Loggers.const_get(class_name).new(options)
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
# ======================================================================= #
|
|
379
|
+
# :section: Metadata
|
|
380
|
+
# ======================================================================= #
|
|
381
|
+
|
|
382
|
+
# Returns an array with the names of every action available.
|
|
383
|
+
#
|
|
384
|
+
# For example:
|
|
385
|
+
# Context::actions
|
|
386
|
+
# # => [ "assert", "break", ... ]
|
|
387
|
+
#
|
|
388
|
+
def self.actions
|
|
389
|
+
_action_methods.map { |m| m.sub(/_action$/, '') }
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
# Returns an array with the names of the arguments of the specified action.
|
|
393
|
+
#
|
|
394
|
+
# For example:
|
|
395
|
+
# Context::action_args 'assert'
|
|
396
|
+
# # => [ "selector", "text" ]
|
|
397
|
+
#
|
|
398
|
+
def self.action_args(action)
|
|
399
|
+
action += '_action' unless _action_methods.include? action
|
|
400
|
+
Action.public_instance_method(action).parameters.map { |att, name| name.to_s }
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# Returns an array with the names of every selector available.
|
|
404
|
+
#
|
|
405
|
+
# If +include_standard_selectors+ is +true+ (default behavior) both
|
|
406
|
+
# standard and custom selector are returned, otherwise only custom
|
|
407
|
+
# selectors are returned.
|
|
408
|
+
#
|
|
409
|
+
# For example:
|
|
410
|
+
# Context::selectors
|
|
411
|
+
# # => [ "class", "id", ... ]
|
|
412
|
+
#
|
|
413
|
+
def self.selectors(include_standard_selectors = true)
|
|
414
|
+
ret = Selector.public_instance_methods(false).map { |a| a.to_s.sub(/_selector$/, '') }
|
|
415
|
+
if include_standard_selectors
|
|
416
|
+
ret += Selenium::WebDriver::SearchContext::FINDERS.map { |k,v| k.to_s }
|
|
417
|
+
end
|
|
418
|
+
ret
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
# Returns an array with the names of every logger available.
|
|
422
|
+
#
|
|
423
|
+
# For example:
|
|
424
|
+
# Context::loggers
|
|
425
|
+
# # => [ "null", "bash", ... ]
|
|
426
|
+
#
|
|
427
|
+
def self.loggers
|
|
428
|
+
Loggers.constants.map { |l| l.to_s.downcase.sub(/logger$/, '') }
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
# Returns the maximum size in characters of an action name.
|
|
432
|
+
#
|
|
433
|
+
# This method is useful to pretty print lists of actions
|
|
434
|
+
#
|
|
435
|
+
# For example:
|
|
436
|
+
# # assuming actions = [ "echo", "assert", "tryload" ]
|
|
437
|
+
# Context::max_action_name_size
|
|
438
|
+
# # => 7
|
|
439
|
+
def self.max_action_name_size
|
|
440
|
+
actions.inject(0) { |s,a| a.size > s ? a.size : s }
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
# ======================================================================= #
|
|
444
|
+
# :section: Variable manipulation methods
|
|
445
|
+
# ======================================================================= #
|
|
446
|
+
|
|
447
|
+
# Recursively replaces occurencies of variable expansions in +s+ with the
|
|
448
|
+
# corresponding variable value.
|
|
449
|
+
#
|
|
450
|
+
# The variable expansion expression format is:
|
|
451
|
+
# '${variable_name}'
|
|
452
|
+
#
|
|
453
|
+
# For example:
|
|
454
|
+
# ctx.variables = { 'a' => '1', 'b' => '2', 'c' => 'a' }
|
|
455
|
+
# ctx.expand '${a}' # => '1'
|
|
456
|
+
# ctx.expand '${b}' # => '2'
|
|
457
|
+
# ctx.expand '${c}' # => 'a'
|
|
458
|
+
# ctx.expand '${${c}}' # => '1'
|
|
459
|
+
#
|
|
460
|
+
def expand(s)
|
|
461
|
+
result = @variables.inject(s) do |s,kv|
|
|
462
|
+
s = s.gsub(/\$\{#{kv[0]}\}/, kv[1].to_s)
|
|
463
|
+
end
|
|
464
|
+
result = expand(result) if result != s
|
|
465
|
+
result
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
# Temporarily alter the value of context variables.
|
|
469
|
+
#
|
|
470
|
+
# This method alters the value of the variables specified in the +vars+
|
|
471
|
+
# hash for the duration of the given +block+. When the +block+ completes,
|
|
472
|
+
# the original value of the context variables is restored.
|
|
473
|
+
#
|
|
474
|
+
# For example:
|
|
475
|
+
# ctx.variables = { 'a' => '1', 'b' => '2', c => 'a' }
|
|
476
|
+
# ctx.with_vars({ 'a' => '10', d => '20' }) do
|
|
477
|
+
# p ctx.variables
|
|
478
|
+
# # => {"a"=>"10", "b"=>"2", "c"=>"a", "d"=>"20"}
|
|
479
|
+
# end
|
|
480
|
+
# p ctx.variables
|
|
481
|
+
# # => {"a"=>"1", "b"=>"2", "c"=>"a"}
|
|
482
|
+
#
|
|
483
|
+
def with_vars(vars)
|
|
484
|
+
current = @variables
|
|
485
|
+
@variables = @variables.merge(vars)
|
|
486
|
+
yield
|
|
487
|
+
ensure
|
|
488
|
+
@variables = current
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
private
|
|
492
|
+
def self._action_methods
|
|
493
|
+
(Action.public_instance_methods(false) \
|
|
494
|
+
- ActionModule.public_instance_methods(false))
|
|
495
|
+
.map { |a| a.to_s }
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def _load_driver
|
|
499
|
+
@driver = Selenium::WebDriver.for(@driver_name, @options[:driver_opt])
|
|
500
|
+
@driver.manage.timeouts.implicit_wait = 1
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
def _load_action(action)
|
|
504
|
+
text = action[:text]
|
|
505
|
+
file = action[:file]
|
|
506
|
+
line = action[:line]
|
|
507
|
+
|
|
508
|
+
data = text.split(' ', 2)
|
|
509
|
+
cmd = data[0].strip.downcase
|
|
510
|
+
args = data[1] ? data[1].strip : ''
|
|
511
|
+
|
|
512
|
+
begin
|
|
513
|
+
args = Context::parse_args(args) || []
|
|
514
|
+
rescue StandardError => e
|
|
515
|
+
raise "#{file} (line #{line+1}): #{e.message}"
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
alias_cmd = @aliases[cmd]
|
|
519
|
+
return Action.new(self, cmd, args, text, file, line) unless alias_cmd
|
|
520
|
+
|
|
521
|
+
action[:text] = args.each_with_index.inject(alias_cmd) do |ret,vi|
|
|
522
|
+
# expand ${1} to args[0], ${2} to args[1], etc.
|
|
523
|
+
ret.gsub("${#{vi[1]+1}}", vi[0])
|
|
524
|
+
end.gsub(/\$\{(\d+)\*(q)?\}/) do |match|
|
|
525
|
+
# expand ${4*} to "#{args[4]} #{args[5]} ..."
|
|
526
|
+
# expand ${4*q} to "\"#{args[4]}\" \"#{args[5]}\" ..."
|
|
527
|
+
a = args[$1..-1]
|
|
528
|
+
a = a.map { |arg| '"'+arg.gsub('"', '""')+'"' } if $2 == 'q'
|
|
529
|
+
a.join(' ')
|
|
530
|
+
end.gsub(/\$\{\d+\}/, '') # remove unexpanded ${1}, etc.
|
|
531
|
+
|
|
532
|
+
_load_action(action)
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
def _parse_file(io, file)
|
|
536
|
+
io.each_line.each_with_index.map do |text,line|
|
|
537
|
+
text = text.sub(/\r?\n$/, '')
|
|
538
|
+
next nil if text =~ /^\s*(#|$)/
|
|
539
|
+
exec_action({ :text => text, :file => file, :line => line })
|
|
540
|
+
end
|
|
541
|
+
.select { |item| item != nil }
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
# ======================================================================= #
|
|
545
|
+
# Hacks required to overcome the String#split(' ') behavior of folding the
|
|
546
|
+
# space characters, coupled with CSV not supporting a regex as :col_sep.
|
|
547
|
+
|
|
548
|
+
# Same as a common String except that split(' ') behaves as split(/\s/).
|
|
549
|
+
class StringProxy # :nodoc:
|
|
550
|
+
def initialize(s)
|
|
551
|
+
@s = s
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
def method_missing(method, *args, &block)
|
|
555
|
+
args[0] = /\s/ if method == :split and args.size > 0 and args[0] == ' '
|
|
556
|
+
ret = @s.send(method, *args, &block)
|
|
557
|
+
end
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
# Same as a common StringIO except that get(sep) returns a StringProxy
|
|
561
|
+
# instead of a regular string.
|
|
562
|
+
class StringIOProxy # :nodoc:
|
|
563
|
+
def initialize(s)
|
|
564
|
+
@s = StringIO.new(s)
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
def method_missing(method, *args, &block)
|
|
568
|
+
ret = @s.send(method, *args, &block)
|
|
569
|
+
return ret unless method == :gets and args.size == 1
|
|
570
|
+
StringProxy.new(ret)
|
|
571
|
+
end
|
|
572
|
+
end
|
|
573
|
+
# ======================================================================= #
|
|
574
|
+
end
|
|
575
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#--
|
|
2
|
+
# Copyright (c) 2014 Patricio Zavolinsky
|
|
3
|
+
#
|
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
# in the Software without restriction, including without limitation the rights
|
|
7
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
# furnished to do so, subject to the following conditions:
|
|
10
|
+
#
|
|
11
|
+
# The above copyright notice and this permission notice shall be included in
|
|
12
|
+
# all copies or substantial portions of the Software.
|
|
13
|
+
#
|
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
# SOFTWARE.
|
|
21
|
+
#++
|
|
22
|
+
|
|
23
|
+
module Bauxite
|
|
24
|
+
# Errors Module
|
|
25
|
+
module Errors
|
|
26
|
+
# Error raised when an assertion fails.
|
|
27
|
+
#
|
|
28
|
+
class AssertionError < StandardError
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Error raised when an invalid file tried to be loaded.
|
|
32
|
+
#
|
|
33
|
+
class FileNotFoundError < StandardError
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#--
|
|
2
|
+
# Copyright (c) 2014 Patricio Zavolinsky
|
|
3
|
+
#
|
|
4
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
# in the Software without restriction, including without limitation the rights
|
|
7
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
# furnished to do so, subject to the following conditions:
|
|
10
|
+
#
|
|
11
|
+
# The above copyright notice and this permission notice shall be included in
|
|
12
|
+
# all copies or substantial portions of the Software.
|
|
13
|
+
#
|
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
# SOFTWARE.
|
|
21
|
+
#++
|
|
22
|
+
|
|
23
|
+
module Bauxite
|
|
24
|
+
# Test loggers module
|
|
25
|
+
#
|
|
26
|
+
# The default Logger class and all custom loggers must be included in this
|
|
27
|
+
# module.
|
|
28
|
+
#
|
|
29
|
+
module Loggers
|
|
30
|
+
# Test logger class.
|
|
31
|
+
#
|
|
32
|
+
# Test loggers handle test output format.
|
|
33
|
+
#
|
|
34
|
+
# The default logger does not provide any output logging.
|
|
35
|
+
#
|
|
36
|
+
# This default behavior can be overriden in a custom logger passed to the
|
|
37
|
+
# Context constructor (see Context::new). By convention, custom loggers are
|
|
38
|
+
# defined in the 'loggers/' directory.
|
|
39
|
+
#
|
|
40
|
+
class NullLogger
|
|
41
|
+
|
|
42
|
+
# Constructs a new null logger instance.
|
|
43
|
+
#
|
|
44
|
+
def initialize(options)
|
|
45
|
+
@options = options
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Executes the given block in a logging context.
|
|
49
|
+
#
|
|
50
|
+
# This default implementation does not provide any logging
|
|
51
|
+
# capabilities.
|
|
52
|
+
#
|
|
53
|
+
# For example:
|
|
54
|
+
# log.log_cmd('echo', 'Hello World!') do
|
|
55
|
+
# puts 'Hello World!'
|
|
56
|
+
# end
|
|
57
|
+
# # => echoes 'Hello World!'
|
|
58
|
+
#
|
|
59
|
+
def log_cmd(action)
|
|
60
|
+
yield
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Returns the prompt shown by the debug console (see Context#debug).
|
|
64
|
+
#
|
|
65
|
+
# For example:
|
|
66
|
+
# log.debug_prompt
|
|
67
|
+
# # => returns the debug prompt
|
|
68
|
+
def debug_prompt
|
|
69
|
+
'debug> '
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Updates the progress of the current action.
|
|
73
|
+
def progress(value)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Logs the specified string.
|
|
77
|
+
#
|
|
78
|
+
# +type+, if specified, should be one of +:error+, +:warning+,
|
|
79
|
+
# +:info+ (default), +:debug+.
|
|
80
|
+
#
|
|
81
|
+
def log(s, type = :info)
|
|
82
|
+
print s
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|