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.
Files changed (124) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/Rakefile +69 -0
  4. data/bin/bauxite +27 -0
  5. data/doc/Bauxite/Action.html +1463 -0
  6. data/doc/Bauxite/ActionModule.html +342 -0
  7. data/doc/Bauxite/Context.html +1439 -0
  8. data/doc/Bauxite/Errors/AssertionError.html +107 -0
  9. data/doc/Bauxite/Errors/FileNotFoundError.html +107 -0
  10. data/doc/Bauxite/Errors.html +100 -0
  11. data/doc/Bauxite/Loggers/CompositeLogger.html +325 -0
  12. data/doc/Bauxite/Loggers/EchoLogger.html +164 -0
  13. data/doc/Bauxite/Loggers/FileLogger.html +215 -0
  14. data/doc/Bauxite/Loggers/NullLogger.html +334 -0
  15. data/doc/Bauxite/Loggers/TerminalLogger.html +586 -0
  16. data/doc/Bauxite/Loggers/XtermLogger.html +287 -0
  17. data/doc/Bauxite/Loggers.html +103 -0
  18. data/doc/Bauxite/Selector.html +422 -0
  19. data/doc/Bauxite/SelectorModule.html +283 -0
  20. data/doc/Bauxite.html +98 -0
  21. data/doc/created.rid +37 -0
  22. data/doc/fonts/Lato-Light.ttf +0 -0
  23. data/doc/fonts/Lato-LightItalic.ttf +0 -0
  24. data/doc/fonts/Lato-Regular.ttf +0 -0
  25. data/doc/fonts/Lato-RegularItalic.ttf +0 -0
  26. data/doc/fonts/SourceCodePro-Bold.ttf +0 -0
  27. data/doc/fonts/SourceCodePro-Regular.ttf +0 -0
  28. data/doc/fonts.css +167 -0
  29. data/doc/images/add.png +0 -0
  30. data/doc/images/arrow_up.png +0 -0
  31. data/doc/images/brick.png +0 -0
  32. data/doc/images/brick_link.png +0 -0
  33. data/doc/images/bug.png +0 -0
  34. data/doc/images/bullet_black.png +0 -0
  35. data/doc/images/bullet_toggle_minus.png +0 -0
  36. data/doc/images/bullet_toggle_plus.png +0 -0
  37. data/doc/images/date.png +0 -0
  38. data/doc/images/delete.png +0 -0
  39. data/doc/images/find.png +0 -0
  40. data/doc/images/loadingAnimation.gif +0 -0
  41. data/doc/images/macFFBgHack.png +0 -0
  42. data/doc/images/package.png +0 -0
  43. data/doc/images/page_green.png +0 -0
  44. data/doc/images/page_white_text.png +0 -0
  45. data/doc/images/page_white_width.png +0 -0
  46. data/doc/images/plugin.png +0 -0
  47. data/doc/images/ruby.png +0 -0
  48. data/doc/images/tag_blue.png +0 -0
  49. data/doc/images/tag_green.png +0 -0
  50. data/doc/images/transparent.png +0 -0
  51. data/doc/images/wrench.png +0 -0
  52. data/doc/images/wrench_orange.png +0 -0
  53. data/doc/images/zoom.png +0 -0
  54. data/doc/index.html +111 -0
  55. data/doc/js/darkfish.js +140 -0
  56. data/doc/js/jquery.js +18 -0
  57. data/doc/js/navigation.js +142 -0
  58. data/doc/js/search.js +109 -0
  59. data/doc/js/search_index.js +1 -0
  60. data/doc/js/searcher.js +228 -0
  61. data/doc/rdoc.css +580 -0
  62. data/doc/table_of_contents.html +510 -0
  63. data/lib/bauxite/actions/alias.rb +51 -0
  64. data/lib/bauxite/actions/assert.rb +49 -0
  65. data/lib/bauxite/actions/assertv.rb +40 -0
  66. data/lib/bauxite/actions/break.rb +39 -0
  67. data/lib/bauxite/actions/click.rb +35 -0
  68. data/lib/bauxite/actions/debug.rb +99 -0
  69. data/lib/bauxite/actions/echo.rb +36 -0
  70. data/lib/bauxite/actions/exec.rb +46 -0
  71. data/lib/bauxite/actions/js.rb +41 -0
  72. data/lib/bauxite/actions/load.rb +49 -0
  73. data/lib/bauxite/actions/open.rb +34 -0
  74. data/lib/bauxite/actions/params.rb +40 -0
  75. data/lib/bauxite/actions/replace.rb +37 -0
  76. data/lib/bauxite/actions/reset.rb +37 -0
  77. data/lib/bauxite/actions/return.rb +62 -0
  78. data/lib/bauxite/actions/ruby.rb +58 -0
  79. data/lib/bauxite/actions/set.rb +39 -0
  80. data/lib/bauxite/actions/source.rb +44 -0
  81. data/lib/bauxite/actions/store.rb +38 -0
  82. data/lib/bauxite/actions/test.rb +61 -0
  83. data/lib/bauxite/actions/tryload.rb +79 -0
  84. data/lib/bauxite/actions/wait.rb +38 -0
  85. data/lib/bauxite/actions/write.rb +40 -0
  86. data/lib/bauxite/application.rb +150 -0
  87. data/lib/bauxite/core/Action.rb +205 -0
  88. data/lib/bauxite/core/Context.rb +575 -0
  89. data/lib/bauxite/core/Errors.rb +36 -0
  90. data/lib/bauxite/core/Logger.rb +86 -0
  91. data/lib/bauxite/core/Selector.rb +156 -0
  92. data/lib/bauxite/loggers/composite.rb +70 -0
  93. data/lib/bauxite/loggers/echo.rb +36 -0
  94. data/lib/bauxite/loggers/file.rb +45 -0
  95. data/lib/bauxite/loggers/terminal.rb +130 -0
  96. data/lib/bauxite/loggers/xterm.rb +79 -0
  97. data/lib/bauxite/selectors/attr.rb +39 -0
  98. data/lib/bauxite/selectors/default.rb +38 -0
  99. data/lib/bauxite/selectors/frame.rb +60 -0
  100. data/lib/bauxite.rb +29 -0
  101. data/test/alias.bxt +6 -0
  102. data/test/assertv.bxt +2 -0
  103. data/test/delay/page.html +5 -0
  104. data/test/delay.bxt +2 -0
  105. data/test/exec.bxt +6 -0
  106. data/test/format/page.html +7 -0
  107. data/test/format.bxt +17 -0
  108. data/test/frame/child_frame.html +7 -0
  109. data/test/frame/grandchild_frame.html +5 -0
  110. data/test/frame/page.html +5 -0
  111. data/test/frame.bxt +6 -0
  112. data/test/js.bxt +5 -0
  113. data/test/load/child.bxt +13 -0
  114. data/test/load.bxt +17 -0
  115. data/test/ruby/custom.rb +5 -0
  116. data/test/ruby.bxt +2 -0
  117. data/test/selectors/page.html +7 -0
  118. data/test/selectors.bxt +7 -0
  119. data/test/stdin.bxt +1 -0
  120. data/test/test/test1.bxt +2 -0
  121. data/test/test/test2.bxt +3 -0
  122. data/test/test/test3.bxt +2 -0
  123. data/test/test.bxt.manual +4 -0
  124. 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