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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +0 -2
  3. data/.travis.yml +16 -0
  4. data/Gemfile +1 -1
  5. data/README.md +14 -4
  6. data/Rakefile +8 -1
  7. data/eyes_selenium.gemspec +26 -17
  8. data/lib/applitools/base/batch_info.rb +19 -0
  9. data/lib/applitools/base/dimension.rb +21 -0
  10. data/lib/applitools/base/environment.rb +19 -0
  11. data/lib/applitools/base/mouse_trigger.rb +33 -0
  12. data/lib/applitools/base/point.rb +21 -0
  13. data/lib/applitools/base/region.rb +77 -0
  14. data/lib/applitools/base/server_connector.rb +113 -0
  15. data/lib/applitools/base/session.rb +15 -0
  16. data/lib/applitools/base/start_info.rb +34 -0
  17. data/lib/applitools/base/test_results.rb +36 -0
  18. data/lib/applitools/base/text_trigger.rb +22 -0
  19. data/lib/applitools/eyes.rb +393 -0
  20. data/lib/applitools/eyes_logger.rb +40 -0
  21. data/lib/applitools/method_tracer.rb +22 -0
  22. data/lib/applitools/selenium/driver.rb +194 -0
  23. data/lib/applitools/selenium/element.rb +66 -0
  24. data/lib/applitools/selenium/keyboard.rb +27 -0
  25. data/lib/applitools/selenium/match_window_data.rb +24 -0
  26. data/lib/applitools/selenium/match_window_task.rb +190 -0
  27. data/lib/applitools/selenium/mouse.rb +62 -0
  28. data/lib/applitools/selenium/viewport_size.rb +128 -0
  29. data/lib/applitools/utils/image_delta_compressor.rb +150 -0
  30. data/lib/applitools/utils/image_utils.rb +63 -0
  31. data/lib/applitools/utils/utils.rb +52 -0
  32. data/lib/applitools/version.rb +3 -0
  33. data/lib/eyes_selenium.rb +9 -29
  34. data/spec/driver_passthrough_spec.rb +25 -25
  35. data/spec/spec_helper.rb +5 -8
  36. data/test/appium_example_script.rb +57 -0
  37. data/{test_script.rb → test/test_script.rb} +7 -9
  38. data/{watir_test.rb → test/watir_test_script.rb} +6 -4
  39. metadata +120 -48
  40. data/appium_eyes_example.rb +0 -56
  41. data/lib/eyes_selenium/capybara.rb +0 -21
  42. data/lib/eyes_selenium/eyes/agent_connector.rb +0 -76
  43. data/lib/eyes_selenium/eyes/batch_info.rb +0 -19
  44. data/lib/eyes_selenium/eyes/dimension.rb +0 -15
  45. data/lib/eyes_selenium/eyes/driver.rb +0 -266
  46. data/lib/eyes_selenium/eyes/element.rb +0 -78
  47. data/lib/eyes_selenium/eyes/environment.rb +0 -15
  48. data/lib/eyes_selenium/eyes/eyes.rb +0 -396
  49. data/lib/eyes_selenium/eyes/eyes_keyboard.rb +0 -25
  50. data/lib/eyes_selenium/eyes/eyes_mouse.rb +0 -60
  51. data/lib/eyes_selenium/eyes/failure_reports.rb +0 -4
  52. data/lib/eyes_selenium/eyes/match_level.rb +0 -8
  53. data/lib/eyes_selenium/eyes/match_window_data.rb +0 -18
  54. data/lib/eyes_selenium/eyes/match_window_task.rb +0 -182
  55. data/lib/eyes_selenium/eyes/mouse_trigger.rb +0 -23
  56. data/lib/eyes_selenium/eyes/region.rb +0 -72
  57. data/lib/eyes_selenium/eyes/screenshot_taker.rb +0 -18
  58. data/lib/eyes_selenium/eyes/session.rb +0 -14
  59. data/lib/eyes_selenium/eyes/start_info.rb +0 -32
  60. data/lib/eyes_selenium/eyes/test_results.rb +0 -32
  61. data/lib/eyes_selenium/eyes/text_trigger.rb +0 -19
  62. data/lib/eyes_selenium/eyes/viewport_size.rb +0 -105
  63. data/lib/eyes_selenium/eyes_logger.rb +0 -47
  64. data/lib/eyes_selenium/utils.rb +0 -6
  65. data/lib/eyes_selenium/utils/image_delta_compressor.rb +0 -149
  66. data/lib/eyes_selenium/utils/image_utils.rb +0 -76
  67. data/lib/eyes_selenium/version.rb +0 -3
