lapis_lazuli 0.6.1
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 +7 -0
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +42 -0
- data/LICENSE +30 -0
- data/README.md +74 -0
- data/Rakefile +1 -0
- data/bin/lapis_lazuli +3 -0
- data/lapis_lazuli.gemspec +32 -0
- data/lib/lapis_lazuli/api.rb +52 -0
- data/lib/lapis_lazuli/argparse.rb +128 -0
- data/lib/lapis_lazuli/ast.rb +160 -0
- data/lib/lapis_lazuli/browser/error.rb +93 -0
- data/lib/lapis_lazuli/browser/find.rb +500 -0
- data/lib/lapis_lazuli/browser/interaction.rb +91 -0
- data/lib/lapis_lazuli/browser/screenshots.rb +70 -0
- data/lib/lapis_lazuli/browser/wait.rb +158 -0
- data/lib/lapis_lazuli/browser.rb +246 -0
- data/lib/lapis_lazuli/cli.rb +110 -0
- data/lib/lapis_lazuli/cucumber.rb +25 -0
- data/lib/lapis_lazuli/generators/cucumber/template/.gitignore +6 -0
- data/lib/lapis_lazuli/generators/cucumber/template/Gemfile +37 -0
- data/lib/lapis_lazuli/generators/cucumber/template/README.md +27 -0
- data/lib/lapis_lazuli/generators/cucumber/template/config/config.yml +29 -0
- data/lib/lapis_lazuli/generators/cucumber/template/config/cucumber.yml +34 -0
- data/lib/lapis_lazuli/generators/cucumber/template/features/example.feature +11 -0
- data/lib/lapis_lazuli/generators/cucumber/template/features/step_definitions/interaction_steps.rb +20 -0
- data/lib/lapis_lazuli/generators/cucumber/template/features/step_definitions/validation_steps.rb +21 -0
- data/lib/lapis_lazuli/generators/cucumber/template/features/support/env.rb +12 -0
- data/lib/lapis_lazuli/generators/cucumber/template/features/support/transition.rb +12 -0
- data/lib/lapis_lazuli/generators/cucumber.rb +128 -0
- data/lib/lapis_lazuli/generic/xpath.rb +49 -0
- data/lib/lapis_lazuli/options.rb +28 -0
- data/lib/lapis_lazuli/placeholders.rb +36 -0
- data/lib/lapis_lazuli/proxy.rb +179 -0
- data/lib/lapis_lazuli/runtime.rb +88 -0
- data/lib/lapis_lazuli/scenario.rb +88 -0
- data/lib/lapis_lazuli/storage.rb +59 -0
- data/lib/lapis_lazuli/version.rb +10 -0
- data/lib/lapis_lazuli/versions.rb +40 -0
- data/lib/lapis_lazuli/world/annotate.rb +45 -0
- data/lib/lapis_lazuli/world/api.rb +35 -0
- data/lib/lapis_lazuli/world/browser.rb +75 -0
- data/lib/lapis_lazuli/world/config.rb +292 -0
- data/lib/lapis_lazuli/world/error.rb +141 -0
- data/lib/lapis_lazuli/world/hooks.rb +109 -0
- data/lib/lapis_lazuli/world/logging.rb +53 -0
- data/lib/lapis_lazuli/world/proxy.rb +59 -0
- data/lib/lapis_lazuli/world/variable.rb +139 -0
- data/lib/lapis_lazuli.rb +75 -0
- data/test/.gitignore +8 -0
- data/test/Gemfile +42 -0
- data/test/README.md +35 -0
- data/test/config/config.yml +37 -0
- data/test/config/cucumber.yml +37 -0
- data/test/features/annotation.feature +23 -0
- data/test/features/browser.feature +10 -0
- data/test/features/button.feature +38 -0
- data/test/features/click.feature +35 -0
- data/test/features/error.feature +30 -0
- data/test/features/find.feature +92 -0
- data/test/features/har.feature +9 -0
- data/test/features/modules.feature +14 -0
- data/test/features/step_definitions/interaction_steps.rb +154 -0
- data/test/features/step_definitions/validation_steps.rb +350 -0
- data/test/features/support/env.rb +21 -0
- data/test/features/text_field.feature +32 -0
- data/test/features/timing.feature +47 -0
- data/test/features/variable.feature +11 -0
- data/test/features/xpath.feature +41 -0
- data/test/server/start.rb +17 -0
- data/test/server/www/button.html +22 -0
- data/test/server/www/error_html.html +9 -0
- data/test/server/www/find.html +66 -0
- data/test/server/www/javascript_error.html +12 -0
- data/test/server/www/text_fields.html +15 -0
- data/test/server/www/timing.html +32 -0
- data/test/server/www/xpath.html +22 -0
- metadata +295 -0
@@ -0,0 +1,500 @@
|
|
1
|
+
#
|
2
|
+
# LapisLazuli
|
3
|
+
# https://github.com/spriteCloud/lapis-lazuli
|
4
|
+
#
|
5
|
+
# Copyright (c) 2013-2015 spriteCloud B.V. and other LapisLazuli contributors.
|
6
|
+
# All rights reserved.
|
7
|
+
#
|
8
|
+
|
9
|
+
require 'test/unit/assertions'
|
10
|
+
require 'lapis_lazuli/argparse'
|
11
|
+
|
12
|
+
|
13
|
+
module LapisLazuli
|
14
|
+
module BrowserModule
|
15
|
+
|
16
|
+
##
|
17
|
+
# Find functionality for LapisLazuli::Browser. Don't use outside of that
|
18
|
+
# class.
|
19
|
+
module Find
|
20
|
+
include LapisLazuli::ArgParse
|
21
|
+
|
22
|
+
##
|
23
|
+
# Finds all elements corresponding to some specification; the supported
|
24
|
+
# specifications include the ones accepted by Watir::Browser.elements.
|
25
|
+
#
|
26
|
+
# Possible specifications are:
|
27
|
+
# - Watir specifications, e.g. { :tag_name => 'a', ... }
|
28
|
+
# - An alternative to the Watir specifications:
|
29
|
+
# { :a => { :id => /some-id/ } } <=> { :tag_name => 'a', :id => /some-id/ }
|
30
|
+
# Note that the value can be an empty hash, e.g. { :a => {} }
|
31
|
+
# This method uses Watir selectors.
|
32
|
+
# - A shortcut version searching for a tag by name, id or content:
|
33
|
+
# { :a => 'name-or-id-or-content' }
|
34
|
+
# This method uses XPath.
|
35
|
+
# - A like specifcation. The value of :like is a hash, which must at least
|
36
|
+
# contain an :element name; in addition, an optional :attribute and
|
37
|
+
# :include field further filters the results.
|
38
|
+
# { :like => {:element => 'a', :attribute => 'class', :include => 'foo' }}
|
39
|
+
# This method uses XPath.
|
40
|
+
# - A shorthand for the above using an array that's interpreted to contain
|
41
|
+
# :element, :attribute and :include in order.
|
42
|
+
# { :like => ['a', 'class', 'foo'] }
|
43
|
+
# This method also uses XPath.
|
44
|
+
#
|
45
|
+
# In addition to the above, you can include the following parameters:
|
46
|
+
# - :filter_by expects a symbol that the elements respond to; if calling
|
47
|
+
# the method returns true, the element is returned, otherwise it is
|
48
|
+
# ignored. Use e.g. { :filter_by => :present? }
|
49
|
+
# - :throw one of true, false. Default is true.
|
50
|
+
def find_all(*args)
|
51
|
+
# Parse args into options
|
52
|
+
options = parse_find_options({}, *args)
|
53
|
+
throw_opt, options = do_throw?(options)
|
54
|
+
|
55
|
+
# Find filtered.
|
56
|
+
opts, func = find_lambda_filtered(options[:selectors][0])
|
57
|
+
|
58
|
+
# Dispatch the call & handle errors
|
59
|
+
return dispatch_call(throw_opt, "Error in find", options, opts, func)
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
##
|
64
|
+
# Same as find_all, but returns only one element.
|
65
|
+
#
|
66
|
+
# The function supports an additional parameter :pick that can be one of
|
67
|
+
# :first, :last or :random, or a numeric value.
|
68
|
+
#
|
69
|
+
# The parameter determines whether the first, last or a random element from
|
70
|
+
# the find_all result set is returned. If a numeric value is given, the nth
|
71
|
+
# element is returned.
|
72
|
+
#
|
73
|
+
# The default for :pick is :first
|
74
|
+
def find(*args)
|
75
|
+
# Parse args into options
|
76
|
+
options = {
|
77
|
+
:pick => :first,
|
78
|
+
}
|
79
|
+
options = parse_find_options(options, *args)
|
80
|
+
pick, options = pick_which?(options)
|
81
|
+
|
82
|
+
# Pick one of the find all results
|
83
|
+
return pick_one(pick, find_all(options))
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
##
|
88
|
+
# Same as find_all, but accepts an array of selectors.
|
89
|
+
#
|
90
|
+
# The function has two modes:
|
91
|
+
# 1. It either tries to find a match for every selector, or
|
92
|
+
# 2. It tries to find a single match from all selectors.
|
93
|
+
#
|
94
|
+
# The mode is specified with the optional :mode parameter, which can be
|
95
|
+
# one of :match_all or :match_any. The default mode is :match_any.
|
96
|
+
#
|
97
|
+
# Note that if you specify the :mode, you can't simultaneously pass a list
|
98
|
+
# of selectors easily, e.g. the following does not parse:
|
99
|
+
#
|
100
|
+
# multi_find_all(:mode => :match_all, selector1, selector2)
|
101
|
+
#
|
102
|
+
# Instead use:
|
103
|
+
#
|
104
|
+
# multi_find_all(:mode => :match_all, :selectors => [selector1, selector2])
|
105
|
+
#
|
106
|
+
# However, using the default mode, you can simplify it all:
|
107
|
+
#
|
108
|
+
# multi_find_all(selector1, selector2)
|
109
|
+
def multi_find_all(*args)
|
110
|
+
# Parse args into options
|
111
|
+
options = {
|
112
|
+
:mode => :match_one,
|
113
|
+
}
|
114
|
+
options = parse_find_options(options, *args)
|
115
|
+
throw_opt, options = do_throw?(options)
|
116
|
+
|
117
|
+
# Find all for the given selectors
|
118
|
+
opts, func = multi_find_lambda(options)
|
119
|
+
|
120
|
+
# Dispatch the call & handle errors
|
121
|
+
return dispatch_call(throw_opt, "Error in multi_find", options, opts, func)
|
122
|
+
end
|
123
|
+
|
124
|
+
|
125
|
+
##
|
126
|
+
# Same as multi_find_all, but accepts the :pick parameter as find does.
|
127
|
+
def multi_find(*args)
|
128
|
+
# Parse args into options
|
129
|
+
options = {
|
130
|
+
:mode => :match_one,
|
131
|
+
:pick => :first,
|
132
|
+
}
|
133
|
+
options = parse_find_options(options, *args)
|
134
|
+
pick, options = pick_which?(options)
|
135
|
+
|
136
|
+
# Pick one of the find all results
|
137
|
+
return pick_one(pick, multi_find_all(options))
|
138
|
+
end
|
139
|
+
|
140
|
+
|
141
|
+
##
|
142
|
+
# Pick implementation for find and multi_find, but can be used standalone.
|
143
|
+
#
|
144
|
+
# pick may be one of :first, :last, :random or a numeric index. Returns the
|
145
|
+
# element from the collection corresponding to the pick parameter.
|
146
|
+
def pick_one(pick, elems)
|
147
|
+
case pick
|
148
|
+
when :first
|
149
|
+
return elems.first
|
150
|
+
when :last
|
151
|
+
return elems.last
|
152
|
+
when :random
|
153
|
+
return elems.to_a.shuffle.first
|
154
|
+
else
|
155
|
+
if pick.is_a? Numeric
|
156
|
+
return elems[pick.to_i]
|
157
|
+
else
|
158
|
+
options[:message] = optional_message("Invalid :pick value #{pick}.", options)
|
159
|
+
options[:groups] = ['find', 'pick']
|
160
|
+
@world.error(options)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
|
166
|
+
|
167
|
+
private
|
168
|
+
|
169
|
+
NON_SELECTOR_OPTS = {
|
170
|
+
:pick => :first,
|
171
|
+
:throw => true,
|
172
|
+
:mode => :match_one,
|
173
|
+
:error => nil,
|
174
|
+
}
|
175
|
+
|
176
|
+
##
|
177
|
+
# Uses parse_args to parse find options. Then ensures that for each
|
178
|
+
# selector, the expected fields exist.
|
179
|
+
def parse_find_options(options, *args)
|
180
|
+
# First, parse the arguments into an options hash
|
181
|
+
options = ERROR_OPTIONS.merge options
|
182
|
+
options = NON_SELECTOR_OPTS.merge options
|
183
|
+
options = parse_args(options, :selectors, *args)
|
184
|
+
|
185
|
+
# Verify/sanitize common options
|
186
|
+
if options.has_key? :mode
|
187
|
+
options[:mode] = options[:mode].to_sym
|
188
|
+
assert [:match_all, :match_one].include?(options[:mode]), ":mode needs to be one of :match_one or :match_all"
|
189
|
+
end
|
190
|
+
|
191
|
+
if options.has_key? :pick
|
192
|
+
if not options[:pick].is_a? Numeric
|
193
|
+
options[:pick] = options[:pick].to_sym
|
194
|
+
end
|
195
|
+
assert ([:first, :last, :random].include?(options[:pick]) or options[:pick].is_a?(Numeric)), ":pick must be one of :first, :last, :random or a numeric value"
|
196
|
+
end
|
197
|
+
|
198
|
+
if options.has_key? :filter_by
|
199
|
+
options[:filter_by] = options.to_sym
|
200
|
+
end
|
201
|
+
|
202
|
+
# Next, expand all selectors.
|
203
|
+
expanded = []
|
204
|
+
options[:selectors].each do |sel|
|
205
|
+
expanded << expand_selector(sel)
|
206
|
+
end
|
207
|
+
options[:selectors] = expanded
|
208
|
+
|
209
|
+
# p "-> options: #{options}"
|
210
|
+
return options
|
211
|
+
end
|
212
|
+
|
213
|
+
|
214
|
+
|
215
|
+
##
|
216
|
+
# Expands a selector and verifies it.
|
217
|
+
def expand_selector(selector)
|
218
|
+
# First convert outer shorthand. Afterwards, selector is guaranteed
|
219
|
+
# to be a hash.
|
220
|
+
if selector.is_a? String
|
221
|
+
selector = {:element => selector}
|
222
|
+
elsif selector.is_a? Symbol
|
223
|
+
selector = {:like => selector}
|
224
|
+
end
|
225
|
+
|
226
|
+
# Now ensure the :like parameter is a full hash
|
227
|
+
if selector.include? :like
|
228
|
+
like_opts = selector[:like]
|
229
|
+
# Convert array shorthand to full Hash
|
230
|
+
if like_opts.is_a? Array and like_opts.length >= 3
|
231
|
+
like_opts = {
|
232
|
+
:element => like_opts[0],
|
233
|
+
:attribute => like_opts[1],
|
234
|
+
:include => like_opts[2]
|
235
|
+
}
|
236
|
+
elsif like_opts.is_a? Symbol
|
237
|
+
like_opts = {
|
238
|
+
:element => like_opts,
|
239
|
+
}
|
240
|
+
end
|
241
|
+
|
242
|
+
selector[:like] = like_opts
|
243
|
+
if not like_opts.has_key? :element
|
244
|
+
selector[:message] = optional_message("Like selector are missing the :element key.", selector)
|
245
|
+
selector[:groups] = ['find', 'selector']
|
246
|
+
@world.error(selector)
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
return selector
|
251
|
+
end
|
252
|
+
|
253
|
+
|
254
|
+
|
255
|
+
##
|
256
|
+
# Return a lambda function that can be executed to find an element.
|
257
|
+
# find(), multi_find(), wait() and multi_wait() use this function,
|
258
|
+
# so look there for documentation.
|
259
|
+
#
|
260
|
+
# There are a number of different modes, triggered by the presence or
|
261
|
+
# absence of particular parameters. Note that the parameters passed
|
262
|
+
# here must have been passed through parse_find_options() before.
|
263
|
+
#
|
264
|
+
# That said:
|
265
|
+
# - The presence of :like will construct an XPath selector from the
|
266
|
+
# sub-fields :element, :attribute and :include, finding the given
|
267
|
+
# element where the given attribute includes the given text. Note
|
268
|
+
# that the special attribute :text is interpreted as meaning the
|
269
|
+
# text content of the element.
|
270
|
+
#
|
271
|
+
def find_lambda(options)
|
272
|
+
# A context is starting position for the search
|
273
|
+
# Example:
|
274
|
+
# parent = ll.browser.find(:div => "some_parent")
|
275
|
+
# ll.browser.find(:a => "some_link", :context => parent)
|
276
|
+
context = @browser
|
277
|
+
has_context = false
|
278
|
+
if options.has_key? :context
|
279
|
+
context = options[:context]
|
280
|
+
options.delete(:context)
|
281
|
+
has_context = true
|
282
|
+
end
|
283
|
+
|
284
|
+
# require 'pp'
|
285
|
+
# pp "find options: #{options}, has context: #{has_context}"
|
286
|
+
|
287
|
+
# Make {:html => x} a shortcut for {:html => {:text => x}}, but only
|
288
|
+
# if it's the only option.
|
289
|
+
if options.has_key? :html and 1 == options.length
|
290
|
+
if options[:html].is_a? String or options[:html].is_a? Regexp
|
291
|
+
options[:html] = { :text => options[:html] }
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
# Do we have :like options? Create an appropriate lambda
|
296
|
+
if options.has_key? :like
|
297
|
+
return find_lambda_like(context, has_context, options)
|
298
|
+
end
|
299
|
+
|
300
|
+
# If one of the options keys is a method of the context, then we'll
|
301
|
+
# invoke that method. The options value is passed to the method if it is
|
302
|
+
# a hash; if it's anything else, it's assumed to be a tag name, id or text
|
303
|
+
# contents we'll find with XPath.
|
304
|
+
options.each do |key, value|
|
305
|
+
# Find the one the browser responds to
|
306
|
+
# Example: text_fields or buttons
|
307
|
+
function_name = "#{key.to_s}s"
|
308
|
+
if not context.respond_to? function_name
|
309
|
+
next
|
310
|
+
end
|
311
|
+
|
312
|
+
# If the value is a hash use it as arguments for this function
|
313
|
+
if value.is_a? Hash
|
314
|
+
return options, lambda {
|
315
|
+
context.send(function_name, value)
|
316
|
+
}
|
317
|
+
else
|
318
|
+
# Find it based on name, id or text
|
319
|
+
str = value.to_s
|
320
|
+
return options, lambda {
|
321
|
+
xpath = "#{'.' if has_context}//*[@name='#{str}' or @id='#{str}' or text()='#{str}']"
|
322
|
+
context.send(
|
323
|
+
function_name,
|
324
|
+
:xpath => xpath
|
325
|
+
)
|
326
|
+
}
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
# Finally, if no field is given, we'll just pass on everything to
|
331
|
+
# the elements function as-is, in case there's a regular Watir selector
|
332
|
+
# in it.
|
333
|
+
return options, lambda {
|
334
|
+
elems = context.elements(options)
|
335
|
+
# XXX Hack for firefox webdriver. When the given options return no
|
336
|
+
# matches, elems.length would be 0, elems.each would not iterate
|
337
|
+
# over anything, but elems[0] is the top-level HTML element, etc.
|
338
|
+
if elems.length <= 0
|
339
|
+
elems = []
|
340
|
+
end
|
341
|
+
elems
|
342
|
+
}
|
343
|
+
end
|
344
|
+
|
345
|
+
|
346
|
+
##
|
347
|
+
# Similar to find_lambda, but filters the returned elements by the given
|
348
|
+
# :filter_by function (defaults to :present?).
|
349
|
+
def find_lambda_filtered(options)
|
350
|
+
options = options.dup
|
351
|
+
|
352
|
+
filter_by = options.fetch(:filter_by, nil)
|
353
|
+
options.delete(:filter_by)
|
354
|
+
|
355
|
+
options, inner = find_lambda(options)
|
356
|
+
|
357
|
+
# Wrap into filter function
|
358
|
+
return options, lambda {
|
359
|
+
elems = inner.call
|
360
|
+
|
361
|
+
# XXX See similar comment in find_lambda()
|
362
|
+
if elems.length <= 0
|
363
|
+
elems = []
|
364
|
+
end
|
365
|
+
|
366
|
+
# If we have elements and want them filtered, deal with that now.
|
367
|
+
if elems and not filter_by.nil?
|
368
|
+
elems = elems.find_all { |elem|
|
369
|
+
elem.send(filter_by)
|
370
|
+
}
|
371
|
+
end
|
372
|
+
elems
|
373
|
+
}
|
374
|
+
end
|
375
|
+
|
376
|
+
|
377
|
+
##
|
378
|
+
# Component of find_lambda; returns the lambda for when :like
|
379
|
+
# options are present.
|
380
|
+
def find_lambda_like(context, has_context, options)
|
381
|
+
# Shortcuts
|
382
|
+
like_opts = options[:like]
|
383
|
+
|
384
|
+
# Basic xpath to find an element
|
385
|
+
xpath = "#{'.' if has_context}//#{like_opts[:element]}"
|
386
|
+
|
387
|
+
# Add options to the xpath
|
388
|
+
if like_opts.include? :attribute and like_opts.include? :include
|
389
|
+
# Create new variable so we don't overwrite the old one
|
390
|
+
attribute = nil
|
391
|
+
# Do we need to match text or an attirbute
|
392
|
+
if like_opts[:attribute].to_sym == :text
|
393
|
+
attribute = "text()"
|
394
|
+
else
|
395
|
+
attribute = "@#{like_opts[:attribute]}"
|
396
|
+
end
|
397
|
+
|
398
|
+
# Add the options to the xpath query
|
399
|
+
xpath = "#{xpath}[#{xp_contains(attribute, like_opts[:include], '')}]"
|
400
|
+
end
|
401
|
+
|
402
|
+
# Create the XPath query
|
403
|
+
return options, lambda {
|
404
|
+
context.elements(
|
405
|
+
:xpath,
|
406
|
+
xpath
|
407
|
+
)
|
408
|
+
}
|
409
|
+
end
|
410
|
+
|
411
|
+
|
412
|
+
##
|
413
|
+
# The heart of multi_find_all, but returns a lambda.
|
414
|
+
#
|
415
|
+
# This function exists for easier implementation of the wait functions.
|
416
|
+
def multi_find_lambda(options)
|
417
|
+
# Collect the lambdas for all selectors
|
418
|
+
lambdas = []
|
419
|
+
options[:selectors].each do |selector|
|
420
|
+
s, func = find_lambda_filtered(selector)
|
421
|
+
lambdas << func
|
422
|
+
end
|
423
|
+
|
424
|
+
# Depending on mode, we need to execute something slightly different
|
425
|
+
case options[:mode]
|
426
|
+
when :match_all
|
427
|
+
return options, lambda {
|
428
|
+
all = []
|
429
|
+
lambdas.each do |func|
|
430
|
+
res = func.call
|
431
|
+
if 0 == res.length
|
432
|
+
all = []
|
433
|
+
break
|
434
|
+
end
|
435
|
+
res.each do |e|
|
436
|
+
all << e
|
437
|
+
end
|
438
|
+
end
|
439
|
+
all
|
440
|
+
}
|
441
|
+
when :match_one
|
442
|
+
return options, lambda {
|
443
|
+
res = []
|
444
|
+
lambdas.each do |func|
|
445
|
+
res = func.call
|
446
|
+
# @world.log.debug("Got: #{res}")
|
447
|
+
if res.length > 0
|
448
|
+
break
|
449
|
+
end
|
450
|
+
end
|
451
|
+
res
|
452
|
+
}
|
453
|
+
else
|
454
|
+
options[:message] = optional_message("Invalid mode '#{options[:mode]}' provided to multi_find_all.", options)
|
455
|
+
options[:groups] = ['find', 'multi', 'mode']
|
456
|
+
@world.error(options)
|
457
|
+
end
|
458
|
+
end
|
459
|
+
|
460
|
+
|
461
|
+
def do_throw?(options)
|
462
|
+
ret = options.fetch(:throw, NON_SELECTOR_OPTS[:throw])
|
463
|
+
options.delete(:throw)
|
464
|
+
return ret, options
|
465
|
+
end
|
466
|
+
|
467
|
+
|
468
|
+
def pick_which?(options)
|
469
|
+
ret = options.fetch(:pick, NON_SELECTOR_OPTS[:pick])
|
470
|
+
options.delete(:pick)
|
471
|
+
return ret, options
|
472
|
+
end
|
473
|
+
|
474
|
+
|
475
|
+
def optional_message(message, opts)
|
476
|
+
if opts.has_key? :message
|
477
|
+
return opts[:message]
|
478
|
+
end
|
479
|
+
return message
|
480
|
+
end
|
481
|
+
|
482
|
+
|
483
|
+
def dispatch_call(throw_opt, message, selectors, opts, func)
|
484
|
+
begin
|
485
|
+
ret = func.call
|
486
|
+
|
487
|
+
if throw_opt and (ret.nil? or ret.length <= 0)
|
488
|
+
raise "Cannot find elements with selectors: #{selectors}"
|
489
|
+
end
|
490
|
+
|
491
|
+
return ret
|
492
|
+
rescue RuntimeError => err
|
493
|
+
opts[:message] = optional_message(message, selectors)
|
494
|
+
opts[:exception] = err
|
495
|
+
@world.error(opts)
|
496
|
+
end
|
497
|
+
end
|
498
|
+
end # module Find
|
499
|
+
end # module BrowserModule
|
500
|
+
end # module LapisLazuli
|
@@ -0,0 +1,91 @@
|
|
1
|
+
#
|
2
|
+
# LapisLazuli
|
3
|
+
# https://github.com/spriteCloud/lapis-lazuli
|
4
|
+
#
|
5
|
+
# Copyright (c) 2013-2014 spriteCloud B.V. and other LapisLazuli contributors.
|
6
|
+
# All rights reserved.
|
7
|
+
#
|
8
|
+
|
9
|
+
require 'test/unit/assertions'
|
10
|
+
|
11
|
+
require 'lapis_lazuli/argparse'
|
12
|
+
|
13
|
+
module LapisLazuli
|
14
|
+
module BrowserModule
|
15
|
+
|
16
|
+
##
|
17
|
+
# Module with helper functions to do with DOM element interaction
|
18
|
+
module Interaction
|
19
|
+
include Test::Unit::Assertions
|
20
|
+
include LapisLazuli::ArgParse
|
21
|
+
|
22
|
+
##
|
23
|
+
# Click types
|
24
|
+
DEFAULT_CLICK_TYPES = [ :method, :event, :js ]
|
25
|
+
|
26
|
+
##
|
27
|
+
# Given an element, fires a click event on it.
|
28
|
+
def on_click(elem)
|
29
|
+
elem.fire_event('onClick')
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
##
|
34
|
+
# Given an element, uses JavaScript to click it.
|
35
|
+
def js_click(elem)
|
36
|
+
self.execute_script('arguments[0].click();', elem)
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
##
|
41
|
+
# Combination of elem.click, on_click and js_click: uses the click method
|
42
|
+
# given as the second parameter; may be one of :method, :event, :js.
|
43
|
+
def click_type(elem, type)
|
44
|
+
type = type.to_sym
|
45
|
+
assert DEFAULT_CLICK_TYPES.include?(type), "Not a valid click type: #{type}"
|
46
|
+
|
47
|
+
case type
|
48
|
+
when :method
|
49
|
+
elem.click
|
50
|
+
when :event
|
51
|
+
on_click(elem)
|
52
|
+
when :js
|
53
|
+
js_click(elem)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
|
58
|
+
##
|
59
|
+
# Forces clicking by trying any of the given types of click on the given
|
60
|
+
# element, until one succeeds. If all fail, the corresponding errors are
|
61
|
+
# raised as an Array
|
62
|
+
def click_types(elem, types = DEFAULT_CLICK_TYPES)
|
63
|
+
errors = []
|
64
|
+
types.each do |type|
|
65
|
+
begin
|
66
|
+
click_type(elem, type)
|
67
|
+
rescue RuntimeError => err
|
68
|
+
errors << err
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
if errors.length > 0
|
73
|
+
raise "Could not click #{elem} given any of these click types: #{types}: #{errors}"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
##
|
79
|
+
# Tries the default click types on all elements passed to it.
|
80
|
+
def force_click(*args)
|
81
|
+
elems = make_list_from_nested(args)
|
82
|
+
|
83
|
+
elems.each do |elem|
|
84
|
+
click_types(elem, DEFAULT_CLICK_TYPES)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
end # module Interaction
|
90
|
+
end # module BrowserModule
|
91
|
+
end # module LapisLazuli
|
@@ -0,0 +1,70 @@
|
|
1
|
+
#
|
2
|
+
# LapisLazuli
|
3
|
+
# https://github.com/spriteCloud/lapis-lazuli
|
4
|
+
#
|
5
|
+
# Copyright (c) 2013-2014 spriteCloud B.V. and other LapisLazuli contributors.
|
6
|
+
# All rights reserved.
|
7
|
+
#
|
8
|
+
|
9
|
+
module LapisLazuli
|
10
|
+
module BrowserModule
|
11
|
+
|
12
|
+
##
|
13
|
+
# Screenshot functionality for browser
|
14
|
+
module Screenshots
|
15
|
+
##
|
16
|
+
# Returns the name of the screenshot, if take_screenshot is called now.
|
17
|
+
def screenshot_name(suffix="")
|
18
|
+
dir = @world.env_or_config("screenshot_dir")
|
19
|
+
|
20
|
+
# Generate the file name according to the new or old scheme.
|
21
|
+
name = nil
|
22
|
+
case @world.env_or_config("screenshot_scheme")
|
23
|
+
when "new"
|
24
|
+
# FIXME random makes this non-repeatable, sadly
|
25
|
+
name = "#{@world.scenario.time[:iso_short]}-#{@world.scenario.id}-#{Random.rand(10000).to_s}.png"
|
26
|
+
else # 'old' and default
|
27
|
+
name = @world.scenario.data.name.gsub(/^.*(\\|\/)/, '').gsub(/[^\w\.\-]/, '_').squeeze('_')
|
28
|
+
name = @world.time[:timestamp] + "_" + name + '.png'
|
29
|
+
end
|
30
|
+
|
31
|
+
# Full file location
|
32
|
+
fileloc = "#{dir}#{File::SEPARATOR}#{name}"
|
33
|
+
|
34
|
+
return fileloc
|
35
|
+
end
|
36
|
+
|
37
|
+
##
|
38
|
+
# Taking a screenshot of the current page.
|
39
|
+
# Using the name as defined at the start of every scenario
|
40
|
+
def take_screenshot(suffix="")
|
41
|
+
# If the target directory does not exist, create it.
|
42
|
+
dir = @world.env_or_config("screenshot_dir")
|
43
|
+
begin
|
44
|
+
Dir.mkdir dir
|
45
|
+
rescue SystemCallError => ex
|
46
|
+
# Swallow this error; it occurs (amongst other situations) when the
|
47
|
+
# directory exists. Checking for an existing directory beforehand is
|
48
|
+
# not concurrency safe.
|
49
|
+
end
|
50
|
+
|
51
|
+
fileloc = self.screenshot_name(suffix)
|
52
|
+
|
53
|
+
# Write screenshot
|
54
|
+
begin
|
55
|
+
# Save the screenshot
|
56
|
+
@browser.screenshot.save fileloc
|
57
|
+
@world.log.debug "Screenshot saved: #{fileloc}"
|
58
|
+
|
59
|
+
# Try to store the screenshot name
|
60
|
+
if @world.respond_to? :annotate
|
61
|
+
@world.annotate :screenshot => fileloc
|
62
|
+
end
|
63
|
+
rescue RuntimeError => e
|
64
|
+
@world.log.debug "Failed to save screenshot to '#{fileloc}'. Error message #{e.message}"
|
65
|
+
end
|
66
|
+
return fileloc
|
67
|
+
end
|
68
|
+
end # module Screenshots
|
69
|
+
end # module BrowserModule
|
70
|
+
end # module LapisLazuli
|