lapis_lazuli 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/Gemfile +4 -0
  4. data/Gemfile.lock +42 -0
  5. data/LICENSE +30 -0
  6. data/README.md +74 -0
  7. data/Rakefile +1 -0
  8. data/bin/lapis_lazuli +3 -0
  9. data/lapis_lazuli.gemspec +32 -0
  10. data/lib/lapis_lazuli/api.rb +52 -0
  11. data/lib/lapis_lazuli/argparse.rb +128 -0
  12. data/lib/lapis_lazuli/ast.rb +160 -0
  13. data/lib/lapis_lazuli/browser/error.rb +93 -0
  14. data/lib/lapis_lazuli/browser/find.rb +500 -0
  15. data/lib/lapis_lazuli/browser/interaction.rb +91 -0
  16. data/lib/lapis_lazuli/browser/screenshots.rb +70 -0
  17. data/lib/lapis_lazuli/browser/wait.rb +158 -0
  18. data/lib/lapis_lazuli/browser.rb +246 -0
  19. data/lib/lapis_lazuli/cli.rb +110 -0
  20. data/lib/lapis_lazuli/cucumber.rb +25 -0
  21. data/lib/lapis_lazuli/generators/cucumber/template/.gitignore +6 -0
  22. data/lib/lapis_lazuli/generators/cucumber/template/Gemfile +37 -0
  23. data/lib/lapis_lazuli/generators/cucumber/template/README.md +27 -0
  24. data/lib/lapis_lazuli/generators/cucumber/template/config/config.yml +29 -0
  25. data/lib/lapis_lazuli/generators/cucumber/template/config/cucumber.yml +34 -0
  26. data/lib/lapis_lazuli/generators/cucumber/template/features/example.feature +11 -0
  27. data/lib/lapis_lazuli/generators/cucumber/template/features/step_definitions/interaction_steps.rb +20 -0
  28. data/lib/lapis_lazuli/generators/cucumber/template/features/step_definitions/validation_steps.rb +21 -0
  29. data/lib/lapis_lazuli/generators/cucumber/template/features/support/env.rb +12 -0
  30. data/lib/lapis_lazuli/generators/cucumber/template/features/support/transition.rb +12 -0
  31. data/lib/lapis_lazuli/generators/cucumber.rb +128 -0
  32. data/lib/lapis_lazuli/generic/xpath.rb +49 -0
  33. data/lib/lapis_lazuli/options.rb +28 -0
  34. data/lib/lapis_lazuli/placeholders.rb +36 -0
  35. data/lib/lapis_lazuli/proxy.rb +179 -0
  36. data/lib/lapis_lazuli/runtime.rb +88 -0
  37. data/lib/lapis_lazuli/scenario.rb +88 -0
  38. data/lib/lapis_lazuli/storage.rb +59 -0
  39. data/lib/lapis_lazuli/version.rb +10 -0
  40. data/lib/lapis_lazuli/versions.rb +40 -0
  41. data/lib/lapis_lazuli/world/annotate.rb +45 -0
  42. data/lib/lapis_lazuli/world/api.rb +35 -0
  43. data/lib/lapis_lazuli/world/browser.rb +75 -0
  44. data/lib/lapis_lazuli/world/config.rb +292 -0
  45. data/lib/lapis_lazuli/world/error.rb +141 -0
  46. data/lib/lapis_lazuli/world/hooks.rb +109 -0
  47. data/lib/lapis_lazuli/world/logging.rb +53 -0
  48. data/lib/lapis_lazuli/world/proxy.rb +59 -0
  49. data/lib/lapis_lazuli/world/variable.rb +139 -0
  50. data/lib/lapis_lazuli.rb +75 -0
  51. data/test/.gitignore +8 -0
  52. data/test/Gemfile +42 -0
  53. data/test/README.md +35 -0
  54. data/test/config/config.yml +37 -0
  55. data/test/config/cucumber.yml +37 -0
  56. data/test/features/annotation.feature +23 -0
  57. data/test/features/browser.feature +10 -0
  58. data/test/features/button.feature +38 -0
  59. data/test/features/click.feature +35 -0
  60. data/test/features/error.feature +30 -0
  61. data/test/features/find.feature +92 -0
  62. data/test/features/har.feature +9 -0
  63. data/test/features/modules.feature +14 -0
  64. data/test/features/step_definitions/interaction_steps.rb +154 -0
  65. data/test/features/step_definitions/validation_steps.rb +350 -0
  66. data/test/features/support/env.rb +21 -0
  67. data/test/features/text_field.feature +32 -0
  68. data/test/features/timing.feature +47 -0
  69. data/test/features/variable.feature +11 -0
  70. data/test/features/xpath.feature +41 -0
  71. data/test/server/start.rb +17 -0
  72. data/test/server/www/button.html +22 -0
  73. data/test/server/www/error_html.html +9 -0
  74. data/test/server/www/find.html +66 -0
  75. data/test/server/www/javascript_error.html +12 -0
  76. data/test/server/www/text_fields.html +15 -0
  77. data/test/server/www/timing.html +32 -0
  78. data/test/server/www/xpath.html +22 -0
  79. 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