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
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
Copyright (c) 2013 Applitools
|
2
|
+
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
you may not use this file except in compliance with the License.
|
5
|
+
You may obtain a copy of the License at
|
6
|
+
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
See the License for the specific language governing permissions and
|
13
|
+
limitations under the License.
|
data/README.md
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
# Applitools
|
2
|
+
|
3
|
+
Applitools Ruby SDK.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'eyes_selenium'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install eyes_selenium
|
18
|
+
|
19
|
+
## Standalone Usage
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
Applitools::Eyes.config[:apikey] = 'XXX'
|
23
|
+
|
24
|
+
eyes = Applitools::Eyes.new
|
25
|
+
eyes.test(app_name: 'my app', test_name: 'my test') do |eyes, driver|
|
26
|
+
driver.get 'http://www.mywebsite.com'
|
27
|
+
eyes.check_window('home page')
|
28
|
+
driver.find_element(:css, "li#pricing").click
|
29
|
+
eyes.check_window('pricing')
|
30
|
+
end
|
31
|
+
```
|
32
|
+
|
33
|
+
## Integrated within your Capybara tests:
|
34
|
+
initializer:
|
35
|
+
```ruby
|
36
|
+
Capybara.run_server = false
|
37
|
+
# This is your api key, make sure you use it in all your tests.
|
38
|
+
Applitools::Eyes.config[:apikey] = 'XXX'
|
39
|
+
let(:eyes) do
|
40
|
+
webdriver = page.driver.browser
|
41
|
+
eyes = Applitools::Eyes.new browser: webdriver
|
42
|
+
end
|
43
|
+
```
|
44
|
+
|
45
|
+
spec:
|
46
|
+
```ruby
|
47
|
+
require 'capybara/rspec'
|
48
|
+
require 'eyes_selenium_ruby'
|
49
|
+
|
50
|
+
|
51
|
+
describe 'Applitools', :type=>:feature, :js=>true do
|
52
|
+
|
53
|
+
describe 'Test Applitools website' do
|
54
|
+
it 'should navigate from the main page to the features page' do
|
55
|
+
|
56
|
+
# Start visual testing with browser viewport set to 1024x768.
|
57
|
+
eyes.open(app_name: 'Applitools', test_name: 'Test Web Page', viewport_size: {width: 1024, height: 768})
|
58
|
+
|
59
|
+
visit 'http://www.applitools.com'
|
60
|
+
|
61
|
+
# Visual validation point #1
|
62
|
+
eyes.check_window('Main Page')
|
63
|
+
|
64
|
+
page.first('.read_more').click
|
65
|
+
|
66
|
+
# Visual validation point #2
|
67
|
+
eyes.check_window('Features Page')
|
68
|
+
|
69
|
+
eyes.close()
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
```
|
75
|
+
|
76
|
+
## Contributing
|
77
|
+
|
78
|
+
1. Fork it
|
79
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
80
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
81
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
82
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'eyes_selenium/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "eyes_selenium"
|
8
|
+
spec.version = Applitools::VERSION
|
9
|
+
spec.authors = ["Applitools team"]
|
10
|
+
spec.email = ["team@applitools.com"]
|
11
|
+
spec.description = "Applitools Ruby SDK"
|
12
|
+
spec.summary = "Applitools Ruby SDK"
|
13
|
+
spec.homepage = "http://www.applitools.com"
|
14
|
+
spec.license = "Apache License, Version 2.0"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "selenium-webdriver", [">= 2.37"]
|
22
|
+
spec.add_dependency "httparty"
|
23
|
+
spec.add_dependency "oily_png", [">= 1.1.0"]
|
24
|
+
|
25
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
26
|
+
spec.add_development_dependency "rake"
|
27
|
+
spec.add_development_dependency "rspec", [">= 2.2.0"]
|
28
|
+
spec.add_development_dependency "capybara", [">= 2.1.0"]
|
29
|
+
spec.add_development_dependency "sinatra"
|
30
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'eyes_selenium/eyes_logger'
|
3
|
+
require 'json'
|
4
|
+
module Applitools
|
5
|
+
include EyesLogger
|
6
|
+
class EyesError < StandardError; end
|
7
|
+
class EyesAbort < EyesError; end
|
8
|
+
|
9
|
+
class TestFailedError < StandardError
|
10
|
+
attr_accessor :test_results
|
11
|
+
def initialize(message, test_results=nil)
|
12
|
+
super(message)
|
13
|
+
@test_results = test_results
|
14
|
+
end
|
15
|
+
end
|
16
|
+
class NewTestError < TestFailedError; end
|
17
|
+
|
18
|
+
require 'eyes_selenium/utils'
|
19
|
+
require "eyes_selenium/eyes/agent_connecter"
|
20
|
+
require "eyes_selenium/eyes/target_app"
|
21
|
+
require 'eyes_selenium/eyes/batch_info'
|
22
|
+
require "eyes_selenium/eyes/dimension"
|
23
|
+
require "eyes_selenium/eyes/driver"
|
24
|
+
require 'eyes_selenium/eyes/element'
|
25
|
+
require 'eyes_selenium/eyes/environment'
|
26
|
+
require 'eyes_selenium/eyes/eyes'
|
27
|
+
require 'eyes_selenium/eyes/eyes_keyboard'
|
28
|
+
require 'eyes_selenium/eyes/eyes_mouse'
|
29
|
+
require 'eyes_selenium/eyes/failure_reports'
|
30
|
+
require 'eyes_selenium/eyes/match_level'
|
31
|
+
require 'eyes_selenium/eyes/match_window_data'
|
32
|
+
require 'eyes_selenium/eyes/match_window_task'
|
33
|
+
require 'eyes_selenium/eyes/mouse_trigger'
|
34
|
+
require 'eyes_selenium/eyes/region'
|
35
|
+
require 'eyes_selenium/eyes/screenshot_taker'
|
36
|
+
require 'eyes_selenium/eyes/session'
|
37
|
+
require 'eyes_selenium/eyes/start_info'
|
38
|
+
require 'eyes_selenium/eyes/test_results'
|
39
|
+
require 'eyes_selenium/eyes/text_trigger'
|
40
|
+
require "eyes_selenium/version"
|
41
|
+
require 'eyes_selenium/eyes/viewport_size'
|
42
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'eyes_selenium'
|
2
|
+
|
3
|
+
# Override create driver to inject into capybara's driver
|
4
|
+
class Applitools::Eyes
|
5
|
+
def test(params={}, &block)
|
6
|
+
begin
|
7
|
+
previous_driver = Capybara.current_driver
|
8
|
+
previous_browser = Capybara.current_session.driver.instance_variable_get(:@browser)
|
9
|
+
Capybara.current_driver = :selenium
|
10
|
+
Capybara.current_session.driver.instance_variable_set(:@browser, driver)
|
11
|
+
open(params)
|
12
|
+
yield(self, driver)
|
13
|
+
close
|
14
|
+
rescue Applitools::EyesError
|
15
|
+
ensure
|
16
|
+
abort_if_not_closed
|
17
|
+
Capybara.current_session.driver.instance_variable_set(:@browser, previous_browser)
|
18
|
+
Capybara.current_driver = previous_driver
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
class Applitools::AgentConnector
|
3
|
+
include HTTParty
|
4
|
+
headers 'Accept' => 'application/json'
|
5
|
+
#debug_output $stdout # comment out when not debugging
|
6
|
+
|
7
|
+
attr_reader :uri, :auth
|
8
|
+
def initialize(base_uri, username, password)
|
9
|
+
# Remove trailing slashes in base uri and add the running sessions endpoint uri.
|
10
|
+
@uri = base_uri.gsub(/\/$/,"") + "/api/sessions/running"
|
11
|
+
@auth = { username: username, password: password }
|
12
|
+
end
|
13
|
+
|
14
|
+
def match_window(session, data)
|
15
|
+
self.class.headers 'Content-Type' => 'application/octet-stream'
|
16
|
+
json_data = data.to_hash.to_json.encode('UTF-8') # Notice that this does not include the screenshot
|
17
|
+
body = [json_data.length].pack('L>') + json_data + data.screenshot
|
18
|
+
|
19
|
+
res = self.class.post(uri + "/#{session.id}",basic_auth: auth, body: body)
|
20
|
+
raise Applitools::EyesError.new("could not connect to server") if res.code != 200
|
21
|
+
res.parsed_response["asExpected"]
|
22
|
+
end
|
23
|
+
|
24
|
+
def start_session(session_start_info)
|
25
|
+
self.class.headers 'Content-Type' => 'application/json'
|
26
|
+
res = self.class.post(uri, basic_auth: auth, body: { startInfo: session_start_info.to_hash }.to_json)
|
27
|
+
status_code = res.response.message
|
28
|
+
parsed_res = res.parsed_response
|
29
|
+
Applitools::Session.new(parsed_res["id"], parsed_res["url"], status_code == "Created" )
|
30
|
+
end
|
31
|
+
|
32
|
+
def stop_session(session, aborted=nil)
|
33
|
+
self.class.headers 'Content-Type' => 'application/json'
|
34
|
+
res = self.class.delete(uri + "/#{session.id}", basic_auth: auth, params: { aborted: aborted.to_s })
|
35
|
+
parsed_res = res.parsed_response
|
36
|
+
parsed_res.delete("$id")
|
37
|
+
Applitools::TestResults.new(*parsed_res.values)
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'selenium-webdriver'
|
3
|
+
class Applitools::Driver
|
4
|
+
|
5
|
+
include Selenium::WebDriver::DriverExtensions::HasInputDevices
|
6
|
+
|
7
|
+
attr_reader :remote_server_url, :remote_session_id, :user_agent, :screenshot_taker
|
8
|
+
attr_accessor :user_inputs, :browser
|
9
|
+
|
10
|
+
DEFAULT_DRIVER = :firefox
|
11
|
+
DRIVER_METHODS = [
|
12
|
+
:title, :execute_script, :execute_async_script, :quit, :close, :get,
|
13
|
+
:post, :page_source, :window_handles, :window_handle, :switch_to,
|
14
|
+
:navigate, :manage, :capabilities
|
15
|
+
]
|
16
|
+
|
17
|
+
## If driver is not provided, Applitools::Driver will default to Firefox driver
|
18
|
+
## Driver param can be a Selenium::WebDriver or a named symbol (:chrome)
|
19
|
+
#
|
20
|
+
## Example:
|
21
|
+
# eyes.open(browser: :chrome) ##=> will create chrome webdriver
|
22
|
+
# eyes.open(browser: Selenium::WebDriver.for(:chrome) ##=> will create the same thing
|
23
|
+
# eyes.open ##=> will create a webdriver according to Applitools::Driver::DEFAULT_DRIVER
|
24
|
+
def initialize(options={})
|
25
|
+
browser_obj = options.delete(:browser) || DEFAULT_DRIVER
|
26
|
+
@browser ||= case browser_obj
|
27
|
+
when Symbol
|
28
|
+
Selenium::WebDriver.for browser_obj
|
29
|
+
else
|
30
|
+
browser_obj
|
31
|
+
end
|
32
|
+
# TODO Remove this if all is good. We don't want to call 'quit' everytime the object is destroyed, we might still want to use the underlying driver
|
33
|
+
#at_exit { quit rescue nil }
|
34
|
+
|
35
|
+
@user_inputs = []
|
36
|
+
@remote_server_url = address_of_remote_server
|
37
|
+
@remote_session_id = remote_session_id
|
38
|
+
@user_agent = get_user_agent
|
39
|
+
begin
|
40
|
+
if browser.capabilities.takes_screenshot?
|
41
|
+
@screenshot_taker = false
|
42
|
+
else
|
43
|
+
@screenshot_taker = Applitools::ScreenshotTaker.new(@remote_server_url, @remote_session_id)
|
44
|
+
end
|
45
|
+
rescue => e
|
46
|
+
raise Applitools::EyesError.new "Can't take screenshots (#{e.message})"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
DRIVER_METHODS.each do |method|
|
51
|
+
define_method method do |*args, &block|
|
52
|
+
browser.send(method,*args, &block)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def screenshot_as(output_type)
|
57
|
+
return browser.screenshot_as(output_type) if !screenshot_taker
|
58
|
+
|
59
|
+
if output_type.downcase.to_sym != :base64
|
60
|
+
raise Applitools::EyesError.new("#{output_type} ouput type not supported for screenshot")
|
61
|
+
end
|
62
|
+
screenshot_taker.screenshot
|
63
|
+
end
|
64
|
+
|
65
|
+
def mouse
|
66
|
+
Applitools::EyesMouse.new(self, browser.mouse)
|
67
|
+
end
|
68
|
+
|
69
|
+
def keyboard
|
70
|
+
Applitools::EyesKeyboard.new(self, browser.keyboard)
|
71
|
+
end
|
72
|
+
|
73
|
+
def find_element(by, selector)
|
74
|
+
Applitools::Element.new(self, browser.find_element(by, selector))
|
75
|
+
end
|
76
|
+
|
77
|
+
def find_elements(by, selector)
|
78
|
+
browser.find_elements(by, selector).map { |el| Applitools::Element.new(self, el) }
|
79
|
+
end
|
80
|
+
|
81
|
+
def create_application
|
82
|
+
Applitools::TargetApp.new(remote_server_url, remote_session_id, user_agent)
|
83
|
+
end
|
84
|
+
|
85
|
+
def clear_user_inputs
|
86
|
+
user_inputs.clear
|
87
|
+
end
|
88
|
+
|
89
|
+
def ie?
|
90
|
+
browser.to_s == 'ie'
|
91
|
+
end
|
92
|
+
|
93
|
+
def firefox?
|
94
|
+
browser.to_s == 'firefox'
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def address_of_remote_server
|
100
|
+
uri = URI(browser.current_url)
|
101
|
+
raise Applitools::EyesError.new("Failed to get remote web driver url") if uri.to_s.empty?
|
102
|
+
|
103
|
+
webdriver_host = uri.host
|
104
|
+
if ['127.0.0.1', 'localhost'].include?(webdriver_host) && !firefox? && !ie?
|
105
|
+
uri.host = get_local_ip || 'localhost'
|
106
|
+
end
|
107
|
+
|
108
|
+
uri
|
109
|
+
end
|
110
|
+
|
111
|
+
def remote_session_id
|
112
|
+
browser.remote_session_id
|
113
|
+
end
|
114
|
+
|
115
|
+
def get_user_agent
|
116
|
+
execute_script "return navigator.userAgent"
|
117
|
+
rescue => e
|
118
|
+
puts "getUserAgent(): Failed to obtain user-agent string (#{e.message})"
|
119
|
+
end
|
120
|
+
|
121
|
+
def get_local_ip
|
122
|
+
begin
|
123
|
+
Socket.ip_address_list.detect do |intf|
|
124
|
+
intf.ipv4? and !intf.ipv4_loopback? and !intf.ipv4_multicast?
|
125
|
+
end.ip_address
|
126
|
+
rescue SocketError => e
|
127
|
+
raise Applitools::EyesError.new("Failed to get local IP! (#{e})")
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
|
133
|
+
## .bridge, .session_id and .server_url are private methods in Selenium::WebDriver gem
|
134
|
+
module Selenium::WebDriver
|
135
|
+
class Driver
|
136
|
+
def remote_session_id
|
137
|
+
bridge.session_id
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
class Remote::Http::Common
|
142
|
+
def get_server_url
|
143
|
+
server_url
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
class Applitools::Element
|
2
|
+
attr_accessor :driver, :web_element
|
3
|
+
|
4
|
+
ELEMENT_METHODS = [
|
5
|
+
:hash, :id, :id=, :bridge=, :submit, :clear, :tag_name, :attribute,
|
6
|
+
:selected?, :enabled?, :displayed?, :text, :css_value, :find_element,
|
7
|
+
:find_elements, :location, :size, :location_once_scrolled_into_view,
|
8
|
+
:ref, :to_json, :as_json
|
9
|
+
]
|
10
|
+
|
11
|
+
ELEMENT_METHODS.each do |method|
|
12
|
+
define_method method do |*args, &block|
|
13
|
+
web_element.send(method,*args, &block)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
alias_method :style, :css_value
|
17
|
+
alias_method :first, :find_element
|
18
|
+
alias_method :all, :find_elements
|
19
|
+
alias_method :[], :attribute
|
20
|
+
|
21
|
+
|
22
|
+
|
23
|
+
def initialize(driver, element)
|
24
|
+
@driver = driver
|
25
|
+
@web_element = element
|
26
|
+
end
|
27
|
+
|
28
|
+
def click
|
29
|
+
current_control = region
|
30
|
+
offset = current_control.middle_offset
|
31
|
+
driver.user_inputs << Applitools::MouseTrigger.new(:click, current_control, offset)
|
32
|
+
|
33
|
+
web_element.click
|
34
|
+
end
|
35
|
+
|
36
|
+
def inspect
|
37
|
+
"EyesWebElement" + web_element.inspect
|
38
|
+
end
|
39
|
+
|
40
|
+
def ==(other)
|
41
|
+
other.kind_of?(web_element.class) && web_element == other
|
42
|
+
end
|
43
|
+
alias_method :eql?, :==
|
44
|
+
|
45
|
+
def send_keys(*args)
|
46
|
+
current_control = region
|
47
|
+
Selenium::WebDriver::Keys.encode(args).each do |key|
|
48
|
+
driver.user_inputs << Applitools::TextTrigger.new(key.to_s, current_control)
|
49
|
+
end
|
50
|
+
|
51
|
+
web_element.send_keys(args)
|
52
|
+
end
|
53
|
+
alias_method :send_key, :send_keys
|
54
|
+
|
55
|
+
def region
|
56
|
+
point = location
|
57
|
+
left, top, width, height = point.x, point.y, 0, 0
|
58
|
+
|
59
|
+
begin
|
60
|
+
dimension = size
|
61
|
+
width, height = dimension.width, dimension.height
|
62
|
+
rescue
|
63
|
+
# Not supported on all platforms.
|
64
|
+
end
|
65
|
+
|
66
|
+
if left < 0
|
67
|
+
width = [0, width + left].max
|
68
|
+
left = 0
|
69
|
+
end
|
70
|
+
|
71
|
+
if top < 0
|
72
|
+
height = [0, height + top].max
|
73
|
+
top = 0
|
74
|
+
end
|
75
|
+
|
76
|
+
return Applitools::Region.new(left, top, width, height)
|
77
|
+
end
|
78
|
+
end
|