eyes_core 3.0.4

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