eyes_selenium 2.16.0 → 2.17.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.
- 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
|