How do I verify I've selected the correct user from a table in Cypress (html-table / angular-material)

Viewed 1869

I'm trying to delete a specific user, and would rather the test fail then delete a different user if the user I want doesn't exist.

The issue, I can't seem to find a way to verify the checkbox is next to the user I want. I've been using a .each() nest to first find the user Mandy Smith, and then select the checkbox with the same index.

HTML: the HTML code

Note, I'm trying to delete the user "Mandy Smith" and while I can just use the following code to select her, depending on how quick the test gets to this point it might actually delete the first user.

Current code that deletes Mandy Smith:

         // looks for the checkbox related to Mandy Smith User
          cy.get('input[type="checkbox"]')
            .each(($elem, index) => {
                if(index === 2) {
                  cy.wrap($elem).click({force:true});
                }
            });

While this will generally grab her, depending how quickly filters are being cleared it can actually choose the Automated User for deletion. I of course don't want this, I'm trying to find a good way to verify the index Mandy Smith is at and apply that to her checkbox.

The checkbox is dynamic, so it will iterate up based on what is clicked, so i don't want to choose the exact checkbox with it's label "#mat-checkbox-11" as that's what it happens to be at while i'm looking at the code, but it was 5 earlier.

I thought this would let me find the index and apply it to the checkbox:

// looks for the checkbox related to Mandy Smith User
    it(`should select Mandy, through verification that it's her index`, () => {
        cy.get('.mat-column-name.ng-star-inserted')
          .each(($name, i) => {
            if($name === 'Mandy Smith') {
                let dex = i;
                cy.get('input[type="checkbox"]')
                  .each(($elem, index) => {
                    if(index === dex) {
                        cy.wrap($elem).click({force:true});
                    }
                });
            }
        });
    });

This actually just ends up deleting the Automated User instead.

I did run some tests and found the string it's returning is this, " Automated User Mandy Smith ". So, even though I'm iterating through the table, it seems to pull both text fields. Am I missing something on how to grab a single set of text, when all the class names are the same?

This means it deletes the first user as the if statement will show Mandy Smith, and then choose the first user which is Automated User instead of the second user, Mandy Smith. Maybe I need to know how to grab text from each class with the same name.

I don't know, I'm a little lost now on how to potentially do this.

EDIT: I found I can invoke text within the if statement, but it never gets within the if statement. I checked what it yielded and pasted that into my if and it still didn't work. Here is the code that is grabbing the elements innerText, and it's meant to compare to the word. Is there a reason it wouldn't evaluate true?

    it(`should select Mandy, through verification that it's her`, () => {
        cy.get('.mat-cell.cdk-cell.text-capitalize.cdk-column-name.mat-column-name')
          .each(($name, i) => {
            //let columnText = ($name).invoke('text');
            if(cy.get($name).invoke('text') === ' Mandy Smith ') {
                cy.log('it made it inside the if');
            }
        });
    });

enter image description here

EDIT 7/15/2020: Here is an image of the entire table. Yellow arrow is her row, right above that is user 1's row. Green arrow is her checkbox, red arrow is her name from the name column

html image

Thanks for everyone's posts. I'm going to be messing with it today and test the various comments and see if I can find a solution. Feel free to keep giving feedback, as I'll check throughout the day.

Solved Thanks everyone here is the answer, I got on the phone with a senior developer and after seeing what I was doing he ran me through a few things. Thanks to @oldschooled for his awesome answer, and the follow up @eric99 for offering breaking it down a bit more for me as well. You two are rockstars.

Answer

    it(`should select Mandy Smith with contains only`, () => {
        cy.contains('td', 'Mandy Smith').siblings().eq(0).children().eq(0).click();
    });
3 Answers

@OldSchool's explanation is correct but you did not follow it correctly.

In comments you show

cy.get('.mat-cell.cdk-cell.text-capitalize.cdk-column-name.mat-column-name')
  .invoke('text')                // REMOVE THIS COMMAND
  .contains('Mandy Smith');

The problem is invoke('text') after a get() that selects multiple elements will concatenate all the text of all those elements (i.e all cells in the name column).

In contrast, .get().contains('Mandy') selects the single cell containing 'Mandy', then using sibling() restricts the next part (finding the checkbox) to that table row only.

