test_right 0.2.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/LICENSE +19 -0
- data/README.md +73 -0
- data/Rakefile +53 -0
- data/lib/test/right/assertions.rb +36 -0
- data/lib/test/right/browser_driver.rb +34 -0
- data/lib/test/right/cli.rb +117 -0
- data/lib/test/right/config.rb +13 -0
- data/lib/test/right/data_factory.rb +22 -0
- data/lib/test/right/errors.rb +15 -0
- data/lib/test/right/feature.rb +38 -0
- data/lib/test/right/generator.rb +28 -0
- data/lib/test/right/runner.rb +83 -0
- data/lib/test/right/utils.rb +20 -0
- data/lib/test/right/value.rb +13 -0
- data/lib/test/right/widget.rb +240 -0
- data/lib/test/right/widget_proxy.rb +14 -0
- data/lib/test/right.rb +21 -0
- metadata +131 -0
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2011 Sauce Labs Inc
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
Test::Right - Opinionated full-stack browser testing
|
2
|
+
=================================================
|
3
|
+
|
4
|
+
Test::Right is a testing framework designed to help users get maximum value out
|
5
|
+
of browser testing. It provides a flexible Page Object model for building
|
6
|
+
robust, reliable tests quickly and easily. No more slogging through XPaths in
|
7
|
+
Selenium IDE: Test::Right is the right way to build browser tests.
|
8
|
+
|
9
|
+
Setup
|
10
|
+
-----
|
11
|
+
|
12
|
+
gem install test_right
|
13
|
+
cd MY_APP
|
14
|
+
test_right install
|
15
|
+
|
16
|
+
The test_right executable will create a default directory structure in
|
17
|
+
test/right for you to put your tests in.
|
18
|
+
|
19
|
+
Begin by setting the base_url setting in test/right/config.yml to the base URL
|
20
|
+
of your application staging environment. Then add the necessary code to reset
|
21
|
+
your application state and launch your server to setup.rb.
|
22
|
+
|
23
|
+
Tests are defined in terms of _actions_ and _properties_ on _widgets_. A
|
24
|
+
widget is a piece of functionality present on one more more pages of your
|
25
|
+
application. A single page can have many widgets, and multiple copies of a
|
26
|
+
widget may appear on the same page.
|
27
|
+
|
28
|
+
To get started, add widget definitions to the widgets/ directory. A widget
|
29
|
+
defines its elements in terms of standard Selenium 2 selectors and actions in
|
30
|
+
terms of those elements. For example, something like this in
|
31
|
+
test/right/widgets/login.rb:
|
32
|
+
|
33
|
+
class LoginWidget < Test::Right::Widget
|
34
|
+
field :username, :id => 'username'
|
35
|
+
button :login, :css => "input[type=submit]"
|
36
|
+
|
37
|
+
action :login |username, password|
|
38
|
+
fill_in :username, username
|
39
|
+
click :login
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class CartWidget < Test::Right::Widget
|
44
|
+
element :count, :id => 'item_count'
|
45
|
+
|
46
|
+
property :number_of_items do
|
47
|
+
get_element(:count).text
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
Once your widgets are setup, write tests for features of your application in
|
52
|
+
test/right/features. For example, something like this would go in
|
53
|
+
test/right/features/shopping_cart.rb:
|
54
|
+
|
55
|
+
class ShoppingCartFeature < Test::Right::Feature
|
56
|
+
def setup
|
57
|
+
with LoginWidget do |w|
|
58
|
+
w.login
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_adding_item
|
63
|
+
with ItemWidget do |w|
|
64
|
+
w.add_an_item
|
65
|
+
end
|
66
|
+
|
67
|
+
with CartWidget do |w|
|
68
|
+
assert_equal, 1, w.number_of_items
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
Learn more on the Test::Right wiki at https://github.com/saucelabs/test_right/wiki
|
data/Rakefile
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
require 'rake/testtask'
|
5
|
+
Rake::TestTask.new(:test) do |test|
|
6
|
+
test.libs << 'lib' << 'test'
|
7
|
+
test.pattern = 'test/**/test_*.rb'
|
8
|
+
test.verbose = true
|
9
|
+
end
|
10
|
+
|
11
|
+
require 'rcov/rcovtask'
|
12
|
+
Rcov::RcovTask.new do |t|
|
13
|
+
#t.test_files = FileList['test/test_*.rb']
|
14
|
+
t.ruby_opts << "-Ilib" # in order to use this rcov
|
15
|
+
t.ruby_opts << "-Itest"
|
16
|
+
t.rcov_opts << "--xrefs" # comment to disable cross-references
|
17
|
+
t.rcov_opts << "--exclude-only" << "test/test*,^\/,^features\/,^widgets\/,test/helper.rb,test/mock_driver.rb"
|
18
|
+
t.verbose = true
|
19
|
+
end
|
20
|
+
|
21
|
+
task :build do
|
22
|
+
unless system "gem build test_right.gemspec"
|
23
|
+
raise "Failed to build"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
desc 'Release gem to rubygems.org'
|
28
|
+
task :release => :build do
|
29
|
+
system "gem push `ls *.gem | sort | tail -n 1`"
|
30
|
+
end
|
31
|
+
|
32
|
+
desc 'tag current version'
|
33
|
+
task :tag do
|
34
|
+
version = nil
|
35
|
+
File.open("test_right.gemspec").each do |line|
|
36
|
+
if line =~ /s.version = "(.*)"/
|
37
|
+
version = $1
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
if version.nil?
|
42
|
+
raise "Couldn't find version"
|
43
|
+
end
|
44
|
+
|
45
|
+
system "git tag v#{version}"
|
46
|
+
end
|
47
|
+
|
48
|
+
desc 'push to github'
|
49
|
+
task :push do
|
50
|
+
system "git push origin master --tags"
|
51
|
+
end
|
52
|
+
|
53
|
+
task :default => [:test, :tag, :push]
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Test
|
2
|
+
module Right
|
3
|
+
module Assertions
|
4
|
+
DEFAULT_ASSERTION_TIMEOUT = 15 # seconds
|
5
|
+
|
6
|
+
def assert(timeout=DEFAULT_ASSERTION_TIMEOUT)
|
7
|
+
if !block_given?
|
8
|
+
raise ArgumentError, "assert requires a block"
|
9
|
+
end
|
10
|
+
|
11
|
+
start = Time.now
|
12
|
+
while Time.now-start < timeout
|
13
|
+
begin
|
14
|
+
if yield
|
15
|
+
return
|
16
|
+
end
|
17
|
+
rescue WidgetNotPresentError
|
18
|
+
end
|
19
|
+
sleep(0.5)
|
20
|
+
end
|
21
|
+
|
22
|
+
raise AssertionFailedError, "Assertion failed after #{timeout} seconds"
|
23
|
+
end
|
24
|
+
|
25
|
+
def assert_equal(expected, actual, timeout=DEFAULT_ASSERTION_TIMEOUT)
|
26
|
+
if actual.is_a? Value
|
27
|
+
assert do
|
28
|
+
expected == actual.value
|
29
|
+
end
|
30
|
+
else
|
31
|
+
raise AssertionFailedError, "Expected #{expected.inspect} but got #{actual.inspect}" unless expected == actual
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'selenium-webdriver'
|
2
|
+
|
3
|
+
module Test
|
4
|
+
module Right
|
5
|
+
class BrowserDriver
|
6
|
+
def initialize(config)
|
7
|
+
@base_url = config[:base_url]
|
8
|
+
if @base_url =~ /\/$/
|
9
|
+
@base_url = @base_url[0..-2]
|
10
|
+
end
|
11
|
+
@driver = Selenium::WebDriver.for :firefox
|
12
|
+
end
|
13
|
+
|
14
|
+
def get(url, options = {})
|
15
|
+
if options[:relative]
|
16
|
+
@driver.get(relative_url(url))
|
17
|
+
else
|
18
|
+
@driver.get(url)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def relative_url(url)
|
25
|
+
raise ConfigurationError, "No base_url in config.yml" if @base_url.nil?
|
26
|
+
@base_url + url
|
27
|
+
end
|
28
|
+
|
29
|
+
def method_missing(name, *args)
|
30
|
+
@driver.send(name, *args)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Test
|
4
|
+
module Right
|
5
|
+
class CLI
|
6
|
+
RUBY_FILE = /^[^.].+.rb$/
|
7
|
+
|
8
|
+
|
9
|
+
def start(argument_list)
|
10
|
+
if argument_list.first == "install"
|
11
|
+
Generator.new(argument_list[1..-1]).generate
|
12
|
+
return
|
13
|
+
else
|
14
|
+
load_and_run_tests
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def load_and_run_tests
|
19
|
+
run_setup
|
20
|
+
|
21
|
+
subdir = false
|
22
|
+
if File.directory? "test/right"
|
23
|
+
Dir.chdir("test/right")
|
24
|
+
subdir = true
|
25
|
+
end
|
26
|
+
|
27
|
+
load_config
|
28
|
+
load_widgets
|
29
|
+
load_features
|
30
|
+
|
31
|
+
Dir.chdir("../..") if subdir
|
32
|
+
|
33
|
+
puts "Running #{features.size} features"
|
34
|
+
runner = Runner.new(config, widgets, features)
|
35
|
+
if runner.run
|
36
|
+
puts "Passed!"
|
37
|
+
else
|
38
|
+
at_exit {
|
39
|
+
exit(1)
|
40
|
+
}
|
41
|
+
puts "Failed:"
|
42
|
+
runner.results.each do |feature, feature_result|
|
43
|
+
puts " #{feature}"
|
44
|
+
feature_result.each do |method, result|
|
45
|
+
if result.is_a? Exception
|
46
|
+
if result.is_a? WidgetActionNotImplemented
|
47
|
+
puts " #{method} => #{result}"
|
48
|
+
else
|
49
|
+
puts " #{method} => #{result.class} - #{result}"
|
50
|
+
result.backtrace.each do |trace|
|
51
|
+
puts " #{trace}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
else
|
55
|
+
puts " #{method} => #{result}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def run_setup
|
63
|
+
if File.exists? "setup.rb"
|
64
|
+
unless system("./setup.rb")
|
65
|
+
raise ConfigurationError, "Setup failed"
|
66
|
+
end
|
67
|
+
elsif File.exists? "test/right/setup.rb"
|
68
|
+
unless system("test/right/setup.rb")
|
69
|
+
raise ConfigurationError, "Setup failed"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def load_config
|
75
|
+
options = {}
|
76
|
+
if File.exists? "config.yml"
|
77
|
+
options = YAML.load(open("config.yml"))
|
78
|
+
end
|
79
|
+
@config = Config.new(options)
|
80
|
+
end
|
81
|
+
|
82
|
+
def config
|
83
|
+
@config
|
84
|
+
end
|
85
|
+
|
86
|
+
def load_widgets
|
87
|
+
raise ConfigurationError, "no widgets/ directory" unless File.directory? "widgets"
|
88
|
+
load_ruby_in_dir("widgets")
|
89
|
+
end
|
90
|
+
|
91
|
+
def widgets
|
92
|
+
Widget.subclasses || []
|
93
|
+
end
|
94
|
+
|
95
|
+
def load_features
|
96
|
+
raise ConfigurationError, "no features/ directory" unless File.directory? "features"
|
97
|
+
load_ruby_in_dir("features")
|
98
|
+
end
|
99
|
+
|
100
|
+
def features
|
101
|
+
Feature.subclasses || []
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
def load_ruby_in_dir(dirname)
|
107
|
+
Dir.foreach dirname do |filename|
|
108
|
+
unless [".", ".."].include? filename
|
109
|
+
if filename =~ RUBY_FILE
|
110
|
+
load "#{dirname}/#{filename}"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module Test
|
4
|
+
module Right
|
5
|
+
class DataFactory
|
6
|
+
def initialize(template)
|
7
|
+
@template = template
|
8
|
+
@used_ids = Set.new([nil])
|
9
|
+
end
|
10
|
+
|
11
|
+
def [](name)
|
12
|
+
base = @template[name] || @template[name.to_s] || name.to_s
|
13
|
+
id = nil
|
14
|
+
while @used_ids.include? id
|
15
|
+
id = rand(100000)
|
16
|
+
end
|
17
|
+
base += id.to_s
|
18
|
+
return base
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Test
|
2
|
+
module Right
|
3
|
+
class Error < StandardError; end
|
4
|
+
class ConfigurationError < Error; end
|
5
|
+
class WidgetActionNotImplemented < Error; end
|
6
|
+
class WidgetNotFoundError < Error; end
|
7
|
+
class SelectorNotFoundError < Error; end
|
8
|
+
class SelectorsNotFoundError < Error; end
|
9
|
+
class ElementNotFoundError < Error; end
|
10
|
+
class AssertionFailedError < Error; end
|
11
|
+
class WidgetConfigurationError < Error; end
|
12
|
+
class WidgetNotPresentError < Error; end
|
13
|
+
class IAmConfusedError < Error; end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Test
|
2
|
+
module Right
|
3
|
+
class Feature
|
4
|
+
extend Utils::SubclassTracking
|
5
|
+
include Assertions
|
6
|
+
|
7
|
+
attr_reader :data
|
8
|
+
|
9
|
+
WIDGET_TIMEOUT = 10 # seconds
|
10
|
+
|
11
|
+
def initialize(driver, data)
|
12
|
+
@driver = driver
|
13
|
+
@data = data
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
|
19
|
+
def with(widget_class)
|
20
|
+
wait_for(widget_class)
|
21
|
+
yield widget_class.new(@driver)
|
22
|
+
end
|
23
|
+
|
24
|
+
def wait_for(widget_class)
|
25
|
+
widget = widget_class.new(@driver)
|
26
|
+
|
27
|
+
timeout = Time.now + WIDGET_TIMEOUT
|
28
|
+
while Time.now < timeout
|
29
|
+
break if widget.exists?
|
30
|
+
sleep(0.25)
|
31
|
+
end
|
32
|
+
if !widget.exists?
|
33
|
+
raise WidgetNotPresentError
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module Test
|
4
|
+
module Right
|
5
|
+
class Generator
|
6
|
+
include FileUtils
|
7
|
+
|
8
|
+
def initialize(args)
|
9
|
+
@args = args
|
10
|
+
end
|
11
|
+
|
12
|
+
def generate
|
13
|
+
mkdir_p("test/right/features")
|
14
|
+
mkdir_p("test/right/widgets")
|
15
|
+
|
16
|
+
open("test/right/config.yml", 'wb') do |file|
|
17
|
+
file.write <<-EOF
|
18
|
+
# The base URL of your staging server
|
19
|
+
base_url: http://example.com/
|
20
|
+
|
21
|
+
# Which browser you want to run your tests in
|
22
|
+
browser: firefox
|
23
|
+
EOF
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# trigger autoload before the threads get ahold of it
|
2
|
+
Selenium::WebDriver::Firefox
|
3
|
+
|
4
|
+
module Test
|
5
|
+
module Right
|
6
|
+
class Runner
|
7
|
+
attr_reader :results, :widget_classes
|
8
|
+
|
9
|
+
def initialize(config, widgets, features)
|
10
|
+
@config = config
|
11
|
+
@widget_classes = widgets
|
12
|
+
@features = features
|
13
|
+
@results = {}
|
14
|
+
@pool = Threadz::ThreadPool.new(:initial_size => 2, :maximum_size => 2)
|
15
|
+
@result_queue = Queue.new
|
16
|
+
@data_template = config[:data] || {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def run
|
20
|
+
@batch = @pool.new_batch
|
21
|
+
|
22
|
+
@features.sort_by{|x| rand(10000)}.all? do |feature|
|
23
|
+
run_feature(feature)
|
24
|
+
end
|
25
|
+
|
26
|
+
@batch.wait_until_done
|
27
|
+
|
28
|
+
process_results
|
29
|
+
end
|
30
|
+
|
31
|
+
def process_results
|
32
|
+
failed = false
|
33
|
+
until @result_queue.empty?
|
34
|
+
feature, method, result = @result_queue.pop
|
35
|
+
if result.is_a? Exception
|
36
|
+
failed = true
|
37
|
+
end
|
38
|
+
@results[feature] ||= {}
|
39
|
+
@results[feature][method] = result
|
40
|
+
end
|
41
|
+
return !failed
|
42
|
+
end
|
43
|
+
|
44
|
+
def run_feature(feature)
|
45
|
+
methods = feature.instance_methods
|
46
|
+
methods.sort_by{|x| rand(10000)}.each do |method_name|
|
47
|
+
if method_name =~ /^test_/
|
48
|
+
@batch << Proc.new do
|
49
|
+
begin
|
50
|
+
method = method_name.to_sym
|
51
|
+
run_test(feature, method_name.to_sym)
|
52
|
+
|
53
|
+
@result_queue << [feature, method, true]
|
54
|
+
rescue => e
|
55
|
+
@result_queue << [feature, method, e]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def run_test(feature, method)
|
63
|
+
if $MOCK_DRIVER
|
64
|
+
driver = MockDriver.new(@config)
|
65
|
+
else
|
66
|
+
driver = BrowserDriver.new(@config)
|
67
|
+
end
|
68
|
+
|
69
|
+
data = DataFactory.new(@data_template)
|
70
|
+
|
71
|
+
begin
|
72
|
+
target = feature.new(driver, data)
|
73
|
+
if target.respond_to? :setup
|
74
|
+
target.setup
|
75
|
+
end
|
76
|
+
target.send(method)
|
77
|
+
ensure
|
78
|
+
driver.quit
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Test
|
2
|
+
module Right
|
3
|
+
module Utils
|
4
|
+
module SubclassTracking
|
5
|
+
def inherited(subclass)
|
6
|
+
@subclasses ||= []
|
7
|
+
@subclasses << subclass
|
8
|
+
end
|
9
|
+
|
10
|
+
def subclasses
|
11
|
+
@subclasses
|
12
|
+
end
|
13
|
+
|
14
|
+
def wipe!
|
15
|
+
@subclasses = []
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,240 @@
|
|
1
|
+
module Test
|
2
|
+
module Right
|
3
|
+
class Widget
|
4
|
+
extend Utils::SubclassTracking
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
attr_reader :root, :name_elements, :validator, :location
|
8
|
+
|
9
|
+
def selector(name)
|
10
|
+
@selectors[name]
|
11
|
+
end
|
12
|
+
|
13
|
+
def element(name, locator)
|
14
|
+
@selectors[name] = locator
|
15
|
+
end
|
16
|
+
|
17
|
+
def lives_at(url)
|
18
|
+
@location = url
|
19
|
+
end
|
20
|
+
|
21
|
+
def rooted_at(root)
|
22
|
+
@root = [root.keys.first, root.values.first]
|
23
|
+
@selectors[:root] = root
|
24
|
+
end
|
25
|
+
|
26
|
+
def named_by(*args)
|
27
|
+
@name_elements ||= []
|
28
|
+
if args.length == 2
|
29
|
+
property, name_element = args
|
30
|
+
@name_elements << [property, name_element.keys.first, name_element.values.first]
|
31
|
+
else
|
32
|
+
name_element = args.first
|
33
|
+
@name_elements << [:text, name_element.keys.first, name_element.values.first]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def validated_by_presence_of(selector)
|
38
|
+
@validator = selector
|
39
|
+
end
|
40
|
+
|
41
|
+
def action(name, &body)
|
42
|
+
define_method name do |*args|
|
43
|
+
self.validate!
|
44
|
+
self.instance_exec *args, &body
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def property(name, &body)
|
49
|
+
define_method name do
|
50
|
+
self.validate!
|
51
|
+
Value.new do
|
52
|
+
self.instance_eval &body
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def [](name)
|
58
|
+
WidgetProxy.new(self, name)
|
59
|
+
end
|
60
|
+
|
61
|
+
alias_method :button, :element
|
62
|
+
alias_method :field, :element
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.inherited(subclass)
|
66
|
+
@subclasses ||= []
|
67
|
+
@subclasses << subclass
|
68
|
+
subclass.instance_eval do
|
69
|
+
@selectors = {}
|
70
|
+
@location = nil
|
71
|
+
end
|
72
|
+
|
73
|
+
subclass.extend(ClassMethods)
|
74
|
+
end
|
75
|
+
|
76
|
+
attr_reader :name
|
77
|
+
|
78
|
+
def initialize(driver, name=nil)
|
79
|
+
@driver = driver
|
80
|
+
@name = name
|
81
|
+
end
|
82
|
+
|
83
|
+
def visit
|
84
|
+
@driver.get(self.class.location, :relative => true)
|
85
|
+
end
|
86
|
+
|
87
|
+
def exists?
|
88
|
+
begin
|
89
|
+
root
|
90
|
+
return true
|
91
|
+
rescue WidgetNotPresentError
|
92
|
+
return false
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def validate!(timeout=15)
|
97
|
+
timeout = Time.now + timeout
|
98
|
+
while Time.now < timeout
|
99
|
+
if self.exists?
|
100
|
+
return
|
101
|
+
end
|
102
|
+
end
|
103
|
+
raise WidgetNotPresentError
|
104
|
+
end
|
105
|
+
|
106
|
+
def method_missing(name, *args)
|
107
|
+
raise WidgetActionNotImplemented, "#{self.class.to_s}##{name.to_s} not implemented"
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
def fill_in(selector_name, value)
|
113
|
+
get_element(selector_name).send_keys(value)
|
114
|
+
end
|
115
|
+
|
116
|
+
def clear(selector_name)
|
117
|
+
get_element(selector_name).clear
|
118
|
+
end
|
119
|
+
|
120
|
+
def click(selector_name)
|
121
|
+
get_element(selector_name).click
|
122
|
+
end
|
123
|
+
|
124
|
+
def select(selector_name)
|
125
|
+
get_element(selector_name).select
|
126
|
+
end
|
127
|
+
|
128
|
+
def navigate_to(url)
|
129
|
+
@driver.get(url)
|
130
|
+
end
|
131
|
+
|
132
|
+
def wait_for_element_present(selector_name, timeout=10)
|
133
|
+
endtime = Time.now + timeout
|
134
|
+
while Time.now < endtime
|
135
|
+
begin
|
136
|
+
get_element(selector_name)
|
137
|
+
return
|
138
|
+
rescue ElementNotFoundError
|
139
|
+
rescue WidgetNotPresentError
|
140
|
+
end
|
141
|
+
end
|
142
|
+
raise ElementNotFoundError
|
143
|
+
end
|
144
|
+
|
145
|
+
def wait_for_element_not_present(selector_name, timeout=10)
|
146
|
+
endtime = Time.now + timeout
|
147
|
+
while Time.now < endtime
|
148
|
+
begin
|
149
|
+
get_element(selector_name)
|
150
|
+
rescue ElementNotFoundError
|
151
|
+
return true
|
152
|
+
rescue WidgetNotPresentError
|
153
|
+
return true
|
154
|
+
end
|
155
|
+
end
|
156
|
+
raise Error, "Element didn't disappear"
|
157
|
+
end
|
158
|
+
|
159
|
+
def get_element(selector_name)
|
160
|
+
selector = find_selector(selector_name)
|
161
|
+
if selector_name == :root
|
162
|
+
begin
|
163
|
+
return @driver.find_element(*self.class.root)
|
164
|
+
rescue Selenium::WebDriver::Error::NoSuchElementError => e
|
165
|
+
raise ElementNotFoundError, e.message
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
target = nil
|
170
|
+
while target.nil?
|
171
|
+
begin
|
172
|
+
target = root.find_element(*selector)
|
173
|
+
rescue Selenium::WebDriver::Error::NoSuchElementError => e
|
174
|
+
raise ElementNotFoundError, "Element #{selector_name} not found on #{self.class.name} using #{selector.inspect}"
|
175
|
+
rescue Selenium::WebDriver::Error::ObsoleteElementError
|
176
|
+
# ignore
|
177
|
+
end
|
178
|
+
end
|
179
|
+
return target
|
180
|
+
end
|
181
|
+
|
182
|
+
def get_elements(selector_name)
|
183
|
+
selector = find_selector(selector_name)
|
184
|
+
begin
|
185
|
+
element = root.find_elements(*selector)
|
186
|
+
rescue Selenium::WebDriver::Error::NoSuchElementError => e
|
187
|
+
raise ElementNotFoundError, e.message
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def find_selector(selector_name)
|
192
|
+
if !self.class.selector(selector_name)
|
193
|
+
raise SelectorNotFoundError, "Selector \"#{selector_name}\" for Widget \"#{self.class}\" not found"
|
194
|
+
end
|
195
|
+
|
196
|
+
selector = self.class.selector(selector_name)
|
197
|
+
return [selector.keys.first, selector.values.first]
|
198
|
+
end
|
199
|
+
|
200
|
+
def root
|
201
|
+
if self.class.root.nil? && self.class.name_elements.nil?
|
202
|
+
return @driver
|
203
|
+
elsif self.class.root && self.class.name_elements.nil?
|
204
|
+
begin
|
205
|
+
return @driver.find_element(*self.class.root)
|
206
|
+
rescue Selenium::WebDriver::Error::NoSuchElementError => e
|
207
|
+
raise WidgetNotPresentError, "#{self.class.name} not found on page"
|
208
|
+
end
|
209
|
+
elsif self.class.root && self.class.name_elements
|
210
|
+
all_instances = @driver.find_elements(*self.class.root)
|
211
|
+
target = all_instances.find do |root_element|
|
212
|
+
self.class.name_elements.any? do |property, how, what|
|
213
|
+
begin
|
214
|
+
element = root_element.find_element(how, what)
|
215
|
+
element_name = nil
|
216
|
+
if :value == property
|
217
|
+
element_name = element.attribute('value')
|
218
|
+
else
|
219
|
+
element_name = element.send(property)
|
220
|
+
end
|
221
|
+
@name == element_name
|
222
|
+
rescue Selenium::WebDriver::Error::ObsoleteElementError
|
223
|
+
false
|
224
|
+
rescue Selenium::WebDriver::Error::NoSuchElementError
|
225
|
+
false
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
if target.nil?
|
230
|
+
raise WidgetNotPresentError, "#{self.class.name} with name \"#{name}\" not found"
|
231
|
+
end
|
232
|
+
|
233
|
+
return target
|
234
|
+
else
|
235
|
+
raise IAmConfusedError
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
data/lib/test/right.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'test/unit/assertions'
|
2
|
+
require 'threadz'
|
3
|
+
|
4
|
+
module Test
|
5
|
+
module Right
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'test/right/errors'
|
10
|
+
require 'test/right/utils'
|
11
|
+
require 'test/right/config'
|
12
|
+
require 'test/right/value'
|
13
|
+
require 'test/right/widget_proxy'
|
14
|
+
require 'test/right/widget'
|
15
|
+
require 'test/right/assertions'
|
16
|
+
require 'test/right/feature'
|
17
|
+
require 'test/right/browser_driver'
|
18
|
+
require 'test/right/data_factory'
|
19
|
+
require 'test/right/runner'
|
20
|
+
require 'test/right/generator'
|
21
|
+
require 'test/right/cli'
|
metadata
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: test_right
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 23
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 2
|
9
|
+
- 0
|
10
|
+
version: 0.2.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Eric Allen
|
14
|
+
- Sean Grove
|
15
|
+
autorequire:
|
16
|
+
bindir: bin
|
17
|
+
cert_chain: []
|
18
|
+
|
19
|
+
date: 2011-05-03 00:00:00 -07:00
|
20
|
+
default_executable:
|
21
|
+
dependencies:
|
22
|
+
- !ruby/object:Gem::Dependency
|
23
|
+
name: selenium-webdriver
|
24
|
+
prerelease: false
|
25
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ">="
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
hash: 23
|
31
|
+
segments:
|
32
|
+
- 0
|
33
|
+
- 2
|
34
|
+
- 0
|
35
|
+
version: 0.2.0
|
36
|
+
type: :runtime
|
37
|
+
version_requirements: *id001
|
38
|
+
- !ruby/object:Gem::Dependency
|
39
|
+
name: threadz
|
40
|
+
prerelease: false
|
41
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ~>
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
hash: 29
|
47
|
+
segments:
|
48
|
+
- 0
|
49
|
+
- 1
|
50
|
+
- 3
|
51
|
+
version: 0.1.3
|
52
|
+
type: :runtime
|
53
|
+
version_requirements: *id002
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: mocha
|
56
|
+
prerelease: false
|
57
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
hash: 35
|
63
|
+
segments:
|
64
|
+
- 0
|
65
|
+
- 9
|
66
|
+
- 12
|
67
|
+
version: 0.9.12
|
68
|
+
type: :development
|
69
|
+
version_requirements: *id003
|
70
|
+
description: An opinionated browser testing framework
|
71
|
+
email: help@saucelabs.com
|
72
|
+
executables: []
|
73
|
+
|
74
|
+
extensions: []
|
75
|
+
|
76
|
+
extra_rdoc_files: []
|
77
|
+
|
78
|
+
files:
|
79
|
+
- LICENSE
|
80
|
+
- README.md
|
81
|
+
- Rakefile
|
82
|
+
- lib/test/right/assertions.rb
|
83
|
+
- lib/test/right/browser_driver.rb
|
84
|
+
- lib/test/right/cli.rb
|
85
|
+
- lib/test/right/config.rb
|
86
|
+
- lib/test/right/data_factory.rb
|
87
|
+
- lib/test/right/errors.rb
|
88
|
+
- lib/test/right/feature.rb
|
89
|
+
- lib/test/right/generator.rb
|
90
|
+
- lib/test/right/runner.rb
|
91
|
+
- lib/test/right/utils.rb
|
92
|
+
- lib/test/right/value.rb
|
93
|
+
- lib/test/right/widget.rb
|
94
|
+
- lib/test/right/widget_proxy.rb
|
95
|
+
- lib/test/right.rb
|
96
|
+
has_rdoc: true
|
97
|
+
homepage:
|
98
|
+
licenses: []
|
99
|
+
|
100
|
+
post_install_message:
|
101
|
+
rdoc_options: []
|
102
|
+
|
103
|
+
require_paths:
|
104
|
+
- lib
|
105
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
106
|
+
none: false
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
hash: 3
|
111
|
+
segments:
|
112
|
+
- 0
|
113
|
+
version: "0"
|
114
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
115
|
+
none: false
|
116
|
+
requirements:
|
117
|
+
- - ">="
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
hash: 3
|
120
|
+
segments:
|
121
|
+
- 0
|
122
|
+
version: "0"
|
123
|
+
requirements: []
|
124
|
+
|
125
|
+
rubyforge_project:
|
126
|
+
rubygems_version: 1.4.2
|
127
|
+
signing_key:
|
128
|
+
specification_version: 3
|
129
|
+
summary: An opinionated browser testing framework
|
130
|
+
test_files: []
|
131
|
+
|