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.
- 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
|