eyes_selenium 2.16.0 → 2.17.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +54 -0
- data/.travis.yml +2 -0
- data/Rakefile +3 -0
- data/eyes_selenium.gemspec +3 -2
- data/lib/applitools/base/image_position.rb +10 -0
- data/lib/applitools/base/mouse_trigger.rb +1 -1
- data/lib/applitools/base/point.rb +18 -5
- data/lib/applitools/base/region.rb +44 -9
- data/lib/applitools/base/server_connector.rb +7 -6
- data/lib/applitools/base/test_results.rb +4 -5
- data/lib/applitools/extensions.rb +17 -0
- data/lib/applitools/eyes.rb +40 -32
- data/lib/applitools/eyes_logger.rb +7 -7
- data/lib/applitools/method_tracer.rb +2 -2
- data/lib/applitools/selenium/browser.rb +220 -0
- data/lib/applitools/selenium/driver.rb +61 -59
- data/lib/applitools/selenium/element.rb +10 -5
- data/lib/applitools/selenium/match_window_data.rb +1 -1
- data/lib/applitools/selenium/match_window_task.rb +25 -18
- data/lib/applitools/selenium/mouse.rb +3 -3
- data/lib/applitools/selenium/viewport_size.rb +57 -48
- data/lib/applitools/utils/image_delta_compressor.rb +148 -150
- data/lib/applitools/utils/image_utils.rb +36 -3
- data/lib/applitools/utils/utils.rb +3 -6
- data/lib/applitools/version.rb +1 -1
- data/lib/eyes_selenium.rb +2 -2
- data/spec/driver_passthrough_spec.rb +1 -1
- data/spec/spec_helper.rb +2 -5
- data/test/appium_example_script.rb +15 -15
- data/test/test_script.rb +1 -1
- data/test/watir_test_script.rb +1 -1
- metadata +23 -4
@@ -3,10 +3,10 @@ require 'forwardable'
|
|
3
3
|
|
4
4
|
module Applitools::EyesLogger
|
5
5
|
class NullLogger < Logger
|
6
|
-
def initialize(*
|
6
|
+
def initialize(*_args)
|
7
7
|
end
|
8
8
|
|
9
|
-
def add(*
|
9
|
+
def add(*_args, &_block)
|
10
10
|
end
|
11
11
|
end
|
12
12
|
|
@@ -16,25 +16,25 @@ module Applitools::EyesLogger
|
|
16
16
|
MANDATORY_METHODS = [:debug, :info, :close].freeze
|
17
17
|
OPTIONAL_METHODS = [:warn, :error, :fatal].freeze
|
18
18
|
|
19
|
-
def_delegators
|
19
|
+
def_delegators :@log_handler, *MANDATORY_METHODS
|
20
20
|
|
21
|
-
|
21
|
+
@log_handler = NullLogger.new
|
22
22
|
|
23
23
|
def log_handler=(log_handler)
|
24
24
|
raise Applitools::EyesError.new('log_handler must implement Logger!') unless valid?(log_handler)
|
25
25
|
|
26
|
-
|
26
|
+
@log_handler = log_handler
|
27
27
|
end
|
28
28
|
|
29
29
|
OPTIONAL_METHODS.each do |method|
|
30
30
|
define_singleton_method(method) do |msg|
|
31
|
-
|
31
|
+
@log_handler.respond_to?(method) ? @log_handler.send(method, msg) : @log_handler.info(msg)
|
32
32
|
end
|
33
33
|
end
|
34
34
|
|
35
35
|
private
|
36
36
|
|
37
37
|
def valid?(log_handler)
|
38
|
-
MANDATORY_METHODS.all? {|method| log_handler.respond_to?(method)}
|
38
|
+
MANDATORY_METHODS.all? { |method| log_handler.respond_to?(method) }
|
39
39
|
end
|
40
40
|
end
|
@@ -15,8 +15,8 @@ module Applitools::MethodTracer
|
|
15
15
|
end
|
16
16
|
end
|
17
17
|
|
18
|
-
instance_methods.each {|method_name| trace_method(base, method_name) }
|
19
|
-
class_methods.each {|method_name| trace_method(base, method_name, false) }
|
18
|
+
instance_methods.each { |method_name| trace_method(base, method_name) }
|
19
|
+
class_methods.each { |method_name| trace_method(base, method_name, false) }
|
20
20
|
end
|
21
21
|
end
|
22
22
|
end
|
@@ -0,0 +1,220 @@
|
|
1
|
+
module Applitools::Selenium
|
2
|
+
class Browser
|
3
|
+
JS_GET_USER_AGENT = (<<-JS).freeze
|
4
|
+
return navigator.userAgent;
|
5
|
+
JS
|
6
|
+
|
7
|
+
JS_GET_DEVICE_PIXEL_RATIO = (<<-JS).freeze
|
8
|
+
return window.devicePixelRatio;
|
9
|
+
JS
|
10
|
+
|
11
|
+
JS_GET_PAGE_METRICS = (<<-JS).freeze
|
12
|
+
return {
|
13
|
+
scrollWidth: document.documentElement.scrollWidth,
|
14
|
+
bodyScrollWidth: document.body.scrollWidth,
|
15
|
+
clientHeight: document.documentElement.clientHeight,
|
16
|
+
bodyClientHeight: document.body.clientHeight,
|
17
|
+
scrollHeight: document.documentElement.scrollHeight,
|
18
|
+
bodyScrollHeight: document.body.scrollHeight
|
19
|
+
};
|
20
|
+
JS
|
21
|
+
|
22
|
+
JS_GET_CURRENT_SCROLL_POSITION = (<<-JS).freeze
|
23
|
+
return (function() {
|
24
|
+
var doc = document.documentElement;
|
25
|
+
var x = (window.scrollX || window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0);
|
26
|
+
var y = (window.scrollY || window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0);
|
27
|
+
|
28
|
+
return {left: parseInt(x, 10) || 0, top: parseInt(y, 10) || 0};
|
29
|
+
}());
|
30
|
+
JS
|
31
|
+
|
32
|
+
JS_SCROLL_TO = (<<-JS).freeze
|
33
|
+
window.scrollTo(%{left}, %{top});
|
34
|
+
JS
|
35
|
+
|
36
|
+
JS_GET_CURRENT_TRANSFORM = (<<-JS).freeze
|
37
|
+
return document.body.style.transform;
|
38
|
+
JS
|
39
|
+
|
40
|
+
JS_SET_TRANSFORM = (<<-JS).freeze
|
41
|
+
return (function() {
|
42
|
+
var originalTransform = document.body.style.transform;
|
43
|
+
document.body.style.transform = '%{transform}';
|
44
|
+
return originalTransform;
|
45
|
+
}());
|
46
|
+
JS
|
47
|
+
|
48
|
+
JS_SET_OVERFLOW = (<<-JS).freeze
|
49
|
+
return (function() {
|
50
|
+
var origOF = document.documentElement.style.overflow;
|
51
|
+
document.documentElement.style.overflow = '%{overflow}';
|
52
|
+
return origOF;
|
53
|
+
}());
|
54
|
+
JS
|
55
|
+
|
56
|
+
EPSILON_WIDTH = 12.freeze
|
57
|
+
MIN_SCREENSHOT_PART_HEIGHT = 10.freeze
|
58
|
+
MAX_SCROLLBAR_SIZE = 50.freeze
|
59
|
+
OVERFLOW_HIDDEN = 'hidden'.freeze
|
60
|
+
|
61
|
+
def initialize(driver, eyes)
|
62
|
+
@driver = driver
|
63
|
+
@eyes = eyes
|
64
|
+
end
|
65
|
+
|
66
|
+
def chrome?
|
67
|
+
@driver.browser == :chrome
|
68
|
+
end
|
69
|
+
|
70
|
+
def user_agent
|
71
|
+
return @user_agent if defined?(@user_agent)
|
72
|
+
|
73
|
+
@user_agent = begin
|
74
|
+
execute_script(JS_GET_USER_AGENT).freeze
|
75
|
+
rescue => e
|
76
|
+
Applitools::EyesLogger.error "Failed to obtain user-agent: (#{e.message})"
|
77
|
+
|
78
|
+
nil
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def image_normalization_factor(image)
|
83
|
+
if image.width == @eyes.viewport_size.extract_viewport_from_browser.width ||
|
84
|
+
(image.width - entire_page_size.width).abs <= EPSILON_WIDTH
|
85
|
+
return 1
|
86
|
+
end
|
87
|
+
|
88
|
+
1.to_f / device_pixel_ratio
|
89
|
+
end
|
90
|
+
|
91
|
+
def entire_page_size
|
92
|
+
max_document_element_height = [page_metrics[:client_height], page_metrics[:scroll_height]].max
|
93
|
+
max_body_height = [page_metrics[:body_client_height], page_metrics[:body_scroll_height]].max
|
94
|
+
|
95
|
+
total_width = [page_metrics[:scroll_width], page_metrics[:body_scroll_width]].max
|
96
|
+
total_height = [max_document_element_height, max_body_height].max
|
97
|
+
|
98
|
+
Applitools::Base::Dimension.new(total_width, total_height)
|
99
|
+
end
|
100
|
+
|
101
|
+
def current_scroll_position
|
102
|
+
position = Applitools::Utils.underscore_hash_keys(execute_script(JS_GET_CURRENT_SCROLL_POSITION))
|
103
|
+
Applitools::Base::Point.new(position[:left], position[:top])
|
104
|
+
end
|
105
|
+
|
106
|
+
def scroll_to(point)
|
107
|
+
execute_script(JS_SCROLL_TO % { left: point.left, top: point.top }, 0.25)
|
108
|
+
end
|
109
|
+
|
110
|
+
def current_transform
|
111
|
+
execute_script(JS_GET_CURRENT_TRANSFORM)
|
112
|
+
end
|
113
|
+
|
114
|
+
# rubocop:disable Style/AccessorMethodName
|
115
|
+
def set_transform(transform)
|
116
|
+
execute_script(JS_SET_TRANSFORM % { transform: transform }, 0.25)
|
117
|
+
end
|
118
|
+
|
119
|
+
def set_overflow(overflow)
|
120
|
+
execute_script(JS_SET_OVERFLOW % { overflow: overflow }, 0.1)
|
121
|
+
end
|
122
|
+
# rubocop:enable Style/AccessorMethodName
|
123
|
+
|
124
|
+
def translate_to(point)
|
125
|
+
set_transform("translate(-#{point.left}px, -#{point.top}px)")
|
126
|
+
end
|
127
|
+
|
128
|
+
def fullpage_screenshot
|
129
|
+
# Scroll to the top/left corner of the screen.
|
130
|
+
original_scroll_position = current_scroll_position
|
131
|
+
scroll_to(Applitools::Base::Point::TOP_LEFT)
|
132
|
+
if current_scroll_position != Applitools::Base::Point::TOP_LEFT
|
133
|
+
raise 'Could not scroll to the top/left corner of the screen!'
|
134
|
+
end
|
135
|
+
|
136
|
+
# Translate to top/left of the page (notice this is different from JavaScript scrolling).
|
137
|
+
if @eyes.use_css_transition
|
138
|
+
original_transform = current_transform
|
139
|
+
translate_to(Applitools::Base::Point::TOP_LEFT)
|
140
|
+
end
|
141
|
+
|
142
|
+
# Hide scrollbars.
|
143
|
+
original_overflow = set_overflow(OVERFLOW_HIDDEN) if @eyes.hide_scrollbars
|
144
|
+
|
145
|
+
# Take screenshot of the (0,0) tile.
|
146
|
+
screenshot = @driver.visible_screenshot
|
147
|
+
|
148
|
+
# Normalize screenshot width/height.
|
149
|
+
size_factor = 1
|
150
|
+
page_size = entire_page_size
|
151
|
+
factor = image_normalization_factor(screenshot)
|
152
|
+
if factor == 0.5
|
153
|
+
size_factor = 2
|
154
|
+
page_size.width *= size_factor
|
155
|
+
page_size.height *= size_factor
|
156
|
+
page_size.width = [page_size.width, screenshot.width].max
|
157
|
+
end
|
158
|
+
|
159
|
+
# NOTE: this is required! Since when calculating the screenshot parts for full size, we use a screenshot size
|
160
|
+
# which is a bit smaller (see comment below).
|
161
|
+
if @eyes.force_fullpage_screenshot && (screenshot.width < page_size.width || screenshot.height < page_size.height)
|
162
|
+
# We use a smaller size than the actual screenshot size in order to eliminate duplication of bottom scroll bars,
|
163
|
+
# as well as footer-like elements with fixed position.
|
164
|
+
max_scrollbar_size = @eyes.use_css_transition ? 0 : MAX_SCROLLBAR_SIZE
|
165
|
+
height = [screenshot.height - (max_scrollbar_size * size_factor), MIN_SCREENSHOT_PART_HEIGHT * size_factor].max
|
166
|
+
screenshot_part_size = Applitools::Base::Dimension.new(screenshot.width, height)
|
167
|
+
|
168
|
+
subregios = Applitools::Base::Region.new(0, 0, page_size.width,
|
169
|
+
page_size.height).subregions(screenshot_part_size)
|
170
|
+
parts = subregios.map do |screenshot_part|
|
171
|
+
# Skip (0,0), as we already got the screenshot.
|
172
|
+
if screenshot_part.left == 0 && screenshot_part.top == 0
|
173
|
+
next Applitools::Base::ImagePosition.new(screenshot, Applitools::Base::Point::TOP_LEFT)
|
174
|
+
end
|
175
|
+
|
176
|
+
process_screenshot_part(screenshot_part, size_factor)
|
177
|
+
end
|
178
|
+
|
179
|
+
screenshot = Applitools::Utils::ImageUtils.stitch_images(page_size, parts)
|
180
|
+
end
|
181
|
+
|
182
|
+
set_overflow(original_overflow) if @eyes.hide_scrollbars
|
183
|
+
set_transform(original_transform) if @eyes.use_css_transition
|
184
|
+
|
185
|
+
scroll_to(original_scroll_position)
|
186
|
+
|
187
|
+
screenshot
|
188
|
+
end
|
189
|
+
|
190
|
+
private
|
191
|
+
|
192
|
+
def execute_script(script, stabilization_time = nil)
|
193
|
+
@driver.execute_script(script).tap { sleep(stabilization_time) if stabilization_time }
|
194
|
+
end
|
195
|
+
|
196
|
+
def device_pixel_ratio
|
197
|
+
@device_pixel_ratio ||= execute_script(JS_GET_DEVICE_PIXEL_RATIO).freeze
|
198
|
+
end
|
199
|
+
|
200
|
+
def page_metrics
|
201
|
+
Applitools::Utils.underscore_hash_keys(execute_script(JS_GET_PAGE_METRICS))
|
202
|
+
end
|
203
|
+
|
204
|
+
def process_screenshot_part(part, size_factor)
|
205
|
+
part_coords = Applitools::Base::Point.new(part.left, part.top)
|
206
|
+
part_coords_normalized = Applitools::Base::Point.new(part.left.to_f / size_factor, part.top.to_f / size_factor)
|
207
|
+
|
208
|
+
if @eyes.use_css_transition
|
209
|
+
translate_to(part_coords_normalized)
|
210
|
+
current_position = part_coords
|
211
|
+
else
|
212
|
+
scroll_to(part_coords_normalized)
|
213
|
+
position = current_scroll_position
|
214
|
+
current_position = Applitools::Base::Point.new(position.left * size_factor, position.top * size_factor)
|
215
|
+
end
|
216
|
+
|
217
|
+
Applitools::Base::ImagePosition.new(@driver.visible_screenshot, current_position)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
@@ -9,13 +9,10 @@ module Applitools::Selenium
|
|
9
9
|
include Selenium::WebDriver::DriverExtensions::HasInputDevices
|
10
10
|
|
11
11
|
RIGHT_ANGLE = 90.freeze
|
12
|
-
ANDROID = 'ANDROID'.freeze
|
13
12
|
IOS = 'IOS'.freeze
|
13
|
+
ANDROID = 'ANDROID'.freeze
|
14
14
|
LANDSCAPE = 'LANDSCAPE'.freeze
|
15
15
|
|
16
|
-
IE = 'ie'.freeze
|
17
|
-
FIREFOX = 'firefox'.freeze
|
18
|
-
|
19
16
|
FINDERS = {
|
20
17
|
class: 'class name',
|
21
18
|
class_name: 'class name',
|
@@ -29,9 +26,10 @@ module Applitools::Selenium
|
|
29
26
|
xpath: 'xpath'
|
30
27
|
}.freeze
|
31
28
|
|
32
|
-
|
29
|
+
attr_reader :browser
|
33
30
|
|
34
31
|
def_delegators :@eyes, :user_inputs, :clear_user_inputs
|
32
|
+
def_delegators :@browser, :user_agent
|
35
33
|
|
36
34
|
# If driver is not provided, Applitools::Selenium::Driver will raise an EyesError exception.
|
37
35
|
def initialize(eyes, options)
|
@@ -39,33 +37,9 @@ module Applitools::Selenium
|
|
39
37
|
|
40
38
|
@is_mobile_device = options.fetch(:is_mobile_device, false)
|
41
39
|
@eyes = eyes
|
40
|
+
@browser = Applitools::Selenium::Browser.new(self, @eyes)
|
42
41
|
|
43
|
-
raise '
|
44
|
-
end
|
45
|
-
|
46
|
-
# Rotates the image as necessary. The rotation is either manually forced by passing a value in
|
47
|
-
# the +rotation+ parameter, or automatically inferred if the +rotation+ parameter is +nil+.
|
48
|
-
#
|
49
|
-
# +driver+:: +Applitools::Selenium::Driver+ The driver which produced the screenshot.
|
50
|
-
# +image+:: +ChunkyPNG::Canvas+ The image to normalize.
|
51
|
-
# +rotation+:: +Integer+|+nil+ The degrees by which to rotate the image: positive values = clockwise rotation,
|
52
|
-
# negative values = counter-clockwise, 0 = force no rotation, +nil+ = rotate automatically when needed.
|
53
|
-
def self.normalize_rotation(driver, image, rotation)
|
54
|
-
return if rotation == 0
|
55
|
-
|
56
|
-
num_quadrants = 0
|
57
|
-
if !rotation.nil?
|
58
|
-
if rotation % RIGHT_ANGLE != 0
|
59
|
-
raise Applitools::EyesError.new("Currently only quadrant rotations are supported. Current rotation: "\
|
60
|
-
"#{rotation}")
|
61
|
-
end
|
62
|
-
num_quadrants = (rotation / RIGHT_ANGLE).to_i
|
63
|
-
elsif rotation.nil? && driver.mobile_device? && driver.landscape_orientation? && image.height > image.width
|
64
|
-
# For Android, we need to rotate images to the right, and for iOS to the left.
|
65
|
-
num_quadrants = driver.android? ? 1 : -1
|
66
|
-
end
|
67
|
-
|
68
|
-
Applitools::Utils::ImageUtils.quadrant_rotate!(image, num_quadrants)
|
42
|
+
raise 'Incapable of taking screenshots!' unless capabilities.takes_screenshot?
|
69
43
|
end
|
70
44
|
|
71
45
|
# Returns:
|
@@ -81,26 +55,12 @@ module Applitools::Selenium
|
|
81
55
|
version.nil? ? nil : version.to_s
|
82
56
|
end
|
83
57
|
|
84
|
-
# Returns:
|
85
|
-
# +true+ if the driver is an Android driver.
|
86
|
-
def android?
|
87
|
-
platform_name.to_s.upcase == ANDROID
|
88
|
-
end
|
89
|
-
|
90
|
-
# Returns:
|
91
|
-
# +true+ if the driver is an iOS driver.
|
92
|
-
def ios?
|
93
|
-
platform_name.to_s.upcase == IOS
|
94
|
-
end
|
95
|
-
|
96
58
|
# Returns:
|
97
59
|
# +true+ if the driver orientation is landscape.
|
98
60
|
def landscape_orientation?
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
Applitools::EyesLogger.debug 'driver has no "orientation" attribute. Assuming: portrait.'
|
103
|
-
end
|
61
|
+
driver.orientation.to_s.upcase == LANDSCAPE
|
62
|
+
rescue NameError
|
63
|
+
Applitools::EyesLogger.debug 'driver has no "orientation" attribute. Assuming: portrait.'
|
104
64
|
end
|
105
65
|
|
106
66
|
# Returns:
|
@@ -119,19 +79,24 @@ module Applitools::Selenium
|
|
119
79
|
#
|
120
80
|
# Returns: +String+ A screenshot in the requested format.
|
121
81
|
def screenshot_as(output_type, rotation = nil)
|
122
|
-
|
123
|
-
|
82
|
+
image = mobile_device? ? visible_screenshot : @browser.fullpage_screenshot
|
83
|
+
|
84
|
+
Applitools::Selenium::Driver.normalize_image(self, image, rotation)
|
124
85
|
|
125
86
|
case output_type
|
126
87
|
when :base64
|
127
|
-
|
88
|
+
image = Applitools::Utils::ImageUtils.base64_from_png_image(image)
|
128
89
|
when :png
|
129
|
-
|
90
|
+
image = Applitools::Utils::ImageUtils.bytes_from_png_image(image)
|
130
91
|
else
|
131
|
-
raise Applitools::EyesError.new("Unsupported screenshot output type: #{output_type
|
92
|
+
raise Applitools::EyesError.new("Unsupported screenshot output type: #{output_type}")
|
132
93
|
end
|
133
94
|
|
134
|
-
|
95
|
+
image.force_encoding('BINARY')
|
96
|
+
end
|
97
|
+
|
98
|
+
def visible_screenshot
|
99
|
+
Applitools::Utils::ImageUtils.png_image_from_base64(driver.screenshot_as(:base64))
|
135
100
|
end
|
136
101
|
|
137
102
|
def mouse
|
@@ -159,12 +124,12 @@ module Applitools::Selenium
|
|
159
124
|
driver.find_elements(how, what).map { |el| Applitools::Selenium::Element.new(self, el) }
|
160
125
|
end
|
161
126
|
|
162
|
-
def
|
163
|
-
|
164
|
-
|
165
|
-
Applitools::EyesLogger.error "Failed to obtain user-agent string (#{e.message})"
|
127
|
+
def android?
|
128
|
+
platform_name.to_s.upcase == ANDROID
|
129
|
+
end
|
166
130
|
|
167
|
-
|
131
|
+
def ios?
|
132
|
+
platform_name.to_s.upcase == IOS
|
168
133
|
end
|
169
134
|
|
170
135
|
private
|
@@ -190,5 +155,42 @@ module Applitools::Selenium
|
|
190
155
|
raise ArgumentError, "wrong number of arguments (#{args.size} for 2)"
|
191
156
|
end
|
192
157
|
end
|
158
|
+
|
159
|
+
def self.normalize_image(driver, image, rotation)
|
160
|
+
normalize_rotation(driver, image, rotation)
|
161
|
+
normalize_width(driver, image)
|
162
|
+
end
|
163
|
+
|
164
|
+
# Rotates the image as necessary. The rotation is either manually forced by passing a value in
|
165
|
+
# the +rotation+ parameter, or automatically inferred if the +rotation+ parameter is +nil+.
|
166
|
+
#
|
167
|
+
# +driver+:: +Applitools::Selenium::Driver+ The driver which produced the screenshot.
|
168
|
+
# +image+:: +ChunkyPNG::Canvas+ The image to normalize.
|
169
|
+
# +rotation+:: +Integer+|+nil+ The degrees by which to rotate the image: positive values = clockwise rotation,
|
170
|
+
# negative values = counter-clockwise, 0 = force no rotation, +nil+ = rotate automatically when needed.
|
171
|
+
def self.normalize_rotation(driver, image, rotation)
|
172
|
+
return if rotation == 0
|
173
|
+
|
174
|
+
num_quadrants = 0
|
175
|
+
if !rotation.nil?
|
176
|
+
if rotation % RIGHT_ANGLE != 0
|
177
|
+
raise Applitools::EyesError.new('Currently only quadrant rotations are supported. Current rotation: '\
|
178
|
+
"#{rotation}")
|
179
|
+
end
|
180
|
+
num_quadrants = (rotation / RIGHT_ANGLE).to_i
|
181
|
+
elsif rotation.nil? && driver.mobile_device? && driver.landscape_orientation? && image.height > image.width
|
182
|
+
# For Android, we need to rotate images to the right, and for iOS to the left.
|
183
|
+
num_quadrants = driver.android? ? 1 : -1
|
184
|
+
end
|
185
|
+
|
186
|
+
Applitools::Utils::ImageUtils.quadrant_rotate!(image, num_quadrants)
|
187
|
+
end
|
188
|
+
|
189
|
+
def self.normalize_width(driver, image)
|
190
|
+
return if driver.mobile_device?
|
191
|
+
|
192
|
+
normalization_factor = driver.browser.image_normalization_factor(image)
|
193
|
+
Applitools::Utils::ImageUtils.scale!(image, normalization_factor) unless normalization_factor == 1
|
194
|
+
end
|
193
195
|
end
|
194
196
|
end
|