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.
Files changed (41) hide show
  1. data/.gitignore +17 -0
  2. data/.rspec +2 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +13 -0
  5. data/README.md +66 -0
  6. data/Rakefile +1 -0
  7. data/eyes_selenium_ruby.gemspec +30 -0
  8. data/lib/eyes_selenium_ruby/capybara.rb +21 -0
  9. data/lib/eyes_selenium_ruby/eyes/agent_connecter.rb +39 -0
  10. data/lib/eyes_selenium_ruby/eyes/batch_info.rb +14 -0
  11. data/lib/eyes_selenium_ruby/eyes/driver.rb +145 -0
  12. data/lib/eyes_selenium_ruby/eyes/element.rb +78 -0
  13. data/lib/eyes_selenium_ruby/eyes/environment.rb +13 -0
  14. data/lib/eyes_selenium_ruby/eyes/eyes.rb +179 -0
  15. data/lib/eyes_selenium_ruby/eyes/eyes_keyboard.rb +25 -0
  16. data/lib/eyes_selenium_ruby/eyes/eyes_mouse.rb +60 -0
  17. data/lib/eyes_selenium_ruby/eyes/failure_reports.rb +4 -0
  18. data/lib/eyes_selenium_ruby/eyes/match_level.rb +7 -0
  19. data/lib/eyes_selenium_ruby/eyes/match_window_data.rb +18 -0
  20. data/lib/eyes_selenium_ruby/eyes/match_window_task.rb +71 -0
  21. data/lib/eyes_selenium_ruby/eyes/mouse_trigger.rb +19 -0
  22. data/lib/eyes_selenium_ruby/eyes/region.rb +22 -0
  23. data/lib/eyes_selenium_ruby/eyes/screenshot_taker.rb +18 -0
  24. data/lib/eyes_selenium_ruby/eyes/session.rb +14 -0
  25. data/lib/eyes_selenium_ruby/eyes/start_info.rb +33 -0
  26. data/lib/eyes_selenium_ruby/eyes/target_app.rb +17 -0
  27. data/lib/eyes_selenium_ruby/eyes/test_results.rb +21 -0
  28. data/lib/eyes_selenium_ruby/eyes/text_trigger.rb +15 -0
  29. data/lib/eyes_selenium_ruby/eyes/viewport_size.rb +109 -0
  30. data/lib/eyes_selenium_ruby/eyes_logger.rb +18 -0
  31. data/lib/eyes_selenium_ruby/utils/image_delta_compressor.rb +147 -0
  32. data/lib/eyes_selenium_ruby/utils.rb +5 -0
  33. data/lib/eyes_selenium_ruby/version.rb +3 -0
  34. data/lib/eyes_selenium_ruby.rb +41 -0
  35. data/spec/capybara_spec.rb +33 -0
  36. data/spec/driver_spec.rb +10 -0
  37. data/spec/eyes_spec.rb +156 -0
  38. data/spec/spec_helper.rb +29 -0
  39. data/spec/test_app.rb +11 -0
  40. data/test_script.rb +22 -0
  41. 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,4 @@
1
+ module Applitools::FailureReports
2
+ IMMEDIATE = 0
3
+ ON_CLOSE = 1
4
+ end
@@ -0,0 +1,7 @@
1
+ module MatchLevel
2
+ NONE = 0
3
+ LAYOUT = 1
4
+ CONTENT = 2
5
+ STRICT = 3
6
+ EXACT = 4
7
+ 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,5 @@
1
+ module Applitools
2
+ module Utils
3
+ require 'eyes_selenium_ruby/utils/image_delta_compressor'
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ module Applitools
2
+ VERSION = "1.1.0"
3
+ 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