bauxite 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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