eyes_selenium 1.6.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 (42) hide show
  1. data/.gitignore +17 -0
  2. data/.rspec +2 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +13 -0
  5. data/README.md +82 -0
  6. data/Rakefile +1 -0
  7. data/eyes_selenium.gemspec +30 -0
  8. data/lib/eyes_selenium.rb +42 -0
  9. data/lib/eyes_selenium/capybara.rb +21 -0
  10. data/lib/eyes_selenium/eyes/agent_connecter.rb +39 -0
  11. data/lib/eyes_selenium/eyes/batch_info.rb +14 -0
  12. data/lib/eyes_selenium/eyes/dimension.rb +15 -0
  13. data/lib/eyes_selenium/eyes/driver.rb +146 -0
  14. data/lib/eyes_selenium/eyes/element.rb +78 -0
  15. data/lib/eyes_selenium/eyes/environment.rb +14 -0
  16. data/lib/eyes_selenium/eyes/eyes.rb +182 -0
  17. data/lib/eyes_selenium/eyes/eyes_keyboard.rb +25 -0
  18. data/lib/eyes_selenium/eyes/eyes_mouse.rb +60 -0
  19. data/lib/eyes_selenium/eyes/failure_reports.rb +4 -0
  20. data/lib/eyes_selenium/eyes/match_level.rb +7 -0
  21. data/lib/eyes_selenium/eyes/match_window_data.rb +18 -0
  22. data/lib/eyes_selenium/eyes/match_window_task.rb +71 -0
  23. data/lib/eyes_selenium/eyes/mouse_trigger.rb +19 -0
  24. data/lib/eyes_selenium/eyes/region.rb +22 -0
  25. data/lib/eyes_selenium/eyes/screenshot_taker.rb +18 -0
  26. data/lib/eyes_selenium/eyes/session.rb +14 -0
  27. data/lib/eyes_selenium/eyes/start_info.rb +34 -0
  28. data/lib/eyes_selenium/eyes/target_app.rb +17 -0
  29. data/lib/eyes_selenium/eyes/test_results.rb +21 -0
  30. data/lib/eyes_selenium/eyes/text_trigger.rb +15 -0
  31. data/lib/eyes_selenium/eyes/viewport_size.rb +104 -0
  32. data/lib/eyes_selenium/eyes_logger.rb +18 -0
  33. data/lib/eyes_selenium/utils.rb +5 -0
  34. data/lib/eyes_selenium/utils/image_delta_compressor.rb +147 -0
  35. data/lib/eyes_selenium/version.rb +3 -0
  36. data/spec/capybara_spec.rb +34 -0
  37. data/spec/driver_spec.rb +10 -0
  38. data/spec/eyes_spec.rb +157 -0
  39. data/spec/spec_helper.rb +29 -0
  40. data/spec/test_app.rb +11 -0
  41. data/test_script.rb +21 -0
  42. metadata +226 -0
