4

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
    Could you show where the checkboxes are in your html? Because input checkbox is not visible on the screenshot – P D Jul 14 '20 at 07:13
  • Maybe I can suggest another approach like using cypress xpath. First install this: https://www.npmjs.com/package/cypress-xpath Then you can select the checkbox using xpath, I assume will be easier if we can see the code with the checkbox and use xpath contains text. – Sanja Paskova Jul 14 '20 at 10:27
  • @PD i have gone ahead and added the full HTML of the table start and end, and marked each point of interest with color coded arrows. Hopefully this helps, sorry about that, and thanks for the request. – SaintAvalon Jul 15 '20 at 19:45
  • 1
    You should prefer `.siblings('td').find('input')` over `.siblings().eq(0).children().eq(0)` since it's less susceptible to breaking if columns are changed. – Ackroydd Jul 18 '20 at 12:45
  • 1
    Also note Cypress has [`.check()`](https://docs.cypress.io/api/commands/check.html) and `.uncheck()` for checkboxes, which might be preferable to `.click()`. – Ackroydd Jul 18 '20 at 12:50

3 Answers3

3

@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.

eric99
  • 1,220
  • 6
  • 12
  • This explains it well and in a better format than possible in the comments. Thank you. – Old Schooled Jul 15 '20 at 07:39
  • Excellent comment, this really helped me understand contains versus should and invoke. Thanks! Might you look at the final picture in my post and see the relationship from Checkbox to Column Name? It comes before Mandy Smith, and the td is closed off after the checkbox is setup, then a new td is created for the column name.If you or @OldSchooled want to check the last picture and maybe give me an example of linking Mandy Smith to her checkbox that would really help my ignorance on the subject and give me a better idea how to use siblings / parents. Thanks to you both for continued effort. – SaintAvalon Jul 15 '20 at 19:22
2

[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.

Old Schooled
  • 922
  • 7
  • 17
  • This won't work, as noted the items will yield both names from the table. Your exact text will give me error you were looking for 'Mandy Smith' we found ' Automated User Mandy Smith'. It will grab every name in the column, that's why I'm using an if and iteration to split the names apart. Mayve if i can chain should off of invoke? I'll give that a good test run, i'll also chuck my check boxes up here. – SaintAvalon Jul 14 '20 at 16:09
  • I should note, the reason i was iterating for the Mandy Smith name column is to find her index, so i could use the index in my checkbox click. Once i know where she is at in the index, i can simply use her index for the checkbox selector. Your current method might get me her name in column, but then I have no clue how that helps me grab her checkbox since it's to the left in a different column. I won't get her index via your route, only maybe confirm she exists. ``` cy.get('.mat-cell.cdk-cell.text-capitalize.cdk-column-name.mat-column-name').invoke('text').contains('Mandy Smith'); ``` – SaintAvalon Jul 14 '20 at 16:34
  • Error, since it won't let me post that much in a comment: cy.contains() failed because it requires the subject be a global window object. The subject received was: > Automated User Mandy Smith The previous command that ran was: > cy.invoke() All 3 subject validations failed on this subject. – SaintAvalon Jul 14 '20 at 16:37
  • 2
    `get().contains()` works - do not use `cy.invoke('text')`, that is the reason you are getting every name in the column. – eric99 Jul 14 '20 at 22:38
  • 1
    `I have no clue how that helps me grab her checkbox since it's to the left in a different column.` -> You don't need the index. Your name column is in the same row as your checkbox, therefore they are related `siblings` and once you retrieve the name column with the `get()` you can navigate to the checkbox using `.siblings`. There is an example and documentation referenced in my answer for you. – Old Schooled Jul 15 '20 at 06:40
  • 1
    To reiterate some things from the answer: If `Mandy Smith` is unique to just this cell and does not appear in other cells, `get().contains()` will return that cell. Then using `.siblings`, you can navigate to the cell containing the checkbox (see docs linked for sibling usage). Do not use `invoke`. I agree with @eric99 that this should work and if it doesn't, we should be able to help you get it working. – Old Schooled Jul 15 '20 at 06:52
  • I guess I'm missing something, I looked at the checkbox which comes before the name column, I updated my original post with the entire block of code for all of Mandy Smith to show relations. I was hoping this would be helpful to point out where the check box is a sibling, or how to call it as a sibling. I'm a bit of a noob, excuse my ignorance. I'll try to find out how they are linked, but my Dev said they aren't and it would be like a grandfather relation. The final image i posted shows Mandy Smith name column, and above it shows the Checkbox. I appreciate your continued help. – SaintAvalon Jul 15 '20 at 19:13
1

Maybe I can suggest another approach like using cypress xpath. First install this: https://www.npmjs.com/package/cypress-xpath

Then you can select the checkbox using xpath if it contains text Mandy Smith etc.

Sanja Paskova
  • 1,092
  • 7
  • 15
  • 1
    Please don't post link-only answers to other Stack Exchange questions. Instead, include the essential portions of the answer here, and *tailor the answer to this specific question.* – Tschallacka Jul 14 '20 at 14:42
  • @Tschalacka the link is not to another Stack Exchange question, it's to a Cypress add-on package. – Ackroydd Jul 18 '20 at 12:31