eyes_core 3.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/ext/eyes_core/extconf.rb +3 -0
  3. data/ext/eyes_core/eyes_core.c +80 -0
  4. data/ext/eyes_core/eyes_core.h +24 -0
  5. data/lib/applitools/capybara.rb +8 -0
  6. data/lib/applitools/chunky_png/resampling.rb +148 -0
  7. data/lib/applitools/chunky_png_patch.rb +8 -0
  8. data/lib/applitools/connectivity/proxy.rb +3 -0
  9. data/lib/applitools/connectivity/server_connector.rb +118 -0
  10. data/lib/applitools/core/app_environment.rb +29 -0
  11. data/lib/applitools/core/app_output.rb +17 -0
  12. data/lib/applitools/core/app_output_with_screenshot.rb +22 -0
  13. data/lib/applitools/core/argument_guard.rb +35 -0
  14. data/lib/applitools/core/batch_info.rb +18 -0
  15. data/lib/applitools/core/eyes_base.rb +463 -0
  16. data/lib/applitools/core/eyes_screenshot.rb +35 -0
  17. data/lib/applitools/core/fixed_cut_provider.rb +61 -0
  18. data/lib/applitools/core/fixed_scale_provider.rb +14 -0
  19. data/lib/applitools/core/helpers.rb +18 -0
  20. data/lib/applitools/core/location.rb +84 -0
  21. data/lib/applitools/core/match_result.rb +16 -0
  22. data/lib/applitools/core/match_results.rb +9 -0
  23. data/lib/applitools/core/match_window_data.rb +34 -0
  24. data/lib/applitools/core/match_window_task.rb +86 -0
  25. data/lib/applitools/core/mouse_trigger.rb +39 -0
  26. data/lib/applitools/core/rectangle_size.rb +46 -0
  27. data/lib/applitools/core/region.rb +180 -0
  28. data/lib/applitools/core/screenshot.rb +49 -0
  29. data/lib/applitools/core/session.rb +15 -0
  30. data/lib/applitools/core/session_start_info.rb +33 -0
  31. data/lib/applitools/core/test_results.rb +55 -0
  32. data/lib/applitools/core/text_trigger.rb +24 -0
  33. data/lib/applitools/core/trigger.rb +8 -0
  34. data/lib/applitools/extensions.rb +18 -0
  35. data/lib/applitools/eyes_logger.rb +45 -0
  36. data/lib/applitools/images/eyes.rb +204 -0
  37. data/lib/applitools/images/eyes_images_screenshot.rb +102 -0
  38. data/lib/applitools/method_tracer.rb +23 -0
  39. data/lib/applitools/sauce.rb +2 -0
  40. data/lib/applitools/utils/eyes_selenium_utils.rb +348 -0
  41. data/lib/applitools/utils/image_delta_compressor.rb +146 -0
  42. data/lib/applitools/utils/image_utils.rb +146 -0
  43. data/lib/applitools/utils/utils.rb +68 -0
  44. data/lib/applitools/version.rb +3 -0
  45. data/lib/eyes_core.rb +70 -0
  46. metadata +273 -0
