Capybara needs to support nesting like Webrat does #384

Closed
dmonopoly opened this Issue Jun 13, 2011 · 21 comments

Comments

Projects
None yet

Capybara's have_selector matcher does not support nesting.

Example:

    require 'spec_helper'
    describe "messages/new.html.erb" do
      let(:message) do
        mock_model("Message").as_new_record.as_null_object
      end
      before do
        assign :message, message
      end
      it "renders a form to create a message" do
        render
        rendered.should have_selector("form",
          :method => "post",
          :action => messages_path
        ) do |form|
          form.should have_selector("input", :type => "submit") # this should fail because no submit button is in the view, but it silently passes
        end
      end

This doesn't work with Capybara, but would work correctly if with Webrat. In any case, shouldn't Capybara support nesting just like Webrat does?

This issue becomes more prominent with something like this:

    rendered.should have_selector("form") do |form|
        form.should have_selector("input",
            :type => "submit",
            :name => "message[title]",
            :value => "the title"
        )
    end

because many of the checks are in the nested area.

Collaborator

jnicklas commented Jun 14, 2011

The fact that have_selector takes a block in Webrat has never made sense to me. Just change it to something like:

within 'form' do
    page.should have_selector("input[type=submit][name='message[title]'][value='the title']")
end

Note that Capybara never claims to be API compatible with Webrat, they are different libraries with similar but not compatible APIs.

jnicklas closed this Jun 14, 2011

Alright - thanks for the tip about APIs.

But I really just want a clean way to test this, and "within" actually isn't available in view specs.

Actually, there's nothing wrong with letting Webrat and Capybara coexist in one project, is there? So I could have both webrat and capybara in my Gemfile?

Collaborator

jnicklas commented Jun 14, 2011

This isn't a support forum, but a quick hint:

rendered.find('form').should have_selector("input[type=submit][name='message[title]'][value='the title']")

This works in view specs, and does much the same thing. In fact you can call almost all methods you can call on the top level for any node returned from find, including stuff like click_link and has_selector?. Another good way to do this is using Object#tap:

rendered.find('form').tap |form|
  form.should have_selector("input[type=submit][name='message[title]'][value='the title']")
end

dwilkie commented Jun 28, 2011

This is not working with Capybara 1.0 and rspec-rails 2.6.1. I even tagged the example group with :type => :request still no joy

rendered.find('form') # undefined method `find' for String

gunn commented Jun 28, 2011

@jnicklas, since lots of people are switching from webrat and this is a mistake people often make, perhaps it would be nice for us to give them a warning when passing ignored options or a block to #have_selector ?

Strangely, with rspec & rails in controller tests, within is available as a method but it doesn't find things properly.

I have:

within('form') do 
  response.body.should have_css('input')
end

And it errors with Unable to find css "form" but replacing the above with just response.body.should have_css('form input') works just fine.

(Any ideas or should I open a new bug?)

Collaborator

joliss commented Jul 21, 2011

@contentfree, your setup seems a little odd -- while there is a response method somewhere deep inside the Capybara code, it shouldn't normally be exposed in your tests. Calling response just gives me undefined method.

Are you having webrat or something else lurking about, perhaps? Those interactions can be nasty. Try p method(:response) to figure out where that method comes from.

@joliss: I'm not sure what's odd about it. This is in a controller spec (not a cuke feature), so response is an available method and used all over the place.

The docs for capybara explicitly state: "rspec-rails will also automatically include Capybara in :controller and :mailer example groups." and within is definitely available as a method in the specs. The problem is that within doesn't find the content in response.body that matches the selector while response.body.should have_css('the same selector') does.

Collaborator

joliss commented Jul 22, 2011

[Cc @dchelimsky. Some context: @contentfree was trying to mix Capybara and ActionController::TestCase methods like so:]

within('form') do 
  response.body.should have_css('input')
end

Re @contentfree: Oh, that's a controller test -- now I see.

I think the reason why it doesn't work is that within is part of Capybara, whereas response is part of ActionController::TestCase. (That's two completely separate testing systems: Capybara exercises the entire stack, and ActionController::TestCase only calls the controller.) As a result, within uses Capybara's current page to find and set a new scope, which is then used by the methods in Capybara's DSL (unless you call them on specific nodes). But response does not care about Capybara's scope.

@dchelimsky, should rspec-rails really include the Capybara::DSL in :controller specs? What was the reason behind that? I'm wondering (in a just putting the thought out there without a technical clue kind of way) if rspec-rails should rather include some subset (or nothing at all) for :controller specs to avoid confusion like this.

Tekhne commented Jul 27, 2011

I was able to modify the work-around provided by @jnicklas so that it works in my view specs under capybara 1.0.0 and rspec-rails 2.6.1:

Capybara.string(rendered).find('form').tap |form|
  form.should have_selector("input[type=submit][name='message[title]'][value='the title']")
end

The trick here was to expose the Capybara DSL by wrapping RSpec's rendered string in a Capybara::Node::Simple object.

honki91 referenced this issue in bbatsov/rails-style-guide Dec 2, 2012

Closed

incorrect view spec example #50

Is there a solution for this? None of the examples above work for me with capybara 2.0.1 and rspec 2.12.0, and this just feels wrong:

rendered.should have_selector("form[action='#{new_account_path}'] input#user_login[type='text']")
rendered.should have_selector("form[action='#{new_account_path}'] input#user_password[type='password']")
rendered.should have_selector("form[action='#{new_account_path}'] input_user_password_confirmation[type='password']")

compared to this:

