Domino
View abstraction for integration testing
Usage
To create a basic Domino class, inherit from Domino and define a selector and attributes:
module Dom
class Post < Domino
selector '#posts .post'
attribute :title # selector defaults to .title
attribute :author_name # selector defaults to .author-name
attribute :body, '.post-body' # example of selector override
# pass a block if you want to convert the value
attribute :comments do |text|
text.to_i
end
attribute :posted_at do |text|
Date.parse(text)
end
# Combination selector is a boolean that combines with the root node. Boolean.
# You can find a post that also has the .active class.
attribute :active, "&.active"
# Combination selector for a data attribute. Finds the value of the data attribute
# on the root node. You can convert it with a callback like any other attribute.
attribute :rating, "&[data-rating]", &:to_i
# Combination selector for a data attribute with no value set. Must convert to
# boolean in the callback.
attribute :blah, "&[data-blah]" do |a|
!a.nil?
end
end
end
Now in your integration test you can use some of Domino's methods:
assert_equal 4, Dom::Post.count
refute_nil Dom::Post.find_by_title('First Post')
# Find by attribute yields the node when using a block.
assert_equal Dom::Post.find_by_title { |node| node.text == "First Post" && node.tag_name == 'p' }
# Multiple attributes, returns first match if any
refute_nil Dom::Post.find_by(title: 'First Post', author: 'Jane Doe')
# Multiple attributes with exception if no match is found
refute_nil Dom::Post.find_by!(title: 'First Post', author: 'Jane Doe')
# Multiple attributes, returns all matches if any
assert_equal ["12/06/2014", "12/01/2014"], Dom::Post.where(author: 'Jane Doe').map(&:posted_on)
What makes it really powerful is defining scoped actions:
module Dom
class Post < Domino
def delete
within(id) { 'Delete' }
end
end
end
refute_nil Dom::Post.find_by_title('First Post')
Dom::Post.find_by_title('First Post').delete
assert_nil Dom::Post.find_by_title('First Post')
Domino::Form
Domino makes it easy to model your forms for testing with Domino::Form
.
To create a basic form, simply inherit from Domino::Form
and define a
selector, a key (optional), and a set of fields.
module Dom
class PersonForm < Domino::Form
selector 'form.person'
# For forms with names like `person[age]`, no need to define the
# locator on each field. Define a key to automatically generate
# locators based on the field name.
key 'person'
# Define a custom selector to click to submit the form
submit_with "input[type='submit']" # this is the default
# locate field by label
field :first_name, 'First Name'
# locate field by automatically generated name (uses key, person[last_name])
field :last_name
# locate field by fully qualified name
field :biography, 'person[bio]'
# locate select field by label, acts as select
# callback mapper operates on selected option nodes
field :favorite_color, 'Favorite Color', as: :select, &:text
# automatically handles select[multiple]
# callback mapper operates on selected option nodes: &:value by default
field :allergies, as: :select
# locate by id, convert value via callback
field :age, 'person_age', &:to_i
# use a custom field type for unusual or composite fields
field :vehicles, '.input.vehicles', as: CheckBoxesField
# locate a field with a name that doesn't use the key
field :is_human, 'is_human', as: :boolean
# still supports attributes for non-input nodes
attribute :action, "&[action]"
attribute :submit_method, "&[method]"
end
end
In the above example, you can define a field to get a reader and writer method for the field. A form will also provide a mass-assignment writer and a save method to submit the form.
person = Dom::PersonForm.find!
person.age #=> 25
person.vehicles #=> ["Car", "Bike"]
person.is_human #=> true
person.favorite_color #=> Blue
person.age = 35
person.age #=> 35
person.set(vehicles: ["Car", "Van"], first_name: "Jessica", last_name: "Jones")
person.attributes #=> { first_name: "Jessica", last_name: "Jones", biography: "", favorite_color: "Blue", age: 35, vehicles: ["Car", "Van"], is_human: true }
Domino::Form
provides basic field types for text inputs and textareas,
single-selects, and boolean fields. You can create custom field types
for more complex form inputs by subclassing Domino::Form::Field
and
overriding the read
and write
methods. For example, if you have a
collection of check boxes, this might suit your needs:
class CheckBoxesField < Domino::Form::Field
def read(node)
node.find(locator).all('input[type=checkbox]').select(&:checked?).map(&:value)
end
def write(node, value)
value = Array(value)
node.find(locator).all('input[type=checkbox]').each do |box|
box.set(value.include?(box.value))
end
end
end
Provide your custom class using the :as
option when defining your field,
as shown in the example above.
Accessing the Attribute/Field Node
The named accessor method for any field or attribute will yield the Capybara node of the attribute if you pass a block. You can use this to check certain properties of the node without having to break out of your Dominos.
Example: Checking available options in a select field
person = Dom::PersonForm.find!
= ["- Select a Color -", "Red", "Blue", "Green"]
assert_equal , person.favorite_color { |n| n.all('option').map(&:text) }
Example: Checking the tag of the node containing the attribute value
person = Dom::Person.find_by!(uuid: "e94bb2d3-71d2-4efb-abd4-ebc0cb58d19f")
assert_equal "h2", person.name { |n| n.tag_name }
Integration with capybara
Domino uses capybara internally to search html for nodes and attributes. If you need to do something special, you can have direct access to the capybara node.
module Dom
class Account < Domino
selector "#accounts li"
# Returns this node text
def text
node.text
end
end
end
For more information about using Capybara nodes, check Capybara Documentation.
Dealing with Asynchronous Behavior
When working with Capybara drivers that support JavaScript, it may be
necessary to wait for elements to appear. Note that the following code
simply collects all Account
dominos currently on the page and
returns the first:
Dom::Account.first # returns nil if account is displayed asynchronously
When you are waiting for a unique domino to appear, you can instead
use the find!
method:
Dom::Account.find! # waits for matching element to appear
If no matching element appears, Capybara will raise an error telling
you about the expected selector. Depending on the
Capybara.match
option,
this will also raise an error if the selector matches multiple nodes.
Integration with Cucumber
Add a features/support/dominos.rb file, in which you define your dominos.
Use them in your steps.
Integration with Test::Unit
Include "domino" in your Gemfile if using bundler, or simply
require 'domino'
If you're not using Bundler.
Now, define your Dominos anywhere you want. The easiest place to start is in your test_helper.rb (doesn't have to be inside a Rails test class).
Example
Check out Domino Example for an example of using Test::Unit and Cucumber with Domino.
Copyright
Copyright (c) 2011 Nick Gauthier, released under the MIT license