@@ -0,0 +1,22 @@
1
+ module Applitools
2
+ class AppOutputWithScreenshot
3
+ attr_reader :app_output, :screenshot
4
+
5
+ def initialize(app_output, screenshot)
6
+ raise Applitools::EyesIllegalArgument.new 'app_output is not kind of Applitools::AppOutput' unless
7
+ app_output.is_a? Applitools::AppOutput
8
+ raise Applitools::EyesIllegalArgument.new 'screenshot is not kind of Applitools::EyesScreenshot' unless
9
+ screenshot.is_a? Applitools::EyesScreenshot
10
+ @app_output = app_output
11
+ @screenshot = screenshot
12
+ end
13
+
14
+ def to_hash
15
+ app_output.to_hash
16
+ end
17
+
18
+ def to_s
19
+ app_output.to_hash.to_s
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,35 @@
1
+ module Applitools
2
+ module ArgumentGuard
3
+ extend self
4
+ def not_nil(param, param_name)
5
+ raise Applitools::EyesIllegalArgument.new "#{param_name} is nil!" if param.nil?
6
+ end
7
+
8
+ def hash(param, param_name, required_fields = [])
9
+ if param.is_a? Hash
10
+ missed_keys = required_fields - param.keys
11
+ error_message = "Expected #{param_name} to include keys #{missed_keys.join ', '}"
12
+ raise Applitools::EyesIllegalArgument.new error_message if missed_keys.any?
13
+ else
14
+ error_message = "#{param_name} expected to be a Hash"
15
+ end_of_message = required_fields.any? ? " containing keys #{required_fields.join(', ')}." : '.'
16
+ error_message << end_of_message
17
+ raise Applitools::EyesIllegalArgument.new error_message
18
+ end
19
+ end
20
+
21
+ def greater_than_or_equal_to_zero(param, param_name)
22
+ raise Applitools::EyesIllegalArgument.new "#{param_name} < 0" if 0 > param
23
+ end
24
+
25
+ def greater_than_zero(param, param_name)
26
+ raise Applitools::EyesIllegalArgument.new "#{param_name} <= 0" if 0 >= param
27
+ end
28
+
29
+ def is_a?(param, param_name, klass)
30
+ return true if param.is_a? klass
31
+ raise Applitools::EyesIllegalArgument.new "Expected #{param_name} to be" \
32
+ " instance of #{klass}"
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,18 @@
1
+ require 'securerandom'
2
+ module Applitools
3
+ class BatchInfo
4
+ def initialize(name = nil, started_at = Time.now)
5
+ @name = name
6
+ @started_at = started_at
7
+ @id = SecureRandom.uuid
8
+ end
9
+
10
+ def to_hash
11
+ {
12
+ id: @id,
13
+ name: @name,
14
+ started_at: @started_at.iso8601
15
+ }
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,463 @@
1
+ require 'applitools/core/helpers'
2
+ require 'applitools/core/eyes_screenshot'
3
+
4
+ module Applitools
5
+ class EyesBase
6
+ extend Forwardable
7
+ extend Applitools::Helpers
8
+
9
+ DEFAULT_MATCH_TIMEOUT = 2 # seconds
10
+ USE_DEFAULT_TIMEOUT = -1
11
+
12
+ SCREENSHOT_AS_IS = Applitools::EyesScreenshot::COORDINATE_TYPES[:screenshot_as_is].freeze
13
+ CONTEXT_RELATIVE = Applitools::EyesScreenshot::COORDINATE_TYPES[:context_relative].freeze
14
+
15
+ MATCH_LEVEL = {
16
+ none: 'None',
17
+ layout: 'Layout',
18
+ layout2: 'Layout2',
19
+ content: 'Content',
20
+ strict: 'Strict',
21
+ exact: 'Exact'
22
+ }.freeze
23
+
24
+ def_delegators 'Applitools::EyesLogger', :logger, :log_handler, :log_handler=
25
+ def_delegators 'Applitools::Connectivity::ServerConnector', :api_key, :api_key=, :server_url, :server_url=,
26
+ :set_proxy, :proxy, :proxy=
27
+
28
+ # @!attribute [rw] verbose_results
29
+ # If set to true it will display test results in verbose format, including all fields returned by the server
30
+ # Default value is false.
31
+ # @return [boolean] verbose_results flag
32
+
33
+ attr_accessor :app_name, :baseline_name, :branch_name, :parent_branch_name, :batch, :agent_id, :full_agent_id,
34
+ :match_timeout, :save_new_tests, :save_failed_tests, :failure_reports, :default_match_settings, :cut_provider,
35
+ :scale_ratio, :host_os, :host_app, :base_line_name, :position_provider, :viewport_size, :verbose_results
36
+
37
+ abstract_attr_accessor :base_agent_id, :inferred_environment
38
+ abstract_method :capture_screenshot, true
39
+ abstract_method :title, true
40
+ abstract_method :set_viewport_size, true
41
+ abstract_method :get_viewport_size, true
42
+
43
+ def initialize(server_url = nil)
44
+ Applitools::Connectivity::ServerConnector.server_url = server_url
45
+ self.disabled = false
46
+ @viewport_size = nil
47
+ self.match_timeout = DEFAULT_MATCH_TIMEOUT
48
+ self.running_session = nil
49
+ self.save_new_tests = true
50
+ self.save_failed_tests = false
51
+ self.agent_id = nil
52
+ self.last_screenshot = nil
53
+ @user_inputs = UserInputArray.new
54
+ self.app_output_provider = Object.new
55
+ self.verbose_results = false
56
+
57
+ get_app_output_method = ->(r, s) { get_app_output_with_screenshot r, s }
58
+
59
+ app_output_provider.instance_eval do
60
+ define_singleton_method :app_output do |r, s|
61
+ get_app_output_method.call(r, s)
62
+ end
63
+ end
64
+
65
+ self.default_match_settings = MATCH_LEVEL[:exact]
66
+ end
67
+
68
+ def full_agent_id
69
+ if !agent_id.nil? && !agent_id.empty?
70
+ "#{agent_id} [#{base_agent_id}]"
71
+ else
72
+ base_agent_id
73
+ end
74
+ end
75
+
76
+ def disabled=(value)
77
+ @disabled = Applitools::Utils.boolean_value value
78
+ end
79
+
80
+ def disabled?
81
+ @disabled
82
+ end
83
+
84
+ def open?
85
+ @open
86
+ end
87
+
88
+ def app_name
89
+ !current_app_name.nil? && !current_app_name.empty? ? current_app_name : @app_name
90
+ end
91
+
92
+ def abort_if_not_closed
93
+ if disabled?
94
+ logger.info "#{__method__} Ignored"
95
+ return
96
+ end
97
+
98
+ self.open = false
99
+ self.last_screenshot = nil
100
+ clear_user_inputs
101
+
102
+ if running_session.nil?
103
+ logger.info 'Closed'
104
+ return
105
+ end
106
+
107
+ logger.info 'Aborting server session...'
108
+ Applitools::Connectivity::ServerConnector.stop_session(running_session, true, false)
109
+ logger.info '---Test aborted'
110
+
111
+ rescue Applitools::EyesError => e
112
+ logger.error e.messages
113
+
114
+ ensure
115
+ self.running_session = nil
116
+ end
117
+
118
+ def open_base(options)
119
+ if disabled?
120
+ logger.info "#{__method__} Ignored"
121
+ return
122
+ end
123
+
124
+ Applitools::ArgumentGuard.hash options, 'open_base parameter', [:test_name]
125
+ default_options = { session_type: 'SEQUENTAL' }
126
+ options = default_options.merge options
127
+
128
+ if app_name.nil?
129
+ Applitools::ArgumentGuard.not_nil options[:app_name], 'options[:app_name]'
130
+ self.current_app_name = options[:app_name]
131
+ else
132
+ self.current_app_name = app_name
133
+ end
134
+
135
+ Applitools::ArgumentGuard.not_nil options[:test_name], 'options[:test_name]'
136
+ self.test_name = options[:test_name]
137
+ logger.info "Agent = #{full_agent_id}"
138
+ logger.info "openBase(app_name: #{options[:app_name]}, test_name: #{options[:test_name]}," \
139
+ " viewport_size: #{options[:viewport_size]})"
140
+
141
+ raise Applitools::EyesError.new 'API key is missing! Please set it using api_key=' if api_key.nil?
142
+
143
+ if open?
144
+ abort_if_not_closed
145
+ raise Applitools::EyesError.new 'A test is already running'
146
+ end
147
+
148
+ self.viewport_size = options[:viewport_size]
149
+ self.session_type = options[:session_type]
150
+
151
+ self.open = true
152
+ rescue Applitools::EyesError => e
153
+ logger.error e.message
154
+ raise e
155
+ end
156
+
157
+ def check_window_base(region_provider, tag, ignore_mismatch, retry_timeout)
158
+ if disabled?
159
+ logger.info "#{__method__} Ignored"
160
+ result = Applitools::MatchResults.new
161
+ result.as_expected = true
162
+ return result
163
+ end
164
+
165
+ raise Applitools::EyesError.new 'Eyes not open' unless open?
166
+ Applitools::ArgumentGuard.not_nil region_provider, 'region_provider'
167
+
168
+ logger.info "check_window_base(#{region_provider}, #{tag}, #{ignore_mismatch}, #{retry_timeout})"
169
+
170
+ tag = '' if tag.nil?
171
+
172
+ if running_session.nil?
173
+ logger.info 'No running session, calling start session..'
174
+ start_session
175
+ logger.info 'Done!'
176
+ @match_window_task = Applitools::MatchWindowTask.new(
177
+ logger,
178
+ running_session,
179
+ match_timeout,
180
+ app_output_provider
181
+ )
182
+ end
183
+
184
+ logger.info 'Calling match_window...'
185
+ result = @match_window_task.match_window(
186
+ user_inputs: user_inputs,
187
+ last_screenshot: last_screenshot,
188
+ region_provider: region_provider,
189
+ tag: tag,
190
+ should_match_window_run_once_on_timeout: should_match_window_run_once_on_timeout,
191
+ ignore_mismatch: ignore_mismatch,
192
+ retry_timeout: retry_timeout
193
+ )
194
+ logger.info 'match_window done!'
195
+
196
+ if result.as_expected?
197
+ clear_user_inputs
198
+ self.last_screenshot = result.screenshot
199
+ else
200
+ unless ignore_mismatch
201
+ clear_user_inputs
202
+ self.last_screenshot = result.screenshot
203
+ end
204
+
205
+ self.should_match_window_run_once_on_timeout = true
206
+
207
+ logger.info "Mistmatch! #{tag}" unless running_session.new_session?
208
+
209
+ if failure_reports == :immediate
210
+ raise Applitools::TestFailedException.new "Mistmatch found in #{session_start_info.scenario_id_or_name}" \
211
+ " of #{session_start_info.app_id_or_name}"
212
+ end
213
+ end
214
+
215
+ logger.info 'Done!'
216
+ result
217
+ end
218
+
219
+ # Closes eyes
220
+ # @param [Boolean] throw_exception If set to +true+ eyes will trow [Applitools::TestFailedError] exception,
221
+ # otherwise the test will pass. Default is true
222
+
223
+ def close(throw_exception = true)
224
+ if disabled?
225
+ logger.info "#{__method__} Ignored"
226
+ return
227
+ end
228
+
229
+ logger.info "close(#{throw_exception})"
230
+ raise Applitools::EyesError.new 'Eyes not open' unless open?
231
+
232
+ self.open = false
233
+ self.last_screenshot = nil
234
+
235
+ clear_user_inputs
236
+
237
+ unless running_session
238
+ logger.info 'Server session was not started'
239
+ logger.info '--- Empty test ended'
240
+ return Applitools::TestResults.new
241
+ end
242
+
243
+ is_new_session = running_session.new_session?
244
+ session_results_url = running_session.url
245
+
246
+ logger.info 'Ending server session...'
247
+
248
+ save = is_new_session && save_new_tests || !is_new_session && save_failed_tests
249
+
250
+ logger.info "Automatically save test? #{save}"
251
+
252
+ results = Applitools::Connectivity::ServerConnector.stop_session running_session, false, save
253
+
254
+ results.is_new = is_new_session
255
+ results.url = session_results_url
256
+
257
+ logger.info results.to_s(verbose_results)
258
+
259
+ if results.failed?
260
+ logger.error "--- Failed test ended. see details at #{session_results_url}"
261
+ error_message = "#{session_start_info.scenario_id_or_name} of #{session_start_info.app_id_or_name}. " \
262
+ "See details at #{session_results_url}."
263
+ raise Applitools::TestFailedError.new error_message, results if throw_exception
264
+ return results
265
+ end
266
+
267
+ if results.new?
268
+ instructions = "Please approve the new baseline at #{session_results_url}"
269
+ logger.info "--- New test ended. #{instructions}"
270
+ error_message = "#{session_start_info.scenario_id_or_name} of #{session_start_info.app_id_or_name}. " \
271
+ "#{instructions}"
272
+ raise Applitools::TestFailedError.new error_message, results if throw_exception
273
+ return results
274
+ end
275
+
276
+ logger.info '--- Test passed'
277
+ return results
278
+ ensure
279
+ self.running_session = nil
280
+ self.current_app_name = nil
281
+ end
282
+
283
+ private
284
+
285
+ attr_accessor :running_session, :last_screenshot, :current_app_name, :test_name, :session_type,
286
+ :scale_provider, :default_match_settings, :session_start_info,
287
+ :should_match_window_run_once_on_timeout, :app_output_provider
288
+
289
+ attr_reader :user_inputs
290
+
291
+ private :full_agent_id, :full_agent_id=
292
+
293
+ def app_environment
294
+ Applitools::AppEnvironment.new os: host_os, hosting_app: host_app,
295
+ display_size: @viewport_size, inferred: inferred_environment
296
+ end
297
+
298
+ def open=(value)
299
+ @open = Applitools::Utils.boolean_value value
300
+ end
301
+
302
+ def clear_user_inputs
303
+ @user_inputs.clear
304
+ end
305
+
306
+ def add_user_input(trigger)
307
+ if disabled?
308
+ logger.info "#{__method__} Ignored"
309
+ return
310
+ end
311
+
312
+ Applitools::ArgumentGuard.not_nil(trigger, 'trigger')
313
+ @user_inputs.add(trigger)
314
+ end
315
+
316
+ def add_text_trigger_base(control, text)
317
+ if disabled?
318
+ logger.info "#{__method__} Ignored"
319
+ return
320
+ end
321
+
322
+ Applitools::ArgumentGuard.not_nil control, 'control'
323
+ Applitools::ArgumentGuard.not_nil text, 'control'
324
+
325
+ control = Applitools::Region.new control.left, control.top, control.width, control.height
326
+
327
+ if last_screenshot.nil?
328
+ logger.info "Ignoring '#{text}' (no screenshot)"
329
+ return
330
+ end
331
+
332
+ control = last_screenshot.intersected_region control, EyesScreenshot::COORDINATE_TYPES[:context_relative],
333
+ EyesScreenshot::COORDINATE_TYPES[:screenshot_as_is]
334
+
335
+ if control.empty?
336
+ logger.info "Ignoring '#{text}' out of bounds"
337
+ return
338
+ end
339
+
340
+ trigger = Applitools::TextTrigger.new text, control
341
+ add_user_input trigger
342
+ logger.info "Added '#{trigger}'"
343
+ end
344
+
345
+ def add_mouse_trigger_base(action, control, cursor)
346
+ if disabled?
347
+ logger.info "#{__method__} Ignored"
348
+ return
349
+ end
350
+
351
+ Applitools::ArgumentGuard.not_nil action, 'action'
352
+ Applitools::ArgumentGuard.not_nil control, 'control'
353
+ Applitools::ArgumentGuard.not_nil cursor, 'cursor'
354
+
355
+ if last_screenshot.nil?
356
+ logger.info "Ignoring '#{action}' (no screenshot)"
357
+ return
358
+ end
359
+
360
+ cursor_in_screenshot = Applitools::Location.new cursor.x, cursor.y
361
+ cursor_in_screenshot.offset(control)
362
+
363
+ begin
364
+ cursor_in_screenshot = last_screenshot.location_in_screenshot cursor_in_screenshot, CONTEXT_RELATIVE
365
+ rescue Applitools::OutOfBoundsException
366
+ logger.info "Ignoring #{action} (out of bounds)"
367
+ return
368
+ end
369
+
370
+ control_screenshot_intersect = last_screenshot.intersected_region control, CONTEXT_RELATIVE, SCREENSHOT_AS_IS
371
+
372
+ unless control_screenshot_intersect.empty?
373
+ l = control_screenshot_intersect.location
374
+ cursor_in_screenshot.offset Applitools::Location.new(-l.x, -l.y)
375
+ end
376
+
377
+ trigger = Applitools::MouseTrigger.new action, control_screenshot_intersect, cursor_in_screenshot
378
+ add_user_input trigger
379
+
380
+ logger.info "Added #{trigger}"
381
+ end
382
+
383
+ def start_session
384
+ logger.info 'start_session()'
385
+
386
+ if viewport_size
387
+ set_viewport_size(viewport_size)
388
+ else
389
+ self.viewport_size = get_viewport_size
390
+ end
391
+
392
+ if batch.nil?
393
+ logger.info 'No batch set'
394
+ test_batch = BatchInfo.new
395
+ else
396
+ logger.info "Batch is #{batch}"
397
+ test_batch = batch
398
+ end
399
+
400
+ app_env = app_environment
401
+
402
+ logger.info "Application environment is #{app_env}"
403
+
404
+ self.session_start_info = SessionStartInfo.new agent_id: base_agent_id, app_id_or_name: app_name,
405
+ scenario_id_or_name: test_name, batch_info: test_batch,
406
+ env_name: baseline_name, environment: app_env,
407
+ default_match_settings: default_match_settings,
408
+ match_level: default_match_settings,
409
+ branch_name: branch_name, parent_branch_name: parent_branch_name
410
+
411
+ logger.info 'Starting server session...'
412
+ self.running_session = Applitools::Connectivity::ServerConnector.start_session session_start_info
413
+
414
+ logger.info "Server session ID is #{running_session.id}"
415
+ test_info = "'#{test_name}' of '#{app_name}' #{app_env}"
416
+ if running_session.new_session?
417
+ logger.info "--- New test started - #{test_info}"
418
+ self.should_match_window_run_once_on_timeout = true
419
+ else
420
+ logger.info "--- Test started - #{test_info}"
421
+ self.should_match_window_run_once_on_timeout = false
422
+ end
423
+ end
424
+
425
+ def get_app_output_with_screenshot(region_provider, last_screenshot)
426
+ logger.info 'Getting screenshot...'
427
+ screenshot = capture_screenshot
428
+ logger.info 'Done getting screenshot!'
429
+ region = region_provider.region
430
+
431
+ unless region.empty?
432
+ screenshot = screenshot.sub_screenshot region, region_provider.coordinate_type, false
433
+ end
434
+
435
+ logger.info 'Compressing screenshot...'
436
+ compress_result = compress_screenshot64 screenshot, last_screenshot
437
+ logger.info 'Done! Getting title...'
438
+ a_title = title
439
+ logger.info 'Done!'
440
+ Applitools::AppOutputWithScreenshot.new(
441
+ Applitools::AppOutput.new(a_title, compress_result),
442
+ screenshot
443
+ )
444
+ end
445
+
446
+ def compress_screenshot64(screenshot, _last_screenshot)
447
+ screenshot # it is a stub
448
+ end
449
+
450
+ class UserInputArray < Array
451
+ def add(trigger)
452
+ raise Applitools::EyesIllegalArgument.new 'trigger must be kind of Trigger!' unless trigger.is_a? Trigger
453
+ self << trigger
454
+ end
455
+
456
+ def to_hash
457
+ map do |trigger|
458
+ trigger.to_hash if trigger.respond_to? :to_hash
459
+ end.compact
460
+ end
461
+ end
462
+ end
463
+ end