cornucopia 0.1.55 → 0.2.0

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 (37) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +0 -2
  3. data/.ruby-version +1 -0
  4. data/Gemfile +4 -1
  5. data/Gemfile.lock +297 -0
  6. data/cornucopia.gemspec +13 -12
  7. data/lib/cornucopia/capybara/finder_diagnostics/find_action/found_element.rb +155 -0
  8. data/lib/cornucopia/capybara/finder_diagnostics/find_action.rb +417 -0
  9. data/lib/cornucopia/capybara/finder_diagnostics.rb +9 -551
  10. data/lib/cornucopia/capybara/finder_extensions.rb +34 -26
  11. data/lib/cornucopia/capybara/install_extensions.rb +6 -6
  12. data/lib/cornucopia/capybara/matcher_extensions.rb +110 -47
  13. data/lib/cornucopia/capybara/page_diagnostics/window_iterator.rb +42 -0
  14. data/lib/cornucopia/capybara/page_diagnostics.rb +1 -30
  15. data/lib/cornucopia/capybara/synchronizable.rb +1 -1
  16. data/lib/cornucopia/site_prism/class_extensions.rb +13 -0
  17. data/lib/cornucopia/site_prism/element_extensions.rb +7 -4
  18. data/lib/cornucopia/site_prism/install_extensions.rb +1 -1
  19. data/lib/cornucopia/site_prism/page_application.rb +3 -3
  20. data/lib/cornucopia/site_prism/section_extensions.rb +3 -11
  21. data/lib/cornucopia/util/report_builder.rb +1 -1
  22. data/lib/cornucopia/version.rb +1 -1
  23. data/spec/dummy/app/assets/config/manifest.js +2 -0
  24. data/spec/dummy/config/application.rb +1 -1
  25. data/spec/lib/capybara/finder_diagnostics_spec.rb +11 -13
  26. data/spec/lib/capybara/finder_extensions_spec.rb +19 -11
  27. data/spec/lib/capybara/matcher_extensions_spec.rb +13 -15
  28. data/spec/lib/capybara/page_diagnostics_spec.rb +1 -1
  29. data/spec/lib/site_prism/element_extensions_spec.rb +3 -3
  30. data/spec/lib/util/configured_report_spec.rb +4 -4
  31. data/spec/lib/util/log_capture_spec.rb +5 -5
  32. data/spec/lib/util/report_builder_spec.rb +3 -3
  33. data/spec/lib/util/report_table_spec.rb +5 -5
  34. data/spec/pages/cornucopia_report_pages/cornucopia_report_test_contents_page.rb +1 -1
  35. data/spec/sample_report.rb +6 -6
  36. data/spec/spec_helper.rb +5 -1
  37. metadata +37 -10
