bauxite 0.6.18 → 0.6.19

Sign up to get free protection for your applications and to get access to all the features.
Files changed (205) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +21 -21
  3. data/README.md +293 -293
  4. data/Rakefile +128 -128
  5. data/bin/bauxite +27 -27
  6. data/doc/Bauxite.html +6 -9
  7. data/doc/Bauxite/Action.html +298 -315
  8. data/doc/Bauxite/ActionModule.html +23 -26
  9. data/doc/Bauxite/Application.html +36 -38
  10. data/doc/Bauxite/Context.html +303 -303
  11. data/doc/Bauxite/Errors.html +6 -9
  12. data/doc/Bauxite/Errors/AssertionError.html +6 -9
  13. data/doc/Bauxite/Errors/FileNotFoundError.html +6 -9
  14. data/doc/Bauxite/Errors/FormatError.html +6 -9
  15. data/doc/Bauxite/Loggers.html +6 -9
  16. data/doc/Bauxite/Loggers/CompositeLogger.html +29 -32
  17. data/doc/Bauxite/Loggers/EchoLogger.html +10 -13
  18. data/doc/Bauxite/Loggers/FileLogger.html +25 -28
  19. data/doc/Bauxite/Loggers/HtmlLogger.html +99 -102
  20. data/doc/Bauxite/Loggers/NullLogger.html +16 -19
  21. data/doc/Bauxite/Loggers/ReportLogger.html +43 -46
  22. data/doc/Bauxite/Loggers/TerminalLogger.html +76 -79
  23. data/doc/Bauxite/Loggers/XtermLogger.html +28 -31
  24. data/doc/Bauxite/Parser.html +87 -89
  25. data/doc/Bauxite/ParserModule.html +19 -22
  26. data/doc/Bauxite/Selector.html +99 -105
  27. data/doc/Bauxite/SelectorModule.html +27 -30
  28. data/doc/README_md.html +117 -103
  29. data/doc/created.rid +58 -58
  30. data/doc/fonts.css +167 -167
  31. data/doc/fonts/Lato-Light.ttf +0 -0
  32. data/doc/fonts/Lato-LightItalic.ttf +0 -0
  33. data/doc/fonts/Lato-Regular.ttf +0 -0
  34. data/doc/fonts/Lato-RegularItalic.ttf +0 -0
  35. data/doc/fonts/SourceCodePro-Bold.ttf +0 -0
  36. data/doc/fonts/SourceCodePro-Regular.ttf +0 -0
  37. data/doc/images/add.png +0 -0
  38. data/doc/images/arrow_up.png +0 -0
  39. data/doc/images/brick.png +0 -0
  40. data/doc/images/brick_link.png +0 -0
  41. data/doc/images/bug.png +0 -0
  42. data/doc/images/bullet_black.png +0 -0
  43. data/doc/images/bullet_toggle_minus.png +0 -0
  44. data/doc/images/bullet_toggle_plus.png +0 -0
  45. data/doc/images/date.png +0 -0
  46. data/doc/images/delete.png +0 -0
  47. data/doc/images/find.png +0 -0
  48. data/doc/images/loadingAnimation.gif +0 -0
  49. data/doc/images/macFFBgHack.png +0 -0
  50. data/doc/images/package.png +0 -0
  51. data/doc/images/page_green.png +0 -0
  52. data/doc/images/page_white_text.png +0 -0
  53. data/doc/images/page_white_width.png +0 -0
  54. data/doc/images/plugin.png +0 -0
  55. data/doc/images/ruby.png +0 -0
  56. data/doc/images/tag_blue.png +0 -0
  57. data/doc/images/tag_green.png +0 -0
  58. data/doc/images/transparent.png +0 -0
  59. data/doc/images/wrench.png +0 -0
  60. data/doc/images/wrench_orange.png +0 -0
  61. data/doc/images/zoom.png +0 -0
  62. data/doc/index.html +117 -103
  63. data/doc/js/darkfish.js +32 -11
  64. data/doc/js/jquery.js +0 -0
  65. data/doc/js/navigation.js +0 -0
  66. data/doc/js/search.js +0 -0
  67. data/doc/js/search_index.js +1 -1
  68. data/doc/js/searcher.js +0 -0
  69. data/doc/rdoc.css +580 -580
  70. data/doc/table_of_contents.html +69 -72
  71. data/lib/bauxite.rb +28 -28
  72. data/lib/bauxite/actions/alias.rb +51 -51
  73. data/lib/bauxite/actions/assert.rb +49 -49
  74. data/lib/bauxite/actions/asserth.rb +60 -60
  75. data/lib/bauxite/actions/assertm.rb +49 -49
  76. data/lib/bauxite/actions/assertv.rb +40 -40
  77. data/lib/bauxite/actions/assertw.rb +47 -47
  78. data/lib/bauxite/actions/break.rb +39 -39
  79. data/lib/bauxite/actions/capture.rb +61 -61
  80. data/lib/bauxite/actions/click.rb +36 -36
  81. data/lib/bauxite/actions/debug.rb +103 -103
  82. data/lib/bauxite/actions/doif.rb +43 -43
  83. data/lib/bauxite/actions/dounless.rb +43 -43
  84. data/lib/bauxite/actions/echo.rb +36 -36
  85. data/lib/bauxite/actions/exec.rb +46 -46
  86. data/lib/bauxite/actions/exit.rb +35 -35
  87. data/lib/bauxite/actions/failif.rb +52 -52
  88. data/lib/bauxite/actions/js.rb +41 -41
  89. data/lib/bauxite/actions/load.rb +49 -49
  90. data/lib/bauxite/actions/open.rb +35 -35
  91. data/lib/bauxite/actions/params.rb +40 -40
  92. data/lib/bauxite/actions/replace.rb +37 -37
  93. data/lib/bauxite/actions/reset.rb +38 -38
  94. data/lib/bauxite/actions/return.rb +68 -68
  95. data/lib/bauxite/actions/ruby.rb +58 -58
  96. data/lib/bauxite/actions/select.rb +48 -48
  97. data/lib/bauxite/actions/set.rb +39 -39
  98. data/lib/bauxite/actions/setif.rb +44 -44
  99. data/lib/bauxite/actions/source.rb +44 -44
  100. data/lib/bauxite/actions/store.rb +38 -38
  101. data/lib/bauxite/actions/submit.rb +37 -37
  102. data/lib/bauxite/actions/test.rb +67 -67
  103. data/lib/bauxite/actions/tryload.rb +71 -71
  104. data/lib/bauxite/actions/wait.rb +38 -38
  105. data/lib/bauxite/actions/write.rb +44 -44
  106. data/lib/bauxite/application.rb +349 -349
  107. data/lib/bauxite/core/action.rb +199 -199
  108. data/lib/bauxite/core/context.rb +791 -791
  109. data/lib/bauxite/core/errors.rb +41 -41
  110. data/lib/bauxite/core/logger.rb +169 -169
  111. data/lib/bauxite/core/parser.rb +85 -85
  112. data/lib/bauxite/core/selector.rb +152 -152
  113. data/lib/bauxite/loggers/composite.rb +91 -91
  114. data/lib/bauxite/loggers/echo.rb +36 -36
  115. data/lib/bauxite/loggers/file.rb +68 -68
  116. data/lib/bauxite/loggers/html.rb +154 -154
  117. data/lib/bauxite/loggers/terminal.rb +134 -134
  118. data/lib/bauxite/loggers/xterm.rb +101 -101
  119. data/lib/bauxite/parsers/csv.rb +43 -43
  120. data/lib/bauxite/parsers/default.rb +42 -42
  121. data/lib/bauxite/parsers/html.rb +79 -79
  122. data/lib/bauxite/selectors/attr.rb +39 -39
  123. data/lib/bauxite/selectors/frame.rb +60 -60
  124. data/lib/bauxite/selectors/json.rb +88 -88
  125. data/lib/bauxite/selectors/sid.rb +38 -38
  126. data/lib/bauxite/selectors/smart.rb +80 -80
  127. data/lib/bauxite/selectors/window.rb +77 -77
  128. data/test/alert.bxt +3 -3
  129. data/test/alert/page.html +4 -4
  130. data/test/alias.bxt +9 -9
  131. data/test/asserth.bxt +2 -2
  132. data/test/assertv.bxt +1 -1
  133. data/test/assertw.bxt +7 -7
  134. data/test/broken.bxt.manual +0 -0
  135. data/test/bug_load_path.bxt.manual +0 -0
  136. data/test/bug_load_path/broken.bxt.manual +0 -0
  137. data/test/bug_load_path/test.bxt +0 -0
  138. data/test/capture.bxt.manual +20 -20
  139. data/test/capture/my_test.bxt +1 -1
  140. data/test/capture/page.html +6 -6
  141. data/test/capture_on_error.bxt.manual +3 -3
  142. data/test/capture_on_error/my_test.bxt +1 -1
  143. data/test/capture_on_error/page.html +2 -2
  144. data/test/debug.bxt.manual +0 -0
  145. data/test/default_selector.bxt.manual +7 -7
  146. data/test/default_selector/page.html +10 -10
  147. data/test/default_selector_var.bxt +1 -1
  148. data/test/delay.bxt +2 -2
  149. data/test/delay/page.html +4 -4
  150. data/test/doif.bxt +6 -6
  151. data/test/dounless.bxt +6 -6
  152. data/test/exec.bxt +6 -6
  153. data/test/exit.bxt +3 -3
  154. data/test/exit/test.bxt +3 -3
  155. data/test/extension.bxt.manual +4 -4
  156. data/test/extension/custom.rb +12 -12
  157. data/test/extension/page.html +4 -4
  158. data/test/failif.bxt +7 -7
  159. data/test/failif/page.html +5 -5
  160. data/test/format.bxt +17 -17
  161. data/test/format/page.html +6 -6
  162. data/test/frame.bxt +6 -6
  163. data/test/frame/child_frame.html +6 -6
  164. data/test/frame/grandchild_frame.html +4 -4
  165. data/test/frame/page.html +4 -4
  166. data/test/js.bxt +4 -4
  167. data/test/json.bxt +19 -19
  168. data/test/json/array.json +3 -3
  169. data/test/json/object.json +13 -13
  170. data/test/load.bxt +18 -18
  171. data/test/load/child.bxt +12 -12
  172. data/test/parsers.bxt +1 -1
  173. data/test/parsers.csv +7 -7
  174. data/test/parsers.html +32 -32
  175. data/test/parsers/page.html +6 -6
  176. data/test/return.bxt +1 -1
  177. data/test/return/f1.bxt +1 -1
  178. data/test/return/f2.bxt +1 -1
  179. data/test/return/f3.bxt +1 -1
  180. data/test/return/f4.bxt +2 -2
  181. data/test/ruby.bxt +1 -1
  182. data/test/ruby/custom.rb +5 -5
  183. data/test/select.bxt +9 -9
  184. data/test/select/page.html +8 -8
  185. data/test/selectors.bxt +7 -7
  186. data/test/selectors/page.html +6 -6
  187. data/test/set_builtin.bxt +5 -0
  188. data/test/set_builtin/page.html +5 -0
  189. data/test/setif.bxt +3 -3
  190. data/test/smart_selector.bxt +17 -17
  191. data/test/smart_selector/page.html +17 -17
  192. data/test/stdin.bxt +0 -0
  193. data/test/submit.bxt +4 -4
  194. data/test/submit/page.html +6 -6
  195. data/test/submit/page2.html +4 -4
  196. data/test/test.bxt.manual +6 -6
  197. data/test/test/test1.bxt +2 -2
  198. data/test/test/test2.bxt +3 -3
  199. data/test/test/test3.bxt +2 -2
  200. data/test/test/test4.bxt +1 -1
  201. data/test/test/test5.bxt +1 -1
  202. data/test/window.bxt +14 -14
  203. data/test/window/page.html +5 -5
  204. data/test/window/popup.html +4 -4
  205. metadata +5 -3
