eyes_selenium 1.6.0

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