How to make Capybara check for visibility after some JS has run?

82,532

Solution 1

I think that the find statement here is the one with the implicit wait, so Capybara will wait until the element is on the page, but won't wait for it to become visible.

Here, you would want Capybara to wait for the visible element to appear, which should be achievable by specifying the visible option:

expect(page).to have_selector('#blah', visible: true)

I haven't tried it, but the ignore_hidden_elements configuration option might be useful here as well, if you wanted find to always wait for visible elements.

Solution 2

This is another way to do it that works perfectly fine for me:

find(:css, "#some_element").should be_visible

Especially for more complex finds, such as

find(:css, "#comment_stream_list li[data-id='#{@id3}']").should_not be_visible

which would assert that an element has been hidden.

Solution 3

If you want to check that an element is on the page but is not visible, visible: false won't work as you might expect. Had me stumped for a bit.

Here's how to do it:

# assert element is present, regardless of visibility
page.should have_css('#some_element', :visible => false)
# assert visible element is not present
page.should have_no_css('#some_element', :visible => true)

Solution 4

Using:

 Ruby:     ruby 1.9.3dev (2011-09-23 revision 33323) [i686-linux]
 Rails:    3.2.9
 Capybara: 2.0.3

I have a Rails application in which there is a link which when clicked should submit an AJAX post request and return a JS response.

Link code:

 link_to("Send Notification", notification_path(user_id: user_id), remote: true, method: :post)

The JS response (.js.haml file) should toggle the following hidden div on the page the link exists:

 #notification_status(style='display:none')

js.haml file contents:

:plain
  var notificationStatusContainer = $('#notification_status');
  notificationStatusContainer.val("#{@notification_status_msg}");
  notificationStatusContainer.show();

I was testing my scenario of sending notification and displaying the notification status message to the user using Cucumber (cucumber-rails gem with built in Capybara support)

I was trying to test that the element having id: notification_status was visible on successful response in my step definition.For this I tried following statements:

page.find('#notification_status').should be_visible
page.should have_selector('#notification_status', visible: true)
page.should have_css('#notification_status', visible: true)
page.find('#notification_status', visible: true)
page.find(:css, 'div#notification_status', visible: true)

Neither of above worked for me and failed my step.Out of the above listed 5 snippets the last 4 failed with following error:

'expected to find css "#notification_status" but there were no matches. Also found "", which matched the selector but not all filters. (Capybara::ExpectationNotMet)'

which was strange because following statement was passing correctly:

page.has_selector?('#notification_status')

And in fact I inspected the page source using

  print page.html

which showed up

<div style='' id='notification_status'></div>

which was expected.

Finally I found this link capybara assert attributes of an element which showed up how to inspect an element's attribute in raw manner.