@@ -1,200 +1,200 @@
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
- # Action common state and behavior.
25
- module ActionModule
26
- # Test context
27
- attr_reader :ctx
28
-
29
- # Parsed action command (i.e. action name)
30
- attr_reader :cmd
31
-
32
- # Raw action text.
33
- attr_reader :text
34
-
35
- # Constructs a new test action instance.
36
- def initialize(ctx, cmd, args, text, file, line)
37
- @ctx = ctx
38
- @cmd = cmd
39
- @args = args
40
- @text = text
41
-
42
- @cmd_real = (respond_to? cmd+'_action') ? (cmd+'_action') : cmd
43
-
44
- unless respond_to? @cmd_real and Context::actions.include? @cmd
45
- raise "#{file} (line #{line+1}): Unknown command #{cmd}."
46
- end
47
- end
48
-
49
- # Returns the action arguments after applying variable expansion.
50
- #
51
- # See Context#expand.
52
- #
53
- # If +quote+ is +true+, the arguments are surrounded by quote
54
- # characters (") and every quote inside an argument is doubled.
55
- #
56
- # For example:
57
- # # assuming
58
- # # action.new(ctx, cmd,
59
- # # [ 'dude', 'say "hi"', '${myvar} ], # args
60
- # # text, file, line)
61
- # # ctx.variables = { 'myvar' => 'world' }
62
- #
63
- # action.args
64
- # # => [ 'dude', 'say "hi"', 'world' ]
65
- #
66
- # action.args(true)
67
- # # => [ '"dude"', '"say ""hi"""', '"world"' ]
68
- #
69
- def args(quote = false)
70
- ret = @args.map { |a| @ctx.expand(a) }
71
- ret = ret.map { |a| '"'+a.gsub('"', '""')+'"' } if quote
72
- ret
73
- end
74
-
75
- # Executes the action evaluating the arguments in the current context.
76
- #
77
- # Note that #execute calls #args to expand variables. This means that
78
- # two calls to #execute on the same instance might yield different results.
79
- #
80
- # For example:
81
- # action = ctx.parse_action('echo ${message}')
82
- #
83
- # ctx.variables = { 'message' => 'hi!' }
84
- # action.execute()
85
- # # => outputs 'hi!'
86
- #
87
- # ctx.variables['message'] = 'hello world'
88
- # action.execute()
89
- # # => outputs 'hello world!' yet the instance of action is same!
90
- #
91
- def execute()
92
- send(@cmd_real, *args)
93
- end
94
- private
95
- def _pattern(s)
96
- if s =~ /^\/.*\/[imxo]*$/
97
- eval(s)
98
- else
99
- /#{s}/
100
- end
101
- end
102
- end
103
-
104
- # Test action class.
105
- #
106
- # Test actions are basic test operations that can be combined to create a test
107
- # case.
108
- #
109
- # Test actions are implemented as public methods of the Action class.
110
- #
111
- # Each test action is defined in a separate file in the 'actions/' directory.
112
- # The name of the file must match the name of the action. Ideally, these files
113
- # should avoid adding public methods other than the action method itself.
114
- # Also, no +attr_accessors+ should be added.
115
- #
116
- # Action methods can use the +ctx+ attribute to refer to the current test
117
- # Context.
118
- #
119
- # For example (new action template):
120
- # # === actions/print_source.rb ======= #
121
- # class Action
122
- # # :category: Action Methods
123
- # def print_source
124
- # # action code goes here, for example:
125
- # puts @ctx.driver.page_source.
126
- # end
127
- # end
128
- # # === end actions/print_source.rb === #
129
- #
130
- # Context::actions.include? 'print_source' # => true
131
- #
132
- # To avoid name clashing with Ruby reserved words, the '_action' suffix can be
133
- # included in the action method name (this suffix will not be considered part
134
- # of the action name).
135
- #
136
- # For example (_action suffix):
137
- # # === actions/break.rb ======= #
138
- # class Action
139
- # # :category: Action Methods
140
- # def break_action
141
- # # do something
142
- # end
143
- # end
144
- # # === end actions/break.rb === #
145
- #
146
- # Context::actions.include? 'break' # => true
147
- #
148
- # If the action requires additional attributes or private methods, the name
149
- # of the action should be used as a prefix to avoid name clashing with other
150
- # actions.
151
- #
152
- # For example (private attributes and methods):
153
- # # === actions/debug.rb ======= #
154
- # class Action
155
- # # :category: Action Methods
156
- # def debug
157
- # _debug_do_stuff
158
- # end
159
- # private
160
- # @@debug_line = 0
161
- # def _debug_do_stuff
162
- # @@debug_line += 1
163
- # end
164
- # end
165
- # # === end actions/debug.rb === #
166
- #
167
- # Context::actions.include? 'debug' # => true
168
- #
169
- # Action methods support delayed execution of the test action. Delayed
170
- # execution is useful in cases where the action output would break the
171
- # standard logging interface.
172
- #
173
- # Delayed execution is implemented by returning a lambda from the action
174
- # method.
175
- #
176
- # For example (delayed execution):
177
- # # === actions/break.rb ======= #
178
- # class Action
179
- # # :category: Action Methods
180
- # def break_action
181
- # lambda { Context::wait }
182
- # end
183
- # end
184
- # # === end actions/break.rb === #
185
- #
186
- # Context::actions.include? 'debug' # => true
187
- #
188
- # Executing this action would yield something like the following:
189
- # break [ OK ]
190
- # Press ENTER to continue
191
- #
192
- # While calling Context::wait directly would yield:
193
- # break Press EN
194
- # TER to continue
195
- # [ OK ]
196
- #
197
- class Action
198
- include Bauxite::ActionModule
199
- end
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
+ # Action common state and behavior.
25
+ module ActionModule
26
+ # Test context
27
+ attr_reader :ctx
28
+
29
+ # Parsed action command (i.e. action name)
30
+ attr_reader :cmd
31
+
32
+ # Raw action text.
33
+ attr_reader :text
34
+
35
+ # Constructs a new test action instance.
36
+ def initialize(ctx, cmd, args, text, file, line)
37
+ @ctx = ctx
38
+ @cmd = cmd
39
+ @args = args
40
+ @text = text
41
+
42
+ @cmd_real = (respond_to? cmd+'_action') ? (cmd+'_action') : cmd
43
+
44
+ unless respond_to? @cmd_real and Context::actions.include? @cmd
45
+ raise "#{file} (line #{line+1}): Unknown command #{cmd}."
46
+ end
47
+ end
48
+
49
+ # Returns the action arguments after applying variable expansion.
50
+ #
51
+ # See Context#expand.
52
+ #
53
+ # If +quote+ is +true+, the arguments are surrounded by quote
54
+ # characters (") and every quote inside an argument is doubled.
55
+ #
56
+ # For example:
57
+ # # assuming
58
+ # # action.new(ctx, cmd,
59
+ # # [ 'dude', 'say "hi"', '${myvar} ], # args
60
+ # # text, file, line)
61
+ # # ctx.variables = { 'myvar' => 'world' }
62
+ #
63
+ # action.args
64
+ # # => [ 'dude', 'say "hi"', 'world' ]
65
+ #
66
+ # action.args(true)
67
+ # # => [ '"dude"', '"say ""hi"""', '"world"' ]
68
+ #
69
+ def args(quote = false)
70
+ ret = @args.map { |a| @ctx.expand(a) }
71
+ ret = ret.map { |a| '"'+a.gsub('"', '""')+'"' } if quote
72
+ ret
73
+ end
74
+
75
+ # Executes the action evaluating the arguments in the current context.
76
+ #
77
+ # Note that #execute calls #args to expand variables. This means that
78
+ # two calls to #execute on the same instance might yield different results.
79
+ #
80
+ # For example:
81
+ # action = ctx.parse_action('echo ${message}')
82
+ #
83
+ # ctx.variables = { 'message' => 'hi!' }
84
+ # action.execute()
85
+ # # => outputs 'hi!'
86
+ #
87
+ # ctx.variables['message'] = 'hello world'
88
+ # action.execute()
89
+ # # => outputs 'hello world!' yet the instance of action is same!
90
+ #
91
+ def execute()
92
+ send(@cmd_real, *args)
93
+ end
94
+ private
95
+ def _pattern(s)
96
+ if s =~ /^\/.*\/[imxo]*$/
97
+ eval(s)
98
+ else
99
+ /#{s}/
100
+ end
101
+ end
102
+ end
103
+
104
+ # Test action class.
105
+ #
106
+ # Test actions are basic test operations that can be combined to create a test
107
+ # case.
108
+ #
109
+ # Test actions are implemented as public methods of the Action class.
110
+ #
111
+ # Each test action is defined in a separate file in the 'actions/' directory.
112
+ # The name of the file must match the name of the action. Ideally, these files
113
+ # should avoid adding public methods other than the action method itself.
114
+ # Also, no +attr_accessors+ should be added.
115
+ #
116
+ # Action methods can use the +ctx+ attribute to refer to the current test
117
+ # Context.
118
+ #
119
+ # For example (new action template):
120
+ # # === actions/print_source.rb ======= #
121
+ # class Action
122
+ # # :category: Action Methods
123
+ # def print_source
124
+ # # action code goes here, for example:
125
+ # puts @ctx.driver.page_source.
126
+ # end
127
+ # end
128
+ # # === end actions/print_source.rb === #
129
+ #
130
+ # Context::actions.include? 'print_source' # => true
131
+ #
132
+ # To avoid name clashing with Ruby reserved words, the '_action' suffix can be
133
+ # included in the action method name (this suffix will not be considered part
134
+ # of the action name).
135
+ #
136
+ # For example (_action suffix):
137
+ # # === actions/break.rb ======= #
138
+ # class Action
139
+ # # :category: Action Methods
140
+ # def break_action
141
+ # # do something
142
+ # end
143
+ # end
144
+ # # === end actions/break.rb === #
145
+ #
146
+ # Context::actions.include? 'break' # => true
147
+ #
148
+ # If the action requires additional attributes or private methods, the name
149
+ # of the action should be used as a prefix to avoid name clashing with other
150
+ # actions.
151
+ #
152
+ # For example (private attributes and methods):
153
+ # # === actions/debug.rb ======= #
154
+ # class Action
155
+ # # :category: Action Methods
156
+ # def debug
157
+ # _debug_do_stuff
158
+ # end
159
+ # private
160
+ # @@debug_line = 0
161
+ # def _debug_do_stuff
162
+ # @@debug_line += 1
163
+ # end
164
+ # end
165
+ # # === end actions/debug.rb === #
166
+ #
167
+ # Context::actions.include? 'debug' # => true
168
+ #
169
+ # Action methods support delayed execution of the test action. Delayed
170
+ # execution is useful in cases where the action output would break the
171
+ # standard logging interface.
172
+ #
173
+ # Delayed execution is implemented by returning a lambda from the action
174
+ # method.
175
+ #
176
+ # For example (delayed execution):
177
+ # # === actions/break.rb ======= #
178
+ # class Action
179
+ # # :category: Action Methods
180
+ # def break_action
181
+ # lambda { Context::wait }
182
+ # end
183
+ # end
184
+ # # === end actions/break.rb === #
185
+ #
186
+ # Context::actions.include? 'debug' # => true
187
+ #
188
+ # Executing this action would yield something like the following:
189
+ # break [ OK ]
190
+ # Press ENTER to continue
191
+ #
192
+ # While calling Context::wait directly would yield:
193
+ # break Press EN
194
+ # TER to continue
195
+ # [ OK ]
196
+ #
197
+ class Action
198
+ include Bauxite::ActionModule
199
+ end
200
200
  end
