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.
- 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,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
|
+
|