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.
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