Résolution d'ambiguïté Capybara


97

Comment résoudre l'ambiguïté à Capybara? Pour une raison quelconque, j'ai besoin de liens avec les mêmes valeurs dans une page mais je ne peux pas créer de test car j'obtiens l'erreur

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

La raison pour laquelle je ne peux pas éviter cela est à cause de la conception. J'essaie de recréer la page Twitter avec des tweets / tags à droite et les tags à gauche de la page. Par conséquent, il sera inévitable que des pages de liens identiques apparaissent sur la même page.


Pouvez-vous également poster un code?
Heena Hussain

8
Vous ne devriez pas attribuer le même identifiant à deux éléments de la page. Si vous avez des liens identiques, n'attribuez pas d'identifiant aux éléments, utilisez plutôt une classe.
Chris Salzberg

Réponses:


147

Ma solution est

first(:link, link).click

au lieu de

click_link(link)

6
Ceci est détaillé dans le Guide de mise à niveau Capybara qui peut vous être utile si vous rencontrez ce problème.
Ritchie

1
À partir de Capybara 2.0, ne le faites pas sauf si vous devez absolument le faire. Voir la réponse de @ Andrey ci-dessous et l'explication des correspondances ambiguës dans le guide de mise à niveau lié ci-dessus.
jim

4
Plus précisément, Capybara 2.0 dispose d'une logique d'attente intelligente pour garantir que les spécifications réussissent ou échouent de manière cohérente sur des machines de vitesses de traitement différentes tout en n'attendant que le temps minimum nécessaire. Utiliser firstcomme suggéré ci-dessus, à moins que vous ne sachiez absolument ce que vous faites, est susceptible d'entraîner des spécifications qui passent pour vous mais qui échouent dans une construction CI ou sur la machine d'un collègue.
jim

1
Pour une bonne discussion, voir: robots.thoughtbot.com/…
jim

74

Un tel comportement de Capybara est intentionnel et je pense qu'il ne devrait pas être corrigé comme suggéré dans la plupart des autres réponses.

Les versions de Capybara antérieures à 2.0 ont renvoyé le premier élément au lieu de lever une exception, mais les responsables ultérieurs de Capybara ont décidé que c'était une mauvaise idée et qu'il était préférable de la soulever. Il a été décidé que dans de nombreuses situations, le retour du premier élément conduit à ne pas renvoyer l'élément que le développeur voulait renvoyer.

La réponse la plus votée ici recommande d'utiliser firstou allau lieu de findmais:

  1. allet firstn'attendez pas que l'élément avec un tel localisateur apparaisse sur la page bien findqu'il attend
  2. all(...).firstet firstne vous protégera pas de la situation où à l'avenir un autre élément avec un tel localisateur peut apparaître sur la page et, par conséquent, vous pouvez trouver un élément incorrect

Il est donc conseillé de choisir un autre localisateur, moins ambigu : par exemple, sélectionnez un élément par id, classe ou autre localisateur css / xpath afin qu'un seul élément le corresponde.


En guise de note, voici quelques localisateurs que je considère généralement utiles pour résoudre une ambiguïté:

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

    C'est plus utile que first('ul > li')d'attendre que le premier liapparaisse sur la page.

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

    C'est mieux que first(:link, 'Create Account').clickd'attendre qu'au moins un lien Créer un compte apparaisse sur la page. Cependant, je pense qu'il est préférable de choisir un localisateur unique qui n'apparaît pas deux fois sur la page.

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

    exact: true dit à Capybara de ne trouver que les correspondances exactes, c'est-à-dire de ne pas trouver "Confirmation du mot de passe"


7
Cela devrait être la meilleure réponse. Essayez toujours d'utiliser un sélecteur qui utilisera les capacités d'attente intégrées de Capybara.
tgf

Merci. J'ai essayé d'utiliser: d'abord mais j'ai réalisé que cela ne fonctionne que dans jQuery. Ce que je cherchais, c'est: first-child
Overload119


24

NOUVELLE RÉPONSE:

Vous pouvez essayer quelque chose comme

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

Il peut y avoir un moyen de le faire qui utilise mieux la syntaxe Capybara disponible - quelque chose du genre, all("a[text='#tag1']").first.clickmais je ne peux pas penser à la syntaxe correcte et je ne trouve pas la documentation appropriée. Cela dit , il est un peu une situation étrange pour commencer, avec deux <a>étiquettes avec le même id, classet le texte. Y a-t-il une chance qu'ils soient des enfants de divs différents, puisque vous pourriez alors faire votre find withinsegment approprié du DOM. (Il serait utile de voir un peu de votre source HTML).


