web-dev-qa-db-fra.com

Élément d'accès dont le parent est masqué - cypress.io

La question est telle que donnée dans le titre, c'est-à-dire d'accéder à l'élément dont le parent est caché. Le problème est que, selon les documents cypress.io :

Un élément est considéré caché si:

  • Sa largeur ou hauteur est 0.
  • Sa propriété CSS (ou ancêtres) est visibilité: masqué.
  • sa propriété CSS (ou ses ancêtres) est display: none.
  • Sa propriété CSS est position: fixed et elle est masquée ou masquée.

Mais le code avec lequel je travaille nécessite que je clique sur un élément dontle parent est caché, l'élément lui-même étant visible.

Ainsi, chaque fois que j'essaie de cliquer sur l'élément, une erreur est générée:

CypressError: Nouvelles tentatives ayant expiré: attendu '<<Mdc-select-item # mdc-select-item-4.mdc-list-item>' pour être 'visible'

Cet élément '<mdc-select-item # mdc-select-item-4.mdc-list-item>' est Non visible car son parent '<Mdc-select-menu.mdc -simple-menu.mdc-select__menu> 'possède la propriété CSS: ' display: none '

 enter image description here

L'élément sur lequel je travaille est un dropdown item, qui est écrit dans pug. L'élément est un composant défini dans angular-mdc-web , qui utilise le mdc-select pour le menu déroulant et le mdc-select-item pour ses éléments (éléments), ce à quoi je dois accéder.

Un exemple de code de structure similaire:

//pug
mdc-select(placeholder="installation type"
            '[closeOnScroll]'="true")
    mdc-select-item(value="false") ITEM1
    mdc-select-item(value="true") ITEM2

Dans ce qui précède, ITEM1 est l'élément auquel je dois accéder. Ce que je fais dans cypress.io comme suit:

//cypress.io
// click on the dropdown menu to show the dropdown (items)
cy.get("mdc-select").contains("installation type").click();
// try to access ITEM1
cy.get('mdc-select-item').contains("ITEM1").should('be.visible').click();

J'ai essayé avec {force:true} de forcer l'élément à cliquer, mais pas de chance. J'ai essayé de sélectionner les éléments à l'aide de {enter} sur la touche mdc-select du parent, mais là encore, aucune chance car elle jette: 

CypressError: cy.type () ne peut être appelé que sur textarea ou: text. Votre sujet Est un: <mdc-select-label Class = "mdc-select__selected-text"> Sélectionnez ... </ mdc-select-label>

