eyes_selenium 2.39.1 → 3.0.6

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