eyes_selenium 1.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +13 -0
- data/README.md +82 -0
- data/Rakefile +1 -0
- data/eyes_selenium.gemspec +30 -0
- data/lib/eyes_selenium.rb +42 -0
- data/lib/eyes_selenium/capybara.rb +21 -0
- data/lib/eyes_selenium/eyes/agent_connecter.rb +39 -0
- data/lib/eyes_selenium/eyes/batch_info.rb +14 -0
- data/lib/eyes_selenium/eyes/dimension.rb +15 -0
- data/lib/eyes_selenium/eyes/driver.rb +146 -0
- data/lib/eyes_selenium/eyes/element.rb +78 -0
- data/lib/eyes_selenium/eyes/environment.rb +14 -0
- data/lib/eyes_selenium/eyes/eyes.rb +182 -0
- data/lib/eyes_selenium/eyes/eyes_keyboard.rb +25 -0
- data/lib/eyes_selenium/eyes/eyes_mouse.rb +60 -0
- data/lib/eyes_selenium/eyes/failure_reports.rb +4 -0
- data/lib/eyes_selenium/eyes/match_level.rb +7 -0
- data/lib/eyes_selenium/eyes/match_window_data.rb +18 -0
- data/lib/eyes_selenium/eyes/match_window_task.rb +71 -0
- data/lib/eyes_selenium/eyes/mouse_trigger.rb +19 -0
- data/lib/eyes_selenium/eyes/region.rb +22 -0
- data/lib/eyes_selenium/eyes/screenshot_taker.rb +18 -0
- data/lib/eyes_selenium/eyes/session.rb +14 -0
- data/lib/eyes_selenium/eyes/start_info.rb +34 -0
- data/lib/eyes_selenium/eyes/target_app.rb +17 -0
- data/lib/eyes_selenium/eyes/test_results.rb +21 -0
- data/lib/eyes_selenium/eyes/text_trigger.rb +15 -0
- data/lib/eyes_selenium/eyes/viewport_size.rb +104 -0
- data/lib/eyes_selenium/eyes_logger.rb +18 -0
- data/lib/eyes_selenium/utils.rb +5 -0
- data/lib/eyes_selenium/utils/image_delta_compressor.rb +147 -0
- data/lib/eyes_selenium/version.rb +3 -0
- data/spec/capybara_spec.rb +34 -0
- data/spec/driver_spec.rb +10 -0
- data/spec/eyes_spec.rb +157 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/test_app.rb +11 -0
- data/test_script.rb +21 -0
- 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,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
|