Разрешение неоднозначности капибары

97

Как устранить двусмысленность в Capybara? По какой-то причине мне нужны ссылки с одинаковыми значениями на странице, но я не могу создать тест, так как получаю сообщение об ошибке

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

Причина, по которой я не могу этого избежать, заключается в дизайне. Я пытаюсь воссоздать страницу Twitter с твитами / тегами справа и тегами слева от страницы. Следовательно, страницы с одинаковыми ссылками неизбежно будут отображаться на одной странице.

Neilmarion
источник
Не могли бы вы также опубликовать код?
Хина Хуссейн,
8
Вы не должны назначать один и тот же идентификатор двум элементам на странице. Если у вас будут идентичные ссылки, не назначайте элементам идентификатор, вместо этого используйте класс.
Крис Зальцберг,

Ответы:

147

Мое решение

first(:link, link).click

вместо того

click_link(link)
е-цинк
источник
6
Это подробно описано в Руководстве по обновлению Capybara , которое может оказаться полезным, если у вас возникла эта проблема.
Ричи
1
Начиная с Capybara 2.0, не делайте этого без крайней необходимости. См. Ответ @Andrey ниже и объяснение неоднозначных совпадений в руководстве по обновлению, указанном выше.
Джим
4
В частности, Capybara 2.0 имеет интеллектуальную логику ожидания, чтобы гарантировать, что спецификации пройдут или не пройдут последовательно на машинах с разной скоростью обработки, ожидая только минимально необходимое время. Использование, firstкак предложено выше, если вы абсолютно не знаете, что делаете, может привести к спецификациям, которые подходят для вас, но терпят неудачу в сборке CI или на машине коллеги.
Джим
1
Хорошее обсуждение см. На сайте robots.oughttbot.com/…
jim
74

Такое поведение Capybara является преднамеренным, и я считаю, что его не следует исправлять, как предлагается в большинстве других ответов.

Версии Capybara до 2.0 возвращали первый элемент вместо того, чтобы вызывать исключение, но более поздние разработчики Capybara решили, что это плохая идея и лучше поднять ее. Было решено, что во многих ситуациях возврат первого элемента приводит к возврату не того элемента, который разработчик хотел вернуть.

Самый популярный ответ здесь рекомендует использовать firstили allвместо findно:

  1. allи firstне ждите пока элемент с таким локатором появится на странице, хотя findи ждет
  2. all(...).firstи firstне защитит вас от ситуации, что в будущем на странице может появиться другой элемент с таким локатором, и в результате вы можете найти неверный элемент

Поэтому рекомендуется выбрать другой, менее неоднозначный локатор : например, выбрать элемент по идентификатору, классу или другому локатору css / xpath, чтобы ему соответствовал только один элемент.


В качестве примечания вот несколько локаторов, которые я обычно считаю полезными при разрешении двусмысленности:

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

    Это более полезно, чем first('ul > li')ждать, пока liна странице появится первое .

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

    Это лучше, чем first(:link, 'Create Account').clickподождать, пока на странице появится хотя бы одна ссылка «Создать учетную запись». Однако я считаю, что лучше выбрать уникальный локатор, который не появляется на странице дважды.

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

    exact: true сообщает Capybara искать только точные совпадения, т.е. не находить «Подтверждение пароля»

Андрей Боталов
источник
7
Это должен быть главный ответ. Всегда старайтесь использовать селектор, который будет использовать встроенные возможности ожидания в Capybara.
tgf
Спасибо. Я пробовал использовать: first, но понял, что работает только в jQuery. Я искал: first-child
Overload119
24

НОВЫЙ ОТВЕТ:

Вы можете попробовать что-то вроде

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

Может быть способ сделать это, который лучше использует доступный синтаксис Capybara - что-то вроде строк, all("a[text='#tag1']").first.clickно я не могу придумать правильный синтаксис, и я не могу найти соответствующую документацию. Это говорит , что это немного странной ситуация , чтобы начать с, имея две <a>метки с одинаковым id, classи текстом. Есть ли вероятность, что они являются дочерними элементами разных div, поскольку тогда вы можете find withinсоздать соответствующий сегмент DOM. (Было бы полезно увидеть немного вашего источника HTML).


