eyes_selenium 2.15.0 → 2.16.0

Sign up to get free protection for your applications and to get access to all the features.
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
+