If you have one and only one table on the page, I would use the following as it's the shortest and uses Cypress selector power to maximum effect.

Soultion

cy.contains('td', 'Mandy Smith')
  .siblings('td')   // get all sibling cells in the 'Mandy Smith' row
  .find('input')    // find the input within these cells
  .click();

Note, if table data is fetched asynchronously, cy.contains(selector, content) is preferred over cy.get(selector).contains(content) since the first one will retry until content arrives.


I amended the Soultion code above after testing it out on the Angular Material Grid example page, using the example 'Table with selection' (searching for 'Helium' which is in the 2nd row).

It turns out the selector used in .siblings(selector) cannot handle the same complexity as .get(selector), e.g

.get('td input')      // succeeds and finds all input cells

.siblings('td input') // fails to find the input, although it does exist

So, using .siblings('td').find('input') gets to the correct place.

From the .find() docs

Get the descendent DOM elements of a specific selector

i.e .find() restricts the search to within the previous subject(s), which are the sibling cells.


This works on the Angular Material Grid example page. If you still have no luck, please post your actual html of the first two rows as text rather than picture, and I will investigate.


If you have more than one grid on the page you will need to enhance the selector in order to initially narrow it down to the required grid.


Exploratory testing

Cypress has a neat feature that allows you to experiment with different commands on a fragment of the DOM.

  • Copy the fragment of HTML that you are working on into a new .html file, placing that file under the /cypress folder, e.g /cypress/myFragment.html.

  • The content of the fragment html file does not need to be a complete HTML doc, you can omit the header and body tags. In this case you need a <table> tag, a <tbody> tag, and paste in between the first two rows, which are enough for this experiment. (Don't forget to close </table> and </tbody>).

  • Write an experimental spec that visits the fragment HTML, e.g cy.visit('cypress/myFragment.html'). Cypress will load this the same as a full HTML page.

  • Follow the cy.visit() with the commands you want to experiment with, e.g the Solution code above.

  • Run just that spec and adjust the commands until they work.

[Edit: check @eric99's additional answer below for a great explanation.]


cy.get('.mat-column-name.ng-star-inserted').contains('mandy smith')

should return the base element you're looking for if the name is a unique identifier within those .mat-column-name elements that matches only one element.

From there, you should be able to do whatever you want with confidence that you have Mandy Smith, like navigating to child/sibling/parent elements and finding a clickable element (e.g. checkbox), making other assertions, etc. This is assuming that the input field has some relation to the name column, i.e. both elements exists in the same row or parent element.

If both elements (name-column and checkbox input) are in the same row, there is no need to cycle through each element returned by the get(), just jump straight to the one containing what you want, then navigate to the sibling input element located within the same row element.

e.g. if your html is structured similar to this:

<tr tid='the-rowest-of-rows'>
  <td tid='the-checkist-of-checkboxes'><input tid='bam' ...></td>
  <td tid='name-column'>The mandiest Mandy Smith</td>
</tr>   

then your selector would look something like.

cy.get('[tid=name-column]').contains('Mandy Smith')
  .siblings('[tid=the-checkist-of-checkboxes]')
  .find('[tid=bam]')
  .click()

(Note: I'm using tids instead of class selectors in the get())

Because I'm not exactly sure how your components are setup (we'd need the raw html surrounding this example, including the input for that, which is missing in your example), I'm guessing it would look something like that.

Depending on how your html is laid out, the relationship could vary and you could need sibling, parent or some variant/mixture of those. The point of this all, however, is:

tl;dr: use get().contains() to retrieve the Mandy Smith element. Navigate from there to child/sibling/parent elements as necessary.

Further reading:

As a side note, for further reading I'd recommend checking out:

paying particular attention to using tids for selecting elements. Selecting by class is, generally, a bad idea. More importantly though, a tid could help you out here. Depending on your frontend framework, you could dynamically add a tid directly to the element you want to click that would correlate to the name you're searching for (i.e. <input tid='input-${name.last}'>) which could lead you directly to the money click.

Also, if you're interested in more information on why your if statements aren't working as expected, the following is a pretty awesome read:

I hope this makes sense to you. Sorry for writing a book. If it doesn't make sense or you have any questions, I'm happy to try and answer them.

Related