eyes_selenium 2.15.0 → 2.16.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +0 -2
- data/.travis.yml +16 -0
- data/Gemfile +1 -1
- data/README.md +14 -4
- data/Rakefile +8 -1
- data/eyes_selenium.gemspec +26 -17
- data/lib/applitools/base/batch_info.rb +19 -0
- data/lib/applitools/base/dimension.rb +21 -0
- data/lib/applitools/base/environment.rb +19 -0
- data/lib/applitools/base/mouse_trigger.rb +33 -0
- data/lib/applitools/base/point.rb +21 -0
- data/lib/applitools/base/region.rb +77 -0
- data/lib/applitools/base/server_connector.rb +113 -0
- data/lib/applitools/base/session.rb +15 -0
- data/lib/applitools/base/start_info.rb +34 -0
- data/lib/applitools/base/test_results.rb +36 -0
- data/lib/applitools/base/text_trigger.rb +22 -0
- data/lib/applitools/eyes.rb +393 -0
- data/lib/applitools/eyes_logger.rb +40 -0
- data/lib/applitools/method_tracer.rb +22 -0
- data/lib/applitools/selenium/driver.rb +194 -0
- data/lib/applitools/selenium/element.rb +66 -0
- data/lib/applitools/selenium/keyboard.rb +27 -0
- data/lib/applitools/selenium/match_window_data.rb +24 -0
- data/lib/applitools/selenium/match_window_task.rb +190 -0
- data/lib/applitools/selenium/mouse.rb +62 -0
- data/lib/applitools/selenium/viewport_size.rb +128 -0
- data/lib/applitools/utils/image_delta_compressor.rb +150 -0
- data/lib/applitools/utils/image_utils.rb +63 -0
- data/lib/applitools/utils/utils.rb +52 -0
- data/lib/applitools/version.rb +3 -0
- data/lib/eyes_selenium.rb +9 -29
- data/spec/driver_passthrough_spec.rb +25 -25
- data/spec/spec_helper.rb +5 -8
- data/test/appium_example_script.rb +57 -0
- data/{test_script.rb → test/test_script.rb} +7 -9
- data/{watir_test.rb → test/watir_test_script.rb} +6 -4
- metadata +120 -48
- data/appium_eyes_example.rb +0 -56
- data/lib/eyes_selenium/capybara.rb +0 -21
- data/lib/eyes_selenium/eyes/agent_connector.rb +0 -76
- data/lib/eyes_selenium/eyes/batch_info.rb +0 -19
- data/lib/eyes_selenium/eyes/dimension.rb +0 -15
- data/lib/eyes_selenium/eyes/driver.rb +0 -266
- data/lib/eyes_selenium/eyes/element.rb +0 -78
- data/lib/eyes_selenium/eyes/environment.rb +0 -15
- data/lib/eyes_selenium/eyes/eyes.rb +0 -396
- data/lib/eyes_selenium/eyes/eyes_keyboard.rb +0 -25
- data/lib/eyes_selenium/eyes/eyes_mouse.rb +0 -60
- data/lib/eyes_selenium/eyes/failure_reports.rb +0 -4
- data/lib/eyes_selenium/eyes/match_level.rb +0 -8
- data/lib/eyes_selenium/eyes/match_window_data.rb +0 -18
- data/lib/eyes_selenium/eyes/match_window_task.rb +0 -182
- data/lib/eyes_selenium/eyes/mouse_trigger.rb +0 -23
- data/lib/eyes_selenium/eyes/region.rb +0 -72
- data/lib/eyes_selenium/eyes/screenshot_taker.rb +0 -18
- data/lib/eyes_selenium/eyes/session.rb +0 -14
- data/lib/eyes_selenium/eyes/start_info.rb +0 -32
- data/lib/eyes_selenium/eyes/test_results.rb +0 -32
- data/lib/eyes_selenium/eyes/text_trigger.rb +0 -19
- data/lib/eyes_selenium/eyes/viewport_size.rb +0 -105
- data/lib/eyes_selenium/eyes_logger.rb +0 -47
- data/lib/eyes_selenium/utils.rb +0 -6
- data/lib/eyes_selenium/utils/image_delta_compressor.rb +0 -149
- data/lib/eyes_selenium/utils/image_utils.rb +0 -76
- data/lib/eyes_selenium/version.rb +0 -3
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
module Applitools::EyesLogger
|
5
|
+
class NullLogger < Logger
|
6
|
+
def initialize(*args)
|
7
|
+
end
|
8
|
+
|
9
|
+
def add(*args, &block)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
extend Forwardable
|
14
|
+
extend self
|
15
|
+
|
16
|
+
MANDATORY_METHODS = [:debug, :info, :close].freeze
|
17
|
+
OPTIONAL_METHODS = [:warn, :error, :fatal].freeze
|
18
|
+
|
19
|
+
def_delegators :@@log_handler, *MANDATORY_METHODS
|
20
|
+
|
21
|
+
@@log_handler = NullLogger.new
|
22
|
+
|
23
|
+
def log_handler=(log_handler)
|
24
|
+
raise Applitools::EyesError.new('log_handler must implement Logger!') unless valid?(log_handler)
|
25
|
+
|
26
|
+
@@log_handler = log_handler
|
27
|
+
end
|
28
|
+
|
29
|
+
OPTIONAL_METHODS.each do |method|
|
30
|
+
define_singleton_method(method) do |msg|
|
31
|
+
@@log_handler.respond_to?(method) ? @@log_handler.send(method, msg) : @@log_handler.info(msg)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def valid?(log_handler)
|
38
|
+
MANDATORY_METHODS.all? {|method| log_handler.respond_to?(method)}
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Applitools::MethodTracer
|
2
|
+
def self.included(base)
|
3
|
+
instance_methods = base.instance_methods(false) + base.private_instance_methods(false)
|
4
|
+
class_methods = base.methods(false)
|
5
|
+
|
6
|
+
base.class_eval do
|
7
|
+
def self.trace_method(base, method_name, instance = true)
|
8
|
+
original_method = instance ? instance_method(method_name) : method(method_name)
|
9
|
+
|
10
|
+
send(instance ? :define_method : :define_singleton_method, method_name) do |*args, &block|
|
11
|
+
Applitools::EyesLogger.debug "-> #{base}##{method_name}"
|
12
|
+
return_value = (instance ? original_method.bind(self) : original_method).call(*args, &block)
|
13
|
+
Applitools::EyesLogger.debug "<- #{base}##{method_name}"
|
14
|
+
return_value
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
instance_methods.each {|method_name| trace_method(base, method_name) }
|
19
|
+
class_methods.each {|method_name| trace_method(base, method_name, false) }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,194 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'socket'
|
3
|
+
require 'selenium-webdriver'
|
4
|
+
|
5
|
+
module Applitools::Selenium
|
6
|
+
class Driver < SimpleDelegator
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
include Selenium::WebDriver::DriverExtensions::HasInputDevices
|
10
|
+
|
11
|
+
RIGHT_ANGLE = 90.freeze
|
12
|
+
ANDROID = 'ANDROID'.freeze
|
13
|
+
IOS = 'IOS'.freeze
|
14
|
+
LANDSCAPE = 'LANDSCAPE'.freeze
|
15
|
+
|
16
|
+
IE = 'ie'.freeze
|
17
|
+
FIREFOX = 'firefox'.freeze
|
18
|
+
|
19
|
+
FINDERS = {
|
20
|
+
class: 'class name',
|
21
|
+
class_name: 'class name',
|
22
|
+
css: 'css selector',
|
23
|
+
id: 'id',
|
24
|
+
link: 'link text',
|
25
|
+
link_text: 'link text',
|
26
|
+
name: 'name',
|
27
|
+
partial_link_text: 'partial link text',
|
28
|
+
tag_name: 'tag name',
|
29
|
+
xpath: 'xpath'
|
30
|
+
}.freeze
|
31
|
+
|
32
|
+
JS_GET_USER_AGENT = 'return navigator.userAgent;'.freeze
|
33
|
+
|
34
|
+
def_delegators :@eyes, :user_inputs, :clear_user_inputs
|
35
|
+
|
36
|
+
# If driver is not provided, Applitools::Selenium::Driver will raise an EyesError exception.
|
37
|
+
def initialize(eyes, options)
|
38
|
+
super(options[:driver])
|
39
|
+
|
40
|
+
@is_mobile_device = options.fetch(:is_mobile_device, false)
|
41
|
+
@eyes = eyes
|
42
|
+
|
43
|
+
raise 'Uncapable of taking screenshots!' unless capabilities.takes_screenshot?
|
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)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns:
|
72
|
+
# +String+ The platform name or +nil+ if it is undefined.
|
73
|
+
def platform_name
|
74
|
+
capabilities['platformName']
|
75
|
+
end
|
76
|
+
|
77
|
+
# Returns:
|
78
|
+
# +String+ The platform version or +nil+ if it is undefined.
|
79
|
+
def platform_version
|
80
|
+
version = capabilities['platformVersion']
|
81
|
+
version.nil? ? nil : version.to_s
|
82
|
+
end
|
83
|
+
|
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
|
+
# Returns:
|
97
|
+
# +true+ if the driver orientation is landscape.
|
98
|
+
def landscape_orientation?
|
99
|
+
begin
|
100
|
+
driver.orientation.to_s.upcase == LANDSCAPE
|
101
|
+
rescue NameError
|
102
|
+
Applitools::EyesLogger.debug 'driver has no "orientation" attribute. Assuming: portrait.'
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Returns:
|
107
|
+
# +true+ if the platform running the test is a mobile platform. +false+ otherwise.
|
108
|
+
def mobile_device?
|
109
|
+
# We CAN'T check if the device is an +Appium::Driver+ since it is not a RemoteWebDriver. Because of that we use a
|
110
|
+
# flag we got as an option in the constructor.
|
111
|
+
@is_mobile_device
|
112
|
+
end
|
113
|
+
|
114
|
+
# Return a PNG screenshot in the given format as a string
|
115
|
+
#
|
116
|
+
# +output_type+:: +Symbol+ The format of the screenshot. Accepted values are +:base64+ and +:png+.
|
117
|
+
# +rotation+:: +Integer+|+nil+ The degrees by which to rotate the image: positive values = clockwise rotation,
|
118
|
+
# negative values = counter-clockwise, 0 = force no rotation, +nil+ = rotate automatically when needed.
|
119
|
+
#
|
120
|
+
# Returns: +String+ A screenshot in the requested format.
|
121
|
+
def screenshot_as(output_type, rotation = nil)
|
122
|
+
screenshot = Applitools::Utils::ImageUtils.png_image_from_base64(driver.screenshot_as(:base64))
|
123
|
+
Applitools::Selenium::Driver.normalize_rotation(self, screenshot, rotation)
|
124
|
+
|
125
|
+
case output_type
|
126
|
+
when :base64
|
127
|
+
screenshot = Applitools::Utils::ImageUtils.base64_from_png_image(screenshot)
|
128
|
+
when :png
|
129
|
+
screenshot = Applitools::Utils::ImageUtils.bytes_from_png_image(screenshot)
|
130
|
+
else
|
131
|
+
raise Applitools::EyesError.new("Unsupported screenshot output type: #{output_type.to_s}")
|
132
|
+
end
|
133
|
+
|
134
|
+
screenshot.force_encoding('BINARY')
|
135
|
+
end
|
136
|
+
|
137
|
+
def mouse
|
138
|
+
Applitools::Selenium::Mouse.new(self, driver.mouse)
|
139
|
+
end
|
140
|
+
|
141
|
+
def keyboard
|
142
|
+
Applitools::Selenium::Keyboard.new(self, driver.keyboard)
|
143
|
+
end
|
144
|
+
|
145
|
+
def find_element(*args)
|
146
|
+
how, what = extract_args(args)
|
147
|
+
|
148
|
+
# Make sure that "how" is a valid locator.
|
149
|
+
raise ArgumentError, "cannot find element by: #{how.inspect}" unless FINDERS[how.to_sym]
|
150
|
+
|
151
|
+
Applitools::Selenium::Element.new(self, driver.find_element(how, what))
|
152
|
+
end
|
153
|
+
|
154
|
+
def find_elements(*args)
|
155
|
+
how, what = extract_args(args)
|
156
|
+
|
157
|
+
raise ArgumentError, "cannot find element by: #{how.inspect}" unless FINDERS[how.to_sym]
|
158
|
+
|
159
|
+
driver.find_elements(how, what).map { |el| Applitools::Selenium::Element.new(self, el) }
|
160
|
+
end
|
161
|
+
|
162
|
+
def user_agent
|
163
|
+
@user_agent ||= execute_script JS_GET_USER_AGENT
|
164
|
+
rescue => e
|
165
|
+
Applitools::EyesLogger.error "Failed to obtain user-agent string (#{e.message})"
|
166
|
+
|
167
|
+
nil
|
168
|
+
end
|
169
|
+
|
170
|
+
private
|
171
|
+
|
172
|
+
def driver
|
173
|
+
@driver ||= __getobj__
|
174
|
+
end
|
175
|
+
|
176
|
+
def extract_args(args)
|
177
|
+
case args.size
|
178
|
+
when 2
|
179
|
+
args
|
180
|
+
when 1
|
181
|
+
arg = args.first
|
182
|
+
|
183
|
+
raise Argu mentError, "expected #{arg.inspect}:#{arg.class} to respond to #shift" unless arg.respond_to?(:shift)
|
184
|
+
|
185
|
+
# This will be a single-entry hash, so use #shift over #first or #[].
|
186
|
+
arg.dup.shift.tap do |arr|
|
187
|
+
raise ArgumentError, "expected #{arr.inspect} to have 2 elements" unless arr.size == 2
|
188
|
+
end
|
189
|
+
else
|
190
|
+
raise ArgumentError, "wrong number of arguments (#{args.size} for 2)"
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Applitools::Selenium
|
2
|
+
class Element < SimpleDelegator
|
3
|
+
TRACE_PREFIX = 'EyesWebElement'.freeze
|
4
|
+
|
5
|
+
def initialize(driver, element)
|
6
|
+
super(element)
|
7
|
+
|
8
|
+
@driver = driver
|
9
|
+
end
|
10
|
+
|
11
|
+
def web_element
|
12
|
+
@web_element ||= __getobj__
|
13
|
+
end
|
14
|
+
|
15
|
+
def click
|
16
|
+
current_control = region
|
17
|
+
offset = current_control.middle_offset
|
18
|
+
@driver.user_inputs << Applitools::Base::MouseTrigger.new(:click, current_control, offset)
|
19
|
+
|
20
|
+
web_element.click
|
21
|
+
end
|
22
|
+
|
23
|
+
def inspect
|
24
|
+
TRACE_PREFIX + web_element.inspect
|
25
|
+
end
|
26
|
+
|
27
|
+
def ==(other)
|
28
|
+
other.kind_of?(web_element.class) && web_element == other
|
29
|
+
end
|
30
|
+
alias_method :eql?, :==
|
31
|
+
|
32
|
+
def send_keys(*args)
|
33
|
+
current_control = region
|
34
|
+
Selenium::WebDriver::Keys.encode(args).each do |key|
|
35
|
+
@driver.user_inputs << Applitools::Base::TextTrigger.new(key.to_s, current_control)
|
36
|
+
end
|
37
|
+
|
38
|
+
web_element.send_keys(*args)
|
39
|
+
end
|
40
|
+
alias_method :send_key, :send_keys
|
41
|
+
|
42
|
+
def region
|
43
|
+
point = location
|
44
|
+
left, top, width, height = point.x, point.y, 0, 0
|
45
|
+
|
46
|
+
begin
|
47
|
+
dimension = size
|
48
|
+
width, height = dimension.width, dimension.height
|
49
|
+
rescue
|
50
|
+
# Not supported on all platforms.
|
51
|
+
end
|
52
|
+
|
53
|
+
if left < 0
|
54
|
+
width = [0, width + left].max
|
55
|
+
left = 0
|
56
|
+
end
|
57
|
+
|
58
|
+
if top < 0
|
59
|
+
height = [0, height + top].max
|
60
|
+
top = 0
|
61
|
+
end
|
62
|
+
|
63
|
+
return Applitools::Base::Region.new(left, top, width, height)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Applitools::Selenium
|
2
|
+
class Keyboard
|
3
|
+
attr_reader :keyboard, :driver
|
4
|
+
|
5
|
+
def initialize(driver, keyboard)
|
6
|
+
@driver = driver
|
7
|
+
@keyboard = keyboard
|
8
|
+
end
|
9
|
+
|
10
|
+
def send_keys(*keys)
|
11
|
+
active_element = Applitools::Selenium::Element.new(driver, driver.switch_to.active_element)
|
12
|
+
current_control = active_element.region
|
13
|
+
Selenium::WebDriver::Keys.encode(keys).each do |key|
|
14
|
+
driver.user_inputs << Applitools::Base::TextTrigger.new(key.to_s, current_control)
|
15
|
+
end
|
16
|
+
keyboard.send_keys(*keys)
|
17
|
+
end
|
18
|
+
|
19
|
+
def press(key)
|
20
|
+
keyboard.press(key)
|
21
|
+
end
|
22
|
+
|
23
|
+
def release(key)
|
24
|
+
keyboard.release(key)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Applitools::Selenium
|
2
|
+
class MatchWindowData
|
3
|
+
attr_reader :user_inputs, :app_output, :tag, :ignore_mismatch, :screenshot
|
4
|
+
|
5
|
+
def initialize(app_output, user_inputs = [], tag, ignore_mismatch, screenshot)
|
6
|
+
@user_inputs = user_inputs
|
7
|
+
@app_output = app_output
|
8
|
+
@tag = tag
|
9
|
+
@ignore_mismatch = ignore_mismatch
|
10
|
+
@screenshot = screenshot
|
11
|
+
end
|
12
|
+
|
13
|
+
# IMPORTANT This method returns a hash WITHOUT the screenshot property. This is on purspose! The screenshot should
|
14
|
+
# not be included as part of the json.
|
15
|
+
def to_hash
|
16
|
+
{
|
17
|
+
user_inputs: user_inputs.map(&:to_hash),
|
18
|
+
app_output: Hash[app_output.each_pair.to_a],
|
19
|
+
tag: @tag,
|
20
|
+
ignore_mismatch: @ignore_mismatch
|
21
|
+
}
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,190 @@
|
|
1
|
+
require 'base64'
|
2
|
+
|
3
|
+
module Applitools::Selenium
|
4
|
+
class MatchWindowTask
|
5
|
+
MATCH_INTERVAL = 0.5.freeze
|
6
|
+
AppOuptut = Struct.new(:title, :screenshot64)
|
7
|
+
|
8
|
+
attr_reader :eyes, :session, :driver, :default_retry_timeout, :last_checked_window,
|
9
|
+
:last_screenshot_bounds
|
10
|
+
|
11
|
+
def initialize(eyes, session, driver, default_retry_timeout)
|
12
|
+
@eyes = eyes
|
13
|
+
|
14
|
+
@session = session
|
15
|
+
@driver = driver
|
16
|
+
@default_retry_timeout = default_retry_timeout
|
17
|
+
@last_checked_window = nil # +ChunkyPNG::Canvas+
|
18
|
+
@last_screenshot_bounds = Applitools::Base::Region::EMPTY # +Applitools::Base::Region+
|
19
|
+
@current_screenshot = nil # +ChunkyPNG::Canvas+
|
20
|
+
end
|
21
|
+
|
22
|
+
def match_window(region, retry_timeout, tag, rotation, run_once_after_wait = false)
|
23
|
+
retry_timeout = default_retry_timeout if retry_timeout < 0
|
24
|
+
|
25
|
+
Applitools::EyesLogger.debug "Retry timeout set to: #{retry_timeout}"
|
26
|
+
|
27
|
+
start = Time.now
|
28
|
+
res = if retry_timeout.zero?
|
29
|
+
run(region, tag, rotation)
|
30
|
+
elsif run_once_after_wait
|
31
|
+
run(region, tag, rotation, retry_timeout)
|
32
|
+
else
|
33
|
+
run_with_intervals(region, tag, rotation, retry_timeout)
|
34
|
+
end
|
35
|
+
elapsed_time = Time.now - start
|
36
|
+
|
37
|
+
Applitools::EyesLogger.debug "match_window(): Completed in #{format('%.2f', elapsed_time)} seconds"
|
38
|
+
|
39
|
+
@last_checked_window = @current_screenshot
|
40
|
+
@last_screenshot_bounds = region.empty? ? Applitools::Base::Region.new(0, 0, last_checked_window.width,
|
41
|
+
last_checked_window.height) : region
|
42
|
+
driver.clear_user_inputs
|
43
|
+
|
44
|
+
res
|
45
|
+
end
|
46
|
+
|
47
|
+
def run(region, tag, rotation, wait_before_run=nil)
|
48
|
+
Applitools::EyesLogger.debug 'Trying matching once...'
|
49
|
+
|
50
|
+
if wait_before_run
|
51
|
+
Applitools::EyesLogger.debug 'Waiting before run...'
|
52
|
+
sleep(wait_before_run)
|
53
|
+
Applitools::EyesLogger.debug 'Wwaiting done!'
|
54
|
+
end
|
55
|
+
|
56
|
+
match(region, tag, rotation)
|
57
|
+
end
|
58
|
+
|
59
|
+
def run_with_intervals(region, tag, rotation, retry_timeout)
|
60
|
+
# We intentionally take the first screenshot before starting the timer, to allow the page just a tad more time to
|
61
|
+
# stabilize.
|
62
|
+
Applitools::EyesLogger.debug 'Matching with intervals...'
|
63
|
+
data = prep_match_data(region, tag, rotation, true)
|
64
|
+
start = Time.now
|
65
|
+
as_expected = Applitools::Base::ServerConnector.match_window(session, data)
|
66
|
+
Applitools::EyesLogger.debug "First call result: #{as_expected}"
|
67
|
+
return true if as_expected
|
68
|
+
Applitools::EyesLogger.debug "Not as expected, performing retry (total timeout #{retry_timeout})"
|
69
|
+
match_retry = Time.now - start
|
70
|
+
while match_retry < retry_timeout
|
71
|
+
Applitools::EyesLogger.debug 'Waiting before match...'
|
72
|
+
sleep(MATCH_INTERVAL)
|
73
|
+
Applitools::EyesLogger.debug 'Done! Matching...'
|
74
|
+
return true if match(region, tag, rotation, true)
|
75
|
+
match_retry = Time.now - start
|
76
|
+
Applitools::EyesLogger.debug "Elapsed time: #{match_retry}"
|
77
|
+
end
|
78
|
+
# Let's try one more time if we still don't have a match.
|
79
|
+
Applitools::EyesLogger.debug 'Last attempt to match...'
|
80
|
+
as_expected = match(region, tag, rotation)
|
81
|
+
Applitools::EyesLogger.debug "Match result: #{as_expected}"
|
82
|
+
as_expected
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def get_clipped_region(region, image)
|
88
|
+
left, top = [region.left, 0].max, [region.top, 0].max
|
89
|
+
max_width = image.width - left
|
90
|
+
max_height = image.height - top
|
91
|
+
width, height = [region.width, max_width].min, [region.height, max_height].min
|
92
|
+
Applitools::Base::Region.new(left, top, width, height)
|
93
|
+
end
|
94
|
+
|
95
|
+
def prep_match_data(region, tag, rotation, ignore_mismatch)
|
96
|
+
Applitools::EyesLogger.debug 'Preparing match data...'
|
97
|
+
title = eyes.title
|
98
|
+
Applitools::EyesLogger.debug 'Getting screenshot...'
|
99
|
+
current_screenshot_encoded = driver.screenshot_as(:png, rotation)
|
100
|
+
Applitools::EyesLogger.debug 'Done! Creating image object from PNG...'
|
101
|
+
@current_screenshot = ChunkyPNG::Image.from_blob(current_screenshot_encoded)
|
102
|
+
Applitools::EyesLogger.debug 'Done!'
|
103
|
+
# If a region was defined, we refer to the sub-image defined by the region.
|
104
|
+
unless region.empty?
|
105
|
+
Applitools::EyesLogger.debug 'Calculating clipped region...'
|
106
|
+
# If the region is out of bounds, clip it
|
107
|
+
clipped_region = get_clipped_region(region, @current_screenshot)
|
108
|
+
raise Applitools::EyesError.new("Region is outside the viewport: #{region}") if clipped_region.empty?
|
109
|
+
Applitools::EyesLogger.debug 'Done! Cropping region...'
|
110
|
+
@current_screenshot.crop!(clipped_region.left, clipped_region.top, clipped_region.width, clipped_region.height)
|
111
|
+
Applitools::EyesLogger.debug 'Done! Creating cropped image object...'
|
112
|
+
current_screenshot_encoded = @current_screenshot.to_blob.force_encoding('BINARY')
|
113
|
+
Applitools::EyesLogger.debug 'Done!'
|
114
|
+
end
|
115
|
+
Applitools::EyesLogger.debug 'Compressing screenshot...'
|
116
|
+
compressed_screenshot = Applitools::Utils::ImageDeltaCompressor.compress_by_raw_blocks(@current_screenshot,
|
117
|
+
current_screenshot_encoded, last_checked_window)
|
118
|
+
Applitools::EyesLogger.debug 'Done! Creating AppOuptut...'
|
119
|
+
app_output = AppOuptut.new(title, nil)
|
120
|
+
user_inputs = []
|
121
|
+
Applitools::EyesLogger.debug 'Handling user inputs...'
|
122
|
+
if !last_checked_window.nil?
|
123
|
+
driver.user_inputs.each do |trigger|
|
124
|
+
Applitools::EyesLogger.debug 'Handling trigger...'
|
125
|
+
if trigger.is_a?(Applitools::Base::MouseTrigger)
|
126
|
+
updated_trigger = nil
|
127
|
+
trigger_left = trigger.control.left + trigger.location.x
|
128
|
+
trigger_top = trigger.control.top + trigger.location.y
|
129
|
+
if last_screenshot_bounds.contains?(trigger_left, trigger_top)
|
130
|
+
trigger.control.intersect(last_screenshot_bounds)
|
131
|
+
if trigger.control.empty?
|
132
|
+
trigger_left -= - last_screenshot_bounds.left
|
133
|
+
trigger_top = trigger_top - last_screenshot_bounds.top
|
134
|
+
updated_trigger = Applitools::Base::MouseTrigger.new(trigger.mouse_action, trigger.control,
|
135
|
+
Applitools::Base::Point.new(trigger_left, trigger_top))
|
136
|
+
else
|
137
|
+
trigger_left = trigger_left - trigger.control.left
|
138
|
+
trigger_top = trigger_top - trigger.control.top
|
139
|
+
control_left = trigger.control.left - last_screenshot_bounds.left
|
140
|
+
control_top = trigger.control.top - last_screenshot_bounds.top
|
141
|
+
updated_control = Applitools::Base::Region.new(control_left, control_top, trigger.control.width,
|
142
|
+
trigger.control.height)
|
143
|
+
updated_trigger = Applitools::Base::MouseTrigger.new(trigger.mouse_action, updated_control,
|
144
|
+
Applitools::Base::Point.new(trigger_left, trigger_top))
|
145
|
+
end
|
146
|
+
Applitools::EyesLogger.debug 'Done with trigger!'
|
147
|
+
user_inputs << updated_trigger
|
148
|
+
else
|
149
|
+
Applitools::EyesLogger.info "Trigger ignored: #{trigger} (out of bounds)"
|
150
|
+
end
|
151
|
+
elsif trigger.is_a?(Applitools::Base::TextTrigger)
|
152
|
+
unless trigger.control.empty?
|
153
|
+
trigger.control.intersect(last_screenshot_bounds)
|
154
|
+
unless trigger.control.empty?
|
155
|
+
control_left = trigger.control.left - last_screenshot_bounds.left
|
156
|
+
control_top = trigger.control.top - last_screenshot_bounds.top
|
157
|
+
updated_control = Applitools::Base::Region.new(control_left, control_top, trigger.control.width,
|
158
|
+
trigger.control.height)
|
159
|
+
updated_trigger = Applitools::Base::TextTrigger.new(trigger.text, updated_control)
|
160
|
+
Applitools::EyesLogger.debug 'Done with trigger!'
|
161
|
+
user_inputs << updated_trigger
|
162
|
+
else
|
163
|
+
Applitools::EyesLogger.info "Trigger ignored: #{trigger} (control out of bounds)"
|
164
|
+
end
|
165
|
+
else
|
166
|
+
Applitools::EyesLogger.info "Trigger ignored: #{trigger} (out of bounds)"
|
167
|
+
end
|
168
|
+
else
|
169
|
+
Applitools::EyesLogger.info "Trigger ignored: #{trigger} (Unrecognized trigger)"
|
170
|
+
end
|
171
|
+
end
|
172
|
+
else
|
173
|
+
Applitools::EyesLogger.info 'Triggers ignored: no previous window checked'
|
174
|
+
end
|
175
|
+
Applitools::EyesLogger.debug 'Creating MatchWindowData object..'
|
176
|
+
match_window_data_obj = Applitools::Selenium::MatchWindowData.new(app_output, user_inputs, tag, ignore_mismatch,
|
177
|
+
compressed_screenshot)
|
178
|
+
Applitools::EyesLogger.debug 'Done creating MatchWindowData object!'
|
179
|
+
match_window_data_obj
|
180
|
+
end
|
181
|
+
|
182
|
+
def match(region, tag, rotation, ignore_mismatch=false)
|
183
|
+
Applitools::EyesLogger.debug 'Match called...'
|
184
|
+
data = prep_match_data(region, tag, rotation, ignore_mismatch)
|
185
|
+
match_result = Applitools::Base::ServerConnector.match_window(session, data)
|
186
|
+
Applitools::EyesLogger.debug 'Match done!'
|
187
|
+
match_result
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|