Search code examples
ruby-on-railsangularjscucumbercapybarapoltergeist

Capybara::Poltergeist::ObsoleteNode when angular updates a table row rendered with ng-repeat


I'm testing angular live-updates with capybara, cucumber, and poltergeist.

I have the following step definition that fails:

Then(/^I should see the following inventory:$/) do |table|
  rows = find(".inventory table").all('tr')
  page_table = rows.map { |r| r.all('th,td').map { |c| c.text.strip } }
  table.dup.diff!(page_table)
end

Error:

The element you are trying to interact with is either not part of the DOM, or is not currently visible on the page (perhaps display: none is set). It's possible the element has been replaced by another element and you meant to interact with the new element. If so you need to do a new 'find' in order to get a reference to the new element. (Capybara::Poltergeist::ObsoleteNode)

However, if I wrap the assertion (really just the #find) with an anticipate block (retries) the test passes.

w/ anticipate block:

Then(/^I should see the following inventory:$/) do |table|
  sleeping(0.1).seconds.between_tries.failing_after(20).tries do
    rows = find(".inventory table").all('tr')
    page_table = rows.map { |r| r.all('th,td').map { |c| c.text.strip } }
    table.dup.diff!(page_table)
  end
end

I HATE this solution, because cucumber/capybara should already have a retry mechanism. So, if that retry timeout is 5 seconds you're potentially really retrying for 5sec * 20retries + additional 2 seconds. Now, I can add wait: 0 on the find action, but these solutions all seem like hacks.

I'm using poltergeist 1.9.8, but have tried upgrading to 2.1 and still no dice. Is there a fix to this?


Solution

  • Capybaras retry mechanism is built into its matchers which you aren't using in this test. You're also using #all which has the disadvantage of the elements it returns not being automatically reloadable, and therefore needs to be used only when the elements are not going to change or have already changed. #all also effectively has a wait time of 0 the way you're using it since an empty array of elements (no matches) is a valid response, so no waiting behavior. If in the test the number of visible rows is changing then you could use the count option to force #all into waiting and implement something like

    Then(/^I should see the following inventory:$/) do |table|
      rows = find(".inventory table").all('tr', count: table.raw.size)
      page_table = rows.map { |r| r.all('th,td').map { |c| c.text.strip } }
      table.dup.diff!(page_table)
    end
    

    That will make #all wait for the expected number of rows to be on the page, which should mean the rows are done changing and calling all('th,td') to find the text becomes safe.

    An option if the number of rows isn't going to change (only the content of them) is to just concatenate all the content together and check the text of the table - it wouldn't 100% be testing the table exactly matches, but in a test environment where you control the data it's probably good enough. This is untested but something along the lines of the following should do that

    Then(/^I should see the following inventory:$/) do |table|
      expect(find(".inventory table")).to have_content(table.raw.flatten.join)
    end
    

    One other option to try would be utilizing Capybara::Node::synchronize do get the retrying happening - something like

    Then(/^I should see the following inventory:$/) do |table|
      inv_table = find(".inventory table")
      inv_table.synchronize do
        page_table = inv_table.all('tr').map { |r| r.all('th,td').map { |c| c.text.strip } }
        table.dup.diff!(page_table)
      end
    end
    

    The #synchronize should allow Capybara to retry the block for up to Capybara.default_max_wait_time until it passes -- By default it will only retry on the errors returned by driver.invalid_elements and Capybara::ElementNotFound - If you also want it to retry on the error returned by diff! (up to the max_wait_time seconds) you'd have to pass options to synchronize like inv_table.synchronize max_wait_time, errors: page.driver.invalid_element_errors + [Capybara::ElementNotFound, WhateverErrorDiffRaisesOnFailure] do ...