rendered.should have_selector("form[action='#{new_account_path}']") do |form|
   form.should have_field("user_login", :type => :text)
   form.should have_field("user_password", :type => :password)
   form.should have_field("user_password_confirmation", :type => :password)
end

i use capybara 2.0.0.beta2 and rspec 2.10.0 (i'm stuck on this not by my own choice)

and use what Tekhne has shown in my view spec with a function:

def find_form(rendered)
  Capybara.string(rendered).find("form").tap do |form|
    yield form
  end
end

it "should render submit button" do
  find_form(rendered) do |form|
    input = form.find_button("Create User")
    input[:type].should eq "submit"
  end
end

As you can see, you can use all finders on the form object, also find_field and check all attributes afterwards.
works like a charm :)

I'm not satisfied with this issue having been closed. I run into jaredmoody's example all the time. Why should I have to duplicate my CSS finders in every call to #have_selector? What's so harmful about allowing blocks? I just don't get why you wouldn't want to offer this functionality.

Collaborator

jnicklas commented Apr 24, 2013

There are about a bajillion options to choose from documented in this thread. The block API in Webrat for have_selector is pretty dumb, if you really think about it, it does not make any sense at all. How about an example like this:

page.should have_selector("form", :minimum => 2) do |form|
  form.should have_selector("fieldset", :maximum => 3) do |input|
    input.should_not have_selector("input")
  end
end

What the hell does that mean? I don't even know. have_selector is an assertion, it's either pass or fail, it has the semantics of an assertion. Using it to scope something on the page is just a flawed idea.

If you're using Capybara for integration tests, this is a complete non-issue, since you can simply use find or within instead of have_selector, which actually makes sense and isn't completely confusing. If you're writing view, or helper specs, all you need to do is wrap the output in Capybara.string and you can scope to your hearts content.

If you're still not satisfied, then write your own have_selector method. This is Ruby after all, just override the method to behave in whatever way you want.

As long as I'm dictator of this project, have_selector will never accept a block.

Collaborator

abotalov commented Apr 24, 2013

To people that request it. @jaredmoody's example can be rewritten to:

within "form[action='#{new_account_path}']" do |form|
   form.should have_field("user_login", :type => :text)
   form.should have_field("user_password", :type => :password)
   form.should have_field("user_password_confirmation", :type => :password)
end

I don't get why you don't like it

We have a simple disagreement then. The #have_selector with a block makes total sense to me, as does scoping something on a page. I do it all the time. Giving an example of a confusing scoping is just setting up a straw man. I can give an equally valid example where the scoping makes sense.

That said, the example using #within actually isn't half bad and seems to work for me so I'll be using that. Thanks!

I still have what I think is a valid concern over not raising some type of warning when a block is passed to #have_selector. After upgrading my project from Webrat to Selenium, my tests that were using scoped #have_selector blocks continued passing. However, they weren't checking what I thought they were checking any longer. It was literally months later before I discovered that #have_selector wasn't handling blocks. So my tests that were previously valid under Webrat were no longer checking what they were supposed to be checking under Capybara. And there was nothing to warn me about this in the Capybara README or the docs.

So basically, by not surfacing some kind of warning to the user that blocks aren't being respected in Capybara, you're potentially causing a world of hurt to anyone who is transitioning from Webrat. Silent failure is rarely a good idea. I'm not sure why you wouldn't want to warn if a block is passed to #have_selector?

From the first sentence of the project's description:

Capybara helps you test web applications by simulating how a real user would interact with your app.

Thus, I tend to use Capybara to simulate the user's action and the associated response from invoking that action. For example, when a user enters an e-mail address and clicks the submit button on a 'Join Mailing List Form', the user should see 'You have been added to the mailing list.' or 'Please confirm by clicking on the link in the e-mail that we sent to bob@example.com.'. In the past when I worked at a lower level of granularity (i.e. talking about the structure of the page), these type of tests would be brittle because the designers are constantly modifying both CSS and HTML. Yes, there may be cases where there are multiple forms on the page but I tend to keep this to a minimum because many times this can be confusing to the user as it would be more development for an automated test. Next, would a user ever check the CSS styling or HTML structure of a form or would they simply navigate to a page and complete the form fields that appear on the screen? I believe that the user would tend to do the latter because the former tends to be more of a view related spec. Last but not least, I only write unit, acceptance, and sometimes controller/helper specs when developing pages for a website. If I'm developing REST APIs, then I would only write both controller and unit specs.

I very much like the 'within' method solution proposed here, however it doesn't appear to work for view specs which is primarily where I want to be testing this sort of behaviour!

As I also get:
NoMethodError:
undefined method `within' for #RSpec::Core::..blahblah...

See: rspec/rspec-rails#387

Collaborator

jnicklas commented Oct 11, 2013

@IAmFledge as I mentioned in my comment, you'll need to wrap your response in Capybara.string. If this is something you do a lot, you could do:

def within(*args, &block)
  Capybara.string(response).within(*args, &block)
end

Or if you want to get really fancy:

Session::DSL_METHODS.each do |method|
  define_method method do |*args, &block|
    Capybara.string(response).send method, *args, &block
  end
end

hisapy commented Jul 25, 2015

Hey guys, I just started "appreciating" view specs and needed some of the scoping of Capybara in there so inspired in this "conversation" and other resources I created

https://gist.github.com/hisapy/0cbd7898334b386afa84

This appreciation came after finally finding a robust and agile Rails BDD flow from Cucumber to RSpec which I'll try to describe in a future blog/tweet post (https://twitter.com/hisa_py).

I hope you enjoy it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment