# Napybara
[![Build Status](https://api.travis-ci.org/gsmendoza/napybara.svg)](https://travis-ci.org/gsmendoza/napybara)
So you're writing an integration test for the following page:
```html
Your messages
- Hello world!
- Kamusta mundo!
```
Wouldn't it be nice if you can write test helpers that followed the page's
structure?
```ruby
messages_page.visit!
messages_page.form.message_row.text_field.node.set 'Hello World!'
messages_page.form.submit!
expect(messages_page.message(Message.find(1))).to have_content('Hello world!')
expect(messages_page.message(Message.find(2))).to have_content('Kamusta mundo!')
expect(messages_page.messages[0]).to have_content('Hello world!')
expect(messages_page.messages[1]).to have_content('Kamusta mundo!')
```
With Napybara, now you can!
## Napybara::Element.new and #node
First off, let's wrap the Capybara session in a Napybara element:
```ruby
let(:messages_page) do
Napybara::Element.new(self)
end
```
In Rails integration tests which use Capybara, `self` is usually the Capybara session.
You can get the Capybara element wrapped by the Napybara element with
`Napybara::Element#node`:
```ruby
expect(messages_page.node).to eq(self)
```
## Finding by selector
You can add finders to the Napybara page with `Napybara::Element#finder`:
```ruby
let(:messages_page) do
Napybara::Element.new(self) do |page|
page.finder :form, 'form.new-message'
end
end
# ...
expect(messages_page.form.node['class']).to eq('new-message')
```
## Finding by object
In order to find an element representing a particular ruby object, you need to
add a separate selector which incorporates the ruby object's id:
```ruby
let(:messages_page) do
Napybara::Element.new(self) do |page|
page.finder :message, '.message', '#message-{id}'
end
end
let(:some_message) do
Message.find(1)
end
# ...
expect(messages_page.message(some_message).node['id'])
.to eq("message-#{some_message.id}")
```
In the above example, the `message` finder looks for an element matching the
given selector (`#message-{id}`) with `some_message`'s id (`1`). So it ends up
looking for "#message-1".
If the ruby object is identified by a method other than the object's id, you can replace `{id}` with the method e.g. `{name}`, `{to_s}`.
## Checking if an element exists
`Napybara::Element#finder` also adds `has_` and `has_no_` methods to the element.
With the Napybara elements above, you can call:
```ruby
expect(messages_page.has_form?).to eq(true)
expect(messages_page).to have_form
expect(messages_page.has_message?(some_message)).to eq(true)
expect(messages_page).to have_message(some_message)
non_existent_message = Message.find(3)
expect(messages_page.has_no_message?(non_existent_message)).to eq(true)
expect(messages_page).to have_no_message(non_existent_message)
```
Due to the magic that Capybara does when finding elements in a Ajaxified page,
it's recommended to call `expect(element).to have_no_...` instead of
`expect(element).to_not have...`, since the former relies on Capybara's Ajax-
friendly `has_no_css?` method.
## Finding all elements matching a selector
`Napybara::Element#finder` adds a pluralized version of the finder. For example,
```ruby
let(:messages_page) do
Napybara::Element.new(self) do |page|
page.finder :message, '.message'
end
end
# ...
expect(messages_page.messages[0].node.text).to eq("Hello world!")
expect(messages_page.messages[1].node.text).to eq("Kamusta mundo!")
```
Napybara uses ActiveSupport to get the plural version of the finder name.
## Finding the parent and root of an element
You can also get the parent and root of an element:
```ruby
let(:messages_page) do
Napybara::Element.new(self) do |page|
page.finder :message_list, '.message-list' do |message_list|
message_list.finder :message, '.message'
end
end
end
# ...
expect(messages_page.message_list.messages[0].parent.selector)
.to eq(messages_page.message_list.selector)
expect(messages_page.message_list.messages[0].root.selector)
.to eq(messages_page.selector)
```
## Adding custom methods to a Napybara element
You can add new methods to a Napybara element with plain Ruby:
```ruby
let(:messages_page) do
Napybara::Element.new(self) do |page|
def page.visit!
node.visit node.messages_path
end
end
end
# ...
messages_page.visit!
```
## Extending a Napybara element with a module
Adding the same methods to multiple Napybara elements? You can share the methods in a module:
```ruby
module PageExtensions
def visit!
node.visit node.messages_path
@visited = true
end
def visited?
!! @visited
end
end
let(:messages_page) do
Napybara::Element.new(capybara_page) do |page|
page.extend PageExtensions
end
end
# ...
messages_page.visit!
expect(messages_page).to be_visited
```
## Extending a Napybara element with a module with finders
And what if you want to share a module with finders? Again, with plain Ruby:
```ruby
module IsAForm
def submit!
submit_button.node.click
end
def self.add_to(form)
form.extend self
form.finder :submit_button, 'input[type=submit]'
end
end
# ...
page.finder :form, 'form.new-message' do |form|
IsAForm.add_to(form)
end
```
It may not sexy, but it gets the job done :)
### Passing Capybara options to the finder
You can pass Capybara options to the finder:
```ruby
let(:messages_page) do
Napybara::Element.new(self) do |page|
page.finder :title, 'head title', visible: false
end
end
# ...
expect(page.title.node.text).to eq('Your messages')
```
## Putting it all together
Oh yeah, the "N" in Napybara stands for nesting. Here's how you can define the
helpers at the start of this README:
```ruby
module PageExtensions
def visit!
node.visit node.messages_path
@visited = true
end
def visited?
!! @visited
end
end
module IsAForm
def submit!
submit_button.node.click
end
def self.add_to(form)
form.extend self
form.finder :submit_button, 'input[type=submit]'
end
end
let(:messages_page) do
Napybara::Element.new(self) do |page|
page.extend PageExtensions
page.finder :form, 'form.new-message' do |form|
IsAForm.add_to form
form.finder :message_row, '.message-row' do |row|
row.finder :text_field, 'input[type=text]'
end
end
page.finder :message, '.message-list .message', '#message-{id}'
end
end
```
## And a few more things: getting the selector of a finder
`Napybara::Element#selector` returns a selector that can be used to find the element:
```ruby
expect(messages_page.form.message_row.text_field.selector)
.to eq('form.new-message .message-row input[type=text]')
expect(messages_page.message(Message.find(2)).selector)
.to eq('#message-2')
expect(messages_page.messages.selector)
.to eq('.message-list .message')
expect(messages_page.messages[1].selector)
.to eq('.message-list .message')
```
Take note that with `messages_page.messages[1]`, it's currently not possible to get the ith match of a selector. We'll have to wait until [`nth-match`](http://www.w3.org/TR/selectors4/#nth-match-pseudo) becomes mainstream.
## Installation
```
$ gem install Napybara
```
## Contributing
I'm still looking for ways to improve Napybara's DSL. If you have an idea, a
pull request would be awesome :)