Since its inception my Symbiont framework has provided a wrapper for Watir WebDriver, which in turn provides a wrapper for Selenium WebDriver. One of the other major libraries out there that wraps Selenium is called Capybara. I wanted to see what kind of support I could provide for that. In this post I’ll cover these changes.
I will note that supporting both the Watir and Capybara APIs is an interesting exercise and perhaps even a questionable one from a design perspective. I’ve very cognizant of my findings in my previous, now defunct, Dialect experiment. Time will tell if I’ve learned anything or, once again, said “Screw you, experience!”
I have plenty of posts on Symbiont so if you haven’t already played around with it, likely this post will mean little to you. If you have played around with Symbiont already, then you definitely have the environment you need. Here I’ll take you through some working examples so feel free to play along.
Page Objects with Capybara
First, of course, make sure that you update your gem:
gem install symbiont
Now create a script file, symbiont-script.rb or whatever you want to name it. Just as you do with the Watir approach, you can simply require Symbiont in your script:
1 |
require 'symbiont' |
Symbiont is predicated upon the page object pattern and that is applied whether you are using Capybara or Watir. How you define the page objects is largely identical in structure but different in implementation. Here is what you can add to the script:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Symbiote < Symbiont::Page url_is 'http://symbiote-app.herokuapp.com/' url_matches /:\d{4}/ element :open_form, "p[id='open']" element :username, "input[id='username']" element :password, "input[id='password']" element :login, "input[id='login-button']" def login_as_admin open_form.click username.set 'admin' password.set 'admin' login.click end end |
This is the same as the Watir approach structurally. But there are some differences. The Watir approach works by having Symbiont treated as a mix-in and thus gets included (or attached) to the page class. The Capybara approach, as you can see, requires you to make your page class an instance of Symbiont::Page
.
For the element definitions, with the Watir approach while you could use element
as shown there, you generally use the type of the element. Further, Capybara defaults to using CSS selectors although you can use XPath as well. So, for example, the open_form
element definition could be done like this with Capybara:
1 |
element :open_form, :xpath, ".//*[@id='open']" |
Just to give you a comparison, here is how the page definition would look with the Watir approach:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class Symbiote attach Symbiont url_is 'http://symbiote-app.herokuapp.com/' url_matches /:\d{4}/ p :open_form, id: 'open' input :username, id: 'username' input :password, id: 'password' button :login, id: 'login-button' def login_as_admin open_form.click username.set 'admin' password.set 'admin' login.click end end |
The Browser Driver
To start things off you need to establish a browser driver. With the Watir approach you do this:
1 |
Symbiont.set_browser |
This defaults to using the built-in browser driver for Watir+Selenium, which is Firefox, but you can change that to do this:
1 |
Symbiont.set_browser :chrome |
The Capybara approach is initially a little more complicated but it also does offer a great deal of modularity. Capybara requires you to configure drivers. So to get the script to work, you would put this in place:
1 2 3 |
Capybara.configure do |config| config.default_driver = :selenium end |
Here this defaults to using the built-in browser driver for Capybara+Selenium which, just like in the Watir case, is Firefox. In Capybara, I could change what browser the :selenium references by registering a driver like this:
1 2 3 |
Capybara.register_driver :selenium do |app| Capybara::Selenium::Driver.new(app, :browser => :chrome) end |
I want to point out one thing here before moving on. As with the Watir version, with the Capybara version you can specify a URL for a page definition. If you’ve set Capybara’s app_host
then you can set the URL as relative to that domain. For example, change the configuration accordingly:
1 2 3 4 |
Capybara.configure do |config| config.default_driver = :selenium config.app_host = 'http://symbiote-app.herokuapp.com/' end |
Now you can change the url_is
assertion to this:
1 |
url_is '/' |
Because the application host has been set, this means your page URLs can be stated relative to that host location.
Once you have you browser driver configured, you simply instantiate the page object.
1 |
@page = Symbiote.new |
Because I jumped around with code examples there between Watir and Capybar, here is what your script should look like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
require 'symbiont' class Symbiote < Symbiont::Page url_is '/' url_matches /:\d{4}/ element :open_form, "p[id='open']" element :username, "input[id='username']" element :password, "input[id='password']" element :login, "input[id='login-button']" def login_as_admin open_form.click username.set 'admin' password.set 'admin' login.click end end Capybara.configure do |config| config.default_driver = :selenium end @page = Symbiote.new |
As you can tell, it’s not terribly different from the Watir version. I’ve tried to keep the basic structural dynamics in place for how the page definition, and the element definitions within it, are set up.
Now let’s actually log in to the application. The code for this is identical to that which you would do for the Watir approach. Add the following to the end of your script:
1 2 3 4 5 |
@page.view @page.open_form.click @page.username.set 'admin' @page.password.set 'admin' @page.login.click |
Here I’m just showing how you can reference the elements on the page object but, as with the Watir approach, you can certainly call the handy login_as_admin
method I placed on the page object, which wraps up all those individual actions. So change the executable portion of your script to:
1 2 3 |
@page = Symbiote.new @page.view @page.login_as_admin |
You might notice that the view
is a bit of an extraneous bit. You can remove that by calling a perform
action on the page itself:
1 2 |
@page = Symbiote.new @page.perform.login_as_admin |
Just to be complete, you can also do something like this:
1 2 3 4 5 6 7 |
Symbiote.new do view open_form.click username.set 'admin' password.set 'admin' login.click end |
… or …
1 2 3 |
Symbiote.new do perform.login_as_admin end |
or even just this
1 |
Symbiote.new.perform.login_as_admin |
Keep in mind here, however, that you are not capturing the @page
instance variable in these last examples. If you don’t need it, then these various shortcuts can potentially be useful to you.
Context Factory for Page Objects
As with the Watir approach, you can use a context factory. You have to include it. So add the following line at the top of your script:
1 2 |
require 'symbiont' include Symbiont::Factory |
With that you can do this:
1 2 3 4 5 6 |
on_view(Symbiote) do @page.open_form.click @page.username.set 'admin' @page.password.set 'admin' @page.login.click end |
Now, this may look similar to what you saw before:
1 2 3 4 5 6 7 |
Symbiote.new do view open_form.click username.set 'admin' password.set 'admin' login.click end |
However you should notice that the context factory keeps the @page
instance variable reference. You can also call the page object method directly from the factory, like this:
1 |
on_view(Symbiote).login_as_admin |
Another thing you can do is call blocks on the page object. Here’s an example:
1 2 3 4 5 |
@page = Symbiote.new @page.view do |action| action.login_as_admin end |
As you can see there is a lot of flexibility in approach. Some of this is a bit more flexible than my current Watir-based approach.
State and Existence on Page Objects
As with the Watir approach, you can get information about the page or the state of the page. Consider the following:
1 2 3 4 5 6 |
@page = Symbiote.new @page.view puts "Page title: #{@page.title}" puts "Page URL: #{@page.current_url}" puts "Page HTML: #{@page.html}" |
You can check if the page is displayed and if it’s secure (should the latter be necessary for you):
1 2 |
puts "Page displayed? #{@page.displayed?}" puts "Page secure? #{@page.secure?}" |
All those statements will do is display true or false but won’t actually cause an exception of any sort. If you include RSpec as part of your script you can also use nice expectations with predicate methods like displayed?
and secure?
. For example, if you add the following to the top of your script:
1 2 |
require 'rspec' include RSpec::Matchers |
Then you could do this:
1 2 |
expect(@page.displayed?).to be_truthy expect(@page).to be_displayed |
Either one of those would work and do the same thing but notice how the second reads a bit nicer due to how RSpec handles predicate methods. You could also check for the opposite condition:
1 |
expect(@page).not_to be_displayed |
Now let’s play around with a page. We already have a page object so let’s get a representation for an element if that element exists on the page:
1 |
puts "Element for open form: #{@page.open_form}" |
That simply returns an object representation of the open_form
element. That’s not always terribly useful by itself but something that is useful is that you can check if an element exists:
1 |
puts "Has an open form element: #{@page.has_open_form?}" |
Notice that because this is a predicate method (has_open_form?
), you can use expectations to make the code read a little nicer:
1 |
expect(@page).to have_open_form |
You can also check for non-existence:
1 2 3 4 5 |
puts "Has a username element: #{@page.has_username?}" puts "Does not have a username element: #{@page.has_no_username?}" expect(@page).to have_no_username expect(@page).not_to have_username |
More To Come
What I showed you here is how you can use Symbiont with Capybara and have much of this look very similar to using the Watir approach. What I haven’t shown you here is that just as Symbiont delegates down to Watir, it also delegates down to Capybara. What this means is that you have the full power of both APIs available to you.
I’m pretty excited about this evolution of Symbiont but there’s a lot more work to do, including on documentation. All of that will be coming soon.