eyes_selenium 2.15.0 → 2.16.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/.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
|