├── .gitignore ├── README.md ├── conf └── selenium.yml.default ├── lib ├── base_page.rb ├── config.rb ├── home_page.rb ├── login_page.rb ├── providers │ ├── csv.rb │ ├── mysql.rb │ ├── static.rb │ └── usernamepassword.csv └── selenium_connection.rb └── spec ├── login_spec.rb ├── soft_assert_spec.rb ├── spec_helper.rb └── support └── matchers └── have_errors.rb /.gitignore: -------------------------------------------------------------------------------- 1 | conf/*.yml 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | (Selenium) Page Objects in Ruby (RSpec) 2 | ======================================= 3 | 4 | Page Objects 101 5 | ---------------- 6 | 7 | 'Page Objects' is a pattern for creating Selenium scripts that makes heavy use of OO principles to enable code reuse and improve maintenance. Rather than having test methods that are a series of Se commands that are sent to the server, your scripts become a series of interactions with objects that represent a page (or part of one) -- thus the name. 8 | 9 | Without Page Objects 10 | def test_example 11 | @selenium.open "/" 12 | @selenium.click "css=div.account_mast a:first", :wait_for => :page 13 | @selenium.type "username", "monkey" 14 | @selenium.type "password", "buttress" 15 | @selenium.click "submit", :wait_for => :page 16 | @selenium.get_text("css=div.error > p").should == "Incorrect username or password." 17 | end 18 | 19 | With Page Objects 20 | describe "Login" do 21 | context "invalid password" do 22 | it "prints error message" do 23 | @home = PageObjects::HomePage.new 24 | @login = @home.goto_login_form 25 | @login.username = "foo" 26 | @login.password = "bar" 27 | @login.login 28 | @login.error_message.should == "Incorrect username or password." 29 | end 30 | end 31 | 32 | As you can see, not only is the script that uses POs [slightly] more human readable, but it is much more maintainable since it really does separate the page interface from the implementation so that _when_ something on the page changes only the POs themselves need to change and not ten billion scripts. 33 | 34 | Anatomy of a Ruby Page Object 35 | ----------------------------- 36 | 37 | Page Objects have two parts 38 | * Elements 39 | * Actions 40 | 41 | _Elements_ 42 | 43 | Elements in Ruby Page Objects are done by overriding the Page's getters and setters to interact with the browser. Here is how the above example types the password into the login form and how it retrieves the resulting error message 44 | 45 | def password=(password) 46 | @browser.type LOCATORS["password"], password 47 | end 48 | 49 | def error_message 50 | @browser.get_text LOCATORS["error_message"] 51 | end 52 | 53 | _Actions_ 54 | 55 | Actions are the part of the page that does something, like submitting a form, or clicking a link. These are implemented as methods on the PO, for example, submitting the login form is implemented as such. 56 | 57 | def login 58 | @browser.click LOCATORS["submit_button"], :wait_for => :page 59 | end 60 | 61 | so you can call it as thus. 62 | 63 | @login.login 64 | 65 | One decision you have to make is whether to have actions that result in changing pages return the PO or not. I'm currently leaning towards that being a good thing. 66 | 67 | Locators 68 | -------- 69 | 70 | One of things POs help you with is isolating your locators since they are tucked away in a class rather than spread throughout your scripts. I _highly_ suggest that you go all the way and move your locators from in the actual Se calls to a constant in the class. 71 | 72 | LOCATORS = { 73 | "username" => "username", 74 | "password" => "password", 75 | "submit_button" => "submit", 76 | "error_message" => "css=div.error p:nth(0)" 77 | } 78 | 79 | Now your locators truly are _change in one spot and fix all the broken-ness_. DRY code is good code. It is a code smell to rethink how you are slicing the page into object if you think you need to have the same locator in multiple classes. 80 | 81 | Sharing the server connection 82 | ----------------------------- 83 | 84 | It has been pointed out to me that what I have done to share the established connection/session to the Se server is borderline evil, but I understand it which trumps evil in my books. In order to make sure we can send / receive from the Se server from any PO, I make the connection to it a Singleton which gets set as a class a in the base PO. Most of the time your actual scripts won't need access to the actual browser connection (that's kinda the point of POs). 85 | 86 | module PageObjects 87 | class BasePage 88 | def initialize 89 | @browser = SeleniumHelpers::SeleniumConnection.instance.connection 90 | end 91 | end 92 | end 93 | 94 | Apparently what I wanted was to use Dependency Injection but I only really understood it last weekend so this works -- if slightly evil. 95 | 96 | Before / After 97 | -------------- 98 | 99 | RSpec is an xUnit style framework which means it has methods that are called before and after each test method. Because we know that we want to start a browser before each run and close it afterwards we specify what we want via RSpec.configure. 100 | 101 | RSpec.configure { |c| 102 | c.before(:each) { 103 | @browser = SeleniumHelpers::SeleniumConnection.instance.connection 104 | @browser.start_new_browser_session 105 | @browser.window_maximize 106 | @browser.open("/") 107 | } 108 | 109 | c.after(:each) { 110 | @browser.close_current_browser_session 111 | } 112 | } 113 | 114 | There is also :suite and :all available if there were things you wanted to do at those points as well. Like, say, reading config files. 115 | 116 | What is really cool about before/after is they can be nested inside layers of describe and context blocks and will execute from furthest out in. 117 | 118 | describe "foo" do 119 | before(:each) do 120 | p 'a' 121 | end 122 | describe "bar" do 123 | before 'a' do 124 | p 'a' 125 | end 126 | 127 | Config Files 128 | ------------ 129 | 130 | The default config format for Ruby (thanks to the Rails kids) is YAML so we'll drive the config from there. So as alluded to above, the before(:all) is a nice way to load things. 131 | 132 | c.before(:all) { 133 | @config = SeleniumHelpers::Configuration.instance.config 134 | } 135 | 136 | But wait, that's another singleton. Will the madness ever end? The reason for this is that Page Objects, scripts, helpers and oracles all need to be able to access the information tucked away in these configs. The first thing to be driven from a file is the initial server connection. 137 | 138 | def initialize 139 | @connection = Selenium::Client::Driver.new \ 140 | :host => SeleniumHelpers::Configuration.instance.config['selenium']['host'], 141 | :port => SeleniumHelpers::Configuration.instance.config['selenium']['port'], 142 | :browser => SeleniumHelpers::Configuration.instance.config['selenium']['browser'], 143 | :url => SeleniumHelpers::Configuration.instance.config['selenium']['base_url'], 144 | :timeout_in_second => SeleniumHelpers::Configuration.instance.config['selenium']['timeout'] 145 | end 146 | 147 | It's a lot to type, and pretty yucky looking, but you don't need to look at it very often. 148 | 149 | Also notice that selenium.yml is not committed but selenium.yml.default is. This is me blatantly borrowing good ideas the from the RoR kids. As you'll see with the CI integration, it allows for easy parallel, cross-browser script execution without the need to get Se-Grid involved. 150 | 151 | Tags 152 | ---- 153 | 154 | The selenium-webdriver gem is darn near idiomatically perfect for dealing with synchronization but I had a hard time recommending it to people as none of the Ruby test runners were able to handle test discovery via tags. But apparently RSpec grew this ability last July and the runner caught up in November. 155 | 156 | Tags solve the venn diagram problem of where to put a script. Selenium scripts cross boundaries that 'unit' scripts don't have to worry about so we often find ourselves asking: Does it go with the admin persona scripts? Or the login ones? Or the smoke tests? Tags lets us not worry about this problem and apply the desired metadata to our scripts. 157 | 158 | context "correct password", :depth => 'shallow', :login => true do 159 | it "goes to account page" 160 | end 161 | 162 | In this example there are two tags in play. The first is for the depth of the script. I tag everything as either 'deep' or 'shallow'. Shallow scripts are ones that are often called 'smoke' or 'sanity' scripts that but I couldn't figure out a nice opposite value. _shallow_ scripts _must_ pass in order to declare a build testable. To run the 'shallow' test you use 163 | 164 | --tag depth:shallow 165 | 166 | The other tag really addresses the venn problem; I wouldn't worry about setting it to false if it isn't applicable -- just don't add it. 167 | 168 | --tag login 169 | 170 | And you can have multiple tags when calling the runner too in order to narrow down what you are looking for. 171 | 172 | --tag depth:shallow --tag login 173 | 174 | Synchronization 175 | --------------- 176 | 177 | One of the nice things about the Ruby drivers for Selenium is its idiomatically correct handling of synchronization using :wait_for after an event. Such as: 178 | 179 | se.click "a_locator", :wait_for => :page 180 | 181 | In general there are three different types os synchronization events. 182 | 183 | 1. Web 1.0 - these events are ones where there is a a page or window reload as a result of an action in the browser. 184 | :wait_for => :page 185 | :wait_for => :popup, :window => 'a window id' 186 | 187 | 2. Web 2.0 - with the rise of AJAX and related technologies we can no longer rely on the browser being reloaded. Now we need to be a bit more tricky about things looking at whether the content itself has changed. 188 | 189 | By default these have hooks for prototype; override this using :javascript_framework 190 | 191 | :wait_for => :ajax 192 | 193 | is the same as 194 | 195 | :wait_for => :ajax, :javascript_framework => :prototype 196 | 197 | :javascript_framework can also be set when you make the connection to the server so that you don't have to remember to type it every single time 198 | 199 | :wait_for => :ajax, :javascript_framework => :jquery 200 | :wait_for => :effects 201 | :wait_for => :effects, :javascript_framework => :jquery 202 | 203 | The rest of the Web 2.0 synchronization hooks deal with the page content directly. The first four are the ones most often used 204 | 205 | :wait_for => :element, :element => 'new_element_id' 206 | :wait_for => :no_element, :element => 'new_element_id' 207 | :wait_for => :visible, :element => 'a_locator' 208 | :wait_for => :not_visible, :element => 'a_locator' 209 | :wait_for => :text, :text => 'some text' 210 | :wait_for => :text, :text => /A Regexp/ 211 | :wait_for => :text, :element => 'a_locator', :text => 'some text' 212 | :wait_for => :text, :element => 'a_locator', :text => /A Regexp/ 213 | :wait_for => :no_text, :text => 'some text' 214 | :wait_for => :no_text, :text => /A Regexp/ 215 | :wait_for => :no_text, :element => 'a_locator', :text => 'some text' 216 | :wait_for => :no_text, :element => 'a_locator', :text => /A Regexp/ 217 | :wait_for => :value, :element => 'a_locator', :value => 'some value' 218 | :wait_for => :no_value, :element => 'a_locator', :value => 'some value' 219 | :wait_for => :visible, :element => 'a_locator' 220 | :wait_for => :not_visible, :element => 'a_locator' 221 | 222 | 3. Web 3.0 - Some sites have just a ridiculous amount of background services being checked, AJAX messages sent back and forth, use Comet events so things like :ajax don't ever end. For this you need to use a (Latch)[FINDME]. 223 | 224 | :wait_for => :condition, :javascript => 'latch condition' 225 | 226 | All :wait_for expressions can also have and explicit timeout (:timeout_in_seconds key). Otherwise the default driver timeout is used (30s). This value can also be set at server connection. 227 | 228 | Expectations 229 | ------------ 230 | 231 | RSpec doesn't use the word _assert_; instead they prefer _expectation_. There are two basic ways of setting up an expectation in RSpec 232 | 233 | * should 234 | * should_not 235 | 236 | Each of these will take either an RSpec _matcher_ or a Ruby expression. And through some tricky meta-programming, each of these is available on all objects. 237 | 238 | Ruby expressions that evaluate for should or should_not are pretty easy to grasp and use the standard comparison operators. The exception here is != which RSpec does not support. This means 239 | 240 | foo.should != 'bar' 241 | 242 | needs to be rewritten as 243 | 244 | foo.should_not == 'bar' 245 | 246 | There is deep Ruby internal reasons for this, but it also just reads nicer. What would be even nicer is to do away with the == altogether and use a built-in matcher such as 247 | 248 | * equal 249 | * include 250 | * respond_to 251 | * raise error 252 | 253 | Which would give us 254 | 255 | foo.should_not equal('bar') 256 | 257 | Creating Se scripts will result in a lot of _should_ and _should_not_ expectations and very few of the others. Remember, RSpec was designed as a code-level BDD framework first so its features reflect its heritage. 258 | 259 | Matcher Magic 260 | ------------- 261 | 262 | One thing I like about Ruby that I wish Python would copy is its notion of predicates which are methods whose name ends in ? and return True or False. If you have a matcher that starts with be_ it will call the predicate function that makes up the rest of the matcher. 263 | 264 | Imagine you had a 265 | 266 | class Person 267 | def admin? 268 | if self.role == :admin 269 | True 270 | else 271 | False 272 | end 273 | end 274 | end 275 | 276 | You could then do 277 | 278 | p.should_not be_admin 279 | 280 | The same magic happens with matchers that start with have_ for functions that begin with has_. 281 | 282 | Providers 283 | --------- 284 | 285 | Test 'data' should not be embedded in your script. Doing so means that you have to edit your script whenever the data changes. To solve this particular problem we can use a number of different 'providers' of information. 286 | 287 | 1. _Static_ - A Static provider is one which will return a data that is contained in its own class definition. It is somewhat akin to putting the data right in the script except that when it changes, it is only this data file that needs to be edited. 288 | 289 | @user = { 290 | "username" => "flying", 291 | "password" => "monkey" 292 | } 293 | 294 | 2. _CSV_ - The next step from the Static provider is to feed the information from a CSV file. This data can be as simple as usernames and password (like this example) or as complicated as the most efficient pair-wise paths from something like (Hexawise)[http://hexawise.com]. 295 | 296 | One useful thing to do with CSV data is to return a random row rather than a specific one. 297 | 298 | def random_row 299 | @csv_content[rand(csv_content.size)] 300 | end 301 | 302 | 3. _Database_ - A powerful way of driving your scripts is to use the information that is in your application already. In some cases you can use the native ORM (such as ActiveRecord) but other times you need to go directly at the database. 303 | 304 | def random_username_and_password 305 | res = @dbh.query("select username, password from provider order by rand() limit 1") 306 | res.fetch_row 307 | end 308 | 309 | Skipping Examples 310 | ----------------- 311 | 312 | Unlike some frameworks, like Nose for Python, there is no way in RSpec to 'skip' an example, you can however make it as Pending programatically. 313 | 314 | if Time.new.strftime("%A") != "Monday" 315 | pending("except its not Monday") 316 | end 317 | 318 | Continuous Integration 319 | ---------------------- 320 | 321 | I recommend that people use something like (Jenkins)[http://jenkins-ci.com] to run all their Se scripts, including ones that they might naturally us something like Se-Grid for. But using Jenkins you can.. 322 | * easily, and visually, see what the current status for all the environments is 323 | * integrate it into a Continuous Delivery process 324 | * execute a single environment without having to change anything in the scripts or configs 325 | * run environments you have machines for behind your firewall, and then other ones you can off load to the Sauce Labs OnDemand cloud 326 | 327 | CI integration is almost always accomplished by the mythical 'JUnit' xml which is implemented everywhere but not documented anywhere. In order to get RSpec to output this, you need to install the ci_reporter gem. Once you have it on the system you have a couple options though I prefer to include it on the commandline so it is there when I want (in the CI environment) and not when I don't (when I'm creating new scripts). 328 | 329 | --require GEM_PATH/lib/ci/reporter/rake/rspec_loader --format CI::Reporter::RSpec 330 | 331 | The reports that it produces will be in the specs/reports directory so you need to specify that dir in the CI job's config as the location. It is likely also a good idea to archive those as well. 332 | 333 | Soft Shoulds 334 | ------------ 335 | 336 | Users of Se-IDE are familiar with the notion of 'hard' asserts (assert*) and 'soft' asserts (verify*). RSpec has only the notion of should (and should_not) that will stop an example immediately on fail. And that makes sense in a pure RSpec world, but not so much the Se one so we do the standard kludge of catching ExpectationNotMetError and adding it to an Array then checking that the array is empty. 337 | 338 | begin 339 | @login.error_message.should == "Incofrrect username or password." 340 | rescue RSpec::Expectations::ExpectationNotMetError => verification_error 341 | @validation_errors << verification_error 342 | end 343 | 344 | is what a soft 'should' looks like in our scripts. And then the check of course looks like 345 | 346 | @validation_errors.should be_empty 347 | 348 | What would be be nice is if someone wrote a _might_ or _oughta_ which would do the script facing side cleaner. Someone other than me of course. :) 349 | 350 | TO-DO 351 | ----- 352 | * ondemand 353 | * fetch video 354 | * fetch logs 355 | * logging 356 | * ci integration 357 | * random data 358 | * custom matchers 359 | * custom exceptions -------------------------------------------------------------------------------- /conf/selenium.yml.default: -------------------------------------------------------------------------------- 1 | selenium: 2 | host: localhost 3 | port: 4444 4 | # yaml doesn't like the * so need the quotes 5 | browser: "*firefox" 6 | base_url: http://saucelabs.com 7 | timeout: 20 8 | 9 | mysql: 10 | host: localhost 11 | user: root 12 | password: "" 13 | database: userpassword 14 | 15 | saucelabs: 16 | ondemand: true 17 | credentials: 18 | username: your_username 19 | key: your_key 20 | server: 21 | host: ondemand.saucelabs.com 22 | port: 4444 23 | browser: 24 | os: windows 2003 25 | browser: iexplore 26 | version: 8 -------------------------------------------------------------------------------- /lib/base_page.rb: -------------------------------------------------------------------------------- 1 | require 'selenium_connection' 2 | 3 | module PageObjects 4 | class BasePage 5 | def initialize 6 | @browser = SeleniumHelpers::SeleniumConnection.instance.connection 7 | end 8 | end 9 | end -------------------------------------------------------------------------------- /lib/config.rb: -------------------------------------------------------------------------------- 1 | require 'singleton' 2 | 3 | require 'spec_helper' 4 | 5 | require 'rubygems' 6 | require 'selenium-webdriver' 7 | require "selenium/client" 8 | 9 | module SeleniumHelpers 10 | class Configuration 11 | include Singleton 12 | 13 | attr_accessor :config 14 | 15 | def initialize 16 | @config = YAML.load(File.read(File.join(File.dirname(__FILE__), '..', 'conf', 'selenium.yml'))) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/home_page.rb: -------------------------------------------------------------------------------- 1 | require 'base_page' 2 | require 'login_page' 3 | 4 | module PageObjects 5 | class HomePage < BasePage 6 | LOCATORS = { 7 | "login" => "css=div.account_mast a:first" 8 | } 9 | 10 | def initialize 11 | super 12 | end 13 | 14 | def goto_login_form 15 | @browser.click LOCATORS["login"], :wait_for => :page 16 | PageObjects::LoginPage.new 17 | end 18 | 19 | end 20 | end -------------------------------------------------------------------------------- /lib/login_page.rb: -------------------------------------------------------------------------------- 1 | require 'base_page' 2 | 3 | module PageObjects 4 | class LoginPage < BasePage 5 | LOCATORS = { 6 | "username" => "username", 7 | "password" => "password", 8 | "submit_button" => "submit", 9 | "error_message" => "css=div.error p:nth(0)" 10 | } 11 | 12 | def initialize 13 | super 14 | end 15 | 16 | # elements 17 | def username=(username) 18 | @browser.type LOCATORS["username"], username 19 | end 20 | 21 | def password=(password) 22 | @browser.type LOCATORS["password"], password 23 | end 24 | 25 | def error_message 26 | @browser.get_text LOCATORS["error_message"] 27 | end 28 | 29 | # actions 30 | def login 31 | @browser.click LOCATORS["submit_button"], :wait_for => :page 32 | end 33 | 34 | 35 | 36 | end 37 | end -------------------------------------------------------------------------------- /lib/providers/csv.rb: -------------------------------------------------------------------------------- 1 | module Providers 2 | module CSV 3 | class UsernamePassword 4 | attr_accessor :user, :csv_content 5 | 6 | def initialize 7 | require 'faster_csv' 8 | 9 | @user = Hash.new 10 | @csv_content = FasterCSV.read(File.join(File.dirname(__FILE__), 'usernamepassword.csv')) 11 | end 12 | 13 | def random_row 14 | @csv_content[rand(csv_content.size)] 15 | end 16 | end 17 | end 18 | end -------------------------------------------------------------------------------- /lib/providers/mysql.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'config')) 2 | include SeleniumHelpers 3 | 4 | module Providers 5 | module Database 6 | class MySQL 7 | def initialize 8 | require 'mysql' 9 | 10 | @dbh = Mysql.real_connect(SeleniumHelpers::Configuration.instance.config['mysql']['host'], 11 | SeleniumHelpers::Configuration.instance.config['mysql']['user'], 12 | SeleniumHelpers::Configuration.instance.config['mysql']['password'], 13 | SeleniumHelpers::Configuration.instance.config['mysql']['database']) 14 | end 15 | 16 | def random_username_and_password 17 | res = @dbh.query("select username, password from provider order by rand() limit 1") 18 | res.fetch_row 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/providers/static.rb: -------------------------------------------------------------------------------- 1 | module Providers 2 | module Static 3 | class UsernamePassword 4 | attr_accessor :user 5 | 6 | def initialize 7 | @user = { 8 | "username" => "flying", 9 | "password" => "monkey" 10 | } 11 | end 12 | end 13 | end 14 | end -------------------------------------------------------------------------------- /lib/providers/usernamepassword.csv: -------------------------------------------------------------------------------- 1 | flying,monkey 2 | orange,apple 3 | blue,yellow 4 | car,truck -------------------------------------------------------------------------------- /lib/selenium_connection.rb: -------------------------------------------------------------------------------- 1 | require 'singleton' 2 | 3 | require 'config' 4 | require 'json' 5 | 6 | require 'rubygems' 7 | require 'selenium-webdriver' 8 | require "selenium/client" 9 | 10 | module SeleniumHelpers 11 | class SeleniumConnection 12 | include Singleton 13 | 14 | attr_accessor :connection 15 | 16 | def initialize 17 | if SeleniumHelpers::Configuration.instance.config['saucelabs']['ondemand'] 18 | browser_string = {:username => SeleniumHelpers::Configuration.instance.config['saucelabs']['credentials']['username'], 19 | :"access-key" => SeleniumHelpers::Configuration.instance.config['saucelabs']['credentials']['key'], 20 | :os => SeleniumHelpers::Configuration.instance.config['saucelabs']['browser']['os'], 21 | :browser => SeleniumHelpers::Configuration.instance.config['saucelabs']['browser']['browser'], 22 | :"browser-version" => SeleniumHelpers::Configuration.instance.config['saucelabs']['browser']['version'] 23 | } 24 | @connection = Selenium::Client::Driver.new \ 25 | :host => SeleniumHelpers::Configuration.instance.config['saucelabs']['server']['host'], 26 | :port => SeleniumHelpers::Configuration.instance.config['saucelabs']['server']['port'], 27 | :browser => browser_string.to_json, 28 | :url => SeleniumHelpers::Configuration.instance.config['selenium']['base_url'], 29 | :timeout_in_second => SeleniumHelpers::Configuration.instance.config['selenium']['timeout'] 30 | else 31 | @connection = Selenium::Client::Driver.new \ 32 | :host => SeleniumHelpers::Configuration.instance.config['selenium']['host'], 33 | :port => SeleniumHelpers::Configuration.instance.config['selenium']['port'], 34 | :browser => SeleniumHelpers::Configuration.instance.config['selenium']['browser'], 35 | :url => SeleniumHelpers::Configuration.instance.config['selenium']['base_url'], 36 | :timeout_in_second => SeleniumHelpers::Configuration.instance.config['selenium']['timeout'] 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/login_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'home_page' 3 | 4 | require 'providers/static' 5 | include Providers::Static 6 | require 'providers/csv' 7 | include Providers::CSV 8 | require 'providers/mysql' 9 | include Providers::Database 10 | 11 | module SauceWebsite 12 | describe "Logging in" do 13 | before(:each) do 14 | @home = PageObjects::HomePage.new 15 | @login = @home.goto_login_form 16 | end 17 | 18 | context "with an invalid password" do 19 | context "not using a provider" do 20 | it "displays an error message", :depth => 'deep', :login => true do 21 | @login.username = "foo" 22 | @login.password = "bar" 23 | @login.login 24 | # 'expectation' 25 | @login.error_message.should == "Incorrect username or password." 26 | end 27 | end 28 | 29 | context "using a static provider" do 30 | it "prints error message", :depth => 'deep', 31 | :login => true do 32 | provided_info = Providers::Static::UsernamePassword.new 33 | @login.username = provided_info.user["username"] 34 | @login.password = provided_info.user["password"] 35 | @login.login 36 | # 'expectation' 37 | @login.error_message.should == "Incorrect username or password." 38 | end 39 | end 40 | 41 | context "using a csv provider" do 42 | context "using a csv provider" do 43 | it "prints error message", :depth => 'deep', 44 | :login => true do 45 | provider = Providers::CSV::UsernamePassword.new 46 | provided_info = provider.random_row 47 | @login.username = provided_info[0] 48 | @login.password = provided_info[1] 49 | @login.login 50 | # 'expectation' 51 | @login.error_message.should == "Incorrect username or password." 52 | end 53 | end 54 | end 55 | 56 | context "using a mysql provider" do 57 | it "prints error message", :depth => 'deep', 58 | :login => true do 59 | provider = Providers::Database::MySQL.new 60 | provided_info = provider.random_row 61 | @login.username = provided_info[0] 62 | @login.password = provided_info[1] 63 | @login.login 64 | # 'expectation' 65 | @login.error_message.should == "Please enter your member name again. Please check your spelling or register for an account." 66 | end 67 | end 68 | end 69 | 70 | context "with a correct password", :depth => 'shallow', :login => true do 71 | # if there is no block passed then it is 'pending 72 | it "goes to account page" 73 | end 74 | 75 | context "skipping examples" do 76 | it "does magic on Mondays" do 77 | if Time.new.strftime("%A") != "Monday" 78 | pending("except its not Monday") 79 | end 80 | Time.new.strftime("%A").should == "Monday" 81 | end 82 | end 83 | end 84 | end -------------------------------------------------------------------------------- /spec/soft_assert_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'home_page' 3 | 4 | module SauceWebsite 5 | describe "Login" do 6 | context "invalid password" do 7 | it "prints error message", :depth => 'deep', :login => true, :soft => true do 8 | @home = PageObjects::HomePage.new 9 | @login = @home.goto_login_form 10 | @login.username = "foo" 11 | @login.password = "bar" 12 | @login.login 13 | # 'expectation' 14 | begin 15 | @login.error_message.should == "Incofrrect username or password." 16 | rescue RSpec::Expectations::ExpectationNotMetError => verification_error 17 | @validation_errors << verification_error 18 | end 19 | end 20 | end 21 | 22 | context "correct password", :depth => 'shallow', :login => true do 23 | # if there is no block passed then it is 'pending 24 | it "goes to account page" 25 | end 26 | end 27 | end -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.expand_path(File.join(File.dirname(__FILE__), '../lib')) 2 | 3 | require 'selenium_connection' 4 | require 'yaml' 5 | 6 | Dir[File.dirname(__FILE__) + "/support/**/*.rb"].each {|f| require f} 7 | 8 | RSpec.configure { |c| 9 | c.before(:all) { 10 | @config = SeleniumHelpers::Configuration.instance.config 11 | } 12 | 13 | c.before(:each) { 14 | @validation_errors = Array.new 15 | 16 | @browser = SeleniumHelpers::SeleniumConnection.instance.connection 17 | @browser.start_new_browser_session 18 | @browser.window_maximize 19 | @browser.open("/") 20 | } 21 | 22 | c.after(:each) { 23 | if SeleniumHelpers::Configuration.instance.config['saucelabs']['ondemand'] 24 | payload = { 25 | :tags => self.example.options.collect{ |k, v| 26 | if v.to_s == 'true' 27 | k 28 | else 29 | "#{k}:#{v}" 30 | end 31 | } 32 | } 33 | @browser.set_context("sauce: job-info=#{payload.to_json}") 34 | end 35 | @browser.close_current_browser_session 36 | @validation_errors.should be_empty 37 | } 38 | } -------------------------------------------------------------------------------- /spec/support/matchers/have_errors.rb: -------------------------------------------------------------------------------- 1 | RSpec::Matchers.define :have_errors do |expected| 2 | match do |actual| 3 | !actual.empty? 4 | end 5 | 6 | failure_message_for_should_not do |actual| 7 | actual.join("\n") 8 | end 9 | end --------------------------------------------------------------------------------