@@ -0,0 +1,14 @@
1
+ class Applitools::Environment
2
+
3
+ attr_accessor :os, :hosting_app, :display_size
4
+ def initialize(os=nil, hosting_app=nil, display_size=nil)
5
+ @os = os
6
+ @hosting_app = hosting_app
7
+ @display_size = display_size
8
+ end
9
+
10
+ def to_hash
11
+ # display_size is an Applitools::ViewportSize object
12
+ { os: os, hostingApp: hosting_app, displaySize: display_size.to_hash}
13
+ end
14
+ end
@@ -0,0 +1,182 @@
1
+ class Applitools::Eyes
2
+ DEFAULT_MATCH_TIMEOUT = 2.0
3
+ AGENT_ID = 'eyes.selenium.ruby'
4
+
5
+ class << self
6
+ attr_accessor :config
7
+ end
8
+
9
+ @config = {
10
+ server_url: 'https://eyes.applitools.com',
11
+ user: AGENT_ID
12
+ # apikey: 'please supply an apikey in your initializer'
13
+ }
14
+
15
+ attr_reader :agent_connector, :disabled
16
+ attr_accessor :app_name, :test_name, :session, :is_open, :aborted, :viewport_size, :match_timeout,
17
+ :failure_reports, :test_batch, :match_level, :driver, :host_os, :host_app, :branch_name, :parent_branch_name,
18
+ :should_match_window_run_once_on_timeout, :session_start_info, :match_window_task
19
+
20
+ def config
21
+ self.class.config
22
+ end
23
+
24
+ def initialize(params={})
25
+ raise "Please supply an apikey: Eyes.config[:apikey] = ..." unless config[:apikey]
26
+
27
+ @disabled = params[:disabled]
28
+
29
+ @driver = create_driver(params)
30
+ return if disabled?
31
+
32
+ @agent_connector = Applitools::AgentConnector.new(config[:server_url], config[:user], config[:apikey])
33
+ @match_timeout = DEFAULT_MATCH_TIMEOUT
34
+ @failure_reports = Applitools::FailureReports::ON_CLOSE
35
+ end
36
+
37
+ def create_driver(params)
38
+ Applitools::Driver.new(browser: params.fetch(:browser, nil))
39
+ end
40
+
41
+ def open(params={})
42
+ return driver if disabled?
43
+
44
+ if open?
45
+ abort_if_not_closed
46
+ msg = 'a test is alread running'
47
+ EyesLogger.info(msg) and raise Applitools::EyesError.new(msg)
48
+ end
49
+
50
+ self.app_name = params.fetch(:app_name)
51
+ self.failure_reports = params.fetch(:failure_reports, self.failure_reports)
52
+ self.match_level = params.fetch(:match_level,MatchLevel::EXACT)
53
+ self.test_name = params.fetch(:test_name)
54
+ self.viewport_size = params.fetch(:viewport_size, nil)
55
+
56
+ self.is_open = true
57
+ driver
58
+ end
59
+
60
+ def open?
61
+ self.is_open
62
+ end
63
+
64
+ def check_window(tag)
65
+ return if disabled?
66
+ raise Applitools::EyesError.new("Eyes not open") if !open?
67
+ if !session
68
+ start_session
69
+ self.match_window_task = Applitools::MatchWindowTask.new(agent_connector, session, driver, match_timeout)
70
+ end
71
+
72
+ as_expected = match_window_task.match_window(tag, should_match_window_run_once_on_timeout)
73
+ if !as_expected
74
+ self.should_match_window_run_once_on_timeout = true
75
+ if !session.new_session?
76
+ #EyesLogger.info %( "mismatch! #{ tag ? "" : "(#{tag})" } )
77
+ if failure_reports.to_i == Applitools::FailureReports::IMMEDIATE
78
+ raise Applitools::TestFailedError.new("Mismatch found in '#{start_info.scenario_id_or_name}'"\
79
+ " of '#{start_info.app_id_or_name}'")
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ def close
86
+ return if disabled?
87
+ self.is_open = false
88
+ return Applitools::TestResults.new if !session
89
+
90
+ session_results_url = session.url
91
+ results = agent_connector.stop_session(session,false)
92
+ new_session = session.new_session?
93
+ self.session = nil
94
+
95
+ if new_session
96
+ instructions = "Please approve the new baseline at #{session_results_url}"
97
+ EyesLogger.info "--- New test ended. #{instructions}"
98
+ message = "' #{session_start_info.scenario_id_or_name} of"\
99
+ " #{session_start_info.app_id_or_name}. #{instructions}"
100
+ raise Applitools::NewTestError.new(message, results)
101
+ elsif 0 < results.mismatches || 0 < results.missing
102
+ EyesLogger.info "--- Failed test ended. See details at #{session_results_url}"
103
+ message = "' #{session_start_info.scenario_id_or_name} of"\
104
+ " #{session_start_info.app_id_or_name}'. See details at #{session_results_url}"
105
+ raise Applitools::TestFailedError.new(message, results)
106
+ end
107
+
108
+ EyesLogger.info "--- Test passed. See details at #{session_results_url}"
109
+ end
110
+
111
+ ## Use this method to perform seamless testing with selenium through eyes driver.
112
+ ## Using Selenium methods inside the 'test' block will send the messages to Selenium
113
+ ## after creating the Eyes triggers for them.
114
+ ##
115
+ ## Example:
116
+ # eyes.test(app_name: 'my app1', test_name: 'my test') do |d|
117
+ # get "http://www.google.com"
118
+ # check_window("initial")
119
+ # end
120
+ def test(params={}, &block)
121
+ begin
122
+ open(params)
123
+ yield(self, driver)
124
+ close
125
+ rescue Applitools::EyesError
126
+ ensure
127
+ abort_if_not_closed
128
+ driver.quit
129
+ end
130
+ end
131
+
132
+
133
+ def abort_if_not_closed
134
+ return if disabled?
135
+ self.is_open = false
136
+ if session
137
+ begin
138
+ agent_connector.stop_session(session,true)
139
+ rescue Applitools::EyesError => e
140
+ EyesLogger.info "Failed to abort server session -> #{e.message} "
141
+ ensure
142
+ self.session = nil
143
+ end
144
+ end
145
+ end
146
+
147
+ private
148
+
149
+ def disabled?
150
+ disabled
151
+ end
152
+
153
+ def start_session
154
+ assign_viewport_size
155
+ test_batch ||= Applitools::BatchInfo.new
156
+ app_env = Applitools::Environment.new(host_os, host_app, viewport_size)
157
+ application = driver.create_application
158
+ self.session_start_info = Applitools::StartInfo.new(AGENT_ID,
159
+ app_name, test_name, test_batch, app_env, application,
160
+ match_level,nil, branch_name, parent_branch_name
161
+ )
162
+ self.session = agent_connector.start_session(session_start_info)
163
+ self.should_match_window_run_once_on_timeout = session.new_session?
164
+ end
165
+
166
+ def viewport_size?
167
+ self.viewport_size
168
+ end
169
+
170
+ def assign_viewport_size
171
+ if viewport_size?
172
+ self.viewport_size = Applitools::ViewportSize.new(driver, viewport_size)
173
+ self.viewport_size.set
174
+ else
175
+ self.viewport_size = Applitools::ViewportSize.new(driver).extract_viewport_from_browser!
176
+ end
177
+ end
178
+
179
+ def to_hash
180
+ Hash[dimension.each_pair.to_a]
181
+ end
182
+ end
@@ -0,0 +1,25 @@
1
+ class Applitools::EyesKeyboard
2
+ attr_reader :keyboard, :driver
3
+
4
+ def initialize(driver, keyboard)
5
+ @driver = driver
6
+ @keyboard = keyboard
7
+ end
8
+
9
+ def send_keys(*keys)
10
+ active_element = Applitools::Element.new(driver, driver.switch_to.active_element)
11
+ current_control = active_element.region
12
+ Selenium::WebDriver::Keys.encode(keys).each do |key|
13
+ driver.user_inputs << Applitools::TextTrigger.new(key.to_s, current_control)
14
+ end
15
+ keyboard.send_keys(*keys)
16
+ end
17
+
18
+ def press(key)
19
+ keyboard.press(key)
20
+ end
21
+
22
+ def release(key)
23
+ keyboard.release(key)
24
+ end
25
+ end
@@ -0,0 +1,60 @@
1
+ class Applitools::EyesMouse
2
+
3
+ attr_reader :driver, :mouse
4
+ def initialize(driver, mouse)
5
+ @driver = driver
6
+ @mouse = mouse
7
+ end
8
+
9
+ def click(element = nil)
10
+ extract_trigger_and_perform(:click, element)
11
+ end
12
+
13
+ def double_click(element = nil)
14
+ extract_trigger_and_perform(:double_click, element)
15
+ end
16
+
17
+ def context_click(element = nil)
18
+ extract_trigger_and_perform(:right_click, element)
19
+ end
20
+
21
+ def down(element = nil)
22
+ extract_trigger_and_perform(:down, element)
23
+ end
24
+
25
+ def up(element = nil)
26
+ extract_trigger_and_perform(:up, element)
27
+ end
28
+
29
+ def move_to(element, right_by = nil, down_by = nil)
30
+ element = element.web_element if element.is_a?(Applitools::Element)
31
+ location = element.location
32
+ location.x = [0,location.x].max
33
+ location.y = [0,location.y].max
34
+ current_control = Applitools::Region.new(0,0, *location.values)
35
+ driver.user_inputs << Applitools::MouseTrigger.new(:move, current_control, location)
36
+ element = element.web_element if element.is_a?(Applitools::Element)
37
+ mouse.move_to(element,right_by, down_by)
38
+ end
39
+
40
+ def move_by(right_by, down_by)
41
+ right = [0,right_by].max
42
+ down = [0,down_by].max
43
+ location = Selenium::WebDriver::Location.new(right,down)
44
+ current_control = Applitools::Region.new(0,0, right, down)
45
+ driver.user_inputs << Applitools::MouseTrigger.new(:move, current_control, location)
46
+ mouse.move_by(right_by,down_by)
47
+ end
48
+
49
+ private
50
+
51
+ def extract_trigger_and_perform(method, element=nil, *args)
52
+ location = element.location
53
+ location.x = [0,location.x].max
54
+ location.y = [0,location.y].max
55
+ current_control = Applitools::Region.new(0,0, *location.values)
56
+ driver.user_inputs << Applitools::MouseTrigger.new(method, current_control, location)
57
+ element = element.web_element if element.is_a?(Applitools::Element)
58
+ mouse.send(method,element,*args)
59
+ end
60
+ end
@@ -0,0 +1,4 @@
1
+ module Applitools::FailureReports
2
+ IMMEDIATE = 0
3
+ ON_CLOSE = 1
4
+ end
@@ -0,0 +1,7 @@
1
+ module MatchLevel
2
+ NONE = 0
3
+ LAYOUT = 1
4
+ CONTENT = 2
5
+ STRICT = 3
6
+ EXACT = 4
7
+ end
@@ -0,0 +1,18 @@
1
+ class Applitools::MatchWindowData
2
+
3
+ attr_reader :user_inputs, :app_output, :tag, :ignore_mismatch, :screenshot
4
+ def initialize(app_output,user_inputs=[], tag, ignore_mismatch, screenshot)
5
+ @user_inputs = user_inputs
6
+ @app_output = app_output
7
+ @tag = tag
8
+ @ignore_mismatch = ignore_mismatch
9
+ @screenshot = screenshot
10
+ end
11
+
12
+ # IMPORTANT This method returns a hash WITHOUT the screenshot property. This is on purspose! The screenshot should
13
+ # not be included as part of the json.
14
+ def to_hash
15
+ {userInputs: user_inputs.map(&:to_hash), appOutput: Hash[app_output.each_pair.to_a],
16
+ tag: @tag, ignoreMismatch: @ignore_mismatch}
17
+ end
18
+ end
@@ -0,0 +1,71 @@
1
+ require 'oily_png'
2
+ require 'base64'
3
+
4
+ class Applitools::MatchWindowTask
5
+
6
+ MATCH_INTERVAL = 0.5
7
+ AppOutput = Struct.new(:title, :screenshot64)
8
+
9
+ attr_reader :agent_connector, :session, :driver, :max_window_load_time
10
+
11
+ def initialize(agent_connector, session, driver, max_window_load_time)
12
+ @agent_connector = agent_connector
13
+ @session = session
14
+ @driver = driver
15
+ @max_window_load_time = max_window_load_time
16
+ @last_checked_window = nil # +ChunkyPNG::Canvas+
17
+ @current_screenshot = nil # +ChunkyPNG::Canvas+
18
+ end
19
+
20
+ def match_window(tag,run_once_after_wait=false)
21
+ res = if max_window_load_time.zero?
22
+ run(tag)
23
+ elsif run_once_after_wait
24
+ run(tag, max_window_load_time)
25
+ else
26
+ run_with_intervals(tag, max_window_load_time)
27
+ end
28
+
29
+ driver.clear_user_inputs and return res
30
+ end
31
+
32
+ def run(tag, wait_before_run=nil)
33
+ sleep(wait_before_run) if wait_before_run
34
+ match(tag)
35
+ end
36
+
37
+ def run_with_intervals(tag, total_run_time)
38
+ iterations = (total_run_time / MATCH_INTERVAL - 1).to_i
39
+ iterations.times do
40
+ sleep(MATCH_INTERVAL)
41
+ return true if match(tag, true)
42
+ end
43
+
44
+ ## lets try one more time if we still don't have a match
45
+ match(tag)
46
+ end
47
+
48
+ private
49
+
50
+ def prep_match_data(tag, ignore_mismatch)
51
+ title = driver.title
52
+ current_screenshot_encoded = Base64.decode64(driver.screenshot_as(:base64))
53
+ @current_screenshot = ChunkyPNG::Image.from_blob(current_screenshot_encoded)
54
+ compressed_screenshot = Applitools::Utils::ImageDeltaCompressor.compress_by_raw_blocks(@current_screenshot,
55
+ current_screenshot_encoded,
56
+ @last_checked_window)
57
+ app_output = AppOutput.new(title, nil)
58
+
59
+ return Applitools::MatchWindowData.new(app_output, driver.user_inputs, tag, ignore_mismatch, compressed_screenshot)
60
+ end
61
+
62
+ def match(tag, ignore_mismatch=false)
63
+ data = prep_match_data(tag, ignore_mismatch)
64
+ as_expected = agent_connector.match_window(session, data)
65
+ # If the server stored this image, it will be used as a base for our next screenshot compression
66
+ if !ignore_mismatch
67
+ @last_checked_window = @current_screenshot
68
+ end
69
+ return as_expected
70
+ end
71
+ end
@@ -0,0 +1,19 @@
1
+ class Applitools::MouseTrigger
2
+
3
+ MouseAction = { click: 1, right_click: 2, double_click: 3, move: 4, down: 5, up: 6 }
4
+
5
+ attr_reader :mouse_action, :control, :location
6
+
7
+ def initialize(mouse_action, control, location)
8
+ @mouse_action = MouseAction[mouse_action]
9
+ @control = control
10
+ @location = location
11
+ end
12
+
13
+ def to_hash
14
+ {
15
+ "$type" => "Applitools.Models.MouseTrigger, Core", mouseAction: mouse_action,
16
+ control: control.to_hash, location: Hash[location.each_pair.to_a]
17
+ }
18
+ end
19
+ end
@@ -0,0 +1,22 @@
1
+ class Applitools::Region
2
+ attr_accessor :left, :top, :height, :width
3
+
4
+ def initialize(left, top, height, width)
5
+ @left = left
6
+ @top = top
7
+ @height = height
8
+ @width = width
9
+ end
10
+
11
+ def middle_offset
12
+ mid_x = width / 2
13
+ mid_y = height / 2
14
+ Selenium::WebDriver::Location.new mid_x, mid_y
15
+ end
16
+
17
+ def to_hash
18
+ {
19
+ "$type" => "Applitools.Utils.Geometry.MutableRegion, Core", left: left, top: top, height: height, width: width
20
+ }
21
+ end
22
+ end
@@ -0,0 +1,18 @@
1
+ require 'httparty'
2
+ class Applitools::ScreenshotTaker
3
+ include HTTParty
4
+ headers 'Accept' => 'application/json'
5
+ headers 'Content-Type' => 'application/json'
6
+
7
+ attr_reader :driver_server_uri, :driver_session_id
8
+
9
+ def initialize(driver_server_uri, driver_session_id)
10
+ @driver_server_uri = driver_server_uri
11
+ @driver_session_id = driver_session_id
12
+ end
13
+
14
+ def screenshot
15
+ res = self.class.get(driver_server_uri.gsub(/\/$/,"") + "/session/#{driver_session_id}/screenshot").to_s
16
+ res.parsed_response['value']
17
+ end
18
+ end