ANCIENNE RÉPONSE: (où je pensais que '# tag1' signifiait que l'élément avait un id"tag1")

Sur lequel des liens voulez-vous cliquer? Si c'est le premier (ou peu importe), vous pouvez faire

find('#tag1').click

Sinon tu peux faire

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

pour cliquer sur le second.


Cette solution sur la première peut être efficace, mais le problème maintenant est qu'elle peut être confondue avec un identifiant css --------- Échec / Erreur: find ('# tag1'). Cliquez sur # ou tout ('# tag1 ') [0] .click Capybara :: ElementNotFound: Impossible de trouver le css "# tag1"
neilmarion

find('#tag1')signifie que vous ne voulez trouver qu'un seul élément avec id tag1. Une exception est soulevée car il y a plusieurs éléments avec identifiant tag1sur la page
Andrei Botalov

Vous pouvez le faire all(:xpath, '//a[text()="#tag1"]').first.click.
Shuhei Kagawa

9

Vous pouvez vous assurer de trouver le premier en utilisant match:

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

Mais surtout, vous ne voulez probablement pas faire cela , car cela conduira à des tests fragiles qui ignorent l'odeur du code de sortie en double, ce qui conduit à son tour à des faux positifs qui continuent de fonctionner alors qu'ils auraient dû échouer, car vous en avez supprimé un correspondant. élément mais le test a heureusement trouvé l'autre.

Le mieux est d'utiliser within:

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

Cela garantit que vous trouvez l'élément que vous vous attendez à trouver, tout en tirant parti des capacités d'attente automatique et de relance automatique de Capybara (que vous perdez si vous utilisez find('.selector').click), et cela rend beaucoup plus clair l'intention.


7

Pour compléter le corpus de connaissances existant ici:

Pour les tests JS, Capybara doit garder deux threads (un pour RSpec, un pour Rails) et un second processus (le navigateur) synchronisés. Pour ce faire, il attend (jusqu'au temps d'attente maximal configuré) dans la plupart des correspondeurs et des méthodes de recherche de nœuds.

Capybara a aussi des méthodes qui n'attendent pas, principalement Node#all. Les utiliser, c'est comme dire à vos spécifications que vous aimeriez qu'elles échouent par intermittence.

La réponse acceptée suggère page.first('selector'). Ceci est indésirable, au moins pour les spécifications JS, parce que les Node#firstutilisationsNode#all .

Cela dit, Node#first va attendre si vous configurez Capybara comme ceci:

# rails_helper.rb
Capybara.wait_on_first_by_default = true

Cette option a été ajoutée dans Capybara 2.5.0 et est fausse par défaut.

Comme Andrei l'a mentionné, vous devriez plutôt utiliser

find('selector', match: :first)

ou modifiez votre sélecteur. L'un ou l'autre fonctionnera bien indépendamment de la configuration ou du pilote.

Pour compliquer davantage les choses, dans les anciennes versions de Capybara (ou avec une option de configuration activée), #findignorera volontiers l'ambiguïté et renverra simplement le premier sélecteur correspondant. Ce n'est pas génial non plus, car cela rend vos spécifications moins explicites, ce qui, j'imagine, n'est plus le comportement par défaut. Je vais laisser de côté les détails car ils ont déjà été discutés ci-dessus.

Plus de ressources:


5

En raison de ce message , vous pouvez le corriger via l'option "match":

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

2

En considérant toutes les options ci-dessus, vous pouvez également essayer ceci

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

Si vous utilisez du concombre, vous pouvez également suivre ceci

Vous pouvez passer le texte en tant que paramètre des étapes du scénario qui peuvent être une étape générique à réutiliser

Quelque chose comme When a user clicks on "text" link

Et en étape de définition When(/^(?:user) clicks on "([^"]*)" (?:link)$/) do |text|

De cette façon, vous pouvez réutiliser la même étape en minimisant les lignes de code et il serait facile d'écrire de nouveaux scénarios de concombre


0

Pour éviter une erreur ambiguë dans le concombre.

Solution 1

first("#tag1").click

Solution 2

Cucumber features/filename.feature --guess
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.