Rozwiązanie niejednoznaczności Kapibary

97

Jak rozwiązać dwuznaczność w Kapibara? Z jakiegoś powodu potrzebuję linków z tymi samymi wartościami na stronie, ale nie mogę utworzyć testu, ponieważ pojawia się błąd

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

Powodem, dla którego nie mogę tego uniknąć, jest projekt. Próbuję odtworzyć stronę twittera z tweetami / tagami po prawej stronie i tagami po lewej stronie. Dlatego nieuniknione będzie, że identyczna strona z linkami pojawi się na tej samej stronie.

neilmarion
źródło
Czy możesz również wysłać kod?
Heena Hussain
8
Nie powinieneś przypisywać tego samego identyfikatora do dwóch elementów na stronie. Jeśli będziesz mieć identyczne linki, nie przypisuj identyfikatora do elementów, zamiast tego użyj klasy.
Chris Salzberg

Odpowiedzi:

147

Moje rozwiązanie to

first(:link, link).click

zamiast

click_link(link)
e-cynk
źródło
6
Jest to szczegółowo opisane w Przewodniku aktualizacji Kapibary, który może okazać się przydatny, jeśli masz ten problem.
Ritchie
1
Od wersji Capybara 2.0 nie rób tego, chyba że absolutnie musisz. Zobacz odpowiedź @ Andrey poniżej i wyjaśnienie niejednoznacznych dopasowań w przewodniku aktualizacji, do którego link znajduje się powyżej.
jim
4
W szczególności Capybara 2.0 ma inteligentną logikę oczekiwania, która zapewnia, że ​​specyfikacje przechodzą lub zawodzą konsekwentnie na maszynach o różnych prędkościach przetwarzania, czekając tylko na minimalny niezbędny czas. Używanie firstzgodnie z powyższą sugestią, chyba że absolutnie wiesz, co robisz, prawdopodobnie spowoduje, że specyfikacje będą dla ciebie dobre, ale zawiodą w kompilacji CI lub na komputerze kolegi.
jim
1
Dobra dyskusja: robots.thoughtbot.com/…
jim
74

Takie zachowanie Kapibary jest celowe i uważam, że nie powinno się tego naprawiać, jak sugeruje większość innych odpowiedzi.

Wersje Kapibary przed 2.0 zwróciły pierwszy element zamiast zgłaszać wyjątek, ale później opiekunowie Kapibary uznali, że to zły pomysł i lepiej go podnieść. Uznano, że w wielu sytuacjach zwrócenie pierwszego elementu prowadzi do zwrócenia nie tego elementu, który deweloper chciał zwrócić.

Najbardziej pozytywną odpowiedzią tutaj jest użycie firstlub allzamiast findale:

  1. alli firstnie czekaj, aż element z takim lokalizatorem pojawi się na stronie, chociaż findczeka
  2. all(...).firsti firstnie uchroni Cię przed sytuacją, że w przyszłości na stronie może pojawić się kolejny element z takim lokalizatorem iw rezultacie możesz znaleźć nieprawidłowy element

Dlatego zaleca się wybranie innego, mniej niejednoznacznego lokalizatora : na przykład wybierz element według identyfikatora, klasy lub innego lokalizatora css / xpath, aby pasował do niego tylko jeden element.


Uwaga: niektóre lokalizatory, które zwykle uważam za przydatne przy rozwiązywaniu niejednoznaczności:

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

    Jest to bardziej przydatne niż first('ul > li')to, że będzie czekało do pierwszegoli pojawi się na stronie.

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

    To jest lepsze niż first(:link, 'Create Account').click będzie czekać, aż na stronie pojawi się co najmniej jeden link Utwórz konto. Uważam jednak, że lepiej jest wybrać unikalny lokalizator, który nie pojawia się na stronie dwukrotnie.

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

    exact: true mówi Kapibarze, aby znalazł tylko dokładne dopasowania, tj. nie znalazł „Potwierdzenia hasła”

Andrei Botalov
źródło
7
To powinna być najlepsza odpowiedź. Zawsze staraj się używać selektora, który będzie korzystał z wbudowanych funkcji oczekiwania w Kapibara.
tgf
Dzięki. Próbowałem użyć: najpierw, ale zdałem sobie sprawę, że działa tylko w jQuery. To, czego szukałem, to: pierwsze dziecko
Overload119
24

NOWA ODPOWIEDŹ:

Możesz spróbować czegoś takiego

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

Może istnieć sposób, aby to zrobić, który lepiej wykorzystuje dostępną składnię Kapibary - coś podobnego, all("a[text='#tag1']").first.clickale nie mogę wymyślić prawidłowej składni od ręki i nie mogę znaleźć odpowiedniej dokumentacji. Który powiedział, że jest to trochę dziwnej sytuacji, aby rozpocząć, posiadające dwa <a>tagi z tego samego id, classi tekstu. Czy jest jakaś szansa, że ​​są dziećmi różnych elementów div, skoro możesz wtedy utworzyć find withinodpowiedni segment DOM. (Przydałoby się zobaczyć trochę twojego źródła HTML).


