eyes_selenium 2.39.1 → 3.0.6

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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/lib/applitools/capybara.rb +1 -1
  3. data/lib/applitools/selenium/border_aware_element_content_location_provider.rb +41 -0
  4. data/lib/applitools/selenium/browser.rb +1 -0
  5. data/lib/applitools/selenium/capybara/capybara_settings.rb +8 -0
  6. data/lib/applitools/selenium/capybara/driver.rb +1 -0
  7. data/lib/applitools/selenium/context_based_scale_provider.rb +36 -0
  8. data/lib/applitools/selenium/css_translate_position_provider.rb +66 -0
  9. data/lib/applitools/selenium/driver.rb +159 -44
  10. data/lib/applitools/selenium/element.rb +100 -8
  11. data/lib/applitools/selenium/element_position_provider.rb +48 -0
  12. data/lib/applitools/selenium/eyes.rb +922 -0
  13. data/lib/applitools/selenium/eyes_target_locator.rb +191 -0
  14. data/lib/applitools/selenium/eyes_web_driver_screenshot.rb +274 -0
  15. data/lib/applitools/selenium/frame.rb +24 -0
  16. data/lib/applitools/selenium/frame_chain.rb +68 -0
  17. data/lib/applitools/selenium/full_page_capture_algorithm.rb +162 -0
  18. data/lib/applitools/selenium/keyboard.rb +1 -0
  19. data/lib/applitools/selenium/match_window_task.rb +0 -197
  20. data/lib/applitools/selenium/mouse.rb +1 -0
  21. data/lib/applitools/selenium/move_to_region_visibility_strategy.rb +33 -0
  22. data/lib/applitools/selenium/nop_region_visibility_strategy.rb +16 -0
  23. data/lib/applitools/selenium/scroll_position_provider.rb +52 -0
  24. data/lib/applitools/selenium/takes_screenshot_image_provider.rb +36 -0
  25. data/lib/applitools/version.rb +1 -1
  26. data/lib/eyes_selenium.rb +19 -30
  27. metadata +24 -254
  28. data/.gitignore +0 -18
  29. data/.rspec +0 -2
  30. data/.rubocop.yml +0 -57
  31. data/.travis.yml +0 -17
  32. data/Gemfile +0 -4
  33. data/LICENSE.txt +0 -13
  34. data/README.md +0 -38
  35. data/Rakefile +0 -11
  36. data/certs/cacert.pem +0 -3557
  37. data/examples/appium_example_script.rb +0 -59
  38. data/examples/capybara_example.rb +0 -82
  39. data/examples/sauce_capybara_example.rb +0 -41
  40. data/examples/sauce_example.rb +0 -34
  41. data/examples/simple_test_script.rb +0 -23
  42. data/examples/watir_test_script.rb +0 -26
  43. data/eyes_selenium.gemspec +0 -89
  44. data/lib/applitools/appium_driver.rb +0 -7
  45. data/lib/applitools/base/batch_info.rb +0 -21
  46. data/lib/applitools/base/dimension.rb +0 -36
  47. data/lib/applitools/base/environment.rb +0 -19
  48. data/lib/applitools/base/image_position.rb +0 -10
  49. data/lib/applitools/base/mouse_trigger.rb +0 -33
  50. data/lib/applitools/base/point.rb +0 -34
  51. data/lib/applitools/base/region.rb +0 -112
  52. data/lib/applitools/base/server_connector.rb +0 -120
  53. data/lib/applitools/base/session.rb +0 -15
  54. data/lib/applitools/base/start_info.rb +0 -34
  55. data/lib/applitools/base/test_results.rb +0 -28
  56. data/lib/applitools/base/text_trigger.rb +0 -22
  57. data/lib/applitools/extensions.rb +0 -17
  58. data/lib/applitools/eyes.rb +0 -460
  59. data/lib/applitools/eyes_logger.rb +0 -38
  60. data/lib/applitools/method_tracer.rb +0 -22
  61. data/lib/applitools/poltergeist/applitools_compatible.rb +0 -28
  62. data/lib/applitools/poltergeist/driver.rb +0 -11
  63. data/lib/applitools/sauce.rb +0 -2
  64. data/lib/applitools/selenium/match_window_data.rb +0 -28
  65. data/lib/applitools/selenium_webdriver.rb +0 -8
  66. data/lib/applitools/utils/image_delta_compressor.rb +0 -148
  67. data/lib/applitools/utils/image_utils.rb +0 -143
  68. data/lib/applitools/utils/utils.rb +0 -49
  69. data/lib/applitools/watir_browser.rb +0 -8
  70. data/spec/driver_passthrough_spec.rb +0 -68
  71. data/spec/fixtures/static_test_file.html +0 -15
  72. data/spec/spec_helper.rb +0 -17
