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.
- checksums.yaml +4 -4
- data/LICENSE +21 -21
- data/README.md +293 -293
- data/Rakefile +128 -128
- data/bin/bauxite +27 -27
- data/doc/Bauxite.html +6 -9
- data/doc/Bauxite/Action.html +298 -315
- data/doc/Bauxite/ActionModule.html +23 -26
- data/doc/Bauxite/Application.html +36 -38
- data/doc/Bauxite/Context.html +303 -303
- data/doc/Bauxite/Errors.html +6 -9
- data/doc/Bauxite/Errors/AssertionError.html +6 -9
- data/doc/Bauxite/Errors/FileNotFoundError.html +6 -9
- data/doc/Bauxite/Errors/FormatError.html +6 -9
- data/doc/Bauxite/Loggers.html +6 -9
- data/doc/Bauxite/Loggers/CompositeLogger.html +29 -32
- data/doc/Bauxite/Loggers/EchoLogger.html +10 -13
- data/doc/Bauxite/Loggers/FileLogger.html +25 -28
- data/doc/Bauxite/Loggers/HtmlLogger.html +99 -102
- data/doc/Bauxite/Loggers/NullLogger.html +16 -19
- data/doc/Bauxite/Loggers/ReportLogger.html +43 -46
- data/doc/Bauxite/Loggers/TerminalLogger.html +76 -79
- data/doc/Bauxite/Loggers/XtermLogger.html +28 -31
- data/doc/Bauxite/Parser.html +87 -89
- data/doc/Bauxite/ParserModule.html +19 -22
- data/doc/Bauxite/Selector.html +99 -105
- data/doc/Bauxite/SelectorModule.html +27 -30
- data/doc/README_md.html +117 -103
- data/doc/created.rid +58 -58
- data/doc/fonts.css +167 -167
- data/doc/fonts/Lato-Light.ttf +0 -0
- data/doc/fonts/Lato-LightItalic.ttf +0 -0
- data/doc/fonts/Lato-Regular.ttf +0 -0
- data/doc/fonts/Lato-RegularItalic.ttf +0 -0
- data/doc/fonts/SourceCodePro-Bold.ttf +0 -0
- data/doc/fonts/SourceCodePro-Regular.ttf +0 -0
- data/doc/images/add.png +0 -0
- data/doc/images/arrow_up.png +0 -0
- data/doc/images/brick.png +0 -0
- data/doc/images/brick_link.png +0 -0
- data/doc/images/bug.png +0 -0
- data/doc/images/bullet_black.png +0 -0
- data/doc/images/bullet_toggle_minus.png +0 -0
- data/doc/images/bullet_toggle_plus.png +0 -0
- data/doc/images/date.png +0 -0
- data/doc/images/delete.png +0 -0
- data/doc/images/find.png +0 -0
- data/doc/images/loadingAnimation.gif +0 -0
- data/doc/images/macFFBgHack.png +0 -0
- data/doc/images/package.png +0 -0
- data/doc/images/page_green.png +0 -0
- data/doc/images/page_white_text.png +0 -0
- data/doc/images/page_white_width.png +0 -0
- data/doc/images/plugin.png +0 -0
- data/doc/images/ruby.png +0 -0
- data/doc/images/tag_blue.png +0 -0
- data/doc/images/tag_green.png +0 -0
- data/doc/images/transparent.png +0 -0
- data/doc/images/wrench.png +0 -0
- data/doc/images/wrench_orange.png +0 -0
- data/doc/images/zoom.png +0 -0
- data/doc/index.html +117 -103
- data/doc/js/darkfish.js +32 -11
- data/doc/js/jquery.js +0 -0
- data/doc/js/navigation.js +0 -0
- data/doc/js/search.js +0 -0
- data/doc/js/search_index.js +1 -1
- data/doc/js/searcher.js +0 -0
- data/doc/rdoc.css +580 -580
- data/doc/table_of_contents.html +69 -72
- data/lib/bauxite.rb +28 -28
- data/lib/bauxite/actions/alias.rb +51 -51
- data/lib/bauxite/actions/assert.rb +49 -49
- data/lib/bauxite/actions/asserth.rb +60 -60
- data/lib/bauxite/actions/assertm.rb +49 -49
- data/lib/bauxite/actions/assertv.rb +40 -40
- data/lib/bauxite/actions/assertw.rb +47 -47
- data/lib/bauxite/actions/break.rb +39 -39
- data/lib/bauxite/actions/capture.rb +61 -61
- data/lib/bauxite/actions/click.rb +36 -36
- data/lib/bauxite/actions/debug.rb +103 -103
- data/lib/bauxite/actions/doif.rb +43 -43
- data/lib/bauxite/actions/dounless.rb +43 -43
- data/lib/bauxite/actions/echo.rb +36 -36
- data/lib/bauxite/actions/exec.rb +46 -46
- data/lib/bauxite/actions/exit.rb +35 -35
- data/lib/bauxite/actions/failif.rb +52 -52
- data/lib/bauxite/actions/js.rb +41 -41
- data/lib/bauxite/actions/load.rb +49 -49
- data/lib/bauxite/actions/open.rb +35 -35
- data/lib/bauxite/actions/params.rb +40 -40
- data/lib/bauxite/actions/replace.rb +37 -37
- data/lib/bauxite/actions/reset.rb +38 -38
- data/lib/bauxite/actions/return.rb +68 -68
- data/lib/bauxite/actions/ruby.rb +58 -58
- data/lib/bauxite/actions/select.rb +48 -48
- data/lib/bauxite/actions/set.rb +39 -39
- data/lib/bauxite/actions/setif.rb +44 -44
- data/lib/bauxite/actions/source.rb +44 -44
- data/lib/bauxite/actions/store.rb +38 -38
- data/lib/bauxite/actions/submit.rb +37 -37
- data/lib/bauxite/actions/test.rb +67 -67
- data/lib/bauxite/actions/tryload.rb +71 -71
- data/lib/bauxite/actions/wait.rb +38 -38
- data/lib/bauxite/actions/write.rb +44 -44
- data/lib/bauxite/application.rb +349 -349
- data/lib/bauxite/core/action.rb +199 -199
- data/lib/bauxite/core/context.rb +791 -791
- data/lib/bauxite/core/errors.rb +41 -41
- data/lib/bauxite/core/logger.rb +169 -169
- data/lib/bauxite/core/parser.rb +85 -85
- data/lib/bauxite/core/selector.rb +152 -152
- data/lib/bauxite/loggers/composite.rb +91 -91
- data/lib/bauxite/loggers/echo.rb +36 -36
- data/lib/bauxite/loggers/file.rb +68 -68
- data/lib/bauxite/loggers/html.rb +154 -154
- data/lib/bauxite/loggers/terminal.rb +134 -134
- data/lib/bauxite/loggers/xterm.rb +101 -101
- data/lib/bauxite/parsers/csv.rb +43 -43
- data/lib/bauxite/parsers/default.rb +42 -42
- data/lib/bauxite/parsers/html.rb +79 -79
- data/lib/bauxite/selectors/attr.rb +39 -39
- data/lib/bauxite/selectors/frame.rb +60 -60
- data/lib/bauxite/selectors/json.rb +88 -88
- data/lib/bauxite/selectors/sid.rb +38 -38
- data/lib/bauxite/selectors/smart.rb +80 -80
- data/lib/bauxite/selectors/window.rb +77 -77
- data/test/alert.bxt +3 -3
- data/test/alert/page.html +4 -4
- data/test/alias.bxt +9 -9
- data/test/asserth.bxt +2 -2
- data/test/assertv.bxt +1 -1
- data/test/assertw.bxt +7 -7
- data/test/broken.bxt.manual +0 -0
- data/test/bug_load_path.bxt.manual +0 -0
- data/test/bug_load_path/broken.bxt.manual +0 -0
- data/test/bug_load_path/test.bxt +0 -0
- data/test/capture.bxt.manual +20 -20
- data/test/capture/my_test.bxt +1 -1
- data/test/capture/page.html +6 -6
- data/test/capture_on_error.bxt.manual +3 -3
- data/test/capture_on_error/my_test.bxt +1 -1
- data/test/capture_on_error/page.html +2 -2
- data/test/debug.bxt.manual +0 -0
- data/test/default_selector.bxt.manual +7 -7
- data/test/default_selector/page.html +10 -10
- data/test/default_selector_var.bxt +1 -1
- data/test/delay.bxt +2 -2
- data/test/delay/page.html +4 -4
- data/test/doif.bxt +6 -6
- data/test/dounless.bxt +6 -6
- data/test/exec.bxt +6 -6
- data/test/exit.bxt +3 -3
- data/test/exit/test.bxt +3 -3
- data/test/extension.bxt.manual +4 -4
- data/test/extension/custom.rb +12 -12
- data/test/extension/page.html +4 -4
- data/test/failif.bxt +7 -7
- data/test/failif/page.html +5 -5
- data/test/format.bxt +17 -17
- data/test/format/page.html +6 -6
- data/test/frame.bxt +6 -6
- data/test/frame/child_frame.html +6 -6
- data/test/frame/grandchild_frame.html +4 -4
- data/test/frame/page.html +4 -4
- data/test/js.bxt +4 -4
- data/test/json.bxt +19 -19
- data/test/json/array.json +3 -3
- data/test/json/object.json +13 -13
- data/test/load.bxt +18 -18
- data/test/load/child.bxt +12 -12
- data/test/parsers.bxt +1 -1
- data/test/parsers.csv +7 -7
- data/test/parsers.html +32 -32
- data/test/parsers/page.html +6 -6
- data/test/return.bxt +1 -1
- data/test/return/f1.bxt +1 -1
- data/test/return/f2.bxt +1 -1
- data/test/return/f3.bxt +1 -1
- data/test/return/f4.bxt +2 -2
- data/test/ruby.bxt +1 -1
- data/test/ruby/custom.rb +5 -5
- data/test/select.bxt +9 -9
- data/test/select/page.html +8 -8
- data/test/selectors.bxt +7 -7
- data/test/selectors/page.html +6 -6
- data/test/set_builtin.bxt +5 -0
- data/test/set_builtin/page.html +5 -0
- data/test/setif.bxt +3 -3
- data/test/smart_selector.bxt +17 -17
- data/test/smart_selector/page.html +17 -17
- data/test/stdin.bxt +0 -0
- data/test/submit.bxt +4 -4
- data/test/submit/page.html +6 -6
- data/test/submit/page2.html +4 -4
- data/test/test.bxt.manual +6 -6
- data/test/test/test1.bxt +2 -2
- data/test/test/test2.bxt +3 -3
- data/test/test/test3.bxt +2 -2
- data/test/test/test4.bxt +1 -1
- data/test/test/test5.bxt +1 -1
- data/test/window.bxt +14 -14
- data/test/window/page.html +5 -5
- data/test/window/popup.html +4 -4
- metadata +5 -3
data/lib/bauxite/core/action.rb
CHANGED
@@ -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
|
data/lib/bauxite/core/context.rb
CHANGED
@@ -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
|