STARE ODPOWIEDŹ: (gdzie pomyślałem, że „# tag1” oznacza, że ​​element ma id„tag1”)

Który z linków chcesz kliknąć? Jeśli to pierwsza (lub nie ma to znaczenia), możesz to zrobić

find('#tag1').click

W przeciwnym razie możesz to zrobić

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

kliknij drugi.

Amit Kumar Gupta
źródło
To rozwiązanie na pierwszym może działać, ale teraz problem polega na tym, że może być mylony z identyfikatorem CSS --------- Błąd / Błąd: znajdź ('# tag1'). Kliknij # lub all ('# tag1 ') [0] .click Kapibara :: ElementNotFound: Nie można znaleźć css "#
tag1
find('#tag1')oznacza, że ​​chcesz znaleźć tylko jeden element o identyfikatorze tag1. Jest wyjątek gdyż istnieje kilka elementów z identyfikatorem tag1na stronie
Andrei Botalov
Możesz to zrobić all(:xpath, '//a[text()="#tag1"]').first.click.
Shuhei Kagawa
9

Możesz upewnić się, że znajdziesz pierwszy, używając match:

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

Ale co ważne, prawdopodobnie nie chcesz tego robić , ponieważ doprowadzi to do kruchych testów , które ignorują zapach kodu duplikatów danych wyjściowych, co z kolei prowadzi do fałszywych alarmów, które działają, gdy powinny się nie udać, ponieważ usunąłeś jeden pasujący element, ale test szczęśliwie odnalazł drugi.

Lepiej jest użyć within:

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

Gwarantuje to, że znajdujesz element, którego spodziewasz się znaleźć, jednocześnie wykorzystując możliwości automatycznego czekania i automatycznego ponawiania kapibary (które tracisz, jeśli używasz find('.selector').click), i sprawia, że ​​jest znacznie jaśniejszy, jaki jest zamiar.

TALlama
źródło
7

Aby dodać do istniejącej wiedzy tutaj:

W przypadku testów JS Capybara musi utrzymywać synchronizację dwóch wątków (jednego dla RSpec, jednego dla Railsów) i drugiego procesu (przeglądarki). Odbywa się to poprzez czekanie (do skonfigurowanego maksymalnego czasu oczekiwania) w większości dopasowań i metod wyszukiwania węzłów.

Kapibara ma również metody, które przede wszystkim nie czekają Node#all. Korzystanie z nich jest jak mówienie specyfikacjom, że chcesz, aby okresowo zawodziły.

Przyjęta odpowiedź sugeruje page.first('selector'). Jest to niepożądane, przynajmniej w przypadku specyfikacji JS, ponieważ Node#firstużywaNode#all .

Powiedział, że Node#first będzie czekać, jeśli skonfigurować Capybara tak:

# rails_helper.rb
Capybara.wait_on_first_by_default = true

Ta opcja została dodana w Kapibara 2.5.0 i domyślnie jest fałszywa.

Jak wspomniał Andrei, powinieneś zamiast tego użyć

find('selector', match: :first)

lub zmień selektor. Albo będzie działać dobrze niezależnie od konfiguracji lub sterownika.

Aby jeszcze bardziej skomplikować sprawę, w starszych wersjach Kapibary (lub z włączoną opcją konfiguracji) #findz radością zignoruje niejednoznaczność i zwróci pierwszy pasujący selektor. To też nie jest świetne, ponieważ sprawia, że ​​twoje specyfikacje są mniej wyraźne, co, jak sądzę, jest powodem, dla którego nie jest już domyślnym zachowaniem. Pominę szczegóły, ponieważ zostały już omówione powyżej.

Więcej zasobów:

johncip
źródło
5

W związku z tym postem możesz to naprawić za pomocą opcji „dopasuj”:

Capybara.configure do |config|
  config.match = :prefer_exact
end
Skydan
źródło
2

Biorąc pod uwagę wszystkie powyższe opcje, możesz spróbować również tego

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

Jeśli używasz ogórka, również możesz to zrobić

Możesz przekazać tekst jako parametr z kroków scenariusza, który może być ogólnym krokiem do ponownego użycia

Coś jak When a user clicks on "text" link

I w definicji kroku When(/^(?:user) clicks on "([^"]*)" (?:link)$/) do |text|

W ten sposób możesz ponownie użyć tego samego kroku, minimalizując linie kodu i łatwo byłoby napisać nowe scenariusze dotyczące ogórków

Kiran Reddy
źródło
0

Aby uniknąć niejednoznacznych błędów w ogórku.

Rozwiązanie 1

first("#tag1").click

Rozwiązanie 2

Cucumber features/filename.feature --guess
Aravin
źródło