@@ -0,0 +1,24 @@
1
+ module Applitools::Selenium
2
+ # @!visibility private
3
+ class Frame
4
+ attr_accessor :reference, :frame_id, :location, :size, :parent_scroll_position
5
+ def initialize(options = {})
6
+ [:reference, :frame_id, :location, :size, :parent_scroll_position].each do |param|
7
+ raise "options[:#{param}] can't be nil" unless options[param]
8
+ send("#{param}=", options[param])
9
+ end
10
+
11
+ raise 'options[:location] must be instance of Applitools::Base::Point' unless
12
+ location.is_a? Applitools::Location
13
+
14
+ raise 'options[:parent_scroll_position] must be instance of Applitools::Location' unless
15
+ location.is_a? Applitools::Location
16
+
17
+ raise 'options[:size] must be instance of Applitools::RectangleSize' unless
18
+ size.is_a? Applitools::RectangleSize
19
+
20
+ return if reference.is_a? Applitools::Selenium::Element
21
+ raise 'options[:reference] must be instance of Applitools::Selenium::Element'
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,68 @@
1
+ module Applitools::Selenium
2
+ # @!visibility private
3
+ class FrameChain
4
+ include Enumerable
5
+
6
+ def initialize(options = {})
7
+ @frames = []
8
+ @frames = options[:other].to_a if options[:other].is_a? self.class
9
+ end
10
+
11
+ def each(*args, &block)
12
+ return @frames.collect unless block_given?
13
+ @frames.each(*args, &block)
14
+ end
15
+
16
+ def same_frame_chain?(other)
17
+ return false unless size == other.size
18
+ all? { |my_elem| my_elem.id == other.next.id }
19
+ end
20
+
21
+ def push(frame)
22
+ return @frames.push(frame) if frame.is_a? Applitools::Selenium::Frame
23
+ raise "frame must be instance of Applitools::Selenium::Frame! (passed #{frame.class})"
24
+ end
25
+
26
+ def pop
27
+ @frames.pop
28
+ end
29
+
30
+ def shift
31
+ @frames.shift
32
+ end
33
+
34
+ def clear
35
+ @frames = []
36
+ end
37
+
38
+ def size
39
+ @frames.size
40
+ end
41
+
42
+ def empty?
43
+ @frames.empty?
44
+ end
45
+
46
+ def current_frame_offset
47
+ @frames.reduce(Applitools::Location.new(0, 0)) do |result, frame|
48
+ result.offset frame.location
49
+ end
50
+ end
51
+
52
+ def current_frame
53
+ @frames.last
54
+ end
55
+
56
+ def default_content_scroll_position
57
+ raise NoFramesException.new 'No frames!' if @frames.empty?
58
+ result = @frames.first.parent_scroll_position
59
+ Applitools::Base::Point.new result.x, result.y
60
+ end
61
+
62
+ def current_frame_size
63
+ @frames.last.size
64
+ end
65
+
66
+ class NoFramesException < RuntimeError; end
67
+ end
68
+ end
@@ -0,0 +1,162 @@
1
+ module Applitools::Selenium
2
+ # @!visibility private
3
+ class FullPageCaptureAlgorithm
4
+ extend Forwardable
5
+ def_delegators 'Applitools::EyesLogger', :logger, :log_handler, :log_handler=
6
+
7
+ MAX_SCROLL_BAR_SIZE = 50
8
+ MIN_SCREENSHOT_PART_HEIGHT = 10
9
+
10
+ def get_stiched_region(options = {})
11
+ logger.info 'get_stiched_region() has been invoked.'
12
+ image_provider = options[:image_provider]
13
+ region_provider = options[:region_to_check]
14
+ origin_provider = options[:origin_provider]
15
+ position_provider = options[:position_provider]
16
+ scale_provider = options[:scale_provider]
17
+ cut_provider = options[:cut_provider]
18
+ wait_before_screenshot = options[:wait_before_screenshots]
19
+ eyes_screenshot_factory = options[:eyes_screenshot_factory]
20
+
21
+ logger.info "Region to check: #{region_provider.region}"
22
+ logger.info "Coordinates type: #{region_provider.coordinate_type}"
23
+
24
+ original_position = origin_provider.state
25
+ current_position = nil
26
+ set_position_retries = 3
27
+ while current_position.nil? ||
28
+ (current_position.x.nonzero? || current_position.y.nonzero?) && set_position_retries > 0
29
+ origin_provider.position = Applitools::Location.new(0, 0)
30
+ sleep wait_before_screenshot
31
+ current_position = origin_provider.current_position
32
+ set_position_retries -= 1
33
+ end
34
+
35
+ unless current_position.x.zero? && current_position.y.zero?
36
+ origin_provider.restore_state original_position
37
+ raise Applitools::EyesError.new 'Couldn\'t set position to the top/left corner!'
38
+ end
39
+
40
+ begin
41
+ entire_size = position_provider.entire_size
42
+ logger.info "Entire size of region context: #{entire_size}"
43
+ rescue Applitools::EyesDriverOperationException => e
44
+ logger.error "Failed to extract entire size of region context: #{e.message}"
45
+ logger.error "Using image size instead: #{image.width}x#{image.height}"
46
+ entire_size = Applitools::RectangleSize.new image.width, image.height
47
+ end
48
+
49
+ logger.info 'Getting top/left image...'
50
+ image = image_provider.take_screenshot
51
+ image = scale_provider.scale_image(image) if scale_provider
52
+ image = cut_provider.cut(image) if cut_provider
53
+ logger.info 'Done! Creating screenshot object...'
54
+ screenshot = eyes_screenshot_factory.call(image)
55
+
56
+ if region_provider.coordinate_type
57
+ left_top_image = screenshot.sub_screenshot(region_provider.region, region_provider.coordinate_type)
58
+ else
59
+ left_top_image = screenshot.sub_screenshot(
60
+ Applitools::Region.from_location_size(Applitools::Location.new(0, 0), entire_size),
61
+ Applitools::EyesScreenshot::COORDINATE_TYPES[:context_relative]
62
+ )
63
+ end
64
+
65
+ image = left_top_image.image
66
+
67
+ # Notice that this might still happen even if we used
68
+ # "getImagePart", since "entirePageSize" might be that of a frame.
69
+
70
+ if image.width >= entire_size.width && image.height >= entire_size.height
71
+ origin_provider.restore_state original_position
72
+ return image
73
+ end
74
+
75
+ part_image_size = Applitools::RectangleSize.new image.width,
76
+ [image.height - MAX_SCROLL_BAR_SIZE, MIN_SCREENSHOT_PART_HEIGHT].max
77
+
78
+ logger.info "Total size: #{entire_size}, image_part_size: #{part_image_size}"
79
+
80
+ # Getting the list of sub-regions composing the whole region (we'll
81
+ # take screenshot for each one).
82
+ entire_page = Applitools::Region.from_location_size Applitools::Location::TOP_LEFT, entire_size
83
+ image_parts = entire_page.sub_regions(part_image_size)
84
+
85
+ logger.info "Creating stitchedImage container. Size: #{entire_size}"
86
+
87
+ # Notice stitched_image uses the same type of image as the screenshots.
88
+ stitched_image = Applitools::Screenshot.from_region entire_size
89
+ logger.info 'Done! Adding initial screenshot..'
90
+ logger.info "Initial part:(0,0) [#{image.width} x #{image.height}]"
91
+
92
+ stitched_image.replace! image, 0, 0
93
+ logger.info 'Done!'
94
+
95
+ last_successful_location = Applitools::Location.new 0, 0
96
+ last_successful_part_size = Applitools::RectangleSize.new image.width, image.height
97
+
98
+ original_stitched_state = position_provider.state
99
+
100
+ logger.info 'Getting the rest of the image parts...'
101
+
102
+ image_parts.each_with_index do |part_region, i|
103
+ next unless i > 0
104
+ logger.info "Taking screenshot for #{part_region}"
105
+
106
+ position_provider.position = part_region.location
107
+ sleep wait_before_screenshot
108
+ current_position = position_provider.current_position
109
+ logger.info "Set position to #{current_position}"
110
+ logger.info 'Getting image...'
111
+
112
+ part_image = image_provider.take_screenshot
113
+ part_image = scale_provider.scale_image part_image if scale_provider
114
+ part_image = cut_provider.cut part_image if cut_provider
115
+
116
+ logger.info 'Done!'
117
+ begin
118
+ region_to_check = Applitools::Region.from_location_size(
119
+ part_region.location.offset(region_provider.region.location), part_region.size
120
+ )
121
+ a_screenshot = eyes_screenshot_factory.call(part_image).sub_screenshot(region_to_check,
122
+ Applitools::EyesScreenshot::COORDINATE_TYPES[:context_relative], false)
123
+ rescue Applitools::OutOfBoundsException => e
124
+ logger.error e.message
125
+ break
126
+ end
127
+
128
+ logger.info 'Stitching part into the image container...'
129
+
130
+ stitched_image.replace! a_screenshot.image, part_region.x, part_region.y
131
+ logger.info 'Done!'
132
+
133
+ last_successful_location = Applitools::Location.for part_region.x, part_region.y
134
+ next unless a_screenshot
135
+ last_successful_part_size = Applitools::RectangleSize.new(
136
+ a_screenshot.image.width,
137
+ a_screenshot.image.height
138
+ )
139
+ end
140
+
141
+ logger.info 'Stitching done!'
142
+
143
+ position_provider.restore_state original_stitched_state
144
+ origin_provider.restore_state original_position
145
+
146
+ actual_image_width = last_successful_location.x + last_successful_part_size.width
147
+ actual_image_height = last_successful_location.y + last_successful_part_size.height
148
+
149
+ logger.info "Extracted entire size: #{entire_size}"
150
+ logger.info "Actual stitched size: #{stitched_image.width} x #{stitched_image.height}"
151
+ logger.info "Actual stitched size: #{actual_image_width} x #{actual_image_height}"
152
+
153
+ if actual_image_width < stitched_image.width || actual_image_height < stitched_image.height
154
+ logger.info 'Trimming unnecessary margins...'
155
+ stitched_image.crop!(0, 0, actual_image_width, actual_image_height)
156
+ logger.info 'Done!'
157
+ end
158
+
159
+ stitched_image
160
+ end
161
+ end
162
+ end
@@ -1,4 +1,5 @@
1
1
  module Applitools::Selenium
