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,102 @@
1
+ module Applitools::Images
2
+ # @!visibility private
3
+ class EyesImagesScreenshot < ::Applitools::EyesScreenshot
4
+ SCREENSHOT_AS_IS = Applitools::EyesScreenshot::COORDINATE_TYPES[:screenshot_as_is].freeze
5
+ CONTEXT_RELATIVE = Applitools::EyesScreenshot::COORDINATE_TYPES[:context_relative].freeze
6
+
7
+ def initialize(image, options = {})
8
+ super image
9
+ return if (location = options[:location]).nil?
10
+ Applitools::ArgumentGuard.is_a? location, 'options[:location]', Applitools::Location
11
+ @bounds = Applitools::Region.new location.x, location.y, image.width, image.height
12
+ end
13
+
14
+ def convert_location(location, from, to)
15
+ Applitools::ArgumentGuard.not_nil location, 'location'
16
+ Applitools::ArgumentGuard.not_nil from, 'from'
17
+ Applitools::ArgumentGuard.not_nil to, 'to'
18
+
19
+ Applitools::ArgumentGuard.is_a? location, 'location', Applitools::Location
20
+
21
+ result = Applitools::Location.new location.x, location.y
22
+ return result if from == to
23
+
24
+ case from
25
+ when SCREENSHOT_AS_IS
26
+ raise "Coordinate type conversation error: #{from} -> #{to}" unless to == CONTEXT_RELATIVE
27
+ result.offset bounds
28
+ return result
29
+ when CONTEXT_RELATIVE
30
+ raise "Coordinate type conversation error: #{from} -> #{to}" unless to == SCREENSHOT_AS_IS
31
+ result.offset(Applitools::Location.new(-bounds.x, -bounds.y))
32
+ return result
33
+ else
34
+ raise "Coordinate type conversation error: #{from} -> #{to}"
35
+ end
36
+ end
37
+
38
+ def convert_region_location(region, from, to)
39
+ Applitools::ArgumentGuard.not_nil region, 'region'
40
+ return Core::Region.new(0, 0, 0, 0) if region.empty?
41
+
42
+ Applitools::ArgumentGuard.not_nil from, 'from'
43
+ Applitools::ArgumentGuard.not_nil to, 'to'
44
+
45
+ updated_location = convert_location region.location, from, to
46
+
47
+ Applitools::Region.new updated_location.x, updated_location.y, region.width, region.height
48
+ end
49
+
50
+ def intersected_region(region, from, to = CONTEXT_RELATIVE)
51
+ Applitools::ArgumentGuard.not_nil region, 'region'
52
+ Applitools::ArgumentGuard.not_nil from, 'coordinates Type (from)'
53
+
54
+ return Applitools::Region.new(0, 0, 0, 0) if region.empty?
55
+
56
+ intersected_region = convert_region_location region, from, to
57
+ intersected_region.intersect bounds
58
+ return intersected_region if intersected_region.empty?
59
+
60
+ intersected_region.location = convert_location intersected_region.location, to, from
61
+ intersected_region
62
+ end
63
+
64
+ def location_in_screenshot(location, coordinates_type)
65
+ Applitools::ArgumentGuard.not_nil location, 'location'
66
+ Applitools::ArgumentGuard.not_nil coordinates_type, 'coordinates_type'
67
+ location = convert_location(location, coordinates_type, CONTEXT_RELATIVE)
68
+
69
+ unless bounds.contains? location.left, location.top
70
+ raise Applitools::OutOfBoundsException.new "Location #{location} is not available in screenshot!"
71
+ end
72
+
73
+ convert_location location, CONTEXT_RELATIVE, SCREENSHOT_AS_IS
74
+ end
75
+
76
+ def sub_screenshot(region, coordinates_type, throw_if_clipped)
77
+ Applitools::ArgumentGuard.not_nil region, 'region'
78
+ Applitools::ArgumentGuard.not_nil coordinates_type, 'coordinates_type'
79
+
80
+ sub_screen_region = intersected_region region, coordinates_type, SCREENSHOT_AS_IS
81
+
82
+ if sub_screen_region.empty? || (throw_if_clipped && !region.size_equals?(sub_screen_region))
83
+ Applitools::OutOfBoundsException.new "Region #{sub_screen_region} (#{coordinates_type}) is out of " \
84
+ " screenshot bounds #{bounds}"
85
+ end
86
+
87
+ sub_screenshot_image = Applitools::Screenshot.new image.crop(sub_screen_region.left, sub_screen_region.top,
88
+ sub_screen_region.width, sub_screen_region.height).to_datastream.to_blob
89
+
90
+ relative_sub_screenshot_region = convert_region_location(sub_screen_region, SCREENSHOT_AS_IS, CONTEXT_RELATIVE)
91
+
92
+ Applitools::Images::EyesImagesScreenshot.new sub_screenshot_image,
93
+ location: relative_sub_screenshot_region.location
94
+ end
95
+
96
+ private
97
+
98
+ def bounds
99
+ @bounds ||= Applitools::Region.new(0, 0, image.width, image.height)
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,23 @@
1
+ # @!visibility private
2
+ module Applitools::MethodTracer
3
+ def self.included(base)
4
+ instance_methods = base.instance_methods(false) + base.private_instance_methods(false)
5
+ class_methods = base.methods(false)
6
+
7
+ base.class_eval do
8
+ def self.trace_method(base, method_name, instance = true)
9
+ original_method = instance ? instance_method(method_name) : method(method_name)
10
+
11
+ send(instance ? :define_method : :define_singleton_method, method_name) do |*args, &block|
12
+ Applitools::EyesLogger.debug "-> #{base}##{method_name}"
13
+ return_value = (instance ? original_method.bind(self) : original_method).call(*args, &block)
14
+ Applitools::EyesLogger.debug "<- #{base}##{method_name}"
15
+ return_value
16
+ end
17
+ end
18
+
19
+ instance_methods.each { |method_name| trace_method(base, method_name) }
20
+ class_methods.each { |method_name| trace_method(base, method_name, false) }
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,2 @@
1
+ require_relative 'capybara' if defined? Capybara
2
+ Applitools.require_dir 'selenium/sauce'
@@ -0,0 +1,348 @@
1
+ module Applitools::Utils
2
+ module EyesSeleniumUtils
3
+ extend self
4
+
5
+ # @!visibility private
6
+ JS_GET_VIEWPORT_SIZE = <<-JS.freeze
7
+ return (function() {
8
+ var height = undefined;
9
+ var width = undefined;
10
+ if (window.innerHeight) {height = window.innerHeight;}
11
+ else if (document.documentElement && document.documentElement.clientHeight)
12
+ {height = document.documentElement.clientHeight;}
13
+ else { var b = document.getElementsByTagName('body')[0];
14
+ if (b.clientHeight) {height = b.clientHeight;}
15
+ };
16
+
17
+ if (window.innerWidth) {width = window.innerWidth;}
18
+ else if (document.documentElement && document.documentElement.clientWidth)
19
+ {width = document.documentElement.clientWidth;}
20
+ else { var b = document.getElementsByTagName('body')[0];
21
+ if (b.clientWidth) {width = b.clientWidth;}
22
+ };
23
+ return [width, height];
24
+ }());
25
+ JS
26
+
27
+ # @!visibility private
28
+ JS_GET_USER_AGENT = <<-JS.freeze
29
+ return navigator.userAgent;
30
+ JS
31
+
32
+ # @!visibility private
33
+ JS_GET_DEVICE_PIXEL_RATIO = <<-JS.freeze
34
+ return window.devicePixelRatio;
35
+ JS
36
+
37
+ # @!visibility private
38
+ JS_GET_PAGE_METRICS = <<-JS.freeze
39
+ return {
40
+ scrollWidth: document.documentElement.scrollWidth,
41
+ bodyScrollWidth: document.body.scrollWidth,
42
+ clientHeight: document.documentElement.clientHeight,
43
+ bodyClientHeight: document.body.clientHeight,
44
+ scrollHeight: document.documentElement.scrollHeight,
45
+ bodyScrollHeight: document.body.scrollHeight
46
+ };
47
+ JS
48
+
49
+ # @!visibility private
50
+ JS_GET_CONTENT_ENTIRE_SIZE = <<-JS.freeze
51
+ var scrollWidth = document.documentElement.scrollWidth;
52
+ var bodyScrollWidth = document.body.scrollWidth;
53
+ var totalWidth = Math.max(scrollWidth, bodyScrollWidth);
54
+ var clientHeight = document.documentElement.clientHeight;
55
+ var bodyClientHeight = document.body.clientHeight;
56
+ var scrollHeight = document.documentElement.scrollHeight;
57
+ var bodyScrollHeight = document.body.scrollHeight;
58
+ var maxDocElementHeight = Math.max(clientHeight, scrollHeight);
59
+ var maxBodyHeight = Math.max(bodyClientHeight, bodyScrollHeight);
60
+ var totalHeight = Math.max(maxDocElementHeight, maxBodyHeight);
61
+ return [totalWidth, totalHeight];
62
+ JS
63
+
64
+ # @!visibility private
65
+ JS_GET_CURRENT_SCROLL_POSITION = <<-JS.freeze
66
+ return (function() {
67
+ var doc = document.documentElement;
68
+ var x = (window.scrollX || window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0);
69
+ var y = (window.scrollY || window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0);
70
+
71
+ return {left: parseInt(x, 10) || 0, top: parseInt(y, 10) || 0};
72
+ }());
73
+ JS
74
+
75
+ # @!visibility private
76
+ JS_SCROLL_TO = <<-JS.freeze
77
+ window.scrollTo(%{left}, %{top});
78
+ JS
79
+
80
+ # @!visibility private
81
+ JS_GET_CURRENT_TRANSFORM = <<-JS.freeze
82
+ return document.body.style.transform;
83
+ JS
84
+
85
+ # @!visibility private
86
+ JS_SET_TRANSFORM = <<-JS.freeze
87
+ return (function() {
88
+ var originalTransform = document.body.style.transform;
89
+ document.body.style.transform = '%{transform}';
90
+ return originalTransform;
91
+ }());
92
+ JS
93
+
94
+ # @!visibility private
95
+ JS_SET_OVERFLOW = <<-JS.freeze
96
+ return (function() {
97
+ var origOF = document.documentElement.style.overflow;
98
+ document.documentElement.style.overflow = '%{overflow}';
99
+ return origOF;
100
+ }());
101
+ JS
102
+
103
+ JS_GET_TRANSFORM_VALUE = <<-JS.freeze
104
+ document.documentElement.style['%{key}']
105
+ JS
106
+
107
+ JS_SET_TRANSFORM_VALUE = <<-JS.freeze
108
+ document.documentElement.style['%{key}'] = '%{value}'
109
+ JS
110
+
111
+ JS_TRANSFORM_KEYS = ['transform', '-webkit-transform'].freeze
112
+
113
+ # @!visibility private
114
+ OVERFLOW_HIDDEN = 'hidden'.freeze
115
+
116
+ BROWSER_SIZE_CALCULATION_RETRIES = 3
117
+
118
+ # Number of attemts to set browser size
119
+ VERIFY_RETRIES = 3
120
+
121
+ # A time delay (in seconds) before next attempt to set browser size
122
+ VERIFY_SLEEP_PERIOD = 1
123
+
124
+ # Maximum different (in pixels) between calculated browser size and real browser size when it tries to achieve
125
+ # target size incrementally
126
+ MAX_DIFF = 3
127
+
128
+ # true if test is running on mobile device
129
+ def mobile_device?
130
+ return $driver if $driver && $driver.is_a?(Appium::Driver)
131
+ nil
132
+ end
133
+
134
+ # true if test is running on Android device
135
+ def android?(driver)
136
+ driver.respond_to?(:appium_device) && driver.appium_device == :android
137
+ end
138
+
139
+ # true if test is running on iOS device
140
+ def ios?(driver)
141
+ driver.respond_to?(:appium_device) && driver.appium_device == :ios
142
+ end
143
+
144
+ # @param [Applitools::Selenium::Driver] driver
145
+ def platform_version(driver)
146
+ driver.respond_to?(:caps) && driver.caps[:platformVersion]
147
+ end
148
+
149
+ # @param [Applitools::Selenium::Driver] executor
150
+ # @return [Applitools::Location] {Applitools::Location} instance which indicates current scroll
151
+ # position
152
+ def current_scroll_position(executor)
153
+ position = Applitools::Utils.symbolize_keys executor.execute_script(JS_GET_CURRENT_SCROLL_POSITION).to_hash
154
+ Applitools::Location.new position[:left], position[:top]
155
+ end
156
+
157
+ # scrolls browser to position specified by point.
158
+ # @param [Applitools::Selenium::Driver] executor
159
+ # @param [Applitools::Location] point position to scroll to. It can be any object,
160
+ # having left and top properties
161
+ def scroll_to(executor, point)
162
+ with_timeout(0.25) { executor.execute_script(JS_SCROLL_TO % { left: point.left, top: point.top }) }
163
+ end
164
+
165
+ # @param [Applitools::Selenium::Driver] executor
166
+ def extract_viewport_size(executor)
167
+ Applitools::EyesLogger.debug 'extract_viewport_size()'
168
+
169
+ begin
170
+ width, height = executor.execute_script(JS_GET_VIEWPORT_SIZE)
171
+ result = Applitools::RectangleSize.from_any_argument width: width, height: height
172
+ Applitools::EyesLogger.debug "Viewport size is #{result}."
173
+ return result
174
+ rescue => e
175
+ Applitools::EyesLogger.error "Failed extracting viewport size using JavaScript: (#{e.message})"
176
+ end
177
+
178
+ Applitools::EyesLogger.info 'Using window size as viewport size.'
179
+
180
+ width, height = executor.manage.window.size.to_a
181
+ width, height = height, width if executor.landscape_orientation? && height > width
182
+
183
+ result = Applitools::RectangleSize.new width, height
184
+ Applitools::EyesLogger.debug "Viewport size is #{result}."
185
+ result
186
+ end
187
+
188
+ # @param [Applitools::Selenium::Driver] executor
189
+ def entire_page_size(executor)
190
+ metrics = page_metrics(executor)
191
+ max_document_element_height = [metrics[:client_height], metrics[:scroll_height]].max
192
+ max_body_height = [metrics[:body_client_height], metrics[:body_scroll_height]].max
193
+
194
+ total_width = [metrics[:scroll_width], metrics[:body_scroll_width]].max
195
+ total_height = [max_document_element_height, max_body_height].max
196
+
197
+ Applitools::RectangleSize.new(total_width, total_height)
198
+ end
199
+
200
+ def current_frame_content_entire_size(executor)
201
+ dimensions = executor.execute_script(JS_GET_CONTENT_ENTIRE_SIZE)
202
+ Applitools::RectangleSize.new(dimensions.first.to_i, dimensions.last.to_i)
203
+ rescue
204
+ raise Applitools::EyesDriverOperationException.new 'Failed to extract entire size!'
205
+ end
206
+
207
+ def current_transforms(executor)
208
+ script =
209
+ "return { #{JS_TRANSFORM_KEYS.map { |tk| "'#{tk}': #{JS_GET_TRANSFORM_VALUE % { key: tk }}" }.join(', ')} };"
210
+ executor.execute_script(script)
211
+ end
212
+
213
+ def set_current_transforms(executor, transform)
214
+ value = {}
215
+ JS_TRANSFORM_KEYS.map { |tk| value[tk] = transform }
216
+ set_transforms(executor, value)
217
+ end
218
+
219
+ def set_transforms(executor, value)
220
+ script = value.keys.map { |k| JS_SET_TRANSFORM_VALUE % { key: k, value: value[k] } }.join('; ')
221
+ executor.execute_script(script)
222
+ end
223
+
224
+ def translate_to(executor, location)
225
+ set_current_transforms(executor, "translate(-#{location.x}px, -#{location.y}px)")
226
+ end
227
+
228
+ # @param [Applitools::Selenium::Driver] executor
229
+ def device_pixel_ratio(executor)
230
+ executor.execute_script(JS_GET_DEVICE_PIXEL_RATIO)
231
+ end
232
+
233
+ # @param [Applitools::Selenium::Driver] executor
234
+ def page_metrics(executor)
235
+ Applitools::Utils.underscore_hash_keys(executor.execute_script(JS_GET_PAGE_METRICS))
236
+ end
237
+
238
+ # @param [Applitools::Selenium::Driver] executor
239
+ def hide_scrollbars(executor)
240
+ set_overflow executor, OVERFLOW_HIDDEN
241
+ end
242
+
243
+ # @param [Applitools::Selenium::Driver] executor
244
+ def set_overflow(executor, overflow)
245
+ with_timeout(0.1) { executor.execute_script(JS_SET_OVERFLOW % { overflow: overflow }) }
246
+ end
247
+
248
+ # @param [Applitools::Selenium::Driver] executor
249
+ # @param [Applitools::RectangleSize] viewport_size
250
+ def set_viewport_size(executor, viewport_size)
251
+ Applitools::ArgumentGuard.not_nil 'viewport_size', viewport_size
252
+ Applitools::EyesLogger.info "Set viewport size #{viewport_size}"
253
+
254
+ required_size = Applitools::RectangleSize.from_any_argument viewport_size
255
+ actual_viewport_size = Applitools::RectangleSize.from_any_argument(extract_viewport_size(executor))
256
+
257
+ Applitools::EyesLogger.info "Initial viewport size: #{actual_viewport_size}"
258
+
259
+ if actual_viewport_size == required_size
260
+ Applitools::EyesLogger.info 'Required size is already set.'
261
+ return
262
+ end
263
+
264
+ # Before resizing the window, set its position to the upper left corner (otherwise, there might not be enough
265
+ # "space" below/next to it and the operation won't be successful).
266
+ begin
267
+ executor.manage.window.position = Selenium::WebDriver::Point.new(0, 0)
268
+ rescue Selenium::WebDriver::Error::UnsupportedOperationError => e
269
+ Applitools::EyesLogger.error e.message << '\n Continue...'
270
+ end
271
+
272
+ set_browser_size_by_viewport_size(executor, actual_viewport_size, required_size)
273
+
274
+ actual_viewport_size = extract_viewport_size(executor)
275
+ return if actual_viewport_size == required_size
276
+
277
+ # Additional attempt. This Solves the "maximized browser" bug
278
+ # (border size for maximized browser sometimes different than
279
+ # non-maximized, so the original browser size calculation is
280
+ # wrong).
281
+
282
+ Applitools::EyesLogger.info 'Trying workaround for maximization...'
283
+
284
+ set_browser_size_by_viewport_size(executor, actual_viewport_size, required_size)
285
+
286
+ actual_viewport_size = extract_viewport_size(executor)
287
+ Applitools::EyesLogger.info "Current viewport size: #{actual_viewport_size}"
288
+ return if actual_viewport_size == required_size
289
+
290
+ width_diff = actual_viewport_size.width - required_size.width
291
+ width_step = width_diff > 0 ? -1 : 1
292
+ height_diff = actual_viewport_size.height - required_size.height
293
+ height_step = height_diff > 0 ? -1 : 1
294
+
295
+ browser_size = Applitools::RectangleSize.from_any_argument(executor.manage.window.size)
296
+
297
+ current_width_change = 0
298
+ current_height_change = 0
299
+
300
+ if width_diff.abs <= MAX_DIFF && height_diff <= MAX_DIFF
301
+ Applitools::EyesLogger.info 'Trying workaround for zoom...'
302
+ while current_width_change.abs <= width_diff && current_height_change.abs <= height_diff
303
+
304
+ current_width_change += width_step if actual_viewport_size.width != required_size.width
305
+ current_height_change += height_step if actual_viewport_size.height != required_size.height
306
+
307
+ set_browser_size executor,
308
+ browser_size.dup + Applitools::RectangleSize.new(current_width_change, current_height_change)
309
+
310
+ actual_viewport_size = Applitools::RectangleSize.from_any_argument extract_viewport_size(executor)
311
+ Applitools::EyesLogger.info "Current viewport size: #{actual_viewport_size}"
312
+ return if actual_viewport_size == required_size
313
+ end
314
+ Applitools::EyesLogger.error 'Zoom workaround failed.'
315
+ end
316
+
317
+ raise Applitools::TestFailedError.new 'Failed to set viewport size'
318
+ end
319
+
320
+ def set_browser_size(executor, required_size)
321
+ retries_left = VERIFY_RETRIES
322
+ current_size = Applitools::RectangleSize.new(0, 0)
323
+ while retries_left > 0 && current_size != required_size
324
+ Applitools::EyesLogger.info "Trying to set browser size to #{required_size}"
325
+ executor.manage.window.size = required_size
326
+ sleep VERIFY_SLEEP_PERIOD
327
+ current_size = Applitools::RectangleSize.from_any_argument(executor.manage.window.size)
328
+ Applitools::EyesLogger.info "Current browser size: #{required_size}"
329
+ retries_left -= 1
330
+ end
331
+ current_size == required_size
332
+ end
333
+
334
+ def set_browser_size_by_viewport_size(executor, actual_viewport_size, required_size)
335
+ browser_size = Applitools::RectangleSize.from_any_argument(executor.manage.window.size)
336
+ Applitools::EyesLogger.info "Current browser size: #{browser_size}"
337
+ required_browser_size = browser_size + required_size - actual_viewport_size
338
+ set_browser_size(executor, required_browser_size)
339
+ end
340
+
341
+ private
342
+
343
+ def with_timeout(timeout, &_block)
344
+ raise 'You have to pass block to method with_timeout' unless block_given?
345
+ yield.tap { sleep timeout }
346
+ end
347
+ end
348
+ end