I'm doing some cypress tests on a rails/react app and I need to check if the value inputted in the form on the last row is, lets say, "Another Random Text". In the provided html below, it's on the 2nd row but it could be in any other last row number.
---- CYPRESS ----
What didn't work
cy.get('.form-something').last().should('have.value', 'Another Random Text')
because it returns cy.should() failed because this element is detached from the DOM.
And by using eq() I couldn't address the last row, just the first or the 2nd last.
Can anyone shine a light?
Thank you in advance
---- HTML ------
<table class="table table-flat">
<thead>
<tr>
<th style="width: 50%;">State</th>
<th>Generic State</th>
<th style="min-width: 100px;"></th>
</tr>
</thead>
<tbody>
<tr class="index-0" data-qa="s-3313">
<td><input class="form-something" type="text" name="name" value="Random Text"></td>
<td data-qa="generic-state">Additional</td>
<td><button class="btn btn-danger btn-sm" data-qa="remove-state"><i class="fa fa-trash"></i></button></td>
</tr>
<tr class="index-1" data-qa="s-3314">
<td><input class="form-something" type="text" name="name" value="Another Random Text"></td>
<td data-qa="generic-state">Other</td>
<td><button class="btn btn-danger btn-sm" data-qa="remove-state"><i class="fa fa-trash"></i></button></td>
</tr>
<tr>
<td colspan="2"></td>
<td><button class="btn btn-success btn-sm" data-qa="add-new-state"><i class="fa fa-plus mr-2"></i>Add</button></td>
</tr>
</tbody>
</table>
If the table get's re-displayed after the text is entered (or some other action like clicking "Add"), elements you query before the action are discarded by the app and replaced by a new version.
Your test still has a reference to the original element, so it's not yet garbage-collected but it is detached from the DOM
So this fails,
cy.get('.form-something').last()
.type('Another Random Text') // action causes detached element
.should('have.value', 'Another Random Text')
but this maybe succeeds
cy.get('.form-something').last()
.type('Another Random Text')
cy.get('.form-something').last()
.should('have.value', 'Another Random Text')
but it's safer to include the full table selector
cy.get('table tbody tr td .form-something').last()
.should('have.value', 'Another Random Text') // requery all parts from table down
You can also use .contains() based on the "Other" text
cy.get('table').should('be.visible')
.contains('td', 'Other') // locate by text
.prev() // previous cell
.find('input.form-something') // get the input
.should('have.value', 'Another Random Text')
Using an alias may help your test.
cy.get('.form-something').last().as('lastInput')
cy.get('#lastInput').should('have.value', 'Another Random Text')
It looks a bit unnecessary, but alias has built-in protection against detached-from-dom errors.
Here is one of the Cypress tests that verifies the feature,
it('can find a custom alias again when detached from DOM', () => {
cy
.get('foobarbazquux:last').as('foo') // creating an alias
.then(() => {
// remove the existing foobarbazquux
cy.$$('foobarbazquux').remove() // detaching from DOM
// and cause it to be re-rendered
cy.$$('body').append(cy.$$('<foobarbazquux>asdf</foobarbazquux>'))
}).get('#foo').should('contain', 'asdf')
})
One safe bet when dealing with element is detached from the DOM errors is to leverage the cypress retry-ability in a proper way.
The trick is to make sure that selecting action is right next to the assertion. In your example that's not the case, because it's broken by the .last() call.
You can try something like this, which should do the trick:
cy.get('.form-something[value="Another Random Text"]').should('exist')
To explain the reasoning behind the answer above: frameworks like React usually load the empty table first, and then fill it up (rerender it). Cypress being too fast often grabs the empty table before its filled, which is afterward rerendered when table data is fetched, resulting in that detached error.
Edit
To solve a problem mentioned in the comment, when you need control of value property rather than value attribute, you can use this:
cy.get('.form-something').should(inputElements => {
expect(inputElements[inputElements.length - 1]).to.have.value('Some test');
});
This will make sure that the last .form-something element contains the right text, and it won't match even if that exact text is in any element other than the last. You can even add multiple assertions that would all together benefit from build-in retry-ability.
You can also use an each loop to find the element with the desired value and then apply the assertion like this:
cy.get('.form-something')
.should('be.visible')
.each(($ele) => {
if ($ele.val().trim() == 'Another Random Text') {
cy.wrap($ele).should('have.value', 'Another Random Text') //Apply assertion
return false //Exit loop once element is found
}
})
Related
I have a table I'm testing that the column headers are correct. The problem is .contains() only ever returns one element.
I can repeat the code for each instance, but it seems quite verbose. Feel I must be missing something in Cypress commands that can do this better.
cy.get('th').eq(0).contains('Batman')
cy.get('th').eq(1).contains('Robin')
cy.get('th').eq(1).contains('The Flash')
/// etc
<table>
<caption>Superheros and sidekicks</caption>
<colgroup>
<col>
<col span="2" class="batman">
<col span="2" class="flash">
</colgroup>
<tr>
<td> </td>
<th scope="col">Batman</th>
<th scope="col">Robin</th>
<th scope="col">The Flash</th>
<th scope="col">Kid Flash</th>
</tr>
<tr>
<th scope="row">Skill</th>
<td>Smarts</td>
<td>Dex, acrobat</td>
<td>Super speed</td>
<td>Super speed</td>
</tr>
</table>
You can tidy up the test by mapping elements to inner texts.
Note, specify the first row with tr:eq(0)
const expectedHeaders = ['Batman', 'Robin', 'The Flash', 'Kid Flash']
cy.get('tr:eq(0) th')
.then($th => [...$th].map(th => th.innerText)) // map to texts
.should('deep.eq', expectedHeaders)
One option would be to select the row above, then use .children() to get a list of all the headers.
You can then chain multiple assertions about that list
cy.get('tr').eq(0) // row containing the heasers
.children('th') // collection of header elements
.should('contain', 'Batman') // assert 1st text
.and('contain', 'Robin') // assert 2nd text
.and('contain', 'The Flash') // assert 3rd text
If you store the expected in an array, you could use .each() to iterate through each th.
const expected = ['Batman', 'Robin', 'Flash', 'Kid Flash']
cy.get('tr').eq(0)
.children('th')
.each(($th, index) => {
cy.wrap($th).should('have.text', expected[index]);
});
here is the xpath I am using
//tr[#id = 'inputSavePaymentAccounts']/descendant::input[#type = 'checkbox' and #name = 'payAck' and #tabindex = '10'
here is how I can get it to check the checkbox
jQuery('.payAck').prop('checked', true)
here is the checking account html that is inside an iframe
<tr id="inputSavePaymentAccounts" class="savePaymentAccounts" style="display: table-row;">
<td class="addCCLabel" style="padding-top: 15px;">Save this Payment Method</td>
<td style="padding-top: 15px;">
<input class="payAck" type="checkbox" onchange="friendlyNameShow()" name="payAck" tabindex="10">
</td>
</tr>
here is the credit card html that is inside the same iframe
<tr id="inputSavePaymentAccounts" class="savePaymentAccounts" style="display: table-row;">
<td class="addCCLabel" style="padding-top: 15px;">Save this Payment Method</td>
<td style="padding-top: 15px;">
<input class="payAck" type="checkbox" onchange="friendlyNameShow()" name="payAck" tabindex="25">
</td>
</tr>
The main issue is that when i use:
jQuery('.payAck').prop('checked', true)
it checkmarks the checkbox however, at the time the checkbox is checked I am suppose to see a text box display, which is not happening with the above jquery. I was hoping the execution of xpath would solve this
see these for a more clearer picture
image of checkbox http://prnt.sc/c12b1p
image of checkbox when it is checked with text box displaying (need this to happen) http://prnt.sc/c12b7q
First of all, I would recommend you remove the inline javascript. Your primary selector in this case is a class and the class is assigned to 2 elements (at least from what I see in your example). When you check the checkbox using jQuery with the class selector, it is going to trigger all the elements that belong to that class and I don't think that is what you intend to do. Also, using click() will work, but that will cause friendlyNameShow() to be called twice and you will need to check the state of the checkbox since you are not explicitly checking/un-checking it. Try using and Id instead.
I've finally made my app in angular 2. Everything is solved, except one thing. When I add item into my table or edited it, I can't see the change until I refresh page or click for example next page button (I have implemented pagination). I included:
<script src="node_modules/systemjs/dist/system-polyfills.js"></script>
<script src="node_modules/angular2/bundles/angular2-polyfills.js"></script>
in this order. My method for adding item is very simple:
addDepartment(item){
this._departmentService.addDepartment(item)
.subscribe(departments => this.department = departments.json());
this.getAll();}
Whhen I add item, and put breakpoint on get method, It is called correctly and I get right information from my DB, but I don't know why view isn't refreshed then. Do you have any idea why is it happened? Thanks for suggestions!
EDIT: department is just department: Department, where Department is interface with properties (departmentNo, departmentName, departmentLocation). The view for adding item looks like:
<form [ngFormModel]="myForm"
(ngSubmit)="addDepartment(newItem); showAddView=false" [hidden]="!showAddView" align="center">
<div>
<label for="editAbrv">Department name:</label><br>
<input type="text" [(ngModel)]="newItem.departmentName" [ngFormControl]="myForm.controls['departmentName']" >
<div *ngIf="myForm.controls['departmentName'].hasError('required')" class="ui error message"><b style="color:red;">Name is required</b></div>
</div>
<br/>
<div>
<label for="editAbrv">Department Location:</label><br>
<input type="text" [(ngModel)]="newItem.departmentLocation" [ngFormControl]="myForm.controls['departmentLocation']" >
<div *ngIf="myForm.controls['departmentLocation'].hasError('required')" class="ui error message"><b style="color:red;">Location is required</b></div>
</div>
<br/>
<div>
<button type="submit" [disabled]="!myForm.valid" class="ui button">Add item</button>
<button><a href="javascript:void(0);" (click)="showHide($event)" >
Cancel
</a></button>
</div>
</form>
and my department table is:
<table align="center">
<thead>
<tr>
<td>#</td>
<td><strong>Department</strong></td>
<td><strong>Department Location</strong></td>
<td><strong>Edit</strong></td>
<td><strong>Delete</strong></td>
</tr>
</thead>
<tbody>
<tr *ngFor="#department of departments | searchString:filter.value ; #i = index">
<td>{{i + 1}}.</td>
<td> {{department.departmentName}}</td>
<td>{{department.departmentLocation}}</td>
<td>
<button class="btnEdit" (click)="showEdit(department)">Edit</button>
</td>
<td>
<button class="btnDelete" (click)="deleteDepartment(department)" >Delete</button>
</td>
</tr>
</tbody>
</table>
With this code, you don't wait for the response of the addDepartment request and execute the getAll request directly.
addDepartment(item){
this._departmentService.addDepartment(item)
.subscribe(departments => this.department = departments.json());
this.getAll();
}
You should move the call to getAll within the callback registered in subscribe. At this moment, the addDepartment is actually done and you can reload the list...
The code could be refactored like this (it's a guess since I haven't the content of addDepartment and getAll methods):
addDepartment(item){
this._departmentService.addDepartment(item)
.subscribe(addedDepartment => {
this.department = this.getAll();
});
}
This issue occurs because of the way you're using departmant and how change detection works. When you use *ngFor="#department of departments", angular change detection looks for a object reference on departments array. When you update/change one of the items in this array object reference to the array itself doesn't change, so angular doesn't run change detection.
You have few options:
1) change reference of the array by replacing it with new array with updated values
2) tell angular explicitly to run change detection (which I think is easier in your case):
constructor(private _cdRef: ChangeDetectorRef) {...}
addDepartment(item){
this._departmentService.addDepartment(item)
.subscribe(departments => this.department = departments.json());
this.getAll();
this._cdRef.markForCheck();
}
My HTML Code looks like this:
<html>
<body>
<div>
</div>
<div>
<table>
<tbody id=a>
<tr>
<td>
<div>
<span>
some Text
</span>
</div>
</td>
</tr>
<tr>
<td>
<div>
<span>
some Text2
</span>
</div>
</td>
</tr>
<tr>
<td>
<div>
<span>
some Text3
</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</body>
I'm trying to select each of the span elements by their text. I'm able to select the tbody by id. I Tried this:
tbody.FindElement(By.XPath(String.Format(".//span[contains(text(), {0}))]", &var)));
(var = somex0020Text)
but this always returns the first <span> element in my table.
I also tried:
tbody.FindElements(By.XPath(String.Format(".//span[contains(text(), {0}))]", &var)));
which returned a list containing every single <span> element in my table, and I don't know why.
I also don't understand why
tbody.FindElement(By.XPath(String.Format(".//span[text() = {0})]", &var)));
throws an Element not found Exception, when the contain method returns a <span> element with just the same text.
I tried by using xpath as:
.//span[contains(text(),'some Text')]
it is selecting all the 3 span.
so to this i have refer to parent element.
for 1st span: .//tbody[#id='a']//tr[1]//span[contains(text(),'some Text')]
for 2nd: .//tbody[#id='a']//tr[2]//span[contains(text(),'some Text')]
for 3rd: .//tbody[#id='a']//tr[3]//span[contains(text(),'some Text')]
through this I can select every span element individually.
You could use the jQuery to get all the span elements within "Table".
Example:
var items = $('table div span');
items.each(function (x) {
alert(items[x].innerHTML);
});
tbody.FindElement(By.XPath(String.Format(".//span[contains(text(), {0}))]", &var)));
(var = somex0020Text)
but this always returns the first Element in my table.
This is an expected behavior in Selenium. As i can see, there are 3 elements with the same xpath as mentioned in your code, in this case Selenium returns the first element.
tbody.FindElements(By.XPath(String.Format(".//span[contains(text(), {0}))]", &var)));
which returned a list containing every single Element in my table, and i dont know why.
This is also an expected behavior in Selenium. FindElements will return all the elements with the same xpath.
So change the value of var to some Text2 or some Text3 to locate the other two elements.
The following xpath will work for some Text2 :
.//span[contains(text(), 'some Text2'))]
Try with this Xpath $x("//tr//span[contains(.,'some Text')]")
For what I can see, you are having a trouble with the contains. All 3 spans are containing this 'some Text' portion.
If you want to check the entire string, you could use .//span[text()='some Text'].
Hope this helps, and have fun with web parsing!
The following is the DOM details:
<div id: "abc_440"
<table class = "listtable" >
<tbody>
<tr>
<td id = "someid" >
<td class = 'someclass_item"> This is Text </td>
<td class = 'someclass_button">
< a > Add </a>
</td>
</tr>
</tbody>
</table>
</div>
I need to click on 'Add' for particular text at "This is Text". I have to use div with ID (abc_440)to locate the corresponding table as there are may divs with this same dom layout. but index at div ID (for example 440) keeps changing to random number. How do I handle it in general ?
Please help.
I think that what you want to do is very similar to the previous Watir question.
Given that the id is randomly generated, it does not sounds like a good way of finding the link. You will likely need to use the text of the same row.
Assuming that the "This is Text" is unique (as you said you want to find the Add link for it), you can find that td, go to the parent row and then get the link.
b.td(:text => 'This is Text').parent.link.click
If you need to ensure that the text is in the second column, then you can do:
b.trs.find{ |tr|
tr.td.exists? and tr.td(:index => 1).text == 'This is Text'
}.link.click
The tr.td.exists? is added in case some of your rows do not have any tds (example a header row), which would cause an exception when checking the second criteria.
Don't mix quotes in HTML tags. id: doesn't work. <td>s should rarely be empty. The Add button should be a button, not an <a>nchor element. Buttons only work in <form>s.
<form id="abc_440" action='someURI'> <!-- The handler for the button. -->
<table class="listtable">
<tbody>
<tr>
<td id = "someid">
<!-- What goes here? -->
</td>
<td class='someclass_item'>
This is Text
</td>
<td class='someclass_button'>
<button name='add'>Add</button>
</td>
</tr>
</tbody>
</table>
</form>
You should be able to find the button through its name attribute, add, and through the form's id attribute, abc_440. Who generates the ids?
Once you have the problem of finding the add button solved, and figuring out where the form's id comes from, please then stop using tables for formatting. There's no need for that. Learn about <form>s, <fieldset>s, <legend>s, and <label>s. I doubt you need the *some_id* part, the text part should probably be a <label>, and you can use CSS to format your <label>s as:
label {
width: 150px;
float: left;
}
fieldset p {
clear: left;
}