2
+ # @!visibility private
2
3
  class Keyboard
3
4
  attr_reader :keyboard, :driver
4
5
 
@@ -1,197 +0,0 @@
1
- require 'base64'
2
-
3
- module Applitools::Selenium
4
- class MatchWindowTask
5
- MATCH_INTERVAL = 0.5
6
- AppOuptut = Struct.new(:title, :screenshot64)
7
-
8
- attr_reader :eyes, :session, :driver, :default_retry_timeout, :last_checked_window,
9
- :last_screenshot_bounds
10
-
11
- def initialize(eyes, session, driver, default_retry_timeout)
12
- @eyes = eyes
13
-
14
- @session = session
15
- @driver = driver
16
- @default_retry_timeout = default_retry_timeout
17
- @last_checked_window = nil # +ChunkyPNG::Canvas+
18
- @last_screenshot_bounds = Applitools::Base::Region::EMPTY # +Applitools::Base::Region+
19
- @current_screenshot = nil # +ChunkyPNG::Canvas+
20
- end
21
-
22
- def match_window(region, retry_timeout, tag, rotation, run_once_after_wait = false)
23
- retry_timeout = default_retry_timeout if retry_timeout < 0
24
-
25
- Applitools::EyesLogger.debug "Retry timeout set to: #{retry_timeout}"
26
-
27
- start = Time.now
28
- res =
29
- if retry_timeout.zero?
30
- run(region, tag, rotation)
31
- elsif run_once_after_wait
32
- run(region, tag, rotation, retry_timeout)
33
- else
34
- run_with_intervals(region, tag, rotation, retry_timeout)
35
- end
36
- elapsed_time = Time.now - start
37
-
38
- Applitools::EyesLogger.debug "match_window(): Completed in #{format('%.2f', elapsed_time)} seconds"
39
-
40
- @last_checked_window = @current_screenshot
41
- @last_screenshot_bounds =
42
- if region.empty?
43
- Applitools::Base::Region.new(0, 0, last_checked_window.width, last_checked_window.height)
44
- else
45
- region
46
- end
47
- driver.clear_user_inputs
48
- GC.start
49
- res
50
- end
51
-
52
- def run(region, tag, rotation, wait_before_run = nil)
53
- Applitools::EyesLogger.debug 'Trying matching once...'
54
-
55
- if wait_before_run
56
- Applitools::EyesLogger.debug 'Waiting before run...'
57
- sleep(wait_before_run)
58
- Applitools::EyesLogger.debug 'Waiting done!'
59
- end
60
-
61
- match(region, tag, rotation)
62
- end
63
-
64
- def run_with_intervals(region, tag, rotation, retry_timeout)
65
- # We intentionally take the first screenshot before starting the timer, to allow the page just a tad more time to
66
- # stabilize.
67
- Applitools::EyesLogger.debug 'Matching with intervals...'
68
- start = Time.now
69
- as_expected = match(region, tag, rotation, true)
70
- Applitools::EyesLogger.debug "First call result: #{as_expected}"
71
- return true if as_expected
72
- Applitools::EyesLogger.debug "Not as expected, performing retry (total timeout #{retry_timeout})"
73
- match_retry = Time.now - start
74
- while match_retry < retry_timeout
75
- Applitools::EyesLogger.debug 'Waiting before match...'
76
- sleep(MATCH_INTERVAL)
77
- Applitools::EyesLogger.debug 'Done! Matching...'
78
- return true if match(region, tag, rotation, true)
79
- match_retry = Time.now - start
80
- Applitools::EyesLogger.debug "Elapsed time: #{match_retry}"
81
- end
82
- # Let's try one more time if we still don't have a match.
83
- Applitools::EyesLogger.debug 'Last attempt to match...'
84
- as_expected = match(region, tag, rotation)
85
- Applitools::EyesLogger.debug "Match result: #{as_expected}"
86
- as_expected
87
- end
88
-
89
- private
90
-
91
- def get_clipped_region(region, image)
92
- left = [region.left, 0].max
93
- top = [region.top, 0].max
94
- max_width = image.width - left
95
- max_height = image.height - top
96
- width = [region.width, max_width].min
97
- height = [region.height, max_height].min
98
- Applitools::Base::Region.new(left, top, width, height)
99
- end
100
-
101
- def prep_match_data(region, tag, rotation, ignore_mismatch)
102
- Applitools::EyesLogger.debug 'Preparing match data...'
103
- title = eyes.title
104
- Applitools::EyesLogger.debug 'Getting screenshot...'
105
- Applitools::EyesLogger.debug 'Done! Creating image object from PNG...'
106
- @current_screenshot = driver.get_screenshot(rotation, tag)
107
- Applitools::EyesLogger.debug 'Done!'
108
- # If a region was defined, we refer to the sub-image defined by the region.
109
- unless region.empty?
110
- Applitools::EyesLogger.debug 'Calculating clipped region...'
111
- # If the region is out of bounds, clip it
112
- clipped_region = get_clipped_region(region, @current_screenshot)
113
- raise Applitools::EyesError.new("Region is outside the viewport: #{region}") if clipped_region.empty?
114
- Applitools::EyesLogger.debug 'Done! Cropping region...'
115
- @current_screenshot.crop!(clipped_region.left, clipped_region.top, clipped_region.width, clipped_region.height)
116
- Applitools::EyesLogger.debug 'Done! Creating cropped image object...'
117
- Applitools::EyesLogger.debug 'Done!'
118
- end
119
-
120
- # FIXME: re-Enable screenshot compression after handling memory leaks.
121
- # Applitools::EyesLogger.debug 'Compressing screenshot...'
122
- # compressed_screenshot = Applitools::Utils::ImageDeltaCompressor.compress_by_raw_blocks(@current_screenshot,
123
- # current_screenshot_encoded, last_checked_window)
124
-
125
- Applitools::EyesLogger.debug 'Done! Creating AppOuptut...'
126
- app_output = AppOuptut.new(title, nil)
127
- user_inputs = []
128
- Applitools::EyesLogger.debug 'Handling user inputs...'
129
- if !last_checked_window.nil?
130
- driver.user_inputs.each do |trigger|
131
- Applitools::EyesLogger.debug 'Handling trigger...'
132
- if trigger.is_a?(Applitools::Base::MouseTrigger)
133
- updated_trigger = nil
134
- trigger_left = trigger.control.left + trigger.location.x
135
- trigger_top = trigger.control.top + trigger.location.y
136
- if last_screenshot_bounds.contains?(trigger_left, trigger_top)
137
- trigger.control.intersect(last_screenshot_bounds)
138
- if trigger.control.empty?
139
- trigger_left -= - last_screenshot_bounds.left
140
- trigger_top -= last_screenshot_bounds.top
141
- updated_trigger = Applitools::Base::MouseTrigger.new(trigger.mouse_action, trigger.control,
142
- Applitools::Base::Point.new(trigger_left, trigger_top))
143
- else
144
- trigger_left -= trigger.control.left
145
- trigger_top -= trigger.control.top
146
- control_left = trigger.control.left - last_screenshot_bounds.left
147
- control_top = trigger.control.top - last_screenshot_bounds.top
148
- updated_control = Applitools::Base::Region.new(control_left, control_top, trigger.control.width,
149
- trigger.control.height)
150
- updated_trigger = Applitools::Base::MouseTrigger.new(trigger.mouse_action, updated_control,
151
- Applitools::Base::Point.new(trigger_left, trigger_top))
152
- end
153
- Applitools::EyesLogger.debug 'Done with trigger!'
154
- user_inputs << updated_trigger
155
- else
156
- Applitools::EyesLogger.info "Trigger ignored: #{trigger} (out of bounds)"
157
- end
158
- elsif trigger.is_a?(Applitools::Base::TextTrigger)
159
- if !trigger.control.empty?
160
- trigger.control.intersect(last_screenshot_bounds)
161
- if !trigger.control.empty?
162
- control_left = trigger.control.left - last_screenshot_bounds.left
163
- control_top = trigger.control.top - last_screenshot_bounds.top
164
- updated_control = Applitools::Base::Region.new(control_left, control_top, trigger.control.width,
165
- trigger.control.height)
166
- updated_trigger = Applitools::Base::TextTrigger.new(trigger.text, updated_control)
167
- Applitools::EyesLogger.debug 'Done with trigger!'
168
- user_inputs << updated_trigger
169
- else
170
- Applitools::EyesLogger.info "Trigger ignored: #{trigger} (control out of bounds)"
171
- end
172
- else
173
- Applitools::EyesLogger.info "Trigger ignored: #{trigger} (out of bounds)"
174
- end
175
- else
176
- Applitools::EyesLogger.info "Trigger ignored: #{trigger} (Unrecognized trigger)"
177
- end
178
- end
179
- else
180
- Applitools::EyesLogger.info 'Triggers ignored: no previous window checked'
181
- end
182
- Applitools::EyesLogger.debug 'Creating MatchWindowData object..'
183
- match_window_data_obj = Applitools::Selenium::MatchWindowData.new(app_output, tag, ignore_mismatch,
184
- @current_screenshot, user_inputs)
185
- Applitools::EyesLogger.debug 'Done creating MatchWindowData object!'
186
- match_window_data_obj
187
- end
188
-
189
- def match(region, tag, rotation, ignore_mismatch = false)
190
- Applitools::EyesLogger.debug 'Match called...'
191
- data = prep_match_data(region, tag, rotation, ignore_mismatch)
192
- match_result = Applitools::Base::ServerConnector.match_window(session, data)
193
- Applitools::EyesLogger.debug 'Match done!'
194
- match_result
195
- end
196
- end
197
- end