eyes_selenium_ruby 1.1.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.
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +13 -0
- data/README.md +66 -0
- data/Rakefile +1 -0
- data/eyes_selenium_ruby.gemspec +30 -0
- data/lib/eyes_selenium_ruby/capybara.rb +21 -0
- data/lib/eyes_selenium_ruby/eyes/agent_connecter.rb +39 -0
- data/lib/eyes_selenium_ruby/eyes/batch_info.rb +14 -0
- data/lib/eyes_selenium_ruby/eyes/driver.rb +145 -0
- data/lib/eyes_selenium_ruby/eyes/element.rb +78 -0
- data/lib/eyes_selenium_ruby/eyes/environment.rb +13 -0
- data/lib/eyes_selenium_ruby/eyes/eyes.rb +179 -0
- data/lib/eyes_selenium_ruby/eyes/eyes_keyboard.rb +25 -0
- data/lib/eyes_selenium_ruby/eyes/eyes_mouse.rb +60 -0
- data/lib/eyes_selenium_ruby/eyes/failure_reports.rb +4 -0
- data/lib/eyes_selenium_ruby/eyes/match_level.rb +7 -0
- data/lib/eyes_selenium_ruby/eyes/match_window_data.rb +18 -0
- data/lib/eyes_selenium_ruby/eyes/match_window_task.rb +71 -0
- data/lib/eyes_selenium_ruby/eyes/mouse_trigger.rb +19 -0
- data/lib/eyes_selenium_ruby/eyes/region.rb +22 -0
- data/lib/eyes_selenium_ruby/eyes/screenshot_taker.rb +18 -0
- data/lib/eyes_selenium_ruby/eyes/session.rb +14 -0
- data/lib/eyes_selenium_ruby/eyes/start_info.rb +33 -0
- data/lib/eyes_selenium_ruby/eyes/target_app.rb +17 -0
- data/lib/eyes_selenium_ruby/eyes/test_results.rb +21 -0
- data/lib/eyes_selenium_ruby/eyes/text_trigger.rb +15 -0
- data/lib/eyes_selenium_ruby/eyes/viewport_size.rb +109 -0
- data/lib/eyes_selenium_ruby/eyes_logger.rb +18 -0
- data/lib/eyes_selenium_ruby/utils/image_delta_compressor.rb +147 -0
- data/lib/eyes_selenium_ruby/utils.rb +5 -0
- data/lib/eyes_selenium_ruby/version.rb +3 -0
- data/lib/eyes_selenium_ruby.rb +41 -0
- data/spec/capybara_spec.rb +33 -0
- data/spec/driver_spec.rb +10 -0
- data/spec/eyes_spec.rb +156 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/test_app.rb +11 -0
- data/test_script.rb +22 -0
- metadata +225 -0
@@ -0,0 +1,60 @@
|
|
1
|
+
class Applitools::EyesMouse
|
2
|
+
|
3
|
+
attr_reader :driver, :mouse
|
4
|
+
def initialize(driver, mouse)
|
5
|
+
@driver = driver
|
6
|
+
@mouse = mouse
|
7
|
+
end
|
8
|
+
|
9
|
+
def click(element = nil)
|
10
|
+
extract_trigger_and_perform(:click, element)
|
11
|
+
end
|
12
|
+
|
13
|
+
def double_click(element = nil)
|
14
|
+
extract_trigger_and_perform(:double_click, element)
|
15
|
+
end
|
16
|
+
|
17
|
+
def context_click(element = nil)
|
18
|
+
extract_trigger_and_perform(:right_click, element)
|
19
|
+
end
|
20
|
+
|
21
|
+
def down(element = nil)
|
22
|
+
extract_trigger_and_perform(:down, element)
|
23
|
+
end
|
24
|
+
|
25
|
+
def up(element = nil)
|
26
|
+
extract_trigger_and_perform(:up, element)
|
27
|
+
end
|
28
|
+
|
29
|
+
def move_to(element, right_by = nil, down_by = nil)
|
30
|
+
element = element.web_element if element.is_a?(Applitools::Element)
|
31
|
+
location = element.location
|
32
|
+
location.x = [0,location.x].max
|
33
|
+
location.y = [0,location.y].max
|
34
|
+
current_control = Applitools::Region.new(0,0, *location.values)
|
35
|
+
driver.user_inputs << Applitools::MouseTrigger.new(:move, current_control, location)
|
36
|
+
element = element.web_element if element.is_a?(Applitools::Element)
|
37
|
+
mouse.move_to(element,right_by, down_by)
|
38
|
+
end
|
39
|
+
|
40
|
+
def move_by(right_by, down_by)
|
41
|
+
right = [0,right_by].max
|
42
|
+
down = [0,down_by].max
|
43
|
+
location = Selenium::WebDriver::Location.new(right,down)
|
44
|
+
current_control = Applitools::Region.new(0,0, right, down)
|
45
|
+
driver.user_inputs << Applitools::MouseTrigger.new(:move, current_control, location)
|
46
|
+
mouse.move_by(right_by,down_by)
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def extract_trigger_and_perform(method, element=nil, *args)
|
52
|
+
location = element.location
|
53
|
+
location.x = [0,location.x].max
|
54
|
+
location.y = [0,location.y].max
|
55
|
+
current_control = Applitools::Region.new(0,0, *location.values)
|
56
|
+
driver.user_inputs << Applitools::MouseTrigger.new(method, current_control, location)
|
57
|
+
element = element.web_element if element.is_a?(Applitools::Element)
|
58
|
+
mouse.send(method,element,*args)
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class Applitools::MatchWindowData
|
2
|
+
|
3
|
+
attr_reader :user_inputs, :app_output, :tag, :ignore_mismatch, :screenshot
|
4
|
+
def initialize(app_output,user_inputs=[], tag, ignore_mismatch, screenshot)
|
5
|
+
@user_inputs = user_inputs
|
6
|
+
@app_output = app_output
|
7
|
+
@tag = tag
|
8
|
+
@ignore_mismatch = ignore_mismatch
|
9
|
+
@screenshot = screenshot
|
10
|
+
end
|
11
|
+
|
12
|
+
# IMPORTANT This method returns a hash WITHOUT the screenshot property. This is on purspose! The screenshot should
|
13
|
+
# not be included as part of the json.
|
14
|
+
def to_hash
|
15
|
+
{userInputs: user_inputs.map(&:to_hash), appOutput: Hash[app_output.each_pair.to_a],
|
16
|
+
tag: @tag, ignore_mismatch: @ignore_mismatch}
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'oily_png'
|
2
|
+
require 'base64'
|
3
|
+
|
4
|
+
class Applitools::MatchWindowTask
|
5
|
+
|
6
|
+
MATCH_INTERVAL = 0.5
|
7
|
+
AppOutput = Struct.new(:title, :screenshot64)
|
8
|
+
|
9
|
+
attr_reader :agent_connector, :session, :driver, :max_window_load_time
|
10
|
+
|
11
|
+
def initialize(agent_connector, session, driver, max_window_load_time)
|
12
|
+
@agent_connector = agent_connector
|
13
|
+
@session = session
|
14
|
+
@driver = driver
|
15
|
+
@max_window_load_time = max_window_load_time
|
16
|
+
@last_checked_window = nil # +ChunkyPNG::Canvas+
|
17
|
+
@current_screenshot = nil # +ChunkyPNG::Canvas+
|
18
|
+
end
|
19
|
+
|
20
|
+
def match_window(tag,run_once_after_wait=false)
|
21
|
+
res = if max_window_load_time.zero?
|
22
|
+
run(tag)
|
23
|
+
elsif run_once_after_wait
|
24
|
+
run(tag, max_window_load_time)
|
25
|
+
else
|
26
|
+
run_with_intervals(tag, max_window_load_time)
|
27
|
+
end
|
28
|
+
|
29
|
+
driver.clear_user_inputs and return res
|
30
|
+
end
|
31
|
+
|
32
|
+
def run(tag, wait_before_run=nil)
|
33
|
+
sleep(wait_before_run) if wait_before_run
|
34
|
+
match(tag)
|
35
|
+
end
|
36
|
+
|
37
|
+
def run_with_intervals(tag, total_run_time)
|
38
|
+
iterations = (total_run_time / MATCH_INTERVAL - 1).to_i
|
39
|
+
iterations.times do
|
40
|
+
sleep(MATCH_INTERVAL)
|
41
|
+
return true if match(tag, true)
|
42
|
+
end
|
43
|
+
|
44
|
+
## lets try one more time if we still don't have a match
|
45
|
+
match(tag)
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def prep_match_data(tag, ignore_mismatch)
|
51
|
+
title = driver.title
|
52
|
+
current_screenshot_encoded = Base64.decode64(driver.screenshot_as(:base64))
|
53
|
+
@current_screenshot = ChunkyPNG::Image.from_blob(current_screenshot_encoded)
|
54
|
+
compressed_screenshot = Applitools::Utils::ImageDeltaCompressor.compress_by_raw_blocks(@current_screenshot,
|
55
|
+
current_screenshot_encoded,
|
56
|
+
@last_checked_window)
|
57
|
+
app_output = AppOutput.new(title, nil)
|
58
|
+
|
59
|
+
return Applitools::MatchWindowData.new(app_output, driver.user_inputs, tag, ignore_mismatch, compressed_screenshot)
|
60
|
+
end
|
61
|
+
|
62
|
+
def match(tag, ignore_mismatch=false)
|
63
|
+
data = prep_match_data(tag, ignore_mismatch)
|
64
|
+
agent_connector.match_window(session, data)
|
65
|
+
|
66
|
+
# If the server stored this image, it will be used as a base for our next screenshot compression
|
67
|
+
if !ignore_mismatch
|
68
|
+
@last_checked_window = @current_screenshot
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class Applitools::MouseTrigger
|
2
|
+
|
3
|
+
MouseAction = { click: 1, right_click: 2, double_click: 3, move: 4, down: 5, up: 6 }
|
4
|
+
|
5
|
+
attr_reader :mouse_action, :control, :location
|
6
|
+
|
7
|
+
def initialize(mouse_action, control, location)
|
8
|
+
@mouse_action = MouseAction[mouse_action]
|
9
|
+
@control = control
|
10
|
+
@location = location
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_hash
|
14
|
+
{
|
15
|
+
"$type" => "Applitools.Models.MouseTrigger, Core", mouseAction: mouse_action,
|
16
|
+
control: control.to_hash, location: Hash[location.each_pair.to_a]
|
17
|
+
}
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class Applitools::Region
|
2
|
+
attr_accessor :left, :top, :height, :width
|
3
|
+
|
4
|
+
def initialize(left, top, height, width)
|
5
|
+
@left = left
|
6
|
+
@top = top
|
7
|
+
@height = height
|
8
|
+
@width = width
|
9
|
+
end
|
10
|
+
|
11
|
+
def middle_offset
|
12
|
+
mid_x = width / 2
|
13
|
+
mid_y = height / 2
|
14
|
+
Selenium::WebDriver::Location.new mid_x, mid_y
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_hash
|
18
|
+
{
|
19
|
+
"$type" => "Applitools.Utils.Geometry.MutableRegion, Core", left: left, top: top, height: height, width: width
|
20
|
+
}
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
class Applitools::ScreenshotTaker
|
3
|
+
include HTTParty
|
4
|
+
headers 'Accept' => 'application/json'
|
5
|
+
headers 'Content-Type' => 'application/json'
|
6
|
+
|
7
|
+
attr_reader :driver_server_uri, :driver_session_id
|
8
|
+
|
9
|
+
def initialize(driver_server_uri, driver_session_id)
|
10
|
+
@driver_server_uri = driver_server_uri
|
11
|
+
@driver_session_id = driver_session_id
|
12
|
+
end
|
13
|
+
|
14
|
+
def screenshot
|
15
|
+
res = self.class.get(driver_server_uri.gsub(/\/$/,"") + "/session/#{driver_session_id}/screenshot").to_s
|
16
|
+
res.parsed_response['value']
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class Applitools::Session
|
2
|
+
attr_reader :eyes, :id, :url
|
3
|
+
attr_accessor :new_session
|
4
|
+
def initialize(session_id, session_url, new_session)
|
5
|
+
@new_session = new_session
|
6
|
+
@id = session_id
|
7
|
+
@url = session_url
|
8
|
+
end
|
9
|
+
|
10
|
+
def new_session?
|
11
|
+
self.new_session
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
@@ -0,0 +1,33 @@
|
|
1
|
+
class Applitools::StartInfo
|
2
|
+
|
3
|
+
ATTRIBUTES = %w[ app_id_or_name ver_id scenario_id_or_name batch_info
|
4
|
+
environment application match_level branch_name parent_branch_name ]
|
5
|
+
|
6
|
+
ATTRIBUTES.each do |attr|
|
7
|
+
attr_accessor attr
|
8
|
+
end
|
9
|
+
|
10
|
+
## add a config file with this stuff, and use hash arg
|
11
|
+
def initialize(app_id_or_name, scenario_id_or_name, batch_info, environment,
|
12
|
+
application, match_level, ver_id=nil, branch_name=nil,
|
13
|
+
parent_branch_name=nil)
|
14
|
+
@app_id_or_name = app_id_or_name
|
15
|
+
@ver_id = ver_id
|
16
|
+
@scenario_id_or_name = scenario_id_or_name
|
17
|
+
@batch_info = batch_info
|
18
|
+
@environment = environment
|
19
|
+
@application = application
|
20
|
+
@match_level = match_level
|
21
|
+
@branch_name = branch_name
|
22
|
+
@parent_branch_name = parent_branch_name
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_hash
|
26
|
+
{
|
27
|
+
AppIdOrName: app_id_or_name, VerId: ver_id, ScenarioIdOrName: scenario_id_or_name,
|
28
|
+
BatchInfo: batch_info.to_hash, Environment: environment.to_hash,
|
29
|
+
Application: application.to_hash, matchLevel: match_level, branchName: branch_name,
|
30
|
+
parentBranchName: parent_branch_name
|
31
|
+
}
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class Applitools::TargetApp
|
2
|
+
|
3
|
+
attr_reader :url, :session_id, :user_agent
|
4
|
+
|
5
|
+
def initialize(url, session_id, user_agent)
|
6
|
+
@url = url
|
7
|
+
@session_id = session_id
|
8
|
+
@user_agent = user_agent
|
9
|
+
end
|
10
|
+
|
11
|
+
def to_hash
|
12
|
+
{
|
13
|
+
"$type" => "Applitools.Framework.TargetWebDriverApplication, Core",
|
14
|
+
url: URI.encode(url.to_s), sessionId: session_id, userAgent: user_agent
|
15
|
+
}
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class Applitools::TestResults
|
2
|
+
attr_reader :steps, :matches, :mismatches, :missing, :exact_matches, :strict_matches,
|
3
|
+
:context_matches, :layout_matches, :none_matches
|
4
|
+
def initialize(steps=0, matches=0, mismatches=0, missing=0,
|
5
|
+
exact_matches=0, strict_matches=0, content_matches=0,
|
6
|
+
layout_matches=0, none_matches=0)
|
7
|
+
@steps = steps
|
8
|
+
@matches = matches
|
9
|
+
@mismatches = mismatches
|
10
|
+
@missing = missing
|
11
|
+
@exact_matches = exact_matches
|
12
|
+
@strict_matches = strict_matches
|
13
|
+
@content_matches = content_matches
|
14
|
+
@layout_matches = layout_matches
|
15
|
+
@none_matches = none_matches
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
"[ steps: #{steps}, matches: #{matches}, mismatches: #{mismatches}, missing: #{missing} ]"
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class Applitools::TextTrigger
|
2
|
+
|
3
|
+
attr_reader :text, :control
|
4
|
+
|
5
|
+
def initialize(text, control)
|
6
|
+
@text = text
|
7
|
+
@control = control
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_hash
|
11
|
+
{
|
12
|
+
"$type" => "Applitools.Models.TextTrigger, Core", text: text, control: control.to_hash
|
13
|
+
}
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
class Applitools::ViewportSize
|
2
|
+
|
3
|
+
GET_VIEWPORT_HEIGHT_JAVASCRIPT_FOR_NORMAL_BROWSER = "return window.innerHeight"
|
4
|
+
GET_VIEWPORT_WIDTH_JAVASCRIPT_FOR_NORMAL_BROWSER = "return window.innerWidth"
|
5
|
+
|
6
|
+
DOCUMENT_CLEAR_SCROLL_BARS_JAVASCRIPT = "var doc = document.documentElement;" +
|
7
|
+
"var previousOverflow = doc.style.overflow;"
|
8
|
+
DOCUMENT_RESET_SCROLL_BARS_JAVASCRIPT = "doc.style.overflow = previousOverflow;"
|
9
|
+
DOCUMENT_RETURN_JAVASCRIPT = "return __applitools_result;"
|
10
|
+
|
11
|
+
GET_VIEWPORT_WIDTH_JAVASCRIPT_FOR_BAD_BROWSERS =
|
12
|
+
DOCUMENT_CLEAR_SCROLL_BARS_JAVASCRIPT +
|
13
|
+
"var __applitools_result = doc.clientWidth;" +
|
14
|
+
DOCUMENT_RESET_SCROLL_BARS_JAVASCRIPT +
|
15
|
+
DOCUMENT_RETURN_JAVASCRIPT
|
16
|
+
|
17
|
+
GET_VIEWPORT_HEIGHT_JAVASCRIPT_FOR_BAD_BROWSERS =
|
18
|
+
DOCUMENT_CLEAR_SCROLL_BARS_JAVASCRIPT +
|
19
|
+
"var __applitools_result = doc.clientHeight;" +
|
20
|
+
DOCUMENT_RESET_SCROLL_BARS_JAVASCRIPT +
|
21
|
+
DOCUMENT_RETURN_JAVASCRIPT
|
22
|
+
|
23
|
+
Dimension = Struct.new(:width, :height)
|
24
|
+
def Dimension.to_h
|
25
|
+
{width: width, height: height}
|
26
|
+
end
|
27
|
+
|
28
|
+
attr_reader :driver
|
29
|
+
attr_accessor :dimension
|
30
|
+
def initialize(driver, dimension=nil)
|
31
|
+
@driver = driver
|
32
|
+
@dimension = dimension
|
33
|
+
end
|
34
|
+
|
35
|
+
def extract_viewport_width
|
36
|
+
begin
|
37
|
+
return driver.execute_script(GET_VIEWPORT_WIDTH_JAVASCRIPT_FOR_NORMAL_BROWSER)
|
38
|
+
rescue => e
|
39
|
+
EyesLogger.info "getViewportSize(): Browser does not support innerWidth (#{e.message})"
|
40
|
+
end
|
41
|
+
|
42
|
+
driver.execute_script(GET_VIEWPORT_WIDTH_JAVASCRIPT_FOR_BAD_BROWSERS)
|
43
|
+
end
|
44
|
+
|
45
|
+
def extract_viewport_height
|
46
|
+
begin
|
47
|
+
return driver.execute_script(GET_VIEWPORT_HEIGHT_JAVASCRIPT_FOR_NORMAL_BROWSER)
|
48
|
+
rescue => e
|
49
|
+
EyesLogger.info "getViewportSize(): Browser does not support innerHeight (#{e.message})"
|
50
|
+
end
|
51
|
+
|
52
|
+
driver.execute_script(GET_VIEWPORT_WIDTH_JAVASCRIPT_FOR_BAD_BROWSERS)
|
53
|
+
end
|
54
|
+
|
55
|
+
def extract_viewport_from_browser!
|
56
|
+
self.dimension = extract_viewport_from_browser
|
57
|
+
end
|
58
|
+
|
59
|
+
def extract_viewport_from_browser
|
60
|
+
width = extract_viewport_width
|
61
|
+
height = extract_viewport_height
|
62
|
+
Dimension.new(width,height)
|
63
|
+
rescue => e
|
64
|
+
EyesLogger.info "getViewportSize(): only window size is available (#{e.message})"
|
65
|
+
width, height = *browser_size.values
|
66
|
+
Dimension.new(width,height)
|
67
|
+
end
|
68
|
+
alias_method :viewport_size, :extract_viewport_from_browser
|
69
|
+
|
70
|
+
def set
|
71
|
+
if !dimension.respond_to?(:width) && !dimension.respond_to?(:height)
|
72
|
+
raise ArgumentError, "expected #{dimension.inspect}:#{dimension.class}" +
|
73
|
+
" to respond to #width and #height"
|
74
|
+
end
|
75
|
+
|
76
|
+
self.browser_size = dimension
|
77
|
+
verify_size(:browser_size)
|
78
|
+
|
79
|
+
cur_viewport_size = extract_viewport_from_browser
|
80
|
+
self.browser_size = Dimension.new(
|
81
|
+
(2 * browser_size.width) - cur_viewport_size.width,
|
82
|
+
(2 * browser_size.height) - cur_viewport_size.height
|
83
|
+
)
|
84
|
+
verify_size(:viewport_size)
|
85
|
+
end
|
86
|
+
|
87
|
+
def verify_size(to_verify, sleep_time=1, retries=3)
|
88
|
+
retries.times do
|
89
|
+
sleep(sleep_time)
|
90
|
+
cur_size = send(to_verify)
|
91
|
+
return if cur_size.values == dimension.values
|
92
|
+
end
|
93
|
+
|
94
|
+
EyesLogger.info(err_msg = "Failed setting #{to_verify} to #{required_dimensions.values}")
|
95
|
+
raise Applitools::TestFailedError.new(err_msg)
|
96
|
+
end
|
97
|
+
|
98
|
+
def browser_size
|
99
|
+
driver.manage.window.size
|
100
|
+
end
|
101
|
+
|
102
|
+
def browser_size=(other)
|
103
|
+
self.driver.manage.window.size = other
|
104
|
+
end
|
105
|
+
|
106
|
+
def to_hash
|
107
|
+
Hash[dimension.each_pair.to_a]
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module EyesLogger
|
4
|
+
def self.logger(to=STDOUT, level=Logger::INFO)
|
5
|
+
return @@log if defined?(@@log)
|
6
|
+
@@log = Logger.new(to)
|
7
|
+
@@log.level = level
|
8
|
+
@@log
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.info(msg)
|
12
|
+
logger.info(msg)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.debug(msg)
|
16
|
+
logger.debug(msg)
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
=begin
|
2
|
+
Applitools SDK class.
|
3
|
+
|
4
|
+
Provides image compression based on image sequences and deflate
|
5
|
+
=end
|
6
|
+
require 'oily_png'
|
7
|
+
require 'base64'
|
8
|
+
|
9
|
+
class Applitools::Utils::ImageDeltaCompressor
|
10
|
+
|
11
|
+
# Compresses the target image based on the source image.
|
12
|
+
# +target+:: +ChunkyPNG::Canvas+ The image to compress based on the source image.
|
13
|
+
# +target_encoded+:: +Array+ The uncompressed image as binary string.
|
14
|
+
# +source+:: +ChunkyPNG::Canvas+ The source image used as a base for compressing the target image.
|
15
|
+
# +block_size+:: +Integer+ The width/height of each block.
|
16
|
+
# ++
|
17
|
+
# Returns +String+ The binary result (either the compressed image, or the uncompressed image if the compression
|
18
|
+
# is greater in length)
|
19
|
+
def self.compress_by_raw_blocks(target, target_encoded, source, block_size = 10)
|
20
|
+
# If we can't compress for any reason, return the target image as is.
|
21
|
+
if source.nil? || (source.height != target.height) || (source.width != target.width)
|
22
|
+
# Returning a COPY of the target binary string
|
23
|
+
return String.new(target_encoded)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Preparing the variables we need
|
27
|
+
target_pixels = target.to_rgb_stream.unpack('C*')
|
28
|
+
source_pixels = source.to_rgb_stream.unpack('C*')
|
29
|
+
image_size = Dimension.new(target.width, target.height)
|
30
|
+
block_columns_count = (target.width / block_size) + ((target.width % block_size) == 0 ? 0 : 1)
|
31
|
+
block_rows_count = (target.height / block_size) + ((target.height % block_size) == 0 ? 0 : 1)
|
32
|
+
|
33
|
+
# IMPORTANT: The "-Zlib::MAX_WBITS" tells ZLib to create raw deflate compression, without the
|
34
|
+
# "Zlib headers" (this isn't documented in the Zlib page, I found this in some internet forum).
|
35
|
+
compressor = Zlib::Deflate.new(Zlib::BEST_COMPRESSION, -Zlib::MAX_WBITS)
|
36
|
+
|
37
|
+
compression_result = ''
|
38
|
+
|
39
|
+
# Writing the data header
|
40
|
+
compression_result += @@PREAMBLE.encode("UTF-8")
|
41
|
+
compression_result += [@@FORMAT_RAW_BLOCKS].pack("C")
|
42
|
+
compression_result += [0].pack("S>") #Source id, Big Endian
|
43
|
+
compression_result += [block_size].pack("S>") #Big Endian
|
44
|
+
|
45
|
+
|
46
|
+
# We perform the compression for each channel
|
47
|
+
3.times do |channel|
|
48
|
+
block_number = 0
|
49
|
+
block_rows_count.times do |block_row|
|
50
|
+
block_columns_count.times do |block_column|
|
51
|
+
actual_channel_index = 2 - channel # Since the image bytes are BGR and the server expects RGB...
|
52
|
+
compare_result = compare_and_copy_block_channel_data(source_pixels, target_pixels, image_size, 3, block_size,
|
53
|
+
block_column, block_row, actual_channel_index)
|
54
|
+
|
55
|
+
if !compare_result.identical
|
56
|
+
channel_bytes = compare_result.channel_bytes
|
57
|
+
string_to_compress = [channel].pack('C')
|
58
|
+
string_to_compress += [block_number].pack('L>')
|
59
|
+
string_to_compress += channel_bytes.pack('C*')
|
60
|
+
compression_result += compressor.deflate(string_to_compress)
|
61
|
+
|
62
|
+
# If the compressed data so far is greater than the uncompressed
|
63
|
+
# representation of the target, just return the target.
|
64
|
+
if compression_result.length > target_encoded.length
|
65
|
+
compressor.close
|
66
|
+
# Returning a COPY of the target bytes
|
67
|
+
return String.new(target_encoded)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
block_number += 1
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
# Compress and flush any remaining uncompressed data in the input buffer.
|
75
|
+
compression_result += compressor.finish
|
76
|
+
compressor.close
|
77
|
+
# Returning the compressed result as a byte array
|
78
|
+
return compression_result
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
### PRIVATE
|
83
|
+
private
|
84
|
+
|
85
|
+
@@PREAMBLE = "applitools"
|
86
|
+
@@FORMAT_RAW_BLOCKS = 3
|
87
|
+
|
88
|
+
Dimension = Struct.new(:width, :height)
|
89
|
+
CompareAndCopyBlockChannelDataResult = Struct.new(:identical, :channel_bytes)
|
90
|
+
|
91
|
+
|
92
|
+
# Computes the width and height of the image data contained in the block
|
93
|
+
# at the input column and row.
|
94
|
+
# +image_size+:: +Dimension+ The image size in pixels.
|
95
|
+
# +block_size+:: The block size for which we would like to compute the image data width and height.
|
96
|
+
# +block_column+:: The block column index.
|
97
|
+
# +block_row+:: The block row index.
|
98
|
+
# ++
|
99
|
+
# Returns the width and height of the image data contained in the block are returned as a +Dimension+.
|
100
|
+
def self.get_actual_block_size(image_size, block_size, block_column, block_row)
|
101
|
+
actual_width = [image_size.width - (block_column * block_size), block_size].min
|
102
|
+
actual_height = [image_size.height - (block_row * block_size), block_size].min
|
103
|
+
Dimension.new(actual_width, actual_height)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Compares a block of pixels between the source and target and copies the target's block bytes to the result.
|
107
|
+
# +source_pixels+:: +Array+ of bytes, representing the pixels of the source image.
|
108
|
+
# +target_pixels+:: +Array+ of bytes, representing the pixels of the target image.
|
109
|
+
# +image_size+:: +Dimension+ The size of the source/target image (remember they must be the same size).
|
110
|
+
# +pixel_length+:: +Integer+ The number of bytes composing a pixel
|
111
|
+
# +block_size+:: +Integer+ The width/height of the block (block is a square, theoretically).
|
112
|
+
# +block_column+:: +Integer+ The block column index (when looking at the images as a grid of blocks).
|
113
|
+
# +block_row+:: +Integer+ The block row index (when looking at the images as a grid of blocks).
|
114
|
+
# +channel+:: +Integer+ The index of the channel we're comparing.
|
115
|
+
# ++
|
116
|
+
# Returns +CompareAndCopyBlockChannelDataResult+ object containing a flag saying whether the blocks are identical
|
117
|
+
# and a copy of the target block's bytes.
|
118
|
+
def self.compare_and_copy_block_channel_data(source_pixels, target_pixels, image_size, pixel_length, block_size,
|
119
|
+
block_column, block_row, channel)
|
120
|
+
identical = true
|
121
|
+
|
122
|
+
actual_block_size = get_actual_block_size(image_size, block_size, block_column, block_row)
|
123
|
+
|
124
|
+
# Getting the actual amount of data in the block we wish to copy
|
125
|
+
actual_block_height = actual_block_size.height
|
126
|
+
actual_block_width = actual_block_size.width
|
127
|
+
|
128
|
+
stride = image_size.width * pixel_length
|
129
|
+
|
130
|
+
# Iterating the block's pixels and comparing the source and target
|
131
|
+
channel_bytes = []
|
132
|
+
actual_block_height.times do |h|
|
133
|
+
offset = (((block_size * block_row) + h) * stride) + (block_size * block_column * pixel_length) + channel
|
134
|
+
actual_block_width.times do |w|
|
135
|
+
source_byte = source_pixels[offset]
|
136
|
+
target_byte = target_pixels[offset]
|
137
|
+
if source_byte != target_byte
|
138
|
+
identical = false
|
139
|
+
end
|
140
|
+
channel_bytes << target_byte
|
141
|
+
offset += pixel_length
|
142
|
+
end
|
143
|
+
end
|
144
|
+
# Returning the compare-and-copy result
|
145
|
+
CompareAndCopyBlockChannelDataResult.new(identical, channel_bytes)
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'eyes_selenium_ruby/eyes_logger'
|
3
|
+
require 'json'
|
4
|
+
module Applitools
|
5
|
+
include EyesLogger
|
6
|
+
class EyesError < StandardError; end
|
7
|
+
class EyesAbort < EyesError; end
|
8
|
+
|
9
|
+
class TestFailedError < StandardError
|
10
|
+
attr_accessor :test_results
|
11
|
+
def initialize(message, test_results=nil)
|
12
|
+
super(message)
|
13
|
+
@test_results = test_results
|
14
|
+
end
|
15
|
+
end
|
16
|
+
class NewTestError < TestFailedError; end
|
17
|
+
|
18
|
+
require 'eyes_selenium_ruby/utils'
|
19
|
+
require "eyes_selenium_ruby/eyes/agent_connecter"
|
20
|
+
require "eyes_selenium_ruby/eyes/target_app"
|
21
|
+
require 'eyes_selenium_ruby/eyes/batch_info'
|
22
|
+
require "eyes_selenium_ruby/eyes/driver"
|
23
|
+
require 'eyes_selenium_ruby/eyes/element'
|
24
|
+
require 'eyes_selenium_ruby/eyes/environment'
|
25
|
+
require 'eyes_selenium_ruby/eyes/eyes'
|
26
|
+
require 'eyes_selenium_ruby/eyes/eyes_keyboard'
|
27
|
+
require 'eyes_selenium_ruby/eyes/eyes_mouse'
|
28
|
+
require 'eyes_selenium_ruby/eyes/failure_reports'
|
29
|
+
require 'eyes_selenium_ruby/eyes/match_level'
|
30
|
+
require 'eyes_selenium_ruby/eyes/match_window_data'
|
31
|
+
require 'eyes_selenium_ruby/eyes/match_window_task'
|
32
|
+
require 'eyes_selenium_ruby/eyes/mouse_trigger'
|
33
|
+
require 'eyes_selenium_ruby/eyes/region'
|
34
|
+
require 'eyes_selenium_ruby/eyes/screenshot_taker'
|
35
|
+
require 'eyes_selenium_ruby/eyes/session'
|
36
|
+
require 'eyes_selenium_ruby/eyes/start_info'
|
37
|
+
require 'eyes_selenium_ruby/eyes/test_results'
|
38
|
+
require 'eyes_selenium_ruby/eyes/text_trigger'
|
39
|
+
require "eyes_selenium_ruby/version"
|
40
|
+
require 'eyes_selenium_ruby/eyes/viewport_size'
|
41
|
+
end
|