├── .gitignore ├── Gemfile ├── lib └── test │ ├── right │ ├── value.rb │ ├── config.rb │ ├── widget_proxy.rb │ ├── utils.rb │ ├── data_factory.rb │ ├── errors.rb │ ├── generator.rb │ ├── browser_driver.rb │ ├── feature.rb │ ├── assertions.rb │ ├── runner.rb │ ├── cli.rb │ └── widget.rb │ └── right.rb ├── test ├── test_config.rb ├── test_data_factory.rb ├── helper.rb ├── test_value.rb ├── mock_driver.rb ├── test_generator.rb ├── test_browser_driver.rb ├── test_assertions.rb ├── test_feature.rb ├── test_runner.rb ├── test_cli.rb └── test_widget.rb ├── bin └── test_right ├── test.watchr ├── test_right.gemspec ├── LICENSE ├── Rakefile └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | coverage 3 | Gemfile.lock 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /lib/test/right/value.rb: -------------------------------------------------------------------------------- 1 | module Test 2 | module Right 3 | class Value 4 | def initialize(&body) 5 | @body = body 6 | end 7 | 8 | def value 9 | @body.call 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/test_config.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | class TestConfig < Test::Unit::TestCase 4 | def test_access 5 | c = Test::Right::Config.new({'foo' => 1}) 6 | 7 | assert_equal 1, c['foo'] 8 | assert_equal 1, c[:foo] 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/test/right/config.rb: -------------------------------------------------------------------------------- 1 | module Test 2 | module Right 3 | class Config 4 | def initialize(options={}) 5 | @options = options 6 | end 7 | 8 | def [](name) 9 | @options[name] || @options[name.to_s] 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /bin/test_right: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'rubygems' 4 | 5 | lib_dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib')) 6 | $LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir) 7 | 8 | require 'test/right' 9 | 10 | Test::Right::CLI.new.start(ARGV) 11 | -------------------------------------------------------------------------------- /test/test_data_factory.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | class TestDataFactory < Test::Unit::TestCase 4 | def test_uses_template 5 | factory = Test::Right::DataFactory.new('foo' => 'bar') 6 | assert factory[:foo].include? 'bar' 7 | assert_not_equal 'bar', factory[:foo] 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/test/right/widget_proxy.rb: -------------------------------------------------------------------------------- 1 | module Test 2 | module Right 3 | class WidgetProxy 4 | def initialize(klass, name) 5 | @klass = klass 6 | @name = name 7 | end 8 | 9 | def new(driver) 10 | @klass.new(driver, @name) 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'test/right' 3 | require 'test/unit' 4 | require 'mock_driver' 5 | require 'mocha' 6 | 7 | $MOCK_DRIVER = true 8 | 9 | module TestRightTestingUtils 10 | def in_new_dir 11 | Dir.mktmpdir do |path| 12 | Dir.chdir(path) do 13 | yield 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/test_value.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | class TestValue < Test::Unit::TestCase 4 | include Test::Right::Assertions 5 | 6 | def test_reevaluates 7 | x = "foo" 8 | 9 | v = Test::Right::Value.new do 10 | x 11 | end 12 | 13 | assert_equal "foo", v 14 | 15 | x = "bar" 16 | 17 | assert_equal "bar", v 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/mock_driver.rb: -------------------------------------------------------------------------------- 1 | class MockDriver < Test::Right::BrowserDriver 2 | def initialize(config=nil) 3 | unless config.nil? 4 | @base_url = config[:base_url] 5 | end 6 | end 7 | 8 | def quit 9 | 10 | end 11 | 12 | private 13 | def method_missing(name, *args) 14 | raise "Method Not found: #{name}" 15 | end 16 | end 17 | 18 | class MockElement 19 | end 20 | -------------------------------------------------------------------------------- /test/test_generator.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | require 'fileutils' 3 | 4 | class TestGenerator < Test::Unit::TestCase 5 | include TestRightTestingUtils 6 | 7 | def setup 8 | @generator = Test::Right::Generator.new([]) 9 | end 10 | 11 | def test_generate 12 | in_new_dir do 13 | @generator.generate 14 | assert File.exists? "test/right/config.yml" 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/test/right/utils.rb: -------------------------------------------------------------------------------- 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 21 | -------------------------------------------------------------------------------- /lib/test/right/data_factory.rb: -------------------------------------------------------------------------------- 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 23 | -------------------------------------------------------------------------------- /lib/test/right.rb: -------------------------------------------------------------------------------- 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' 22 | -------------------------------------------------------------------------------- /lib/test/right/errors.rb: -------------------------------------------------------------------------------- 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 16 | -------------------------------------------------------------------------------- /lib/test/right/generator.rb: -------------------------------------------------------------------------------- 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 29 | -------------------------------------------------------------------------------- /test.watchr: -------------------------------------------------------------------------------- 1 | def growl(title, message) 2 | growlnotify = `which growlnotify`.chomp 3 | image = message.include?('0 failures, 0 errors') ? "~/.watchr_images/passed.jpg" : "~/.watchr_images/failed.jpg" 4 | options = "-w -n Watchr --image '#{File.expand_path(image)}' -m '#{message}' '#{title}'" 5 | system %(#{growlnotify} #{options} &) 6 | end 7 | 8 | def run_test_file(file) 9 | system('clear') 10 | result = `rake test TEST=#{file}` 11 | growl file, result.split("\n").last rescue nil 12 | puts result 13 | end 14 | 15 | watch("test/test_.*\.rb") {|match| run_test_file(match[0])} 16 | watch("lib/test/right/(.*)\.rb") {|match| run_test_file("test/test_#{match[1]}.rb")} 17 | -------------------------------------------------------------------------------- /test_right.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | Gem::Specification.new do |s| 4 | s.name = %q{test_right} 5 | s.version = "0.2.0" 6 | 7 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 8 | s.authors = ["Eric Allen", "Sean Grove"] 9 | s.date = %q{2011-05-03} 10 | s.description = %q{An opinionated browser testing framework} 11 | s.summary = %q{An opinionated browser testing framework} 12 | s.email = %q{help@saucelabs.com} 13 | s.files = [ 14 | "LICENSE", 15 | "README.md", 16 | "Rakefile", 17 | ] + Dir.glob("lib/**/*") 18 | 19 | s.executables = ['test_right'] 20 | 21 | s.add_dependency("selenium-webdriver", ">= 0.2.0") 22 | s.add_dependency("threadz", "~> 0.1.3") 23 | 24 | s.add_development_dependency("mocha", ">= 0.9.12") 25 | end 26 | -------------------------------------------------------------------------------- /lib/test/right/browser_driver.rb: -------------------------------------------------------------------------------- 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 35 | -------------------------------------------------------------------------------- /lib/test/right/feature.rb: -------------------------------------------------------------------------------- 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 39 | -------------------------------------------------------------------------------- /lib/test/right/assertions.rb: -------------------------------------------------------------------------------- 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 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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. 20 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 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] 54 | -------------------------------------------------------------------------------- /test/test_browser_driver.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | class TestBrowserDriver < Test::Unit::TestCase 4 | def test_extracts_base_url 5 | Selenium::WebDriver.expects(:for).with(:firefox) 6 | config = Test::Right::Config.new({'base_url' => "http://saucelabs.com/"}) 7 | driver = Test::Right::BrowserDriver.new(config) 8 | base_url = driver.instance_eval {@base_url} 9 | assert_equal "http://saucelabs.com", base_url 10 | end 11 | 12 | def test_delegates 13 | selenium = Object.new 14 | selenium.expects(:foo) 15 | Selenium::WebDriver.expects(:for).with(:firefox).returns(selenium) 16 | config = Test::Right::Config.new({'base_url' => "http://saucelabs.com/"}) 17 | driver = Test::Right::BrowserDriver.new(config) 18 | driver.foo 19 | end 20 | 21 | def test_get_relative 22 | selenium = Object.new 23 | selenium.expects(:get).with("http://saucelabs.com/foo") 24 | Selenium::WebDriver.expects(:for).with(:firefox).returns(selenium) 25 | config = Test::Right::Config.new({'base_url' => "http://saucelabs.com/"}) 26 | driver = Test::Right::BrowserDriver.new(config) 27 | driver.get("/foo", :relative => true) 28 | end 29 | 30 | def test_get_absolute 31 | selenium = Object.new 32 | selenium.expects(:get).with("foo") 33 | Selenium::WebDriver.expects(:for).with(:firefox).returns(selenium) 34 | config = Test::Right::Config.new({'base_url' => "http://saucelabs.com/"}) 35 | driver = Test::Right::BrowserDriver.new(config) 36 | driver.get("foo") 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/test_assertions.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | class TestAssertions < Test::Unit::TestCase 4 | def setup 5 | @assertable = Object.new 6 | @assertable.send(:extend, Test::Right::Assertions) 7 | end 8 | 9 | def test_assert 10 | assert_raises Test::Right::AssertionFailedError do 11 | x = Object.new 12 | x.send(:extend, Test::Right::Assertions) 13 | x.assert(0.1) { 14 | false 15 | } 16 | end 17 | 18 | assert_nothing_raised do 19 | x = Object.new 20 | x.send(:extend, Test::Right::Assertions) 21 | x.assert { 22 | true 23 | } 24 | end 25 | 26 | assert_raises ArgumentError do 27 | @assertable.assert true 28 | end 29 | end 30 | 31 | def test_ignores_missing_widget 32 | raised = false 33 | @assertable.assert { 34 | if raised 35 | true 36 | else 37 | raised = true 38 | raise Test::Right::WidgetNotPresentError 39 | end 40 | } 41 | end 42 | 43 | def test_spinassert 44 | assert_nothing_raised do 45 | x = Object.new 46 | y = false 47 | x.send(:extend, Test::Right::Assertions) 48 | t = Thread.new do 49 | sleep(1) 50 | y = true 51 | end 52 | x.assert { 53 | y 54 | } 55 | end 56 | 57 | assert_raises Test::Right::AssertionFailedError do 58 | x = Object.new 59 | y = false 60 | x.send(:extend, Test::Right::Assertions) 61 | x.assert(1) { 62 | y 63 | } 64 | end 65 | end 66 | 67 | def test_assert_equal 68 | assert_nothing_raised do 69 | @assertable.assert_equal 1, 1 70 | end 71 | assert_raises Test::Right::AssertionFailedError do 72 | @assertable.assert_equal 1, 2 73 | end 74 | end 75 | 76 | def test_assert_equal_on_value 77 | x = 1 78 | v = Test::Right::Value.new do 79 | x 80 | end 81 | 82 | Thread.new do 83 | sleep 0.5 84 | x = 2 85 | end 86 | 87 | assert_nothing_raised do 88 | @assertable.assert_equal 1, v 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /test/test_feature.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | class TestFeature < Test::Unit::TestCase 4 | def setup 5 | eval <<-CLASSDEFS 6 | class FooWidget < Test::Right::Widget 7 | def bar 8 | end 9 | end 10 | CLASSDEFS 11 | @driver = MockDriver.new 12 | end 13 | def teardown 14 | Test::Right::Feature.wipe! 15 | Test::Right::Widget.wipe! 16 | end 17 | 18 | def test_with 19 | eval <<-CLASSDEFS 20 | class FeatureThatUsesWith < Test::Right::Feature 21 | def use_with 22 | with FooWidget do |w| 23 | w.bar 24 | end 25 | end 26 | end 27 | CLASSDEFS 28 | 29 | widget = mock() 30 | widget.expects(:exists?).at_least_once.returns(true) 31 | widget.expects(:bar) 32 | 33 | FooWidget.expects(:new).at_least_once.returns(widget) 34 | feature = FeatureThatUsesWith.new(@driver, nil) 35 | feature.use_with 36 | 37 | FooWidget.expects(:new).returns(widget) 38 | widget.expects(:exists?).at_least_once.returns(false) 39 | 40 | saved_timeout = Test::Right::Feature::WIDGET_TIMEOUT 41 | begin 42 | Test::Right::Feature.send(:const_set, 'WIDGET_TIMEOUT', 0.1) 43 | assert_raises Test::Right::WidgetNotPresentError do 44 | feature.use_with 45 | end 46 | ensure 47 | Test::Right::Feature.send(:const_set, 'WIDGET_TIMEOUT', saved_timeout) 48 | end 49 | end 50 | 51 | def test_wait_for 52 | eval <<-CLASSDEFS 53 | class FeatureThatUsesWaitFor < Test::Right::Feature 54 | def use_wait_for 55 | wait_for FooWidget 56 | end 57 | end 58 | CLASSDEFS 59 | 60 | widget = mock() 61 | widget.expects(:exists?).at_least_once.returns(true) 62 | 63 | FooWidget.expects(:new).at_least_once.returns(widget) 64 | feature = FeatureThatUsesWaitFor.new(@driver, nil) 65 | feature.use_wait_for 66 | end 67 | 68 | def test_data 69 | $data_value = nil 70 | 71 | eval <<-CLASSDEFS 72 | class FeatureThatUsesData < Test::Right::Feature 73 | def use_data 74 | $data_value = data[:foo] 75 | end 76 | end 77 | CLASSDEFS 78 | 79 | data = {:foo => 'foo'} 80 | feature = FeatureThatUsesData.new(@driver, data) 81 | feature.use_data 82 | assert_equal 'foo', $data_value 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 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 do |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 74 | -------------------------------------------------------------------------------- /lib/test/right/runner.rb: -------------------------------------------------------------------------------- 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 | @quit_on_fail = config[:quit_on_fail].nil? ? true : config[:quit_on_fail] 18 | end 19 | 20 | def run 21 | @batch = @pool.new_batch 22 | 23 | @features.sort_by{|x| rand(10000)}.all? do |feature| 24 | run_feature(feature) 25 | end 26 | 27 | @batch.wait_until_done 28 | 29 | process_results 30 | end 31 | 32 | def process_results 33 | failed = false 34 | until @result_queue.empty? 35 | feature, method, result = @result_queue.pop 36 | if result.is_a? Exception 37 | failed = true 38 | end 39 | @results[feature] ||= {} 40 | @results[feature][method] = result 41 | end 42 | return !failed 43 | end 44 | 45 | def run_feature(feature) 46 | methods = feature.instance_methods 47 | methods.sort_by{|x| rand(10000)}.each do |method_name| 48 | if method_name =~ /^test_/ 49 | @batch << Proc.new do 50 | begin 51 | method = method_name.to_sym 52 | run_test(feature, method_name.to_sym) 53 | 54 | @result_queue << [feature, method, true] 55 | rescue => e 56 | @result_queue << [feature, method, e] 57 | end 58 | end 59 | end 60 | end 61 | end 62 | 63 | def run_test(feature, method) 64 | if $MOCK_DRIVER 65 | driver = MockDriver.new(@config) 66 | else 67 | driver = BrowserDriver.new(@config) 68 | end 69 | 70 | data = DataFactory.new(@data_template) 71 | 72 | success = true 73 | begin 74 | target = feature.new(driver, data) 75 | if target.respond_to? :setup 76 | target.setup 77 | end 78 | target.send(method) 79 | rescue Exception => ex 80 | success = false 81 | raise ex 82 | ensure 83 | # Run any teardown logic if it exists 84 | begin 85 | if target and target.respond_to? :teardown 86 | target.teardown 87 | end 88 | rescue Exception => ex 89 | success = false 90 | raise ex 91 | ensure 92 | driver.quit if success || @quit_on_fail 93 | end 94 | end 95 | end 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /test/test_runner.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | class TestRunner < Test::Unit::TestCase 4 | def setup 5 | eval <<-CLASSDEFS 6 | class PassingFeature < Test::Right::Feature 7 | def test_pass 8 | end 9 | end 10 | 11 | class FailingFeature < Test::Right::Feature 12 | def test_fail 13 | raise "FAIL" 14 | end 15 | end 16 | 17 | class SimpleWidget < Test::Right::Widget 18 | field :foo, :id => 'foo' 19 | def an_action 20 | end 21 | end 22 | 23 | class TwoWordWidget < Test::Right::Widget 24 | def an_action 25 | end 26 | end 27 | CLASSDEFS 28 | end 29 | 30 | def teardown 31 | Test::Right::Widget.wipe! 32 | Test::Right::Feature.wipe! 33 | end 34 | 35 | def test_runs_nothing_on_nothing 36 | runner = Test::Right::Runner.new(Test::Right::Config.new, [], []) 37 | assert runner.run 38 | end 39 | 40 | def test_passes 41 | runner = Test::Right::Runner.new(Test::Right::Config.new, [], [PassingFeature]) 42 | 43 | assert runner.run, "Runner should have passed: #{runner.results}" 44 | assert runner.results.include?(PassingFeature), "Didn't run Google Search" 45 | end 46 | 47 | def test_fails 48 | runner = Test::Right::Runner.new(Test::Right::Config.new, [], [FailingFeature]) 49 | 50 | assert !runner.run, "Runner should have failed" 51 | assert runner.results[FailingFeature][:test_fail] != true 52 | end 53 | 54 | def test_random_test_order 55 | $test_sequence = [] 56 | eval <<-FEATURES 57 | class FeatureA < Test::Right::Feature 58 | def test_aa 59 | $test_sequence << :aa 60 | end 61 | def test_ab 62 | $test_sequence << :ab 63 | end 64 | def test_ac 65 | $test_sequence << :ac 66 | end 67 | end 68 | 69 | class FeatureB < Test::Right::Feature 70 | def test_ba 71 | $test_sequence << :ba 72 | end 73 | def test_bb 74 | $test_sequence << :bb 75 | end 76 | def test_bc 77 | $test_sequence << :bc 78 | end 79 | end 80 | FEATURES 81 | 82 | runner = Test::Right::Runner.new(Test::Right::Config.new, [], [FeatureA, FeatureB]) 83 | assert runner.run, runner.results.inspect 84 | first_sequence = Array.new($test_sequence) 85 | 86 | $test_sequence = [] 87 | assert runner.run, runner.results.inspect 88 | second_sequence = Array.new($test_sequence) 89 | 90 | assert_not_equal first_sequence, second_sequence 91 | end 92 | 93 | def test_setup 94 | eval <<-CLASSDEFS 95 | class FeatureWithSetup < Test::Right::Feature 96 | def setup 97 | @foo = 1 98 | end 99 | 100 | def test_foo 101 | raise "Foo not defined" if @foo.nil? 102 | end 103 | end 104 | CLASSDEFS 105 | runner = Test::Right::Runner.new(Test::Right::Config.new, [], [FeatureWithSetup]) 106 | assert runner.run 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /test/test_cli.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | require 'tmpdir' 3 | 4 | class TestCLI < Test::Unit::TestCase 5 | include TestRightTestingUtils 6 | 7 | def test_fails_with_no_widgets_dir 8 | assert_raises Test::Right::ConfigurationError do 9 | cli = Test::Right::CLI.new 10 | cli.load_widgets 11 | end 12 | end 13 | 14 | def test_finds_widgets 15 | in_new_dir do 16 | make_widget 17 | 18 | cli = Test::Right::CLI.new 19 | cli.load_widgets 20 | 21 | assert !cli.widgets.empty?, "No widgets loaded" 22 | end 23 | end 24 | 25 | def test_avoids_non_widgets 26 | in_new_dir do 27 | make_widget("foo.rb.zzz") 28 | 29 | cli = Test::Right::CLI.new 30 | cli.load_widgets 31 | 32 | assert cli.widgets.empty?, "Loaded something that's not a widget: #{cli.widgets}" 33 | end 34 | end 35 | 36 | def test_finds_features 37 | in_new_dir do 38 | make_feature 39 | 40 | cli = Test::Right::CLI.new 41 | cli.load_features 42 | 43 | assert !cli.features.empty?, "No features loaded" 44 | end 45 | end 46 | 47 | def test_start 48 | in_new_dir do 49 | make_widget 50 | make_feature 51 | 52 | cli = Test::Right::CLI.new 53 | cli.start([]) 54 | assert true # Start didn't cause any errors 55 | end 56 | end 57 | 58 | def test_generate 59 | in_new_dir do 60 | cli = Test::Right::CLI.new 61 | cli.start(["install"]) 62 | assert File.exists? "test/right" 63 | end 64 | end 65 | 66 | def test_load_config 67 | in_new_dir do 68 | make_config 69 | 70 | cli = Test::Right::CLI.new 71 | cli.load_config 72 | assert_equal "http://TESTING", cli.config[:base_url] 73 | end 74 | end 75 | 76 | def test_finds_testright 77 | in_new_dir do 78 | Dir.mkdir("test") 79 | Dir.mkdir("test/right") 80 | Dir.chdir("test/right") do 81 | make_widget 82 | make_feature 83 | end 84 | cli = Test::Right::CLI.new 85 | assert_nothing_raised do 86 | cli.start([]) 87 | end 88 | end 89 | end 90 | 91 | def test_setup 92 | in_new_dir do 93 | File.open "setup.rb", "wb" do |f| 94 | f.print <<-EOF 95 | #!/usr/bin/env ruby 96 | File.open "foo.tmp", "wb" do |f| 97 | f.print "foo" 98 | end 99 | EOF 100 | 101 | f.chmod(0755) 102 | end 103 | make_widget 104 | make_feature 105 | 106 | cli = Test::Right::CLI.new 107 | cli.start([]) 108 | assert File.exists?("foo.tmp"), "CLI didn't run setup.rb" 109 | end 110 | end 111 | 112 | private 113 | 114 | def make_config 115 | File.open "config.yml", "wb" do |f| 116 | f.print <<-CONFIG 117 | base_url: http://TESTING 118 | CONFIG 119 | end 120 | end 121 | 122 | def make_widget(filename="foo_widget.rb") 123 | Dir.mkdir("widgets") 124 | 125 | File.open "widgets/#{filename}", 'wb' do |f| 126 | f.print <<-WIDGET 127 | class FooWidget < Test::Right::Widget 128 | end 129 | WIDGET 130 | end 131 | end 132 | 133 | def make_feature 134 | Dir.mkdir("features") 135 | 136 | File.open "features/login.rb", 'wb' do |f| 137 | f.print <<-WIDGET 138 | class LoginFeature < Test::Right::Feature 139 | end 140 | WIDGET 141 | end 142 | end 143 | end 144 | -------------------------------------------------------------------------------- /lib/test/right/cli.rb: -------------------------------------------------------------------------------- 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(argument_list) 15 | end 16 | end 17 | 18 | def load_and_run_tests(features_to_run) 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 | run_features = features 34 | run_features.select! { |f| features_to_run.include?(f.to_s) } if features_to_run.length > 0 35 | 36 | puts "Running #{run_features.size} feature(s)" 37 | runner = Runner.new(config, widgets, run_features) 38 | if runner.run 39 | puts "Passed!" 40 | else 41 | at_exit { 42 | exit(1) 43 | } 44 | puts "Failed:" 45 | runner.results.each do |feature, feature_result| 46 | puts " #{feature}" 47 | feature_result.each do |method, result| 48 | if result.is_a? Exception 49 | if result.is_a? WidgetActionNotImplemented 50 | puts " #{method} => #{result}" 51 | else 52 | puts " #{method} => #{result.class} - #{result}" 53 | result.backtrace.each do |trace| 54 | puts " #{trace}" 55 | end 56 | end 57 | else 58 | puts " #{method} => #{result}" 59 | end 60 | end 61 | end 62 | end 63 | end 64 | 65 | def run_setup 66 | if File.exists? "setup.rb" 67 | unless system("./setup.rb") 68 | raise ConfigurationError, "Setup failed" 69 | end 70 | elsif File.exists? "test/right/setup.rb" 71 | unless system("test/right/setup.rb") 72 | raise ConfigurationError, "Setup failed" 73 | end 74 | end 75 | end 76 | 77 | def load_config 78 | options = {} 79 | if File.exists? "config.yml" 80 | options = YAML.load(open("config.yml")) 81 | end 82 | @config = Config.new(options) 83 | end 84 | 85 | def config 86 | @config 87 | end 88 | 89 | def load_widgets 90 | raise ConfigurationError, "no widgets/ directory" unless File.directory? "widgets" 91 | load_ruby_in_dir("widgets") 92 | end 93 | 94 | def widgets 95 | Widget.subclasses || [] 96 | end 97 | 98 | def load_features 99 | raise ConfigurationError, "no features/ directory" unless File.directory? "features" 100 | load_ruby_in_dir("features") 101 | end 102 | 103 | def features 104 | Feature.subclasses || [] 105 | end 106 | 107 | private 108 | 109 | def load_ruby_in_dir(dirname) 110 | Dir.foreach dirname do |filename| 111 | unless [".", ".."].include? filename 112 | if filename =~ RUBY_FILE 113 | load "#{dirname}/#{filename}" 114 | end 115 | end 116 | end 117 | end 118 | end 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /lib/test/right/widget.rb: -------------------------------------------------------------------------------- 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(absolute_url = nil) 84 | if absolute_url 85 | @driver.get(absolute_url, :relative => false) 86 | else 87 | @driver.get(self.class.location, :relative => true) 88 | end 89 | end 90 | 91 | def exists? 92 | begin 93 | root 94 | return true 95 | rescue WidgetNotPresentError 96 | return false 97 | end 98 | end 99 | 100 | def validate!(timeout=15) 101 | timeout = Time.now + timeout 102 | while Time.now < timeout 103 | if self.exists? 104 | return 105 | end 106 | end 107 | raise WidgetNotPresentError 108 | end 109 | 110 | def method_missing(name, *args) 111 | raise WidgetActionNotImplemented, "#{self.class.to_s}##{name.to_s} not implemented" 112 | end 113 | 114 | private 115 | 116 | def fill_in(selector_name, value) 117 | get_element(selector_name).send_keys(value) 118 | end 119 | 120 | def clear(selector_name) 121 | get_element(selector_name).clear 122 | end 123 | 124 | def click(selector_name) 125 | get_element(selector_name).click 126 | end 127 | 128 | def select(selector_name) 129 | get_element(selector_name).select 130 | end 131 | 132 | def navigate_to(url) 133 | @driver.get(url) 134 | end 135 | 136 | def wait_for_element_present(selector_name, timeout=10) 137 | endtime = Time.now + timeout 138 | while Time.now < endtime 139 | begin 140 | get_element(selector_name) 141 | return 142 | rescue ElementNotFoundError 143 | rescue WidgetNotPresentError 144 | end 145 | end 146 | raise ElementNotFoundError 147 | end 148 | 149 | def wait_for_element_not_present(selector_name, timeout=10) 150 | endtime = Time.now + timeout 151 | while Time.now < endtime 152 | begin 153 | get_element(selector_name) 154 | rescue ElementNotFoundError 155 | return true 156 | rescue WidgetNotPresentError 157 | return true 158 | end 159 | end 160 | raise Error, "Element didn't disappear" 161 | end 162 | 163 | def get_element(selector_name) 164 | selector = find_selector(selector_name) 165 | if selector_name == :root 166 | begin 167 | return @driver.find_element(*self.class.root) 168 | rescue Selenium::WebDriver::Error::NoSuchElementError => e 169 | raise ElementNotFoundError, e.message 170 | end 171 | end 172 | 173 | target = nil 174 | while target.nil? 175 | begin 176 | target = root.find_element(*selector) 177 | rescue Selenium::WebDriver::Error::NoSuchElementError => e 178 | raise ElementNotFoundError, "Element #{selector_name} not found on #{self.class.name} using #{selector.inspect}" 179 | rescue Selenium::WebDriver::Error::ObsoleteElementError 180 | # ignore 181 | end 182 | end 183 | return target 184 | end 185 | 186 | def get_elements(selector_name) 187 | selector = find_selector(selector_name) 188 | begin 189 | element = root.find_elements(*selector) 190 | rescue Selenium::WebDriver::Error::NoSuchElementError => e 191 | raise ElementNotFoundError, e.message 192 | end 193 | end 194 | 195 | def find_selector(selector_name) 196 | if !self.class.selector(selector_name) 197 | raise SelectorNotFoundError, "Selector \"#{selector_name}\" for Widget \"#{self.class}\" not found" 198 | end 199 | 200 | selector = self.class.selector(selector_name) 201 | return [selector.keys.first, selector.values.first] 202 | end 203 | 204 | def root 205 | if self.class.root.nil? && self.class.name_elements.nil? 206 | return @driver 207 | elsif self.class.root && self.class.name_elements.nil? 208 | begin 209 | return @driver.find_element(*self.class.root) 210 | rescue Selenium::WebDriver::Error::NoSuchElementError => e 211 | raise WidgetNotPresentError, "#{self.class.name} not found on page" 212 | end 213 | elsif self.class.root && self.class.name_elements 214 | all_instances = @driver.find_elements(*self.class.root) 215 | target = all_instances.find do |root_element| 216 | self.class.name_elements.any? do |property, how, what| 217 | begin 218 | element = root_element.find_element(how, what) 219 | element_name = nil 220 | if :value == property 221 | element_name = element.attribute('value') 222 | else 223 | element_name = element.send(property) 224 | end 225 | @name == element_name 226 | rescue Selenium::WebDriver::Error::ObsoleteElementError 227 | false 228 | rescue Selenium::WebDriver::Error::NoSuchElementError 229 | false 230 | end 231 | end 232 | end 233 | if target.nil? 234 | raise WidgetNotPresentError, "#{self.class.name} with name \"#{name}\" not found" 235 | end 236 | 237 | return target 238 | else 239 | raise IAmConfusedError 240 | end 241 | end 242 | end 243 | end 244 | end 245 | -------------------------------------------------------------------------------- /test/test_widget.rb: -------------------------------------------------------------------------------- 1 | require 'helper' 2 | 3 | class TestWidget < Test::Unit::TestCase 4 | def setup 5 | eval <<-CLASSDEFS 6 | class SimpleWidget < Test::Right::Widget 7 | end 8 | 9 | class RootedWidget < Test::Right::Widget 10 | rooted_at :id => 'root' 11 | element :foo, :id => 'foo' 12 | end 13 | 14 | class WidgetThatDoesThings < Test::Right::Widget 15 | field :foo, :id => 'foo' 16 | element :notfoo, :id => 'notfoo' 17 | element :frobbable, :id => 'f' 18 | element :foos, :css => '.foo' 19 | 20 | def fill 21 | fill_in :foo, "bar" 22 | end 23 | 24 | def clickit 25 | click :foo 26 | end 27 | 28 | def selectit 29 | select :foo 30 | end 31 | 32 | def clearit 33 | clear :foo 34 | end 35 | 36 | def go_to_google 37 | navigate_to "http://www.google.com" 38 | end 39 | 40 | def frob 41 | get_element(:frobbable).frob 42 | end 43 | 44 | def frob_nonexistant 45 | get_element(:not_frobbable).frob 46 | end 47 | 48 | def get_foos 49 | get_elements(:foos) 50 | end 51 | end 52 | CLASSDEFS 53 | 54 | @driver = MockDriver.new 55 | @widget = WidgetThatDoesThings.new(@driver) 56 | end 57 | 58 | def teardown 59 | self.class.send(:remove_const, :WidgetThatDoesThings) 60 | 61 | Test::Right::Widget.wipe! 62 | Test::Right::Feature.wipe! 63 | end 64 | 65 | def test_raises_when_action_not_implemented 66 | assert_raises Test::Right::WidgetActionNotImplemented do 67 | SimpleWidget.new(nil).fail_action 68 | end 69 | end 70 | 71 | def test_raises_helpfully 72 | begin 73 | SimpleWidget.new(nil).fail_action 74 | raise "Shouldn't have gotten to this point" 75 | rescue Test::Right::WidgetActionNotImplemented => e 76 | assert e.message.include?("SimpleWidget"), e.message 77 | assert e.message.include?("fail_action"), e.message 78 | end 79 | end 80 | 81 | def test_fill_in 82 | element = MockElement.new 83 | 84 | @driver.expects(:find_element).with(:id, 'foo').returns(element) 85 | element.expects(:send_keys).with('bar') 86 | 87 | @widget.fill 88 | end 89 | 90 | def test_click 91 | element = MockElement.new 92 | 93 | @driver.expects(:find_element).with(:id, 'foo').returns(element) 94 | element.expects(:click) 95 | 96 | @widget.clickit 97 | end 98 | 99 | def test_select 100 | element = MockElement.new 101 | 102 | @driver.expects(:find_element).with(:id, 'foo').returns(element) 103 | element.expects(:select) 104 | 105 | @widget.selectit 106 | end 107 | 108 | def test_clear 109 | element = MockElement.new 110 | 111 | @driver.expects(:find_element).with(:id, 'foo').returns(element) 112 | element.expects(:clear) 113 | 114 | @widget.clearit 115 | end 116 | 117 | 118 | def test_navigate_to 119 | @driver.expects(:get).with("http://www.google.com") 120 | 121 | @widget.go_to_google 122 | end 123 | 124 | def test_lives_at 125 | @driver.expects(:get).with("/foo/bar", :relative => true) 126 | 127 | @widget.visit 128 | end 129 | 130 | def test_get_element 131 | mock_element = mock() 132 | mock_element.expects(:frob) 133 | @driver.expects(:find_element).with(:id, 'f').returns(mock_element) 134 | assert_nothing_raised do 135 | @widget.frob 136 | end 137 | 138 | assert_raises Test::Right::SelectorNotFoundError do 139 | @widget.frob_nonexistant 140 | end 141 | 142 | @driver.expects(:find_element).with(:id, 'foo').raises(Selenium::WebDriver::Error::NoSuchElementError) 143 | assert_raises Test::Right::ElementNotFoundError do 144 | @widget.instance_eval do 145 | get_element :foo 146 | end 147 | end 148 | 149 | 150 | mock_foo = mock() 151 | @driver.expects(:find_element).with(:id, 'root').returns(mock_element) 152 | mock_element.expects(:find_element).with(:id, 'foo').returns(mock_foo) 153 | widget = RootedWidget.new(@driver) 154 | assert_equal mock_foo, widget.send(:get_element, :foo) 155 | end 156 | 157 | def test_get_elements 158 | @driver.expects(:find_elements).with(:css, '.foo') 159 | assert_nothing_raised do 160 | @widget.get_foos 161 | end 162 | end 163 | 164 | def test_lives_at 165 | WidgetThatDoesThings.instance_eval do 166 | lives_at "/foo/bar" 167 | end 168 | 169 | assert_equal "/foo/bar", WidgetThatDoesThings.instance_eval{@location} 170 | end 171 | 172 | def test_rooted_at 173 | WidgetThatDoesThings.instance_eval do 174 | rooted_at :css => '.widget' 175 | 176 | def frob 177 | click :thingie 178 | end 179 | end 180 | 181 | target = mock() 182 | @driver.expects(:find_element).with(:css, '.widget').returns(target) 183 | target.expects(:find_element).with(:id, 'f').returns(target) 184 | target.expects(:frob) 185 | 186 | @widget.frob 187 | end 188 | 189 | def test_action 190 | WidgetThatDoesThings.instance_eval do 191 | rooted_at :css => '.widget' 192 | 193 | action :frob do 194 | click :foo 195 | end 196 | 197 | action :withargs do |arg| 198 | $got_arg = arg 199 | end 200 | end 201 | 202 | target = mock() 203 | @driver.expects(:find_element).at_least_once.with(:css, '.widget').returns(target) 204 | target.expects(:find_element).with(:id, 'foo').returns(target) 205 | target.expects(:click) 206 | 207 | @widget.frob 208 | 209 | $got_arg = false 210 | @widget.withargs true 211 | assert $got_arg 212 | end 213 | 214 | def test_property 215 | WidgetThatDoesThings.instance_eval do 216 | rooted_at :css => '.widget' 217 | 218 | property :fooness do 219 | get_element(:foo).text 220 | end 221 | end 222 | 223 | target = mock() 224 | @driver.expects(:find_element).at_least_once.with(:css, '.widget').returns(target) 225 | target.expects(:find_element).at_least_once.with(:id, 'foo').returns(target) 226 | target.expects(:text).returns("I am text") 227 | 228 | fooness = @widget.fooness 229 | assertable = Object.new 230 | assertable.send(:extend, Test::Right::Assertions) 231 | assertable.assert_equal "I am text", fooness 232 | 233 | target.expects(:text).returns("I am more text") 234 | assertable.assert_equal "I am more text", fooness 235 | end 236 | 237 | def test_validated_by_presence_of 238 | WidgetThatDoesThings.instance_eval do 239 | rooted_at :id => 'foo' 240 | 241 | validated_by_presence_of :root 242 | end 243 | 244 | @driver.expects(:find_element).with(:id, 'foo') 245 | assert @widget.exists? 246 | end 247 | 248 | def test_exists 249 | WidgetThatDoesThings.instance_eval do 250 | rooted_at :id => 'foo' 251 | 252 | validated_by_presence_of :root 253 | end 254 | 255 | @widget.expects(:root).raises(Test::Right::WidgetNotPresentError) 256 | assert !@widget.exists? 257 | end 258 | 259 | def test_named_by 260 | WidgetThatDoesThings.instance_eval do 261 | rooted_at :css => '.widget' 262 | named_by :css => '.name' 263 | named_by :value, :css => '.name' 264 | end 265 | 266 | assert_equal [[:text, :css, '.name'], [:value, :css, '.name']], WidgetThatDoesThings.name_elements 267 | end 268 | 269 | def test_indexing_to_find_by_name 270 | WidgetThatDoesThings.instance_eval do 271 | rooted_at :css => '.widget' 272 | named_by :css => '.name' 273 | end 274 | 275 | w = WidgetThatDoesThings['foo'].new(@driver) 276 | assert w.is_a? WidgetThatDoesThings 277 | assert_equal "foo", w.instance_eval{@name} 278 | 279 | target = mock() 280 | @driver.expects(:find_elements).with(:css, '.widget').returns([target]) 281 | name_element = mock() 282 | name_element.expects(:text).returns("foo") 283 | target.expects(:find_element).with(:css, '.name').returns(name_element) 284 | 285 | assert w.exists? 286 | end 287 | 288 | def test_element 289 | WidgetThatDoesThings.instance_eval do 290 | element :foo, :id => "foo" 291 | end 292 | assert_equal([:id, 'foo'], @widget.instance_eval{find_selector(:foo)}) 293 | end 294 | 295 | def test_wait_for_element_present 296 | @driver.expects(:find_element).with(:id, 'foo').returns(mock()) 297 | w = WidgetThatDoesThings.new(@driver) 298 | w.instance_eval do 299 | wait_for_element_present(:foo) 300 | end 301 | end 302 | 303 | def test_wait_for_element_not_present 304 | @driver.expects(:find_element).with(:id, 'notfoo').raises(Selenium::WebDriver::Error::NoSuchElementError) 305 | w = WidgetThatDoesThings.new(@driver) 306 | w.instance_eval do 307 | wait_for_element_not_present(:notfoo) 308 | end 309 | end 310 | 311 | %w.each do |thingtype| 312 | define_method "test_"+thingtype do 313 | WidgetThatDoesThings.instance_eval do 314 | self.send(thingtype.to_sym, :foo, :id => "foo") 315 | end 316 | assert !@widget.instance_eval{find_selector(:foo)}.empty?, "No selector was added to widget" 317 | end 318 | end 319 | 320 | end 321 | --------------------------------------------------------------------------------