Also I found in Capybara documentation for visible? method (http://rubydoc.info/github/jnicklas/capybara/master/Capybara/Node/Element#visible%3F-instance_method) following information:

 Not all drivers support CSS, so the result may be inaccurate.

Thus I came to the conclusion that when testing visibility of an element do not rely on results of Capybara's visible? method when using a CSS selector and using the solution suggested in link capybara assert attributes of an element

I came up with following:

 module CustomMatchers
   def should_be_visible(css_selector)
    find(css_selector)['style'].should_not include('display:none', 'display: none')
   end
 end

 World(CustomMatchers)

Usage:

should_be_visible('#notification_status')

Solution 5

What visible means is not obvious

The failure may come from a misunderstanding of what is considered visible or not as it is non-obvious, not driver portable, and under-documented. Some tests:

HTML:

<div id="visible-empty"                                                                   ></div>
<div id="visible-empty-background"      style="width:10px; height:10px; background:black;"></div>
<div id="visible-empty-background-same" style="width:10px; height:10px; background:white;"></div>
<div id="visible-visibility-hidden"     style="visibility:hidden;"                        >a</div>
<div id="visible-display-none"          style="display:none;"                             >a</div>

The only thing Rack test considers as invisible is inline display: none (not internal CSS since it does not do selectors):

!all('#visible-empty',                 visible: true).empty? or raise
!all('#visible-empty-background',      visible: true).empty? or raise
!all('#visible-empty-background-same', visible: true).empty? or raise
!all('#visible-visibiility-hidden',    visible: true).empty? or raise
 all('#visible-display-none',          visible: true).empty? or raise

Poltergeist has a similar behavior, but it can deal with internal CSS and Js style.display manipulation:

Capybara.current_driver = :poltergeist
!all('#visible-empty',                 visible: true).empty? or raise
!all('#visible-empty-background',      visible: true).empty? or raise
!all('#visible-empty-background-same', visible: true).empty? or raise
!all('#visible-visibiility-hidden',    visible: true).empty? or raise
 all('#visible-display-none',          visible: true).empty? or raise

Selenium behaves quite differently: if considers an empty element invisible and visibility-hidden as well as display: none:

Capybara.current_driver = :selenium
 all('#visible-empty',                 visible: true).empty? or raise
!all('#visible-empty-background',      visible: true).empty? or raise
!all('#visible-empty-background-same', visible: true).empty? or raise
 all('#visible-visibiility-hidden',    visible: true).empty? or raise
 all('#visible-display-none',          visible: true).empty? or raise

Another common catch is the default value of visible:

  • it used to be false (sees both visible and invisible elements),
  • currently is true
  • is controlled by the Capybara.ignore_hidden_elements option.

Reference.

Full runnable test on my GitHub.

Share:
82,532

Related videos on Youtube

Kevin Davis
Author by

Kevin Davis

Product Manager at Microsoft, working on Yammer. Dabbling in my own projects, usually with Rails.

Updated on December 27, 2020

Comments

  • Kevin Davis
    Kevin Davis over 3 years

    After loading a page I have code that runs and hides and shows various items based on data returned by an xhr.

    My integration test looks something like this:

    it "should not show the blah" do
        page.find('#blah').visible?.should be_true
    end 
    

    When I manually go to the page in the context this test runs, #blah is not visible as I expect. I suspect that Capybara is looking at the initial state of the page (invisible in this case), evaluating the state of the DOM and failing the test before the JS runs.

    Yes, I set the :js => true on the containing describe block :)

    Any ideas would be greatly appreciated! I'm hoping I don't have to put an intentional delay in here, that feels flaky and will slow things down.

    • Ciro Santilli OurBigBook.com
      Ciro Santilli OurBigBook.com over 9 years
      What is the driver? What is the expected HTML once the XHR finished?
  • Phương Nguyễn
    Phương Nguyễn over 11 years
    The should_not be_visible doesn't look good to me as Capybara run wait_until { base.visible? } when Element#visible? being invoked.
  • Marc-André Lafortune
    Marc-André Lafortune about 11 years
    You can't use should_not with capybara, because of ajax
  • Zubin
    Zubin about 11 years
    @Marc-AndréLafortune, to test ajax with capybara use Capybara.javascript_driver.
  • Marc-André Lafortune
    Marc-André Lafortune about 11 years
    with javascript_drive, have_css will wait for your whole timeout duration before giving up, because it expects to find the element. E.g. you have to use should have_no_content instead of should_not have_content.
  • Zubin
    Zubin about 11 years
    @Marc-AndréLafortune, that's interesting - didn't realise that. Handy to know!
  • solidcell
    solidcell about 11 years
    @phng-nguyn, #wait_until has been removed from recent versions of Capybara.
  • installero
    installero about 11 years
    This is actually a better variant than the "page.should have_selector ...", because in case if a failing text, it's would say that the element is actually invisible explicitly, whereas the mentioned variant just evokes 'there were no matches' error, that is a little bit frustrating.
  • parhamr
    parhamr over 10 years
    This solution is not future compatible, as wait_until was removed from Capybara 2.
  • Chris Beck
    Chris Beck about 10 years
    in case anyone initially misses @Kevin's note (like me), you must have js:true on the containing block for this to work.
  • Ian Vaughan
    Ian Vaughan over 9 years
    Use Timeout.timeout
  • Theymiss Developer
    Theymiss Developer about 9 years
    Great answer - thanks for the effort you put into this. Testing visibility of elements in a suite using multiple drivers is non-trivial.
  • Jason Swett
    Jason Swett about 9 years
    expect(page).not_to have_selector("#some_element", visible: true) worked fine for me using javascript_driver, without waiting the whole timeout duration.
  • Jason Swett
    Jason Swett about 9 years
    Scratch that. I'm having some awful race condition now.
  • Jason Swett
    Jason Swett about 9 years
    Okay, apparently sometimes you have to wait for the page to load before using have_selector. I used find() with some arbitrary element on the page. I remember having this issue before. My fix feels slightly like a hack, but I don't know of a better solution.
  • rizidoro
    rizidoro about 9 years
    "it used to be false (sees both visible and invisible elements), currently is true, and is controlled by the Capybara.ignore_hidden_elements option." See this blog post for more info: elabs.se/blog/60-introducing-capybara-2-1
  • Zubin
    Zubin about 9 years
    Updated method for second assertion, as @Marc-AndréLafortune correctly pointed out.
  • Marc-André Lafortune
    Marc-André Lafortune about 9 years
    Actually, since my comment, this has been changed and you can (and should) use should_not now, sorry!
  • Chuck Bergeron
    Chuck Bergeron almost 9 years
    This is awesome. It helped me get to: expect(page).to have_selector('#flash-message', visible: false, text: "Signed in Successfully") -- thanks for posting this answer
  • trueinViso
    trueinViso over 8 years
    The visible option had no effect for me when testing the visibility of a jquery slide down element. I had to use expect(find('#blah').visible?).to be_falsey.
  • johncip
    johncip about 8 years
    This is not the same as the accepted solution. For this code, Capybara will only wait until the element is in the DOM, and then check visibility immediately. If you're expecting the visibility to change, this can add a race condition to the specs.
  • Nakilon
    Nakilon almost 8 years
    Better use Selenium::WebDriver::Wait
  • user1735921
    user1735921 almost 8 years
    I want something like is_visible? then do that else do something else
  • Thomas Walpole
    Thomas Walpole over 7 years
    If you specify 'visible: :hidden` that will only match elements on the page that are not visible so you only need one assert/expectation.
  • Antrikshy
    Antrikshy about 7 years
    Does anybody know if .should_not be_visible covers display: none set by jQuery? It doesn't seem to work for me.
  • Nickolay Kondratenko
    Nickolay Kondratenko about 7 years
    if you use selenium only