Cypress Commands für Vuetify

In diesem Artikel möchte ich Euch erklären, wie wir HTML-Elemente mit Cypress anzusteuern, wenn ein Framework wie Vuetify zum Einsatz kommt.

Elemente im DOM finden

In Cypress ist es Best Practice, die HTML-Elemente per data-attribute anzusteuern. Will man beispielsweise einen Button klicken, setzt man auf genau dieses Element ein data-cy Attribut (vgl. https://docs.cypress.io/guides/references/best-practices#How-It-Works) und kann den Button gezielt ansteuern. Außerdem ist es für alle anderen Entwickler ersichtlich, dass über dieses Element ein Test ausgeführt wird, was bei einem Ansteuern über Tag, Klasse oder ID nicht gegeben ist.

In der Theorie klingt das schlüssig; aber wie benutzt man data-cy Attribute, wenn man zusätzlich ein Framework wie Vuetify einsetzt, bei dem man keine volle Kontrolle über das gerenderte HTML hat? 

Cypress und generiertes HTML

Nehmen wir ein einfaches Beispiel: Ich möchte in einem Select einen Wert auswählen. Dafür stellt Vuetify das <v-select> zur Verfügung (siehe https://vuetifyjs.com/en/components/selects/#examples).
Das Beispiel in Codepen: https://codepen.io/cbruchwaldnetfonds/pen/dyVVGVN?editors=1010

In der Vue Komponente fügen wir an diesem Element das data-cy Attribut hinzu und nennen es “StateSelect”. Durch diese Benennung wird beim Lesen des Tests direkt klar, dass es sich bei dem Element um ein Select handelt und nicht etwa um ein Input, eine Radio-Auswahl oder einen einfachen Text.

Das von Vuetify erzeugte HTML sieht nun aber ganz anders aus als der von uns geschriebene Code: unser Attribut wird in der Komponente zwar durchgereicht, jedoch steht es tief verschachtelt auf einem Input, das von anderen Elementen verdeckt wird. Das heißt darauf können wir mit cy.get(`data-cy=”StateSelect”`).click() nicht zugreifen, um das Select zu öffnen. Stattdessen nutzen wir cy.get(`data-cy=”StateSelect”`).click({ force: true }), um das Element anzuklicken, obwohl es verdeckt ist. Alternativ können wir auch ein Elternelement auswählen mit cy.get(`data-cy=”StateSelect”`).parents(…).click().

<div
  role="button"
  aria-haspopup="listbox"
  aria-expanded="false"
  aria-owns="list-3"
  class="v-input__slot"
>
  <div class="v-select__slot">
    <label
      for="input-3"
      class="v-label v-label--active theme--light primary--text"
      style="left: 0px; right: auto; position: absolute"
      >Select</label
    >
    <div class="v-select__selections">
      <input
        data-cy="StateSelect"
        id="input-3"
        readonly="readonly"
        type="text"
        aria-readonly="false"
        autocomplete="off"
      />
    </div>
    ...
  </div>
  ...
</div>

Anmerkung: Das data-cy=”StateSelect” finden wir auf dem Input wieder, nicht auf dem Wrapper auf den geklickt werden soll.

Nun öffnet sich das Overlay, in dem die Optionen aufgelistet werden. Das setzt Vuetify mit einem <v-menu> um. Sofern es nicht mittels der “append” Prop anders konfiguriert wurde, wird es vollkommen losgelöst vom Elternelement (dem Select) ins HTML eingehängt.

<div id="app">
  <div data-app="true" class="v-application v-application--is-ltr theme--light" id="inspire">
    <div class="v-application--wrap">...</div>
    <div
      class="v-menu__content theme--light menuable__content__active"
      style="
        max-height: 304px;
        min-width: 351px;
        top: 12px;
        left: 387px;
        transform-origin: left top;
        z-index: 8;
      "
    >
      ...
    </div>
  </div>
</div>

Anmerkung: Das Overlay hat keine Verbindung zu dem Select, durch das es geöffnet wurde.

Da das Menü zudem zur Laufzeit generiert wird, haben wir keine Möglichkeit, dort ein data-cy Attribut zu benutzen. In dem Fall müssen wir das Menü über eine Klasse ansprechen wie z.B. mit cy.get('.menuable__content__active').

Diese “active” Klasse sorgt dafür, dass Cypress nach dem geöffneten Menü sucht und keins ansteuert, das eventuell noch im DOM hängt, aber zugeklappt ist. Nun können wir innerhalb dieses Menüs auf den Textstring klicken.

cy.get('.menuable__content__active').contains('Georgia').click();

Damit das Overlay nach dem Klick auf den Namen geschlossen wird (falls das Select Mehrfacheingaben zulässt), lassen wir Cypress am Ende noch auf den Body klicken.

cy.get('body').click(0, 0);

Als Ergebnis haben wir also diese Sequenz:

cy.get(`data-cy=”StateSelect”`).click({ force: true });
cy.get('.menuable__content__active').contains('Georgia').click();
cy.get('body').click(0, 0);

Beim Ausfüllen komplexerer Formulare werden die Nachteile deutlich:

  1. Code wird oft wiederholt
  2. Der Test wird unübersichtlich und schwer lesbar 
  3. Anpassungen von Vuetify (Klassen, Struktur) müssen an jeder Stelle gepflegt werden
  4. Jeder Tester könnte den Code dafür anders schreiben; uneinheitlich und unübersichtlich

Aus diesen Gründen haben wir uns dafür entschieden für die gängigen Vuetify Komponenten wie Input, Radio, Checkbox und Select unsere eigenen Commands zu schreiben.

Globale Vuetify Commands

Ein globaler Command soll in möglichst vielen Fällen Anwendung finden. Aus diesem Grund ergänzen wir noch diese Anforderungen:

  1. Manchmal wollen wir im Cypress Test z.B. nicht nach einem bestimmten Text suchen (der sich ggf. durch Übersetzungen oder Kontext ändern kann), sondern eine Option anhand der Position auswählen, z.B. bei einem Select einfach die erste Option anklicken. 
  2. Wenn ein Select mehrfach existiert (z.B. automatisch generiert), soll es mittels .eq() möglich sein ein bestimmtes Select anzusteuern.

Der fertige Command sieht dann so aus:

Cypress.Commands.add(
  'vSelect',
  (dataElementName, textOrPosition, dataElementPosition = 0, closeOverlay = true) => {
    cy.get(`[data-cy="${dataElementName}"]`).eq(dataElementPosition).click({ force: true });
 
    if (typeof textOrPosition === 'string') {
      cy.get('.menuable__content__active').contains(textOrPosition).click();
    } else {
      cy.get('.menuable__content__active .v-select-list')
        .find('.v-list-item')
        .eq(textOrPosition)
        .click();
    }
    if (closeOverlay) {
      cy.get('body').click(0, 0);
    }
  }
);

Im Test selbst lässt sich der Command viel besser lesen, da sich dort nur noch die wesentlichen Informationen befinden. Das sähe dann so aus:

cy.vSelect('depotSelect', 'Depot Foo');
cy.vSelect('kontoSelect', 2);

Fazit

Mit den Cypress eigenen Bordmitteln lassen sich einfache Helfer-Commands schreiben, die den Umgang mit Framework-HTML stark vereinfacht. Außerdem tragen sie dazu bei den Test einheitlicher, wartbarer und lesbarer zu schreiben.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.