bauxite 0.6.18 → 0.6.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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