水豚歧义解决方案

如何解决水豚的歧义?由于某种原因,我需要在页面中使用具有相同值的链接,但是由于我得到了错误信息,所以无法创建测试

Failure/Error: click_link("#tag1")
Capybara::Ambiguous:
Ambiguous match, found 2 elements matching link "#tag1"

我之所以无法避免这一点,是因为这个设计。我试图重新创建 Twitter 页面,右边是 tweets/tag,左边是 tag。因此,相同的链接页面出现在同一个页面上是不可避免的。

56496 次浏览

NEW ANSWER:

You can try something like

all('a').select {|elt| elt.text == "#tag1" }.first.click

There may be a way to do this which makes better use of the available Capybara syntax -- something along the lines of all("a[text='#tag1']").first.click but I can't think of the correct syntax off hand and I can't find the appropriate documentation. That said it's a bit of a strange situation to begin with, having two <a> tags with the same id, class, and text. Is there any chance they are children of different divs, since you could then do your find within the appropriate segment of the DOM. (It would help to see a bit of your HTML source).


OLD ANSWER: (where I thought '#tag1' meant the element had an id of "tag1")

Which of the links do you want to click on? If it's the first (or it doesn't matter), you can do

find('#tag1').click

Otherwise you can do

all('#tag1')[1].click

to click the second one.

My solution is

first(:link, link).click

instead of

click_link(link)

Due to this post, you can fix it via "match" option:

Capybara.configure do |config|
config.match = :prefer_exact
end

Such behavior of Capybara is intentional and I believe it shouldn't be fixed as suggested in most of other answers.

Versions of Capybara before 2.0 returned the first element instead of raising exception but later maintainers of Capybara decided that it's a bad idea and it's better to raise it. It was decided that in many situations returning first element leads to returning not the element that the developer wanted to be returned.

The most upvoted answer here recommend to use first or all instead of find but:

  1. all and first don't wait till element with such locator will appear on the page though find does wait
  2. all(...).first and first won't protect you from situation that in future another element with such locator may appear on the page and as the result you may find incorrect element

So it's adviced to choose another, less ambiguous locator: for example select element by id, class or other css/xpath locator so that only one element will match it.


As a note here are some locators that I usually consider useful when resolving ambiguity:

  • find('ul > li:first-child')

    It's more useful than first('ul > li') as it will wait till first li will appear on the page.

  • click_link('Create Account', match: :first)

    It's better than first(:link, 'Create Account').click as it will wait till at least one Create Account link will appear on the page. However I believe it's better to choose unique locator that doesn't appear on the page twice.

  • fill_in('Password', with: 'secret', exact: true)

    exact: true tells Capybara to find only exact matches, i.e. not find "Password Confirmation"

The above solution works great but for those curious you can also use the following syntax.

click_link(link_name, match: :first)

You can find more information here:

http://taimoorchangaizpucitian.wordpress.com/2013/09/06/capybara-click-link-different-cases-and-solutions/

You can ensure that you find the first one using match:

find('.selector', match: :first).click

But importantly, you probably do not want to do this, as it will lead to brittle tests that are ignoring the duplicate-output code smell, which in turn leads to false positives that keep working when they should have failed, because you removed one matching element but the test happily found the other one.

The better bet is to use within:

within('#sidebar') do
find('.selector).click
end

This ensures that you're finding the element you expect to find, while still leveraging the auto-wait and auto-retry capabilities of Capybara (which you lose if you use find('.selector').click), and it makes it much clearer what the intent is.

To avoid ambiguous error in cucumber.

Solution 1

first("#tag1").click

Solution 2

Cucumber features/filename.feature --guess

To add to the existing body of knowledge here:

For JS tests, Capybara has to keep two threads (one for RSpec, one for Rails) and a second process (the browser) in sync. It does this by waiting (up to the configured maximum wait time) in most matchers and node-finding methods.

Capybara also has methods that don't wait, primarily Node#all. Using them is like telling your specs that you'd like them to fail intermittently.

The accepted answer suggests page.first('selector'). This is undesirable, at least for JS specs, because ABC1 uses Node#all.

That said, Node#first will wait if you configure Capybara like so:

# rails_helper.rb
Capybara.wait_on_first_by_default = true

This option was added in Capybara 2.5.0 and is false by default.

As Andrei mentioned, you should instead use

find('selector', match: :first)

or change your selector. Either will work well regardless of config or driver.

To further complicate things, in old versions of Capybara (or with a config option enabled), #find will happily ignore ambiguity and just return the first matching selector. This isn't great either, as it makes your specs less explicit, which I imagine is why no longer the default behavior. I'll leave out the specifics because they've been discussed above already.

More resources:

As considering all the above options, you can try this too

find("a", text: text, match: :prefer_exact).click

If you are using cucumber, you can follow this too

You can pass the text as a parameter from the scenario steps which can be generic step to reuse again

Something like When a user clicks on "text" link

And in step definition When(/^(?:user) clicks on "([^"]*)" (?:link)$/) do |text|

This way, you can reuse the same step by minimizing the lines of code and would be easy to write new cucumber scenarios