@@ -0,0 +1,417 @@
1
+ # frozen_string_literal: true
2
+
3
+ require ::File.expand_path("../../util/configuration", File.dirname(__FILE__))
4
+ require ::File.expand_path("../../util/report_builder", File.dirname(__FILE__))
5
+ require ::File.expand_path("find_action/found_element", File.dirname(__FILE__))
6
+
7
+ module Cornucopia
8
+ module Capybara
9
+ class FinderDiagnostics
10
+ # At the end of the day, almost everything in Capybara calls all to find the element that needs
11
+ # to be worked on. The main difference is if it is synchronized or not.
12
+ #
13
+ # The FindAction class also uses all, but it is never synchronized, and its primary purpose
14
+ # is really to output a bunch of diagnostic information to try to help you do a postmortem on
15
+ # just what is happening.
16
+ #
17
+ # A lot of things could be happening, so a lot of information is output, not all of which is
18
+ # relevant or even useful in every situation.
19
+ #
20
+ # The first thing output is the error (the problem)
21
+ # Then what action was being tried is output (context)
22
+ #
23
+ # In case the problem was with finding the desired element, a list of all elements which
24
+ # could be found using the passed in parameters is output.
25
+ #
26
+ # In case the problem was with context (inside a within block on purpose or accidentally)
27
+ # a list of all other elements which could be found on the page using the passed in
28
+ # parameters is output.
29
+ #
30
+ # In case the problem is something else, we output a screenshot and the page HTML.
31
+ #
32
+ # In case the problem has now solved itself, we try the action again. (This has a very low
33
+ # probability of working as this basically devolves to just duplicating what Capybara is already doing,
34
+ # but it seems like it is worth a shot at least...)
35
+ #
36
+ # In case the problem is a driver bug (specifically Selenium which has some known issues) and
37
+ # is in fact why I even bother trying to do this, try performing the action via javascript.
38
+ # NOTE: As noted in many blogs this type of workaround is not generally a good idea as it can
39
+ # result in false-positives. However since Selenium is buggy, this is the
40
+ # best solution I have other than going to capybara-webkit or poltergeist
41
+ class FindAction
42
+ @@diagnosed_finders = {}
43
+
44
+ Cornucopia::Util::ReportBuilder.on_close do
45
+ Cornucopia::Capybara::FinderDiagnostics::FindAction.clear_diagnosed_finders
46
+ end
47
+
48
+ # Clears the class variable @@diagnosed_finders between tests if called.
49
+ # This is done so that finder analysis is called at least once per test.
50
+ def self.clear_diagnosed_finders
51
+ @@diagnosed_finders = {}
52
+ end
53
+
54
+ attr_accessor :return_value
55
+ attr_accessor :support_options
56
+
57
+ def initialize(test_object, report_options, support_options, function_name, *args, **options, &block)
58
+ @test_object = test_object
59
+ @function_name = function_name
60
+ @args = args
61
+ @block = block
62
+ @support_options = support_options
63
+ @options = options
64
+ @report_options = report_options || {}
65
+
66
+ @report_options[:report] ||= Cornucopia::Util::ReportBuilder.current_report
67
+ end
68
+
69
+ def run
70
+ begin
71
+ simple_run
72
+ rescue
73
+ error = $!
74
+ if perform_analysis(support_options[:__cornucopia_retry_with_found])
75
+ # Cornucopia::Util::Configuration.alternate_retry)
76
+ @return_value
77
+ else
78
+ raise error
79
+ end
80
+ end
81
+ end
82
+
83
+ def simple_run(cornucopia_args = {})
84
+ simple_run_args = args.clone
85
+ simple_run_options = options.clone.merge(cornucopia_args)
86
+
87
+ test_object.send(function_name, *simple_run_args, **simple_run_options)
88
+ end
89
+
90
+ # def can_dump_details?(attempt_retry, attempt_alternate_retry)
91
+ def can_dump_details?(attempt_retry)
92
+ can_dump = false
93
+
94
+ if (Object.const_defined?("Capybara"))
95
+ can_dump = !@@diagnosed_finders.keys.include?(dump_detail_args(attempt_retry))
96
+ end
97
+
98
+ can_dump
99
+ end
100
+
101
+ def capybara_session
102
+ if Object.const_defined?("::Capybara") &&
103
+ ::Capybara.send(:session_pool).present?
104
+ my_page = ::Capybara.current_session
105
+
106
+ my_page if (my_page && my_page.current_url.present? && my_page.current_url != "about:blank")
107
+ end
108
+ rescue StandardError
109
+ nil
110
+ end
111
+
112
+ # def dump_detail_args(attempt_retry, attempt_alternate_retry)
113
+ def dump_detail_args(attempt_retry)
114
+ check_args = search_args.clone
115
+ my_page = capybara_session
116
+
117
+ check_args << options.clone
118
+ check_args << !!attempt_retry
119
+ # check_args << !!attempt_alternate_retry
120
+
121
+ if (my_page && my_page.current_url.present? && my_page.current_url != "about:blank")
122
+ check_args << Digest::SHA2.hexdigest(my_page.html)
123
+ end
124
+
125
+ check_args
126
+ end
127
+
128
+ # def perform_retry(attempt_retry, attempt_alternate_retry, report, report_table)
129
+ def perform_retry(attempt_retry, report, report_table)
130
+ retry_successful = false
131
+
132
+ if attempt_retry && retry_action_with_found_element(report, report_table)
133
+ retry_successful = true
134
+ # else
135
+ # if attempt_alternate_retry && alternate_action_with_found_element(report, report_table)
136
+ # retry_successful = true
137
+ # end
138
+ end
139
+
140
+ retry_successful
141
+ end
142
+
143
+ # def perform_analysis(attempt_retry, attempt_alternate_retry)
144
+ def perform_analysis(attempt_retry)
145
+ retry_successful = false
146
+
147
+ time = Benchmark.measure do
148
+ puts " Cornucopia::FinderDiagnostics::perform_analysis" if Cornucopia::Util::Configuration.benchmark
149
+
150
+ if can_dump_details?(attempt_retry)
151
+ generate_report "An error occurred while processing \"#{function_name.to_s}\":",
152
+ $! do |report, report_table|
153
+ retry_successful = perform_retry(attempt_retry, report, report_table)
154
+ end
155
+
156
+ dump_args = dump_detail_args(attempt_retry)
157
+ @@diagnosed_finders[dump_args] = { tried: true }
158
+ else
159
+ retry_successful = perform_retry(attempt_retry, nil, nil)
160
+ end
161
+ end
162
+
163
+ puts " Cornucopia::FinderDiagnostics::perform_analysis time: #{time}" if Cornucopia::Util::Configuration.benchmark
164
+
165
+ retry_successful
166
+ end
167
+
168
+ def generate_report(message, error = nil, &block)
169
+ if report_options[:report] && report_options[:table]
170
+ generate_report_in_table report_options[:table], error, &block
171
+ else
172
+ report_options[:report] ||= Cornucopia::Util::ReportBuilder.current_report
173
+ report_options[:table] = nil
174
+
175
+ report_options[:report].within_section(message) do |_report|
176
+ generate_report_in_table report_options[:table], error, &block
177
+ end
178
+ end
179
+ end
180
+
181
+ def generate_report_in_table(table, error = nil, &block)
182
+ report_options[:table] = table
183
+
184
+ init_search_args
185
+ all_elements
186
+ all_other_elements
187
+ guessed_types
188
+
189
+ configured_report = Cornucopia::Util::Configuration.report_configuration(:capybara_finder_diagnostics)
190
+
191
+ configured_report.add_report_objects(finder: self, exception: error)
192
+ configured_report.generate_report(report_options[:report], report_table: report_options[:table], &block)
193
+ end
194
+
195
+ def retry_action_with_found_element report, report_table
196
+ return_result = false
197
+ result = "Failed"
198
+
199
+ case function_name.to_s
200
+ when "assert_selector"
201
+ if found_element
202
+ @return_value = true
203
+ return_result = true
204
+ result = "Found"
205
+ end
206
+
207
+ when "assert_no_selector"
208
+ unless found_element
209
+ @return_value = true
210
+ return_result = true
211
+ result = "Not Found"
212
+ end
213
+
214
+ when "find", "all"
215
+ if found_element
216
+ result = "Success"
217
+
218
+ @return_value = found_element
219
+
220
+ return_result = true
221
+ end
222
+ end
223
+
224
+ report_table.write_stats "Retrying action:", result if report_table && return_result
225
+
226
+ return_result
227
+ end
228
+
229
+ # def alternate_action_with_found_element report, report_table
230
+ # return_result = false
231
+ #
232
+ # result = "Could not attempt to try the action through an alternate method."
233
+ # if found_element &&
234
+ # ::Capybara.current_session.respond_to?(:evaluate_script)
235
+ # begin
236
+ # native_id = get_attribute found_element, "id"
237
+ # if (native_id)
238
+ # case function_name.to_s
239
+ # when "click_link_or_button", "click_link", "click_button"
240
+ # @return_value = ::Capybara.current_session.evaluate_script("$(\"\##{native_id}\")[0].click()")
241
+ # when "fill_in"
242
+ # @return_value = ::Capybara.current_session.evaluate_script("$(\"\##{native_id}\")[0].val(\"#{options[:with]}\")")
243
+ # when "choose", "check"
244
+ # @return_value = ::Capybara.current_session.evaluate_script("$(\"\##{native_id}\")[0].val(\"checked\", true)")
245
+ # when "uncheck"
246
+ # @return_value = ::Capybara.current_session.evaluate_script("$(\"\##{native_id}\")[0].val(\"checked\", false)")
247
+ # when "select"
248
+ # @return_value = ::Capybara.current_session.evaluate_script("$(\"\##{native_id}\")[0].val(\"selected\", true)")
249
+ # when "unselect"
250
+ # @return_value = ::Capybara.current_session.evaluate_script("$(\"\##{native_id}\")[0].val(\"selected\", false)")
251
+ # else
252
+ # result = "Could not decide what to do with #{function_name}"
253
+ # raise new Exception("unknown action")
254
+ # end
255
+ #
256
+ # return_result = true
257
+ # end
258
+ # rescue
259
+ # result ||= "Still couldn't do the action - #{$!.to_s}."
260
+ # end
261
+ # end
262
+ #
263
+ # report_table.write_stats "Trying alternate action:", result if report_table
264
+ # return_result
265
+ # end
266
+
267
+ def found_element
268
+ if function_name.to_sym == :all
269
+ all_elements.map(&:found_element)
270
+ else
271
+ if all_elements && all_elements.length == 1
272
+ all_elements[0].try(:found_element)
273
+ else
274
+ nil
275
+ end
276
+ end
277
+ end
278
+
279
+ def all_elements
280
+ unless @all_elements
281
+ all_options = options.clone
282
+
283
+ if all_options && all_options.has_key?(:from)
284
+ from_within = nil
285
+
286
+ report_options[:report].within_table(report_table: report_options[:table]) do |report_table|
287
+ Cornucopia::Util::ReportTable.new(nested_table: report_table,
288
+ nested_table_label: "Within block:") do |sub_report|
289
+ sub_report_options = report_options.clone
290
+ sub_report_options[:table] = sub_report
291
+ from_within = Cornucopia::Capybara::FinderDiagnostics::FindAction.
292
+ new(test_object,
293
+ sub_report_options,
294
+ {},
295
+ :find,
296
+ :select,
297
+ all_options[:from])
298
+
299
+ from_within.generate_report_in_table(sub_report, nil)
300
+ end
301
+ end
302
+
303
+ from_element = from_within.found_element
304
+ if search_args[0].is_a?(Symbol)
305
+ search_args[0] = :option
306
+ end
307
+ all_options.delete(:from)
308
+
309
+ unless from_element
310
+ @all_elements = []
311
+ return @all_elements
312
+ end
313
+ else
314
+ from_element = test_object
315
+ end
316
+
317
+ begin
318
+ @all_elements = from_element.all(*search_args, **all_options.merge(visible: false, __cornucopia_no_analysis: true)).to_a
319
+ rescue
320
+ @all_elements = []
321
+ end
322
+
323
+ if @all_elements
324
+ @all_elements = @all_elements.map do |element|
325
+ FoundElement.new(element)
326
+ end.compact
327
+ end
328
+ end
329
+
330
+ @all_elements
331
+ end
332
+
333
+ def all_other_elements
334
+ unless @all_other_elements
335
+ from_element = capybara_session
336
+ all_options = options.clone
337
+ all_options.delete :from
338
+
339
+ return unless from_element
340
+
341
+ begin
342
+ @all_other_elements = from_element.all(*search_args, **all_options.merge(visible: false, __cornucopia_no_analysis: true)).to_a
343
+ rescue
344
+ @all_other_elements = []
345
+ end
346
+
347
+ if @all_other_elements
348
+ @all_other_elements = @all_other_elements.map do |element|
349
+ FoundElement.new(element) unless all_elements.include?(element)
350
+ end
351
+
352
+ @all_other_elements = @all_other_elements - @all_elements
353
+ @all_other_elements.compact!
354
+ end
355
+ end
356
+
357
+ @all_other_elements
358
+ end
359
+
360
+ def search_args
361
+ init_search_args
362
+ @search_args
363
+ end
364
+
365
+ def options
366
+ init_search_args
367
+ @options
368
+ end
369
+
370
+ def init_search_args
371
+ unless @search_args
372
+ @search_args = args.clone
373
+
374
+ if guessed_types.length > 0 && @search_args[0] != guessed_types[0]
375
+ @search_args.insert(0, guessed_types[0])
376
+ end
377
+ if guessed_types.length <= 0 && @search_args[0] != ::Capybara.default_selector
378
+ @search_args.insert(0, ::Capybara.default_selector)
379
+ end
380
+ end
381
+ end
382
+
383
+ # a list of guesses as to what kind of object is being searched for
384
+ def guessed_types
385
+ unless @guessed_types
386
+ if search_args.length > 0
387
+ if search_args[0].is_a?(Symbol)
388
+ @guessed_types = [search_args[0]]
389
+ else
390
+ @guessed_types = %i[id css xpath link_or_button fillable_field radio_button checkbox select option
391
+ file_field table field fieldset content].select do |test_type|
392
+ begin
393
+ test_object.all(test_type, *search_args, **options.merge(visible: false, __cornucopia_no_analysis: true)).length > 0
394
+ rescue
395
+ # Normally bad form, but for this function, we just don't want this to throw errors.
396
+ # We are only concerned with whatever actually succeeds.
397
+ false
398
+ end
399
+ end
400
+ end
401
+ end
402
+ end
403
+
404
+ @guessed_types
405
+ end
406
+
407
+ private
408
+
409
+ attr_reader :test_object,
410
+ :function_name,
411
+ :args,
412
+ :block,
413
+ :report_options
414
+ end
415
+ end
416
+ end
417
+ end