Napybara
So you're writing an integration test for the following page:
<html>
<head>
<title>Your messages</title>
</head>
<body>
<ul class='message-list'>
<li class="message" id="message-1">Hello world!</li>
<li class="message" id="message-2">Kamusta mundo!</li>
</ul>
<form class='new-message'>
<div class="message-row" />
<label for='message'>Message</label>
<input id='message' type='text' name='message'>
</div>
<input type='submit' value='Send'/>
</form>
</html>
Wouldn't it be nice if you can write test helpers that followed the page's structure?
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:
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
:
expect(messages_page.node).to eq(self)
Finding by selector
You can add finders to the Napybara page with Napybara::Element#finder
:
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:
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:
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,
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:
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:
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:
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:
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:
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:
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:
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
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 :)