Également essayé d'utiliser la commande select , mais ce n'est pas possible car le moteur Cypress n'est pas en mesure d'identifier l'élément en tant qu'élément select (car ce n'est pas le cas, le fonctionnement interne est différent). Il jette:

CypressError: cy.select () ne peut être appelé que sur un. Votre sujet Est un: <mdc-select-label Class = "mdc-select__selected-text"> Sélectionnez ... </ mdc-select-label>

Le problem est que le mdc-select-menu qui est le parent du mdc-select-item a la propriété display:none selon certains calculs internes lors de l’ouverture des éléments déroulants.

 enter image description here

Cette propriété est écrasée en display:flex, mais cela n’aide en rien.

 enter image description here

Tout en idées. Cela fonctionne dans Selenium, mais pas avec cypress.io. Avez-vous une idée de ce que pourrait être un bidouillage possible pour la situation autre que de passer à d'autres frameworks ou de changer le code de l'interface utilisateur?

8
Kaushik NP

Après beaucoup de grincements de dents, je pense avoir une réponse. 

Je pense que la cause fondamentale est que mdc-select-item a display:flex, ce qui lui permet de dépasser les limites de ses parents (à proprement parler, cela ressemble à une mauvaise application de flex d'affichage, si je me souviens bien du tutoriel, cependant ...). 

Cypress effectue beaucoup de vérifications par les parents lors de la détermination de la visibilité, voir visibilité.coffee ,

## WARNING:
## developer beware. visibility is a sink hole
## that leads to sheer madness. you should
## avoid this file before its too late.
...
when $parent = parentHasDisplayNone($el.parent())
  parentNode = $elements.stringify($parent, "short")

  "This element '#{node}' is not visible because its parent '#{parentNode}' has CSS property: 'display: none'"
...
when $parent = parentHasNoOffsetWidthOrHeightAndOverflowHidden($el.parent())
  parentNode  = $elements.stringify($parent, "short")
  width       = elOffsetWidth($parent)
  height      = elOffsetHeight($parent)

  "This element '#{node}' is not visible because its parent '#{parentNode}' has CSS property: 'overflow: hidden' and an effective width and height of: '#{width} x #{height}' pixels."

Mais, lorsque vous utilisez .should('be.visible'), nous sommes bloqués avec les propriétés parent qui échouent à la vérification de la visibilité de l'enfant, même si nous pouvons réellement voir l'enfant.
Nous avons besoin d’un autre test. 

Le contournement

Ref jquery.js , il s'agit d'une définition de la visibilité de l'élément lui-même (en ignorant les propriétés parent).

jQuery.expr.pseudos.visible = function( elem ) {
  return !!( elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length );
}

nous pourrions donc utiliser cela comme base pour une alternative.

describe('Testing select options', function() {

  // Change this function if other criteria are required.
  const isVisible = (elem) => !!( 
    elem.offsetWidth || 
    elem.offsetHeight || 
    elem.getClientRects().length 
  )

  it('checks select option is visible', function() {

    const doc = cy.visit('http://localhost:4200')
    cy.get("mdc-select").contains("installation type").click()

    //cy.get('mdc-select-item').contains("ITEM1").should('be.visible') //this will fail
    cy.get('mdc-select-item').contains("ITEM1").then (item1 => {
      expect(isVisible(item1[0])).to.be.true
    });
  });

  it('checks select option is not visible', function() {

    const doc = cy.visit('http://localhost:4200')
    cy.get("mdc-select").contains("installation type").click()

    cy.document().then(function(document) {

      const item1 = document.querySelectorAll('mdc-select-item')[0]
      item1.style.display = 'none'

      cy.get('mdc-select-item').contains("ITEM1").then (item => {
        expect(isVisible(item[0])).to.be.false
      })
    })
  });

  it('checks select option is clickable', function() {

    const doc = cy.visit('http://localhost:4200')
    cy.get("mdc-select").contains("installation type").click()

    //cy.get('mdc-select-item').contains("ITEM1").click()    // this will fail
    cy.get('mdc-select-item').contains("ITEM1").then (item1 => {

      cy.get('mdc-select-item').contains("ITEM2").then (item2 => {
        expect(isVisible(item2[0])).to.be.true  //visible when list is first dropped
      });

      item1.click();
      cy.wait(500)

      cy.get('mdc-select-item').contains("ITEM2").then (item2 => {
        expect(isVisible(item2[0])).to.be.false  // not visible after item1 selected
      });
    });

  })

Note de bas de page - Utilisation de 'then' (ou 'each')  

La manière dont vous utilisez normalement l'assertion dans cyprès se fait via des chaînes de commandes, qui englobent les éléments testés et traitent des tâches telles que les tentatives et l'attente des modifications du DOM. 

Cependant, dans ce cas, nous avons une contradiction entre l'assertion de visibilité standard .should('be.visible') et le cadre utilisé pour générer la page. Nous utilisons donc then(fn) ( ref ) pour accéder au DOM non enveloppé. Nous pouvons ensuite appliquer notre propre version du test de visibilité en utilisant la syntaxe de stand jasmine expect. 

Il s'avère que vous pouvez également utiliser une fonction avec .should(fn), cela fonctionne aussi

it('checks select option is visible - 2', function() {
  const doc = cy.visit('http://localhost:4200')
  cy.get("mdc-select").contains("installation type").click()

  cy.get('mdc-select-item').contains("ITEM1").should(item1 => {
    expect(isVisible(item1[0])).to.be.true
  });
});

L'utilisation de should au lieu de then ne fait aucune différence dans le test de visibilité, mais notez que la version should peut réessayer la fonction plusieurs fois; elle ne peut donc pas être utilisée avec le test click (par exemple).

De la docs,

Quelle est la différence entre .then () et .should () /. Et ()?

L'utilisation de .then () vous permet simplement d'utiliser le sujet renvoyé dans une fonction de rappel et doit être utilisée lorsque vous devez manipuler certaines valeurs ou effectuer certaines actions.

Lorsque vous utilisez une fonction de rappel avec .should () ou .and (), par contre, il existe une logique spéciale pour réexécuter la fonction de rappel jusqu'à ce qu'aucune assertion ne soit renvoyée à l'intérieur. Vous devez faire attention aux effets secondaires dans une fonction de rappel .should () ou .and () que vous ne voudriez pas exécuter plusieurs fois.

Vous pouvez également résoudre le problème en étendant les assertions chai, mais la documentation à ce sujet n’est pas exhaustive, elle risque donc d’être plus laborieuse.

5
Richard Matsen

Dans la documentation, Cypress select syntax , la syntaxe est la suivante:

cy.get('mdc-select-item').select('ITEM1')

Vous aurez peut-être aussi besoin du {force: true}. Voir ici select_spec.coffee pour des exemples de leurs propres tests, par exemple

it "can forcibly click even when element is invisible", (done) ->
  select = cy.$$("select:first").hide()
  select.click -> done()
  cy.get("select:first").select("de_dust2", {force: true})
1
Richard Matsen

Je suis tombé sur ce sujet mais je n'ai pas été capable d'exécuter votre exemple. J'ai donc essayé un peu et ma solution finale est la suivante. peut-être que quelqu'un d'autre a aussi besoin de ça. S'il vous plaît noter que j'utilise TypeScript.

Premièrement: Définir une commande personnalisée

Cypress.Commands.add("isVisible", { prevSubject: true}, (p1: string) => {
      cy.get(p1).should((jq: JQuery<HTMLElement>) => {
        if (!jq || jq.length === 0) {
            //assert.fail(); seems that we must not assetr.fail() otherwise cypress will exit immediately
            return;
        }

        const elem: HTMLElement = jq[0];
        const doc: HTMLElement = document.documentElement;
        const pageLeft: number = (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0);
        const pageTop: number = (window.pageYOffset || doc.scrollTop)  - (doc.clientTop || 0);
        let elementLeft: number;
        let elementTop: number;
        let elementHeight: number;
        let elementWidth: number;

        const length: number = elem.getClientRects().length;

        if (length > 0) {
            // TODO: select correct border box!!
            elementLeft = elem.getClientRects()[length - 1].left;
            elementTop = elem.getClientRects()[length - 1].top;
            elementWidth = elem.getClientRects()[length - 1].width;
            elementHeight = elem.getClientRects()[length - 1].height;
        }

        const val: boolean = !!( 
            elementHeight > 0 && 
            elementWidth > 0 && 
            elem.getClientRects().length > 0 &&
            elementLeft >= pageLeft &&
            elementLeft <= window.outerWidth &&
            elementTop >= pageTop &&
            elementTop <= window.outerHeight
        );

        assert.isTrue(val);
      });
});

S'il vous plaît noter le TODO. Dans mon cas, je visais un bouton qui a deux zones de bordure. Le premier avec la hauteur et la largeur 0. Je dois donc sélectionner le second. Veuillez adapter cela à vos besoins.

Deuxièmement: Utilisez-le

cy.wrap("#some_id_or_other_locator").isVisible();
0
Josef Biehler