СТАРЫЙ ОТВЕТ: (где я подумал, что «# tag1» означает, что элемент имеет значение id«tag1»)

На какую из ссылок вы хотите перейти? Если это первое (или это не имеет значения), вы можете сделать

find('#tag1').click

В противном случае вы можете сделать

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

чтобы щелкнуть второй.

Амит Кумар Гупта
источник
Это решение для первого может работать, но теперь проблема в том, что его можно ошибочно принять за идентификатор css --------- Failure / Error: find ('# tag1'). Click # or all ('# tag1 ') [0] .click Capybara :: ElementNotFound: Невозможно найти css "# tag1"
neilmarion 01
find('#tag1')означает, что вы хотите найти только один элемент с идентификатором tag1. Исключение возникает из-за того, что tag1на странице есть несколько элементов с id
Андрей Боталов
Вы можете это сделать all(:xpath, '//a[text()="#tag1"]').first.click.
Shuhei Kagawa
9

Вы можете убедиться, что найдете первый, используя match:

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

Но, что важно, вы, вероятно, не захотите этого делать , так как это приведет к нестабильным тестам , игнорирующим запах кода дублированного вывода, что, в свою очередь, приводит к ложным срабатываниям, которые продолжают работать, когда они должны были выйти из строя, потому что вы удалили одно совпадение. элемент, но тест успешно нашел другой.

Лучше использовать within:

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

Это гарантирует, что вы найдете элемент, который ожидаете найти, при этом по-прежнему используя возможности автоматического ожидания и автоповтора Capybara (которые вы теряете, если используете find('.selector').click), и делает гораздо более ясным, какова цель.

ТАЛлама
источник
7

Чтобы добавить к существующим знаниям здесь:

Для тестов JS Capybara должен поддерживать синхронизацию двух потоков (один для RSpec, один для Rails) и второй процесс (браузер). Это достигается путем ожидания (до настроенного максимального времени ожидания) в большинстве сопоставлений и методов поиска узлов.

У Capybara также есть методы, которые в первую очередь не ждут Node#all. Их использование - это все равно что говорить вашим спецификациям, что вы хотели бы, чтобы они периодически выходили из строя.

Принятый ответ подсказывает page.first('selector'). Это нежелательно, по крайней мере, для спецификаций JS, потому что Node#firstиспользуетNode#all .

Тем не менее, Node#first будет ждать, если вы настроите Capybara так:

# rails_helper.rb
Capybara.wait_on_first_by_default = true

Эта опция была добавлена ​​в Capybara 2.5.0 и по умолчанию является ложной.

Как упоминал Андрей, вместо этого вы должны использовать

find('selector', match: :first)

или измените свой селектор. Любой из них будет работать независимо от конфигурации или драйвера.

Чтобы еще больше усложнить ситуацию, в старых версиях Capybara (или с включенной опцией конфигурации), #findбудет с радостью игнорировать двусмысленность и просто вернуть первый соответствующий селектор. Это тоже не очень хорошо, так как делает ваши спецификации менее явными, и я думаю, поэтому больше не поведение по умолчанию. Я опущу детали, потому что они уже обсуждались выше.

Дополнительные ресурсы:

Джонцип
источник
5

Из-за этого поста вы можете исправить это с помощью опции "сопоставить":

Capybara.configure do |config|
  config.match = :prefer_exact
end
Skydan
источник
2

Учитывая все вышеперечисленные варианты, вы тоже можете попробовать это

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

Если вы используете огурец, вы тоже можете это сделать.

Вы можете передать текст в качестве параметра из шагов сценария, который может быть общим шагом для повторного использования

Что-то вроде When a user clicks on "text" link

И в определении шага When(/^(?:user) clicks on "([^"]*)" (?:link)$/) do |text|

Таким образом, вы можете повторно использовать один и тот же шаг, сведя к минимуму строки кода, и вам будет легко написать новые сценарии огурца.

Киран Редди
источник
0

Чтобы избежать неоднозначной ошибки в огурце.

Решение 1

first("#tag1").click

Решение 2

Cucumber features/filename.feature --guess
Аравин
источник