@@ -0,0 +1,62 @@
1
+ module Applitools::Selenium
2
+ class Mouse
3
+ attr_reader :driver, :mouse
4
+
5
+ def initialize(driver, mouse)
6
+ @driver = driver
7
+ @mouse = mouse
8
+ end
9
+
10
+ def click(element = nil)
11
+ extract_trigger_and_perform(:click, element)
12
+ end
13
+
14
+ def double_click(element = nil)
15
+ extract_trigger_and_perform(:double_click, element)
16
+ end
17
+
18
+ def context_click(element = nil)
19
+ extract_trigger_and_perform(:right_click, element)
20
+ end
21
+
22
+ def down(element = nil)
23
+ extract_trigger_and_perform(:down, element)
24
+ end
25
+
26
+ def up(element = nil)
27
+ extract_trigger_and_perform(:up, element)
28
+ end
29
+
30
+ def move_to(element, right_by = nil, down_by = nil)
31
+ element = element.web_element if element.is_a?(Applitools::Selenium::Element)
32
+ location = element.location
33
+ location.x = [0, location.x].max.round
34
+ location.y = [0, location.y].max.round
35
+ current_control = Applitools::Base::Region.new(0, 0, *location.values)
36
+ driver.user_inputs << Applitools::Base::MouseTrigger.new(:move, current_control, location)
37
+ element = element.web_element if element.is_a?(Applitools::Selenium::Element)
38
+ mouse.move_to(element,right_by, down_by)
39
+ end
40
+
41
+ def move_by(right_by, down_by)
42
+ right = [0, right_by].max.round
43
+ down = [0, down_by].max.round
44
+ location = Applitools::Base::Point.new(right,down)
45
+ current_control = Applitools::Base::Region.new(0, 0, right, down)
46
+ driver.user_inputs << Applitools::Base::MouseTrigger.new(:move, current_control, location)
47
+ mouse.move_by(right_by,down_by)
48
+ end
49
+
50
+ private
51
+
52
+ def extract_trigger_and_perform(method, element = nil, *args)
53
+ location = element.location
54
+ location.x = [0, location.x].max.round
55
+ location.y = [0, location.y].max.round
56
+ current_control = Applitools::Base::Region.new(0, 0, *location.values)
57
+ driver.user_inputs << Applitools::Base::MouseTrigger.new(method, current_control, location)
58
+ element = element.web_element if element.is_a?(Applitools::Selenium::Element)
59
+ mouse.send(method, element, *args)
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,128 @@
1
+ module Applitools::Selenium
2
+ class ViewportSize
3
+ JS_GET_VIEWPORT_HEIGHT = <<-EOF
4
+ var height = undefined;
5
+ if (window.innerHeight) {
6
+ height = window.innerHeight;
7
+ }
8
+ else if (document.documentElement && document.documentElement.clientHeight) {
9
+ height = document.documentElement.clientHeight;
10
+ } else {
11
+ var b = document.getElementsByTagName("body")[0];
12
+ if (b.clientHeight) {
13
+ height = b.clientHeight;
14
+ }
15
+ }
16
+
17
+ return height;
18
+ EOF
19
+
20
+ JS_GET_VIEWPORT_WIDTH = <<-EOF
21
+ var width = undefined;
22
+ if (window.innerWidth) {
23
+ width = window.innerWidth
24
+ } else if (document.documentElement && document.documentElement.clientWidth) {
25
+ width = document.documentElement.clientWidth;
26
+ } else {
27
+ var b = document.getElementsByTagName("body")[0];
28
+ if (b.clientWidth) {
29
+ width = b.clientWidth;
30
+ }
31
+ }
32
+
33
+ return width;
34
+ EOF
35
+
36
+ VERIFY_SLEEP_PERIOD = 1.freeze
37
+ VERIFY_RETRIES = 3.freeze
38
+
39
+ def initialize(driver, dimension = nil)
40
+ @driver = driver
41
+ @dimension = dimension
42
+ end
43
+
44
+ def extract_viewport_width
45
+ @driver.execute_script(JS_GET_VIEWPORT_WIDTH)
46
+ end
47
+
48
+ def extract_viewport_height
49
+ @driver.execute_script(JS_GET_VIEWPORT_HEIGHT)
50
+ end
51
+
52
+ def extract_viewport_from_browser!
53
+ @dimension = extract_viewport_from_browser
54
+ end
55
+
56
+ def extract_viewport_from_browser
57
+ width, height = nil, nil
58
+ begin
59
+ width = extract_viewport_width
60
+ height = extract_viewport_height
61
+ rescue => e
62
+ Applitools::EyesLogger.error "Failed extracting viewport size using JavaScript: (#{e.message})"
63
+ end
64
+
65
+ if width.nil? || height.nil?
66
+ Applitools::EyesLogger.info "Using window size as viewport size."
67
+
68
+ width, height = *browser_size.values
69
+ width, height = width.ceil, height.ceil
70
+
71
+ if @driver.landscape_orientation? && height > width
72
+ width, height = height, width
73
+ end
74
+ end
75
+
76
+ Applitools::Base::Dimension.new(width,height)
77
+ end
78
+
79
+ alias_method :viewport_size, :extract_viewport_from_browser
80
+
81
+ def set
82
+ if @dimension.is_a?(Hash) && @dimension.has_key?(:width) && @dimension.has_key?(:height)
83
+ # If @dimension is hash of width/height, we convert it to a struct with width/height properties.
84
+ @dimension = Struct.new(:width, :height).new(@dimension[:width], @dimension[:height])
85
+ elsif !@dimension.respond_to?(:width) || !@dimension.respond_to?(:height)
86
+ raise ArgumentError, "expected #{@dimension.inspect}:#{@dimension.class} to respond to #width and #height, or be "\
87
+ ' a hash with these keys.'
88
+ end
89
+
90
+ set_browser_size(@dimension)
91
+ verify_size(:browser_size)
92
+
93
+ cur_viewport_size = extract_viewport_from_browser
94
+
95
+ set_browser_size(Applitools::Base::Dimension.new((2 * browser_size.width) - cur_viewport_size.width,
96
+ (2 * browser_size.height) - cur_viewport_size.height))
97
+ verify_size(:viewport_size)
98
+ end
99
+
100
+ def verify_size(to_verify, sleep_time = VERIFY_SLEEP_PERIOD, retries = VERIFY_RETRIES)
101
+ cur_size = nil
102
+
103
+ retries.times do
104
+ sleep(sleep_time)
105
+ cur_size = send(to_verify)
106
+
107
+ return if cur_size.values == @dimension.values
108
+ end
109
+
110
+ err_msg = "Failed setting #{to_verify} to #{@dimension.values} (current size: #{cur_size.values})"
111
+
112
+ Applitools::EyesLogger.error(err_msg)
113
+ raise Applitools::TestFailedError.new(err_msg)
114
+ end
115
+
116
+ def browser_size
117
+ @driver.manage.window.size
118
+ end
119
+
120
+ def set_browser_size(other)
121
+ @driver.manage.window.size = other
122
+ end
123
+
124
+ def to_hash
125
+ Hash[@dimension.each_pair.to_a]
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,150 @@
1
+ require 'oily_png'
2
+
3
+ module Applitools::Utils
4
+ module ImageDeltaCompressor
5
+ extend self
6
+
7
+ BLOCK_SIZE = 10.freeze
8
+
9
+ # Compresses the target image based on the source image.
10
+ #
11
+ # +target+:: +ChunkyPNG::Canvas+ The image to compress based on the source image.
12
+ # +target_encoded+:: +Array+ The uncompressed image as binary string.
13
+ # +source+:: +ChunkyPNG::Canvas+ The source image used as a base for compressing the target image.
14
+ # +block_size+:: +Integer+ The width/height of each block.
15
+ #
16
+ # Returns +String+ The binary result (either the compressed image, or the uncompressed image if the compression
17
+ # is greater in length).
18
+ def compress_by_raw_blocks(target, target_encoded, source, block_size = BLOCK_SIZE)
19
+ # If we can't compress for any reason, return the target image as is.
20
+ if source.nil? || (source.height != target.height) || (source.width != target.width)
21
+ # Returning a COPY of the target binary string.
22
+ return String.new(target_encoded)
23
+ end
24
+
25
+ # Preparing the variables we need.
26
+ target_pixels = target.to_rgb_stream.unpack('C*')
27
+ source_pixels = source.to_rgb_stream.unpack('C*')
28
+ image_size = Dimension.new(target.width, target.height)
29
+ block_columns_count = (target.width / block_size) + ((target.width % block_size) == 0 ? 0 : 1)
30
+ block_rows_count = (target.height / block_size) + ((target.height % block_size) == 0 ? 0 : 1)
31
+
32
+ # IMPORTANT: The "-Zlib::MAX_WBITS" tells ZLib to create raw deflate compression, without the
33
+ # "Zlib headers" (this isn't documented in the Zlib page, I found this in some internet forum).
34
+ compressor = Zlib::Deflate.new(Zlib::BEST_COMPRESSION, -Zlib::MAX_WBITS)
35
+
36
+ compression_result = ''
37
+
38
+ # Writing the data header.
39
+ compression_result += PREAMBLE.encode('UTF-8')
40
+ compression_result += [FORMAT_RAW_BLOCKS].pack('C')
41
+ compression_result += [0].pack('S>') #Source id, Big Endian
42
+ compression_result += [block_size].pack('S>') #Big Endian
43
+
44
+ # We perform the compression for each channel.
45
+ 3.times do |channel|
46
+ block_number = 0
47
+ block_rows_count.times do |block_row|
48
+ block_columns_count.times do |block_column|
49
+ actual_channel_index = 2 - channel # Since the image bytes are BGR and the server expects RGB...
50
+ compare_result = compare_and_copy_block_channel_data(source_pixels, target_pixels, image_size, 3, block_size,
51
+ block_column, block_row, actual_channel_index)
52
+
53
+ unless compare_result.identical
54
+ channel_bytes = compare_result.channel_bytes
55
+ string_to_compress = [channel].pack('C')
56
+ string_to_compress += [block_number].pack('L>')
57
+ string_to_compress += channel_bytes.pack('C*')
58
+
59
+ compression_result += compressor.deflate(string_to_compress)
60
+
61
+ # If the compressed data so far is greater than the uncompressed representation of the target, just return
62
+ # the target.
63
+ if compression_result.length > target_encoded.length
64
+ compressor.finish
65
+ compressor.close
66
+ # Returning a copy of the target bytes.
67
+ return String.new(target_encoded)
68
+ end
69
+ end
70
+
71
+ block_number += 1
72
+ end
73
+ end
74
+ end
75
+
76
+ # Compress and flush any remaining uncompressed data in the input buffer.
77
+ compression_result += compressor.finish
78
+ compressor.close
79
+
80
+ # Returning the compressed result as a byte array.
81
+ compression_result
82
+ end
83
+
84
+ private
85
+
86
+ PREAMBLE = 'applitools'.freeze
87
+ FORMAT_RAW_BLOCKS = 3.freeze
88
+
89
+ Dimension = Struct.new(:width, :height)
90
+ CompareAndCopyBlockChannelDataResult = Struct.new(:identical, :channel_bytes)
91
+
92
+ # Computes the width and height of the image data contained in the block at the input column and row.
93
+ # +image_size+:: +Dimension+ The image size in pixels.
94
+ # +block_size+:: The block size for which we would like to compute the image data width and height.
95
+ # +block_column+:: The block column index.
96
+ # +block_row+:: The block row index.
97
+ # ++
98
+ # Returns the width and height of the image data contained in the block are returned as a +Dimension+.
99
+ def get_actual_block_size(image_size, block_size, block_column, block_row)
100
+ actual_width = [image_size.width - (block_column * block_size), block_size].min
101
+ actual_height = [image_size.height - (block_row * block_size), block_size].min
102
+ Dimension.new(actual_width, actual_height)
103
+ end
104
+
105
+ # Compares a block of pixels between the source and target and copies the target's block bytes to the result.
106
+ # +source_pixels+:: +Array+ of bytes, representing the pixels of the source image.
107
+ # +target_pixels+:: +Array+ of bytes, representing the pixels of the target image.
108
+ # +image_size+:: +Dimension+ The size of the source/target image (remember they must be the same size).
109
+ # +pixel_length+:: +Integer+ The number of bytes composing a pixel
110
+ # +block_size+:: +Integer+ The width/height of the block (block is a square, theoretically).
111
+ # +block_column+:: +Integer+ The block column index (when looking at the images as a grid of blocks).
112
+ # +block_row+:: +Integer+ The block row index (when looking at the images as a grid of blocks).
113
+ # +channel+:: +Integer+ The index of the channel we're comparing.
114
+ # ++
115
+ # Returns +CompareAndCopyBlockChannelDataResult+ object containing a flag saying whether the blocks are identical
116
+ # and a copy of the target block's bytes.
117
+ def compare_and_copy_block_channel_data(source_pixels, target_pixels, image_size, pixel_length, block_size,
118
+ block_column, block_row, channel)
119
+ identical = true
120
+
121
+ actual_block_size = get_actual_block_size(image_size, block_size, block_column, block_row)
122
+
123
+ # Getting the actual amount of data in the block we wish to copy.
124
+ actual_block_height = actual_block_size.height
125
+ actual_block_width = actual_block_size.width
126
+
127
+ stride = image_size.width * pixel_length
128
+
129
+ # Iterating the block's pixels and comparing the source and target.
130
+ channel_bytes = []
131
+ actual_block_height.times do |h|
132
+ offset = (((block_size * block_row) + h) * stride) + (block_size * block_column * pixel_length) + channel
133
+ actual_block_width.times do |w|
134
+ source_byte = source_pixels[offset]
135
+ target_byte = target_pixels[offset]
136
+ if source_byte != target_byte
137
+ identical = false
138
+ end
139
+ channel_bytes << target_byte
140
+ offset += pixel_length
141
+ end
142
+ end
143
+
144
+ # Returning the compare-and-copy result.
145
+ CompareAndCopyBlockChannelDataResult.new(identical, channel_bytes)
146
+ end
147
+
148
+ include Applitools::MethodTracer
149
+ end
150
+ end
@@ -0,0 +1,63 @@
1
+ require 'oily_png'
2
+ require 'base64'
3
+
4
+ module Applitools::Utils
5
+ module ImageUtils
6
+ extend self
7
+
8
+ # Creates an image object from the PNG bytes.
9
+ # +png_bytes+:: +String+ A binary string of the PNG bytes of the image.
10
+ #
11
+ # Returns:
12
+ # +ChunkyPNG::Canvas+ An image object.
13
+ def png_image_from_bytes(png_bytes)
14
+ ChunkyPNG::Image.from_blob(png_bytes)
15
+ end
16
+
17
+ # Creates an image instance from a base64 representation of its PNG encoding.
18
+ #
19
+ # +png_bytes64+:: +String+ The Base64 representation of a PNG image.
20
+ #
21
+ # Returns:
22
+ # +ChunkyPNG::Canvas+ An image object.
23
+ def png_image_from_base64(png_bytes64)
24
+ png_image_from_bytes(Base64.decode64(png_bytes64))
25
+ end
26
+
27
+ # Get the raw PNG bytes of an image.
28
+ #
29
+ # +ChunkyPNG::Canvas+ The image object for which to get the PNG bytes.
30
+ #
31
+ # Returns:
32
+ # +String+ The PNG bytes of the image.
33
+ def bytes_from_png_image(image)
34
+ image.to_blob
35
+ end
36
+
37
+ # Get the Base64 representation of the raw PNG bytes of an image.
38
+ #
39
+ # +ChunkyPNG::Canvas+ The image object for which to get the PNG bytes.
40
+ #
41
+ # Returns:
42
+ # +String+ the Base64 representation of the raw PNG bytes of an image.
43
+ def base64_from_png_image(image)
44
+ Base64.encode64(bytes_from_png_image(image))
45
+ end
46
+
47
+ # Rotates a matrix 90 deg clockwise or counter clockwise (depending whether num_quadrants is positive or negative,
48
+ # respectively).
49
+ #
50
+ # +image+:: +ChunkyPNG::Canvas+ The image to rotate.
51
+ # +num_quadrants+:: +Integer+ The number of rotations to perform. Positive values are used for clockwise rotation
52
+ # and negative values are used for counter-clockwise rotation.
53
+ #
54
+ def quadrant_rotate!(image, num_quadrants)
55
+ image.tap do |img|
56
+ rotate_method = num_quadrants > 0 ? img.method(:rotate_right!) : img.method(:rotate_left!)
57
+ (0..(num_quadrants.abs - 1)).each { rotate_method.call }
58
+ end
59
+ end
60
+
61
+ include Applitools::MethodTracer
62
+ end
63
+ end
@@ -0,0 +1,52 @@
1
+ require 'bigdecimal'
2
+
3
+ module Applitools::Utils
4
+ extend self
5
+
6
+ def underscore(str)
7
+ str.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').gsub(/([a-z\d])([A-Z])/,'\1_\2').tr('-', '_').downcase
8
+ end
9
+
10
+ def uncapitalize(str)
11
+ str[0, 1].downcase + str[1..-1]
12
+ end
13
+
14
+ def camelcase(str)
15
+ tokens = str.split('_')
16
+ uncapitalize(tokens.shift) + tokens.map(&:capitalize).join
17
+ end
18
+
19
+ def wrap(object)
20
+ if object.nil?
21
+ []
22
+ elsif object.respond_to?(:to_ary)
23
+ object.to_ary || [object]
24
+ else
25
+ [object]
26
+ end
27
+ end
28
+
29
+ def underscore_hash_keys(hash)
30
+ convert_hash_keys(hash, :underscore)
31
+ end
32
+
33
+ def camelcase_hash_keys(hash)
34
+ convert_hash_keys(hash, :camelcase)
35
+ end
36
+
37
+ private
38
+
39
+ def convert_hash_keys(value, method)
40
+ case value
41
+ when Array
42
+ value.map {|v| convert_hash_keys(v, method) }
43
+ when Hash
44
+ Hash[value.map {|k, v| [send(method, k.to_s).to_sym,
45
+ convert_hash_keys(v, method)]}]
46
+ else
47
+ value
48
+ end
49
+ end
50
+ end
51
+
52
+