@@ -1,791 +1,791 @@
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
- require 'pathname'
27
-
28
- # Load dependencies and extensions without leaking dir into the global scope
29
- lambda do
30
- dir = File.expand_path(File.dirname(__FILE__))
31
- Dir[File.join(dir, '*.rb')].each { |file| require file }
32
- Dir[File.join(dir, '..', 'actions' , '*.rb')].each { |file| require file }
33
- Dir[File.join(dir, '..', 'selectors', '*.rb')].each { |file| require file }
34
- Dir[File.join(dir, '..', 'loggers' , '*.rb')].each { |file| require file }
35
- Dir[File.join(dir, '..', 'parsers' , '*.rb')].each { |file| require file }
36
- end.call
37
-
38
- # Bauxite Namespace
39
- module Bauxite
40
- # The Main test context. This class includes state and helper functions
41
- # used by clients execute tests and by actions and selectors to interact
42
- # with the test engine (i.e. Selenium WebDriver).
43
- #
44
- # === Context variables
45
- # Context variables are a key/value pairs scoped to the a test context.
46
- #
47
- # Variables can be set using different actions. For example:
48
- # - Action#set sets a variable to a literal string.
49
- # - Action#store sets a variable to the value of an element in the page.
50
- # - Action#exec sets a variable to the output of an external command
51
- # (i.e. stdout).
52
- # - Action#js sets a variable to the result of Javascript command.
53
- # - Action#replace sets a variable to the result of doing a
54
- # find-and-replace operation on a literal.
55
- #
56
- # Variables can be expanded in every Action argument (e.g. selectors,
57
- # texts, expressions, etc.). To obtain the value of a variable through
58
- # variable expansion the following syntax must be used:
59
- # ${var_name}
60
- #
61
- # For example:
62
- # set field "greeting"
63
- # set name "John"
64
- # write "${field}_textbox" "Hi, my name is ${name}!"
65
- # click "${field}_button"
66
- #
67
- # === Variable scope
68
- # When the main test starts (via the #start method), the test is bound
69
- # to the global scope. The variables defined in the global scope are
70
- # available to every test Action.
71
- #
72
- # The global scope can have nested variable scopes created by special
73
- # actions. The variables defined in a scope +A+ are only available to that
74
- # scope and scopes nested within +A+.
75
- #
76
- # Every time an Action loads a file, a new nested scope is created.
77
- # File-loading actions include:
78
- # - Action#load
79
- # - Action#tryload
80
- # - Action#ruby
81
- # - Action#test
82
- #
83
- # A nested scope can bubble variables to its parent scope with the special
84
- # action:
85
- # - Action#return_action
86
- #
87
- # === Built-in variable
88
- # Bauxite has a series of built-in variables that provide information of
89
- # the current test context and allow dynamic constomizations of the test
90
- # behavior.
91
- #
92
- # The built-in variables are:
93
- # [<tt>__FILE__</tt>] The file where the current action is defined.
94
- # [<tt>__DIR__</tt>] The directory where <tt>__FILE__</tt> is.
95
- # [<tt>__SELECTOR__</tt>] The default selector used when the selector
96
- # specified does not contain an <tt>=</tt>
97
- # character.
98
- # [<tt>__DEBUG__</tt>] Set to true if the current action is being executed
99
- # by the debug console.
100
- # [<tt>__RETURN__</tt>] Used internally by Action#return_action to indicate
101
- # which variables should be returned to the parent
102
- # scope.
103
- #
104
- # In general, variables surrounded by double underscores and variables
105
- # whose names are only numbers are reserved for Bauxite and should not be
106
- # used as part of a functional test. The obvious exception is when trying
107
- # to change the test behavior by changing the built-in variables.
108
- #
109
- class Context
110
- # Logger instance.
111
- attr_reader :logger
112
-
113
- # Test options.
114
- attr_reader :options
115
-
116
- # Context variables.
117
- attr_accessor :variables
118
-
119
- # Test containers.
120
- attr_accessor :tests
121
-
122
- # Constructs a new test context instance.
123
- #
124
- # +options+ is a hash with the following values:
125
- # [:driver] selenium driver symbol (defaults to +:firefox+)
126
- # [:timeout] selector timeout in seconds (defaults to +10s+)
127
- # [:logger] logger implementation name without the 'Logger' suffix
128
- # (defaults to 'null' for Loggers::NullLogger).
129
- # [:verbose] if +true+, show verbose error information (e.g.
130
- # backtraces) if an error occurs (defaults to +false+)
131
- # [:debug] if +true+, break into the #debug console if an error occurs
132
- # (defaults to +false+)
133
- # [:wait] if +true+, call ::wait before stopping the test engine with
134
- # #stop (defaults to +false+)
135
- # [:extensions] an array of directories that contain extensions to be
136
- # loaded
137
- #
138
- def initialize(options)
139
- @options = options
140
- @driver_name = (options[:driver] || :firefox).to_sym
141
- @variables = {
142
- '__TIMEOUT__' => (options[:timeout] || 10).to_i,
143
- '__DEBUG__' => false,
144
- '__SELECTOR__' => options[:selector] || 'sid',
145
- '__OUTPUT__' => options[:output],
146
- '__DIR__' => File.absolute_path(Dir.pwd)
147
- }
148
- @aliases = {}
149
- @tests = []
150
-
151
- client = Selenium::WebDriver::Remote::Http::Default.new
152
- client.timeout = (@options[:open_timeout] || 60).to_i
153
- @options[:driver_opt] = {} unless @options[:driver_opt]
154
- @options[:driver_opt][:http_client] = client
155
-
156
- _load_extensions(options[:extensions] || [])
157
-
158
- @logger = Context::load_logger(options[:logger], options[:logger_opt])
159
-
160
- @parser = Parser.new(self)
161
- end
162
-
163
- # Starts the test engine and executes the actions specified. If no action
164
- # was specified, returns without stopping the test engine (see #stop).
165
- #
166
- # For example:
167
- # lines = [
168
- # 'open "http://www.ruby-lang.org"',
169
- # 'write "name=q" "ljust"',
170
- # 'click "name=sa"',
171
- # 'break'
172
- # ]
173
- # ctx.start(lines)
174
- # # => navigates to www.ruby-lang.org, types ljust in the search box
175
- # # and clicks the "Search" button.
176
- #
177
- def start(actions = [])
178
- return unless actions.size > 0
179
- begin
180
- actions.each do |action|
181
- begin
182
- break if exec_action(action) == :break
183
- rescue StandardError => e
184
- print_error(e)
185
- raise unless @options[:debug]
186
- debug
187
- end
188
- end
189
- ensure
190
- stop
191
- end
192
- end
193
-
194
- # Stops the test engine and starts a new engine with the same provider.
195
- #
196
- # For example:
197
- # ctx.reset_driver
198
- # => closes the browser and opens a new one
199
- #
200
- def reset_driver
201
- @driver.quit if @driver
202
- @driver = nil
203
- end
204
-
205
- # Stops the test engine.
206
- #
207
- # Calling this method at the end of the test is mandatory if #start was
208
- # called without +actions+.
209
- #
210
- # Note that the recommeneded way of executing tests is by passing a list
211
- # of +actions+ to #start instead of using the #start / #stop pattern.
212
- #
213
- # For example:
214
- # ctx.start(:firefox) # => opens firefox
215
- #
216
- # # test stuff goes here
217
- #
218
- # ctx.stop # => closes firefox
219
- #
220
- def stop
221
- Context::wait if @options[:wait]
222
- begin
223
- @logger.finalize(self)
224
- rescue StandardError => e
225
- print_error(e)
226
- raise
227
- ensure
228
- @driver.quit if @driver
229
- end
230
- end
231
-
232
- # Finds an element by +selector+.
233
- #
234
- # The element found is yielded to the given +block+ (if any) and returned.
235
- #
236
- # Note that the recommeneded way to call this method is by passing a
237
- # +block+. This is because the method ensures that the element context is
238
- # maintained for the duration of the +block+ but it makes no guarantees
239
- # after the +block+ completes (the same applies if no +block+ was given).
240
- #
241
- # For example:
242
- # ctx.find('css=.my_button') { |element| element.click }
243
- # ctx.find('css=.my_button').click
244
- #
245
- # For example (where using a +block+ is mandatory):
246
- # ctx.find('frame=|myframe|css=.my_button') { |element| element.click }
247
- # # => .my_button clicked
248
- #
249
- # ctx.find('frame=|myframe|css=.my_button').click
250
- # # => error, cannot click .my_button (no longer in myframe scope)
251
- #
252
- def find(selector, &block) # yields: element
253
- with_timeout Selenium::WebDriver::Error::NoSuchElementError do
254
- Selector.new(self, @variables['__SELECTOR__']).find(selector, &block)
255
- end
256
- end
257
-
258
- # Test engine driver instance (Selenium WebDriver).
259
- def driver
260
- _load_driver unless @driver
261
- @driver
262
- end
263
-
264
- # Breaks into the debug console.
265
- #
266
- # For example:
267
- # ctx.debug
268
- # # => this breaks into the debug console
269
- def debug
270
- exec_parsed_action('debug', [], false)
271
- end
272
-
273
- # Returns the value of the specified +element+.
274
- #
275
- # This method takes into account the type of element and selectively
276
- # returns the inner text or the value of the +value+ attribute.
277
- #
278
- # For example:
279
- # # assuming <input type='text' value='Hello' />
280
- # # <span id='label'>World!</span>
281
- #
282
- # ctx.get_value(ctx.find('css=input[type=text]'))
283
- # # => returns 'Hello'
284
- #
285
- # ctx.get_value(ctx.find('label'))
286
- # # => returns 'World!'
287
- #
288
- def get_value(element)
289
- if ['input','select','textarea'].include? element.tag_name.downcase
290
- element.attribute('value')
291
- else
292
- element.text
293
- end
294
- end
295
-
296
- # ======================================================================= #
297
- # :section: Advanced Helpers
298
- # ======================================================================= #
299
-
300
- # Executes the specified action string handling errors, logging and debug
301
- # history.
302
- #
303
- # If +log+ is +true+, log the action execution (default behavior).
304
- #
305
- # For example:
306
- # ctx.exec_action 'open "http://www.ruby-lang.org"'
307
- # # => navigates to www.ruby-lang.org
308
- #
309
- def exec_action(text)
310
- data = Context::parse_action_default(text, '<unknown>', 0)
311
- exec_parsed_action(data[:action], data[:args], true, text)
312
- end
313
-
314
- # Executes the specified +file+.
315
- #
316
- # For example:
317
- # ctx.exec_file('file')
318
- # # => executes every action defined in 'file'
319
- #
320
- def exec_file(file)
321
- current_dir = @variables['__DIR__' ]
322
- current_file = @variables['__FILE__']
323
- current_line = @variables['__LINE__']
324
-
325
- @parser.parse(file) do |action, args, text, file, line|
326
- @variables['__DIR__'] = File.absolute_path(File.dirname(file))
327
- @variables['__FILE__'] = file
328
- @variables['__LINE__'] = line
329
- break if exec_parsed_action(action, args, true, text) == :break
330
- end
331
-
332
- @variables['__DIR__' ] = current_dir
333
- @variables['__FILE__'] = current_file
334
- @variables['__LINE__'] = current_line
335
- end
336
-
337
- # Executes the specified action handling errors, logging and debug
338
- # history.
339
- #
340
- # If +log+ is +true+, log the action execution (default behavior).
341
- #
342
- # This method if part of the action execution chain and is intended
343
- # for advanced use (e.g. in complex actions). To execute an Action
344
- # directly, the #exec_action method is preferred.
345
- #
346
- # For example:
347
- # ctx.exec_action 'open "http://www.ruby-lang.org"'
348
- # # => navigates to www.ruby-lang.org
349
- #
350
- def exec_parsed_action(action, args, log = true, text = nil)
351
- action = get_action(action, args, text)
352
- ret = nil
353
- if log
354
- @logger.log_cmd(action) do
355
- Readline::HISTORY << action.text
356
- ret = exec_action_object(action)
357
- end
358
- else
359
- ret = exec_action_object(action)
360
- end
361
-
362
- if ret.respond_to? :call # delayed actions (after log_cmd)
363
- ret.call
364
- else
365
- ret
366
- end
367
- rescue Selenium::WebDriver::Error::UnhandledAlertError
368
- raise Bauxite::Errors::AssertionError, "Unexpected modal present"
369
- end
370
-
371
- # Executes the given block retrying for at most <tt>${__TIMEOUT__}</tt>
372
- # seconds. Note that this method does not take into account the time it
373
- # takes to execute the block itself.
374
- #
375
- # For example
376
- # ctx.with_timeout StandardError do
377
- # ctx.find ('element_with_delay') do |e|
378
- # # do something with e
379
- # end
380
- # end
381
- #
382
- def with_timeout(*error_types)
383
- stime = Time.new
384
- timeout ||= stime + @variables['__TIMEOUT__']
385
- yield
386
- rescue *error_types => e
387
- t = Time.new
388
- rem = timeout - t
389
- raise if rem < 0
390
-
391
- @logger.progress(rem.round)
392
-
393
- sleep(1.0/10.0) if (t - stime).to_i < 1
394
- retry
395
- end
396
-
397
- # Executes the given block using the specified driver +timeout+.
398
- #
399
- # Note that the driver +timeout+ is the time (in seconds) Selenium
400
- # will wait for a specific element to appear in the page (using any
401
- # of the available Selector strategies).
402
- #
403
- # For example
404
- # ctx.with_driver_timeout 0.5 do
405
- # ctx.find ('find_me_quickly') do |e|
406
- # # do something with e
407
- # end
408
- # end
409
- #
410
- def with_driver_timeout(timeout)
411
- current = @driver_timeout
412
- driver.manage.timeouts.implicit_wait = timeout
413
- yield
414
- ensure
415
- @driver_timeout = current
416
- driver.manage.timeouts.implicit_wait = current
417
- end
418
-
419
- # Prompts the user to press ENTER before resuming execution.
420
- #
421
- # For example:
422
- # Context::wait
423
- # # => echoes "Press ENTER to continue" and waits for user input
424
- #
425
- def self.wait
426
- Readline.readline("Press ENTER to continue\n")
427
- end
428
-
429
- # Constructs a Logger instance using +name+ as a hint for the logger
430
- # type.
431
- #
432
- def self.load_logger(loggers, options)
433
- if loggers.is_a? Array
434
- return Loggers::CompositeLogger.new(options, loggers)
435
- end
436
-
437
- name = loggers
438
-
439
- log_name = (name || 'null').downcase
440
-
441
- class_name = "#{log_name.capitalize}Logger"
442
-
443
- unless Loggers.const_defined? class_name.to_sym
444
- raise NameError,
445
- "Invalid logger '#{log_name}'"
446
- end
447
-
448
- Loggers.const_get(class_name).new(options)
449
- end
450
-
451
- # Adds an alias named +name+ to the specified +action+ with the
452
- # arguments specified in +args+.
453
- #
454
- def add_alias(name, action, args)
455
- @aliases[name] = { :action => action, :args => args }
456
- end
457
-
458
- # Default action parsing strategy.
459
- #
460
- def self.parse_action_default(text, file = '<unknown>', line = 0)
461
- data = text.split(' ', 2)
462
- begin
463
- args_text = data[1] ? data[1].strip : ''
464
- args = []
465
-
466
- unless args_text == ''
467
- # col_sep must be a regex because String.split has a
468
- # special case for a single space char (' ') that produced
469
- # unexpected results (i.e. if line is '"a b"' the
470
- # resulting array contains ["a b"]).
471
- #
472
- # ...but...
473
- #
474
- # CSV expects col_sep to be a string so we need to work
475
- # some dark magic here. Basically we proxy the StringIO
476
- # received by CSV to returns strings for which the split
477
- # method does not fold the whitespaces.
478
- #
479
- args = CSV.new(StringIOProxy.new(args_text), { :col_sep => ' ' })
480
- .shift
481
- .select { |a| a != nil } || []
482
- end
483
-
484
- {
485
- :action => data[0].strip.downcase,
486
- :args => args
487
- }
488
- rescue StandardError => e
489
- raise "#{file} (line #{line+1}): #{e.message}"
490
- end
491
- end
492
-
493
- # Returns an executable Action object constructed from the specified
494
- # arguments resolving action aliases.
495
- #
496
- # This method if part of the action execution chain and is intended
497
- # for advanced use (e.g. in complex actions). To execute an Action
498
- # directly, the #exec_action method is preferred.
499
- #
500
- def get_action(action, args, text = nil)
501
- while (alias_action = @aliases[action])
502
- action = alias_action[:action]
503
- args = alias_action[:args].map do |a|
504
- a.gsub(/\$\{(\d+)(\*q?)?\}/) do |match|
505
- # expand ${1} to args[0], ${2} to args[1], etc.
506
- # expand ${4*} to "#{args[4]} #{args[5]} ..."
507
- # expand ${4*q} to "\"#{args[4]}\" \"#{args[5]}\" ..."
508
- idx = $1.to_i-1
509
- if $2 == nil
510
- args[idx] || ''
511
- else
512
- range = args[idx..-1]
513
- range = range.map { |arg| '"'+arg.gsub('"', '""')+'"' } if $2 == '*q'
514
- range.join(' ')
515
- end
516
- end
517
- end
518
- end
519
-
520
- text = ([action] + args.map { |a| '"'+a.gsub('"', '""')+'"' }).join(' ') unless text
521
- file = @variables['__FILE__']
522
- line = @variables['__LINE__']
523
-
524
- Action.new(self, action, args, text, file, line)
525
- end
526
-
527
- # Executes the specified action object injecting built-in variables.
528
- # Note that the result returned by this method might be a lambda.
529
- # If this is the case, a further +call+ method must be issued.
530
- #
531
- # This method if part of the action execution chain and is intended
532
- # for advanced use (e.g. in complex actions). To execute an Action
533
- # directly, the #exec_action method is preferred.
534
- #
535
- # For example:
536
- # action = ctx.get_action("echo", ['Hi!'], 'echo "Hi!"')
537
- # ret = ctx.exec_action_object(action)
538
- # ret.call if ret.respond_to? :call
539
- #
540
- def exec_action_object(action)
541
- action.execute
542
- end
543
-
544
- # Prints the specified +error+ using the Logger configured and
545
- # handling the verbose option.
546
- #
547
- # For example:
548
- # begin
549
- # # => some code here
550
- # rescue StandardError => e
551
- # @ctx.print_error e
552
- # # => additional error handling code here
553
- # end
554
- #
555
- def print_error(e, capture = true)
556
- if @logger
557
- @logger.log "#{e.message}\n", :error
558
- else
559
- puts e.message
560
- end
561
- if @options[:verbose]
562
- p e
563
- puts e.backtrace
564
- end
565
- if capture and @options[:capture]
566
- with_vars(e.variables) do
567
- exec_parsed_action('capture', [] , false)
568
- e.variables['__CAPTURE__'] = @variables['__CAPTURE__']
569
- end
570
- end
571
- end
572
-
573
- # Returns the output path for +path+ accounting for the
574
- # <tt>__OUTPUT__</tt> variable.
575
- #
576
- # For example:
577
- # # assuming --output /mnt/disk
578
- #
579
- # ctx.output_path '/tmp/myfile.txt'
580
- # # => returns '/tmp/myfile.txt'
581
- #
582
- # ctx.output_path 'myfile.txt'
583
- # # => returns '/mnt/disk/myfile.txt'
584
- #
585
- def output_path(path)
586
- unless Pathname.new(path).absolute?
587
- output = @variables['__OUTPUT__']
588
- if output
589
- Dir.mkdir output unless Dir.exists? output
590
- path = File.join(output, path)
591
- end
592
- end
593
- path
594
- end
595
-
596
- # ======================================================================= #
597
- # :section: Metadata
598
- # ======================================================================= #
599
-
600
- # Returns an array with the names of every action available.
601
- #
602
- # For example:
603
- # Context::actions
604
- # # => [ "assert", "break", ... ]
605
- #
606
- def self.actions
607
- _action_methods.map { |m| m.sub(/_action$/, '') }
608
- end
609
-
610
- # Returns an array with the names of the arguments of the specified action.
611
- #
612
- # For example:
613
- # Context::action_args 'assert'
614
- # # => [ "selector", "text" ]
615
- #
616
- def self.action_args(action)
617
- action += '_action' unless _action_methods.include? action
618
- Action.public_instance_method(action).parameters.map { |att, name| name.to_s }
619
- end
620
-
621
- # Returns an array with the names of every selector available.
622
- #
623
- # If +include_standard_selectors+ is +true+ (default behavior) both
624
- # standard and custom selector are returned, otherwise only custom
625
- # selectors are returned.
626
- #
627
- # For example:
628
- # Context::selectors
629
- # # => [ "class", "id", ... ]
630
- #
631
- def self.selectors(include_standard_selectors = true)
632
- ret = Selector.public_instance_methods(false).map { |a| a.to_s.sub(/_selector$/, '') }
633
- if include_standard_selectors
634
- ret += Selenium::WebDriver::SearchContext::FINDERS.map { |k,v| k.to_s }
635
- end
636
- ret
637
- end
638
-
639
- # Returns an array with the names of every logger available.
640
- #
641
- # For example:
642
- # Context::loggers
643
- # # => [ "null", "bash", ... ]
644
- #
645
- def self.loggers
646
- Loggers.constants.map { |l| l.to_s.downcase.sub(/logger$/, '') }
647
- end
648
-
649
- # Returns an array with the names of every parser available.
650
- #
651
- # For example:
652
- # Context::parsers
653
- # # => [ "default", "html", ... ]
654
- #
655
- def self.parsers
656
- (Parser.public_instance_methods(false) \
657
- - ParserModule.public_instance_methods(false))
658
- .map { |p| p.to_s }
659
- end
660
-
661
- # Returns the maximum size in characters of an action name.
662
- #
663
- # This method is useful to pretty print lists of actions
664
- #
665
- # For example:
666
- # # assuming actions = [ "echo", "assert", "tryload" ]
667
- # Context::max_action_name_size
668
- # # => 7
669
- def self.max_action_name_size
670
- actions.inject(0) { |s,a| a.size > s ? a.size : s }
671
- end
672
-
673
- # ======================================================================= #
674
- # :section: Variable manipulation methods
675
- # ======================================================================= #
676
-
677
- # Recursively replaces occurencies of variable expansions in +s+ with the
678
- # corresponding variable value.
679
- #
680
- # The variable expansion expression format is:
681
- # '${variable_name}'
682
- #
683
- # For example:
684
- # ctx.variables = { 'a' => '1', 'b' => '2', 'c' => 'a' }
685
- # ctx.expand '${a}' # => '1'
686
- # ctx.expand '${b}' # => '2'
687
- # ctx.expand '${c}' # => 'a'
688
- # ctx.expand '${${c}}' # => '1'
689
- #
690
- def expand(s)
691
- result = @variables.inject(s) do |s,kv|
692
- s = s.gsub(/\$\{#{kv[0]}\}/, kv[1].to_s)
693
- end
694
- result = expand(result) if result != s
695
- result
696
- end
697
-
698
- # Temporarily alter the value of context variables.
699
- #
700
- # This method alters the value of the variables specified in the +vars+
701
- # hash for the duration of the given +block+. When the +block+ completes,
702
- # the original value of the context variables is restored.
703
- #
704
- # For example:
705
- # ctx.variables = { 'a' => '1', 'b' => '2', c => 'a' }
706
- # ctx.with_vars({ 'a' => '10', d => '20' }) do
707
- # p ctx.variables
708
- # # => {"a"=>"10", "b"=>"2", "c"=>"a", "d"=>"20"}
709
- # end
710
- # p ctx.variables
711
- # # => {"a"=>"1", "b"=>"2", "c"=>"a"}
712
- #
713
- def with_vars(vars)
714
- current = @variables
715
- @variables = @variables.merge(vars)
716
- ret_vars = nil
717
-
718
- ret = yield
719
-
720
- returned = @variables['__RETURN__']
721
- if returned == ['*']
722
- ret_vars = @variables.clone
723
- ret_vars.delete '__RETURN__'
724
- elsif returned != nil
725
- ret_vars = @variables.select { |k,v| returned.include? k }
726
- end
727
- rescue StandardError => e
728
- e.instance_variable_set "@variables", @variables
729
- def e.variables
730
- @variables
731
- end
732
- raise
733
- ensure
734
- @variables = current
735
- @variables.merge!(ret_vars) if ret_vars
736
- ret
737
- end
738
-
739
- private
740
- def self._action_methods
741
- (Action.public_instance_methods(false) \
742
- - ActionModule.public_instance_methods(false))
743
- .map { |a| a.to_s }
744
- end
745
-
746
- def _load_driver
747
- @driver = Selenium::WebDriver.for(@driver_name, @options[:driver_opt])
748
- @driver.manage.timeouts.implicit_wait = 1
749
- @driver_timeout = 1
750
- end
751
-
752
- def _load_extensions(dirs)
753
- dirs.each do |d|
754
- d = File.join(Dir.pwd, d) unless Dir.exists? d
755
- d = File.absolute_path(d)
756
- Dir[File.join(d, '**', '*.rb')].each { |file| require file }
757
- end
758
- end
759
-
760
- # ======================================================================= #
761
- # Hacks required to overcome the String#split(' ') behavior of folding the
762
- # space characters, coupled with CSV not supporting a regex as :col_sep.
763
-
764
- # Same as a common String except that split(' ') behaves as split(/\s/).
765
- class StringProxy # :nodoc:
766
- def initialize(s)
767
- @s = s
768
- end
769
-
770
- def method_missing(method, *args, &block)
771
- args[0] = /\s/ if method == :split and args.size > 0 and args[0] == ' '
772
- ret = @s.send(method, *args, &block)
773
- end
774
- end
775
-
776
- # Same as a common StringIO except that get(sep) returns a StringProxy
777
- # instead of a regular string.
778
- class StringIOProxy # :nodoc:
779
- def initialize(s)
780
- @s = StringIO.new(s)
781
- end
782
-
783
- def method_missing(method, *args, &block)
784
- ret = @s.send(method, *args, &block)
785
- return ret unless method == :gets and args.size == 1
786
- StringProxy.new(ret)
787
- end
788
- end
789
- # ======================================================================= #
790
- end
791
- end
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
+ require 'pathname'
27
+
28
+ # Load dependencies and extensions without leaking dir into the global scope
29
+ lambda do
30
+ dir = File.expand_path(File.dirname(__FILE__))
31
+ Dir[File.join(dir, '*.rb')].each { |file| require file }
32
+ Dir[File.join(dir, '..', 'actions' , '*.rb')].each { |file| require file }
33
+ Dir[File.join(dir, '..', 'selectors', '*.rb')].each { |file| require file }
34
+ Dir[File.join(dir, '..', 'loggers' , '*.rb')].each { |file| require file }
35
+ Dir[File.join(dir, '..', 'parsers' , '*.rb')].each { |file| require file }
36
+ end.call
37
+
38
+ # Bauxite Namespace
39
+ module Bauxite
40
+ # The Main test context. This class includes state and helper functions
41
+ # used by clients execute tests and by actions and selectors to interact
42
+ # with the test engine (i.e. Selenium WebDriver).
43
+ #
44
+ # === Context variables
45
+ # Context variables are a key/value pairs scoped to the a test context.
46
+ #
47
+ # Variables can be set using different actions. For example:
48
+ # - Action#set sets a variable to a literal string.
49
+ # - Action#store sets a variable to the value of an element in the page.
50
+ # - Action#exec sets a variable to the output of an external command
51
+ # (i.e. stdout).
52
+ # - Action#js sets a variable to the result of Javascript command.
53
+ # - Action#replace sets a variable to the result of doing a
54
+ # find-and-replace operation on a literal.
55
+ #
56
+ # Variables can be expanded in every Action argument (e.g. selectors,
57
+ # texts, expressions, etc.). To obtain the value of a variable through
58
+ # variable expansion the following syntax must be used:
59
+ # ${var_name}
60
+ #
61
+ # For example:
62
+ # set field "greeting"
63
+ # set name "John"
64
+ # write "${field}_textbox" "Hi, my name is ${name}!"
65
+ # click "${field}_button"
66
+ #
67
+ # === Variable scope
68
+ # When the main test starts (via the #start method), the test is bound
69
+ # to the global scope. The variables defined in the global scope are
70
+ # available to every test Action.
71
+ #
72
+ # The global scope can have nested variable scopes created by special
73
+ # actions. The variables defined in a scope +A+ are only available to that
74
+ # scope and scopes nested within +A+.
75
+ #
76
+ # Every time an Action loads a file, a new nested scope is created.
77
+ # File-loading actions include:
78
+ # - Action#load
79
+ # - Action#tryload
80
+ # - Action#ruby
81
+ # - Action#test
82
+ #
83
+ # A nested scope can bubble variables to its parent scope with the special
84
+ # action:
85
+ # - Action#return_action
86
+ #
87
+ # === Built-in variable
88
+ # Bauxite has a series of built-in variables that provide information of
89
+ # the current test context and allow dynamic constomizations of the test
90
+ # behavior.
91
+ #
92
+ # The built-in variables are:
93
+ # [<tt>__FILE__</tt>] The file where the current action is defined.
94
+ # [<tt>__DIR__</tt>] The directory where <tt>__FILE__</tt> is.
95
+ # [<tt>__SELECTOR__</tt>] The default selector used when the selector
96
+ # specified does not contain an <tt>=</tt>
97
+ # character.
98
+ # [<tt>__DEBUG__</tt>] Set to true if the current action is being executed
99
+ # by the debug console.
100
+ # [<tt>__RETURN__</tt>] Used internally by Action#return_action to indicate
101
+ # which variables should be returned to the parent
102
+ # scope.
103
+ #
104
+ # In general, variables surrounded by double underscores and variables
105
+ # whose names are only numbers are reserved for Bauxite and should not be
106
+ # used as part of a functional test. The obvious exception is when trying
107
+ # to change the test behavior by changing the built-in variables.
108
+ #
109
+ class Context
110
+ # Logger instance.
111
+ attr_reader :logger
112
+
113
+ # Test options.
114
+ attr_reader :options
115
+
116
+ # Context variables.
117
+ attr_accessor :variables
118
+
119
+ # Test containers.
120
+ attr_accessor :tests
121
+
122
+ # Constructs a new test context instance.
123
+ #
124
+ # +options+ is a hash with the following values:
125
+ # [:driver] selenium driver symbol (defaults to +:firefox+)
126
+ # [:timeout] selector timeout in seconds (defaults to +10s+)
127
+ # [:logger] logger implementation name without the 'Logger' suffix
128
+ # (defaults to 'null' for Loggers::NullLogger).
129
+ # [:verbose] if +true+, show verbose error information (e.g.
130
+ # backtraces) if an error occurs (defaults to +false+)
131
+ # [:debug] if +true+, break into the #debug console if an error occurs
132
+ # (defaults to +false+)
133
+ # [:wait] if +true+, call ::wait before stopping the test engine with
134
+ # #stop (defaults to +false+)
135
+ # [:extensions] an array of directories that contain extensions to be
136
+ # loaded
137
+ #
138
+ def initialize(options)
139
+ @options = options
140
+ @driver_name = (options[:driver] || :firefox).to_sym
141
+ @variables = {
142
+ '__TIMEOUT__' => (options[:timeout] || 10).to_i,
143
+ '__DEBUG__' => false,
144
+ '__SELECTOR__' => options[:selector] || 'sid',
145
+ '__OUTPUT__' => options[:output],
146
+ '__DIR__' => File.absolute_path(Dir.pwd)
147
+ }
148
+ @aliases = {}
149
+ @tests = []
150
+
151
+ client = Selenium::WebDriver::Remote::Http::Default.new
152
+ client.timeout = (@options[:open_timeout] || 60).to_i
153
+ @options[:driver_opt] = {} unless @options[:driver_opt]
154
+ @options[:driver_opt][:http_client] = client
155
+
156
+ _load_extensions(options[:extensions] || [])
157
+
158
+ @logger = Context::load_logger(options[:logger], options[:logger_opt])
159
+
160
+ @parser = Parser.new(self)
161
+ end
162
+
163
+ # Starts the test engine and executes the actions specified. If no action
164
+ # was specified, returns without stopping the test engine (see #stop).
165
+ #
166
+ # For example:
167
+ # lines = [
168
+ # 'open "http://www.ruby-lang.org"',
169
+ # 'write "name=q" "ljust"',
170
+ # 'click "name=sa"',
171
+ # 'break'
172
+ # ]
173
+ # ctx.start(lines)
174
+ # # => navigates to www.ruby-lang.org, types ljust in the search box
175
+ # # and clicks the "Search" button.
176
+ #
177
+ def start(actions = [])
178
+ return unless actions.size > 0
179
+ begin
180
+ actions.each do |action|
181
+ begin
182
+ break if exec_action(action) == :break
183
+ rescue StandardError => e
184
+ print_error(e)
185
+ raise unless @options[:debug]
186
+ debug
187
+ end
188
+ end
189
+ ensure
190
+ stop
191
+ end
192
+ end
193
+
194
+ # Stops the test engine and starts a new engine with the same provider.
195
+ #
196
+ # For example:
197
+ # ctx.reset_driver
198
+ # => closes the browser and opens a new one
199
+ #
200
+ def reset_driver
201
+ @driver.quit if @driver
202
+ @driver = nil
203
+ end
204
+
205
+ # Stops the test engine.
206
+ #
207
+ # Calling this method at the end of the test is mandatory if #start was
208
+ # called without +actions+.
209
+ #
210
+ # Note that the recommeneded way of executing tests is by passing a list
211
+ # of +actions+ to #start instead of using the #start / #stop pattern.
212
+ #
213
+ # For example:
214
+ # ctx.start(:firefox) # => opens firefox
215
+ #
216
+ # # test stuff goes here
217
+ #
218
+ # ctx.stop # => closes firefox
219
+ #
220
+ def stop
221
+ Context::wait if @options[:wait]
222
+ begin
223
+ @logger.finalize(self)
224
+ rescue StandardError => e
225
+ print_error(e)
226
+ raise
227
+ ensure
228
+ @driver.quit if @driver
229
+ end
230
+ end
231
+
232
+ # Finds an element by +selector+.
233
+ #
234
+ # The element found is yielded to the given +block+ (if any) and returned.
235
+ #
236
+ # Note that the recommeneded way to call this method is by passing a
237
+ # +block+. This is because the method ensures that the element context is
238
+ # maintained for the duration of the +block+ but it makes no guarantees
239
+ # after the +block+ completes (the same applies if no +block+ was given).
240
+ #
241
+ # For example:
242
+ # ctx.find('css=.my_button') { |element| element.click }
243
+ # ctx.find('css=.my_button').click
244
+ #
245
+ # For example (where using a +block+ is mandatory):
246
+ # ctx.find('frame=|myframe|css=.my_button') { |element| element.click }
247
+ # # => .my_button clicked
248
+ #
249
+ # ctx.find('frame=|myframe|css=.my_button').click
250
+ # # => error, cannot click .my_button (no longer in myframe scope)
251
+ #
252
+ def find(selector, &block) # yields: element
253
+ with_timeout Selenium::WebDriver::Error::NoSuchElementError do
254
+ Selector.new(self, @variables['__SELECTOR__']).find(selector, &block)
255
+ end
256
+ end
257
+
258
+ # Test engine driver instance (Selenium WebDriver).
259
+ def driver
260
+ _load_driver unless @driver
261
+ @driver
262
+ end
263
+
264
+ # Breaks into the debug console.
265
+ #
266
+ # For example:
267
+ # ctx.debug
268
+ # # => this breaks into the debug console
269
+ def debug
270
+ exec_parsed_action('debug', [], false)
271
+ end
272
+
273
+ # Returns the value of the specified +element+.
274
+ #
275
+ # This method takes into account the type of element and selectively
276
+ # returns the inner text or the value of the +value+ attribute.
277
+ #
278
+ # For example:
279
+ # # assuming <input type='text' value='Hello' />
280
+ # # <span id='label'>World!</span>
281
+ #
282
+ # ctx.get_value(ctx.find('css=input[type=text]'))
283
+ # # => returns 'Hello'
284
+ #
285
+ # ctx.get_value(ctx.find('label'))
286
+ # # => returns 'World!'
287
+ #
288
+ def get_value(element)
289
+ if ['input','select','textarea'].include? element.tag_name.downcase
290
+ element.attribute('value')
291
+ else
292
+ element.text
293
+ end
294
+ end
295
+
296
+ # ======================================================================= #
297
+ # :section: Advanced Helpers
298
+ # ======================================================================= #
299
+
300
+ # Executes the specified action string handling errors, logging and debug
301
+ # history.
302
+ #
303
+ # If +log+ is +true+, log the action execution (default behavior).
304
+ #
305
+ # For example:
306
+ # ctx.exec_action 'open "http://www.ruby-lang.org"'
307
+ # # => navigates to www.ruby-lang.org
308
+ #
309
+ def exec_action(text)
310
+ data = Context::parse_action_default(text, '<unknown>', 0)
311
+ exec_parsed_action(data[:action], data[:args], true, text)
312
+ end
313
+
314
+ # Executes the specified +file+.
315
+ #
316
+ # For example:
317
+ # ctx.exec_file('file')
318
+ # # => executes every action defined in 'file'
319
+ #
320
+ def exec_file(file)
321
+ current_dir = @variables['__DIR__' ]
322
+ current_file = @variables['__FILE__']
323
+ current_line = @variables['__LINE__']
324
+
325
+ @parser.parse(file) do |action, args, text, file, line|
326
+ @variables['__DIR__'] = File.absolute_path(File.dirname(file))
327
+ @variables['__FILE__'] = file
328
+ @variables['__LINE__'] = line
329
+ break if exec_parsed_action(action, args, true, text) == :break
330
+ end
331
+
332
+ @variables['__DIR__' ] = current_dir
333
+ @variables['__FILE__'] = current_file
334
+ @variables['__LINE__'] = current_line
335
+ end
336
+
337
+ # Executes the specified action handling errors, logging and debug
338
+ # history.
339
+ #
340
+ # If +log+ is +true+, log the action execution (default behavior).
341
+ #
342
+ # This method if part of the action execution chain and is intended
343
+ # for advanced use (e.g. in complex actions). To execute an Action
344
+ # directly, the #exec_action method is preferred.
345
+ #
346
+ # For example:
347
+ # ctx.exec_action 'open "http://www.ruby-lang.org"'
348
+ # # => navigates to www.ruby-lang.org
349
+ #
350
+ def exec_parsed_action(action, args, log = true, text = nil)
351
+ action = get_action(action, args, text)
352
+ ret = nil
353
+ if log
354
+ @logger.log_cmd(action) do
355
+ Readline::HISTORY << action.text
356
+ ret = exec_action_object(action)
357
+ end
358
+ else
359
+ ret = exec_action_object(action)
360
+ end
361
+
362
+ if ret.respond_to? :call # delayed actions (after log_cmd)
363
+ ret.call
364
+ else
365
+ ret
366
+ end
367
+ rescue Selenium::WebDriver::Error::UnhandledAlertError
368
+ raise Bauxite::Errors::AssertionError, "Unexpected modal present"
369
+ end
370
+
371
+ # Executes the given block retrying for at most <tt>${__TIMEOUT__}</tt>
372
+ # seconds. Note that this method does not take into account the time it
373
+ # takes to execute the block itself.
374
+ #
375
+ # For example
376
+ # ctx.with_timeout StandardError do
377
+ # ctx.find ('element_with_delay') do |e|
378
+ # # do something with e
379
+ # end
380
+ # end
381
+ #
382
+ def with_timeout(*error_types)
383
+ stime = Time.new
384
+ timeout ||= stime + @variables['__TIMEOUT__'].to_i
385
+ yield
386
+ rescue *error_types => e
387
+ t = Time.new
388
+ rem = timeout - t
389
+ raise if rem < 0
390
+
391
+ @logger.progress(rem.round)
392
+
393
+ sleep(1.0/10.0) if (t - stime).to_i < 1
394
+ retry
395
+ end
396
+
397
+ # Executes the given block using the specified driver +timeout+.
398
+ #
399
+ # Note that the driver +timeout+ is the time (in seconds) Selenium
400
+ # will wait for a specific element to appear in the page (using any
401
+ # of the available Selector strategies).
402
+ #
403
+ # For example
404
+ # ctx.with_driver_timeout 0.5 do
405
+ # ctx.find ('find_me_quickly') do |e|
406
+ # # do something with e
407
+ # end
408
+ # end
409
+ #
410
+ def with_driver_timeout(timeout)
411
+ current = @driver_timeout
412
+ driver.manage.timeouts.implicit_wait = timeout
413
+ yield
414
+ ensure
415
+ @driver_timeout = current
416
+ driver.manage.timeouts.implicit_wait = current
417
+ end
418
+
419
+ # Prompts the user to press ENTER before resuming execution.
420
+ #
421
+ # For example:
422
+ # Context::wait
423
+ # # => echoes "Press ENTER to continue" and waits for user input
424
+ #
425
+ def self.wait
426
+ Readline.readline("Press ENTER to continue\n")
427
+ end
428
+
429
+ # Constructs a Logger instance using +name+ as a hint for the logger
430
+ # type.
431
+ #
432
+ def self.load_logger(loggers, options)
433
+ if loggers.is_a? Array
434
+ return Loggers::CompositeLogger.new(options, loggers)
435
+ end
436
+
437
+ name = loggers
438
+
439
+ log_name = (name || 'null').downcase
440
+
441
+ class_name = "#{log_name.capitalize}Logger"
442
+
443
+ unless Loggers.const_defined? class_name.to_sym
444
+ raise NameError,
445
+ "Invalid logger '#{log_name}'"
446
+ end
447
+
448
+ Loggers.const_get(class_name).new(options)
449
+ end
450
+
451
+ # Adds an alias named +name+ to the specified +action+ with the
452
+ # arguments specified in +args+.
453
+ #
454
+ def add_alias(name, action, args)
455
+ @aliases[name] = { :action => action, :args => args }
456
+ end
457
+
458
+ # Default action parsing strategy.
459
+ #
460
+ def self.parse_action_default(text, file = '<unknown>', line = 0)
461
+ data = text.split(' ', 2)
462
+ begin
463
+ args_text = data[1] ? data[1].strip : ''
464
+ args = []
465
+
466
+ unless args_text == ''
467
+ # col_sep must be a regex because String.split has a
468
+ # special case for a single space char (' ') that produced
469
+ # unexpected results (i.e. if line is '"a b"' the
470
+ # resulting array contains ["a b"]).
471
+ #
472
+ # ...but...
473
+ #
474
+ # CSV expects col_sep to be a string so we need to work
475
+ # some dark magic here. Basically we proxy the StringIO
476
+ # received by CSV to returns strings for which the split
477
+ # method does not fold the whitespaces.
478
+ #
479
+ args = CSV.new(StringIOProxy.new(args_text), { :col_sep => ' ' })
480
+ .shift
481
+ .select { |a| a != nil } || []
482
+ end
483
+
484
+ {
485
+ :action => data[0].strip.downcase,
486
+ :args => args
487
+ }
488
+ rescue StandardError => e
489
+ raise "#{file} (line #{line+1}): #{e.message}"
490
+ end
491
+ end
492
+
493
+ # Returns an executable Action object constructed from the specified
494
+ # arguments resolving action aliases.
495
+ #
496
+ # This method if part of the action execution chain and is intended
497
+ # for advanced use (e.g. in complex actions). To execute an Action
498
+ # directly, the #exec_action method is preferred.
499
+ #
500
+ def get_action(action, args, text = nil)
501
+ while (alias_action = @aliases[action])
502
+ action = alias_action[:action]
503
+ args = alias_action[:args].map do |a|
504
+ a.gsub(/\$\{(\d+)(\*q?)?\}/) do |match|
505
+ # expand ${1} to args[0], ${2} to args[1], etc.
506
+ # expand ${4*} to "#{args[4]} #{args[5]} ..."
507
+ # expand ${4*q} to "\"#{args[4]}\" \"#{args[5]}\" ..."
508
+ idx = $1.to_i-1
509
+ if $2 == nil
510
+ args[idx] || ''
511
+ else
512
+ range = args[idx..-1]
513
+ range = range.map { |arg| '"'+arg.gsub('"', '""')+'"' } if $2 == '*q'
514
+ range.join(' ')
515
+ end
516
+ end
517
+ end
518
+ end
519
+
520
+ text = ([action] + args.map { |a| '"'+a.gsub('"', '""')+'"' }).join(' ') unless text
521
+ file = @variables['__FILE__']
522
+ line = @variables['__LINE__']
523
+
524
+ Action.new(self, action, args, text, file, line)
525
+ end
526
+
527
+ # Executes the specified action object injecting built-in variables.
528
+ # Note that the result returned by this method might be a lambda.
529
+ # If this is the case, a further +call+ method must be issued.
530
+ #
531
+ # This method if part of the action execution chain and is intended
532
+ # for advanced use (e.g. in complex actions). To execute an Action
533
+ # directly, the #exec_action method is preferred.
534
+ #
535
+ # For example:
536
+ # action = ctx.get_action("echo", ['Hi!'], 'echo "Hi!"')
537
+ # ret = ctx.exec_action_object(action)
538
+ # ret.call if ret.respond_to? :call
539
+ #
540
+ def exec_action_object(action)
541
+ action.execute
542
+ end
543
+
544
+ # Prints the specified +error+ using the Logger configured and
545
+ # handling the verbose option.
546
+ #
547
+ # For example:
548
+ # begin
549
+ # # => some code here
550
+ # rescue StandardError => e
551
+ # @ctx.print_error e
552
+ # # => additional error handling code here
553
+ # end
554
+ #
555
+ def print_error(e, capture = true)
556
+ if @logger
557
+ @logger.log "#{e.message}\n", :error
558
+ else
559
+ puts e.message
560
+ end
561
+ if @options[:verbose]
562
+ p e
563
+ puts e.backtrace
564
+ end
565
+ if capture and @options[:capture]
566
+ with_vars(e.variables) do
567
+ exec_parsed_action('capture', [] , false)
568
+ e.variables['__CAPTURE__'] = @variables['__CAPTURE__']
569
+ end
570
+ end
571
+ end
572
+
573
+ # Returns the output path for +path+ accounting for the
574
+ # <tt>__OUTPUT__</tt> variable.
575
+ #
576
+ # For example:
577
+ # # assuming --output /mnt/disk
578
+ #
579
+ # ctx.output_path '/tmp/myfile.txt'
580
+ # # => returns '/tmp/myfile.txt'
581
+ #
582
+ # ctx.output_path 'myfile.txt'
583
+ # # => returns '/mnt/disk/myfile.txt'
584
+ #
585
+ def output_path(path)
586
+ unless Pathname.new(path).absolute?
587
+ output = @variables['__OUTPUT__']
588
+ if output
589
+ Dir.mkdir output unless Dir.exists? output
590
+ path = File.join(output, path)
591
+ end
592
+ end
593
+ path
594
+ end
595
+
596
+ # ======================================================================= #
597
+ # :section: Metadata
598
+ # ======================================================================= #
599
+
600
+ # Returns an array with the names of every action available.
601
+ #
602
+ # For example:
603
+ # Context::actions
604
+ # # => [ "assert", "break", ... ]
605
+ #
606
+ def self.actions
607
+ _action_methods.map { |m| m.sub(/_action$/, '') }
608
+ end
609
+
610
+ # Returns an array with the names of the arguments of the specified action.
611
+ #
612
+ # For example:
613
+ # Context::action_args 'assert'
614
+ # # => [ "selector", "text" ]
615
+ #
616
+ def self.action_args(action)
617
+ action += '_action' unless _action_methods.include? action
618
+ Action.public_instance_method(action).parameters.map { |att, name| name.to_s }
619
+ end
620
+
621
+ # Returns an array with the names of every selector available.
622
+ #
623
+ # If +include_standard_selectors+ is +true+ (default behavior) both
624
+ # standard and custom selector are returned, otherwise only custom
625
+ # selectors are returned.
626
+ #
627
+ # For example:
628
+ # Context::selectors
629
+ # # => [ "class", "id", ... ]
630
+ #
631
+ def self.selectors(include_standard_selectors = true)
632
+ ret = Selector.public_instance_methods(false).map { |a| a.to_s.sub(/_selector$/, '') }
633
+ if include_standard_selectors
634
+ ret += Selenium::WebDriver::SearchContext::FINDERS.map { |k,v| k.to_s }
635
+ end
636
+ ret
637
+ end
638
+
639
+ # Returns an array with the names of every logger available.
640
+ #
641
+ # For example:
642
+ # Context::loggers
643
+ # # => [ "null", "bash", ... ]
644
+ #
645
+ def self.loggers
646
+ Loggers.constants.map { |l| l.to_s.downcase.sub(/logger$/, '') }
647
+ end
648
+
649
+ # Returns an array with the names of every parser available.
650
+ #
651
+ # For example:
652
+ # Context::parsers
653
+ # # => [ "default", "html", ... ]
654
+ #
655
+ def self.parsers
656
+ (Parser.public_instance_methods(false) \
657
+ - ParserModule.public_instance_methods(false))
658
+ .map { |p| p.to_s }
659
+ end
660
+
661
+ # Returns the maximum size in characters of an action name.
662
+ #
663
+ # This method is useful to pretty print lists of actions
664
+ #
665
+ # For example:
666
+ # # assuming actions = [ "echo", "assert", "tryload" ]
667
+ # Context::max_action_name_size
668
+ # # => 7
669
+ def self.max_action_name_size
670
+ actions.inject(0) { |s,a| a.size > s ? a.size : s }
671
+ end
672
+
673
+ # ======================================================================= #
674
+ # :section: Variable manipulation methods
675
+ # ======================================================================= #
676
+
677
+ # Recursively replaces occurencies of variable expansions in +s+ with the
678
+ # corresponding variable value.
679
+ #
680
+ # The variable expansion expression format is:
681
+ # '${variable_name}'
682
+ #
683
+ # For example:
684
+ # ctx.variables = { 'a' => '1', 'b' => '2', 'c' => 'a' }
685
+ # ctx.expand '${a}' # => '1'
686
+ # ctx.expand '${b}' # => '2'
687
+ # ctx.expand '${c}' # => 'a'
688
+ # ctx.expand '${${c}}' # => '1'
689
+ #
690
+ def expand(s)
691
+ result = @variables.inject(s) do |s,kv|
692
+ s = s.gsub(/\$\{#{kv[0]}\}/, kv[1].to_s)
693
+ end
694
+ result = expand(result) if result != s
695
+ result
696
+ end
697
+
698
+ # Temporarily alter the value of context variables.
699
+ #
700
+ # This method alters the value of the variables specified in the +vars+
701
+ # hash for the duration of the given +block+. When the +block+ completes,
702
+ # the original value of the context variables is restored.
703
+ #
704
+ # For example:
705
+ # ctx.variables = { 'a' => '1', 'b' => '2', c => 'a' }
706
+ # ctx.with_vars({ 'a' => '10', d => '20' }) do
707
+ # p ctx.variables
708
+ # # => {"a"=>"10", "b"=>"2", "c"=>"a", "d"=>"20"}
709
+ # end
710
+ # p ctx.variables
711
+ # # => {"a"=>"1", "b"=>"2", "c"=>"a"}
712
+ #
713
+ def with_vars(vars)
714
+ current = @variables
715
+ @variables = @variables.merge(vars)
716
+ ret_vars = nil
717
+
718
+ ret = yield
719
+
720
+ returned = @variables['__RETURN__']
721
+ if returned == ['*']
722
+ ret_vars = @variables.clone
723
+ ret_vars.delete '__RETURN__'
724
+ elsif returned != nil
725
+ ret_vars = @variables.select { |k,v| returned.include? k }
726
+ end
727
+ rescue StandardError => e
728
+ e.instance_variable_set "@variables", @variables
729
+ def e.variables
730
+ @variables
731
+ end
732
+ raise
733
+ ensure
734
+ @variables = current
735
+ @variables.merge!(ret_vars) if ret_vars
736
+ ret
737
+ end
738
+
739
+ private
740
+ def self._action_methods
741
+ (Action.public_instance_methods(false) \
742
+ - ActionModule.public_instance_methods(false))
743
+ .map { |a| a.to_s }
744
+ end
745
+
746
+ def _load_driver
747
+ @driver = Selenium::WebDriver.for(@driver_name, @options[:driver_opt])
748
+ @driver.manage.timeouts.implicit_wait = 1
749
+ @driver_timeout = 1
750
+ end
751
+
752
+ def _load_extensions(dirs)
753
+ dirs.each do |d|
754
+ d = File.join(Dir.pwd, d) unless Dir.exists? d
755
+ d = File.absolute_path(d)
756
+ Dir[File.join(d, '**', '*.rb')].each { |file| require file }
757
+ end
758
+ end
759
+
760
+ # ======================================================================= #
761
+ # Hacks required to overcome the String#split(' ') behavior of folding the
762
+ # space characters, coupled with CSV not supporting a regex as :col_sep.
763
+
764
+ # Same as a common String except that split(' ') behaves as split(/\s/).
765
+ class StringProxy # :nodoc:
766
+ def initialize(s)
767
+ @s = s
768
+ end
769
+
770
+ def method_missing(method, *args, &block)
771
+ args[0] = /\s/ if method == :split and args.size > 0 and args[0] == ' '
772
+ ret = @s.send(method, *args, &block)
773
+ end
774
+ end
775
+
776
+ # Same as a common StringIO except that get(sep) returns a StringProxy
777
+ # instead of a regular string.
778
+ class StringIOProxy # :nodoc:
779
+ def initialize(s)
780
+ @s = StringIO.new(s)
781
+ end
782
+
783
+ def method_missing(method, *args, &block)
784
+ ret = @s.send(method, *args, &block)
785
+ return ret unless method == :gets and args.size == 1
786
+ StringProxy.new(ret)
787
+ end
788
+ end
789
+ # ======================================================================= #
790
+ end
791
+ end