Nightwatch .execute() how to pass parameter to execute function, is it possible? - nightwatch

Please be patient - I am a beginner in programming. Tester for long time but programming is not my domain.
My test is:
from the backend I get some list with some element (e.g. 5 text strings)
I click some element on page which displayed those 5 elements (of course I don't know if listed elements are correct or not)
I need to check if list of elements displayed on ui is the list received from backend
Problem:
I cannot access the elements by Nightwatch api css selector, at least I could not manage (Angular app) to do it with Nightwatch
I found I could do it with .execute()
My code is (failing):
browser
.click(selector.HEADER.APPS_GRID, function () {
for (var app in appsList) {
let appShortName = appsList[app].shortName
let appLongName = appsList[app].longName
let appUrl = appsList[app].url
let appVisibility = appsList[app].visibility
browser.execute(function(app){
var appShortNameDisplayed = document.getElementsByClassName('logo-as-text')[app].innerText
var appLongNameDisplayed = document.getElementsByClassName('app-name')[app].innerText
return [appShortNameDisplayed, appLongNameDisplayed]
}, function(result){
console.log(result.value[0])
})
}
})
It fails in lines:
var appShortNameDisplayed = document.getElementsByClassName('logo-as-text')[app].innerText
var appLongNameDisplayed = document.getElementsByClassName('app-name')[app].innerText
unfortunately I have to make query with [app] - iterating by elements of object. If I skip [app].innerText I get some data like element-6066-11e4-a52e-4f735466cecf instead of text values displayed on page
I get error:
Error while running .executeScript() protocol action: TypeError: document.getElementsByClassName(...)[app] is undefined
Is it possible to pass the "app" param (counter) to the document query?
Or is it the way I have to make one query that will return as many data as necessary and then handle data returned in this block
function(result) {
console.log(result.value[0])
})
The fragment of html page is
<div _ngcontent-c8="" class="ep-app-icon mt-auto mb-auto text-center logo-as-text"> XXX </div>
... and I need to get this "XXX" text.

As your own comment suggests, there is an args argument to .execute that is an array. The array elements will be the arguments in the function passed to execute.
See https://nightwatchjs.org/api/commands/#execute

.executeAsync(function(){
var buttons=document.getElementsByTagName('button');
buttons[2].click();
return buttons;
},[],function(result){
console.log('done')
})
Try Async it works for sure

Related

Do something "if an element with some text is not present"

This is basically for a ReactSelect element (behaves like a Select2 element with multi-select enabled), on which I'm trying to select some values which are not already selected.
If an option is selected, then there'll be an element as given below in the DOM
<div class="select__multi-value__label">option1</div>
and hence that options won't be present in the list. So, any code to click() that "option" will fail.
Is there a way to check whether an element with some particular text is available in the DOM?
something like,
options = ['option1','option2','option3'];
options.forEach(option =>{
cy.get('[test-id="react-select"]').then(reactSelect =>{
if(reactSelect.find('[class="select__multi-value__label"]').contains(option).length == 0){
//code to select that option
cy.get('div.select__menu-list > div[role="option"]').contains(option).click();
}
})
})
This find().contains() part doesn't work.
How can I achieve this?
Thanks for any help.
Edit
Adding to the solution given below, can I get an exact match selector - say using a Regex?
let r = new RegExp("^" + option + "$");
...........
const selector = `div.select__multi-value__label:contains(${r})`;
This somehow doesn't work. Found a thread that recommends using filter(), but I don't know how to use it.
Is it possible?
You can do it by moving the contains() inside the selector,
options = ['option1','option2','option3'];
options.forEach(option =>{
cy.get('[test-id="react-select"]').then(reactSelect =>{
const selector = `[class="select__multi-value__label"]:contains(${option})`
if(reactSelect.find(selector).length == 0){
//code to select that option
cy.get('div.select__menu-list > div[role="option"]')
.contains(option)
.click();
}
})
})
The .find().contains() part in your code is using a different .contains() to to the Cypress .contains().
It's invoking the jQuery .contains() which has a different purpose
Description: Check to see if a DOM element is a descendant of another DOM element.
I suppose you could also select the options directly and iterate them
options = ['option1','option2','option3'];
cy.get('div.select__menu-list > div[role="option"]')
.each(option =>{
if (options.includes(option.text()) {
option.click();
}
})
Exact match
options = ['option1','option2','option3'];
options.forEach(option =>{
cy.get('[test-id="react-select"]').then(reactSelect =>{
const matchedOptions = reactSelect
.find('[class="select__multi-value__label"]')
.filter((index, el) => el.innerText === option) // exact
if(matchedOptions.length === 0){
//code to select that option
cy.get('div.select__menu-list > div[role="option"]')
.contains(option)
.click();
}
})
})
You should avoid conditionals in tests as it goes against best practice.
A solution in this case following good practices would be to mock the response that comes from the API for you to handle the request as you want, so you will know exactly when there will be specific text on the screen instead of being a random behavior and you won't have to do conditionals.
You can read more about mocks here: https://docs.cypress.io/api/commands/intercept#Stubbing-a-response
But I also advise you to read this Cypress documentation on testing with conditionals: https://docs.cypress.io/guides/core-concepts/conditional-testing#What-you-ll-learn

How to use values from DOM in cypress test?

if I have a page containing:
<span data-testid="credit-balance">
10
</span>
In Cypress, how do I extract the value to a variable to use in tests?
Something along the lines of:
const creditBalance = cy.get('[data-testid="credit-balance"]').value();
Assigning return values with const, var, and let is considered an anti pattern while using Cypress.
However, when you find yourself wanting to do so, is best practice to accomplish this with closures.
it("uses closures to reference dom element", () => {
cy.get("[data-testid=credit-balance]").then(($span) => {
// $span is the object that the previous command yielded
const creditBalance = $span.text();
cy.log(creditBalance);
})
});
Another way to do this would be to use Aliases if you want to store and compare values or share values between tests using hooks.
it("aliasing the value from dom element", () => {
cy.get("[data-testid=credit-balance]").as("creditBalance")
cy.get("#creditBalance").should("contain", 10)
});
How you approach this really depends on the objective of your test. I recommend checking out more examples from the documentation: try Variables and Aliases , Best Practices, and FAQ
If you would like to retrieve the value and perform any assertions with it, a fast, efficient method would also be the use .invoke
it('Getting the value and performing an assertion', () =>{
cy.get('selector').invoke('val').should('eq',10)
})
Doc
The Cypress documentation has an example how to Compare text values of two elements
// will keep text from title element
let titleText
cy.get('.company-details')
.find('.title')
.then(($title) => {
// save text from the first element
titleText = $title.text(); //original uses normalizeText($title.text())
})
cy.get('.company-details')
.find('.identifier')
.should(($identifier) => {
// we can massage text before comparing
const idText = $identifier.text(); //original uses normalizeText($identifier.text())
// text from the title element should already be set
expect(idText, 'ID').to.equal(titleText)
})
If you have to get the value rather than text, use this. It worked for me.
<span data-testid="credit-balance" value='100'></span>
Like above
cy.get('[data-testid="credit-balance"]')
.invoke("val")
.then(($amount) => {
// $span is the object that the previous command yielded
cy.log($amount);
});

Filling MVC DropdownList with AJAX source and bind selected value

I have view for showing Member details. Inside there is an EditorFor element which represents subjects taken by the member.
#Html.EditorFor(m => m.Subject)
Inside the editor template there is a Html.DropDownListFor() which shows the selected subject and button to delete that subject
I am using an Html.DropDownListFor element as :
#Html.DropDownListFor(m => m.SubjectID, Enumerable.Empty<SelectListItem>(), "Select Subject",
new { #class = "form-control subjects" })
The second parameter(source) is set empty since I want to load the source from an AJAX request; like below:
$.getJSON(url, function (response) {
$.each(response.Subjects, function (index, item) {
$('.subjects').append($('<option></option>').text(item.Text).val(item.ID));
});
// code to fill other dropdowns
});
Currently the html loads before the dropdowns are filled. So the values of all subject dropdowns are set to the default "Select Subject". Is there a way around this or is it the wrong approach?
Note: There are a number of dropdowns in this page. That's why I would prefer to load them an AJAX request and cache it instead of putting in viewModel and filling it for each request.
** EDIT **
In AJAX call, the action returns a json object containing dropdowns used across all pages. This action is decorated with [Output Cache] to avoid frequent trips to server. Changed the code in $.each to reflect this.
You can initially assign the value of the property to a javascript variable, and use that to set the value of the selected option in the ajax callback after the options have been appended.
// Store initial value
var selectedId = #Html.Raw(Json.Encode(Model.Subject.SubjectID))
var subjects = $('.subjects'); // cache it
$.getJSON(url, function (response) {
$.each(response, function (index, item) {
subjects.append($('<option></option>').text(item.Text).val(item.ID));
});
// Set selected option
subjects.val(selectedId);
});
However its not clear why you are making an ajax call, unless you are generating cascading dropdownlists which does not appear to be the case. What you doing is basically saying to the client - here is some data, but I forgot to send what you need, so waste some more time and resources establishing another connection to get the rest of the data. And you are not caching anything despite what you think.
If on the other hand your Subject property is actually a collection of objects (in which case, it should be Subjects - plural), then the correct approach using an EditorTemplate is explained in Option 1 of this answer.
Note also if Subject is a collection, then the var selectedId = .. code above would need to be modified to generate an array of the SubjectID values, for example
var selectedIds = #Html.Raw(Json.Encode(Model.Subject.Select(x => x.SubjectID)))
and then the each dropdownlist value will need to be set in a loop
$.each(subjects, function(index, item) {
$(this).val(selectedIds[index]);
});
If your JSON tells you what option they have selected you can simply do the following after you have populated your dropdown:
$('.form-control.subjects').get(0).selectedIndex = [SELECTED_INDEX];
Where [SELECTED_INDEX] is the index of the element you want to select inside the dropdown.

Meteor infinite loop with Session variables

i'm rather new to Meteor and have a problem, where can't figure out how to solve it.
I want to store dates in a collection. I can pickup the place of the meeting using google maps, which gives me a String with the coordinates.
I reverse geocode the coordinates with jaymc:google-reverse-geocode which is basically working (i can console.log the results).
When using Session variables i can output the result, but they keep changing itself. The entrys get there result, then first and second entry change their result, then they change again and so on.
I tried to use ReactiveVar and ReactiveDict but with no result. I can't get any results returned from the reverseGeocode function.
Here's the code:
{{#each termine}}
<div class="listTermine">
<p class="title">{{title}}</p>
<p class="desc">{{desc}}</p>
<p class="location">{{getAddress}}</p>
<p class="dates">
<span class="glyphicon glyphicon-time" aria-hidden="true"></span>
{{formatDate startDate}} bis {{formatDate endDate}}
</p>
</div>
{{/each}}
Template.showTermine.helpers({
getAddress: function() {
var locArray = Termine.findOne({
"title": this.title
}, {
fields: {
locations: 1
}
});
latlngArray = locArray.locations.toString();
var latlong = latlngArray.split(",");
var lat = latlong[0];
var lng = latlong[1];
reverseGeocode.getLocation(lat, lng, function(location) {
Session.set('location', reverseGeocode.getAddrStr());
})
// commented out to prevent infinite loop
//return Session.get('location');
}
});
this is because a Reactive variable (like a Session variable) will cause the whole function it is included in to re-run each time it is changed.
So here you have the Session.set() in the same function as the Session.get(), so it will re-run the getAddress function each time Session.set() is called, which will re-run the thing in a loop.
Since you're returning the result in the same function, you really don't need a Session variable at all here:
you can simply:
reverseGeocode.getLocation(lat, lng, function(location) {
return reverseGeocode.getAddrStr();
})
if this doesn't work (because you're doing an asynchronous call to .getLocation), then you should do this call somewhere else
The best place to do this would be in the Template.mytemplate.onCreated() event
Template.showTermine.onCreated(function() {
var locArray = Termine.findOne({
"title": this.title
}, {
fields: {
locations: 1
}
});
latlngArray = locArray.locations.toString();
var latlong = latlngArray.split(",");
var lat = latlong[0];
var lng = latlong[1];
reverseGeocode.getLocation(lat, lng, function(location) {
Session.set('location', reverseGeocode.getAddrStr());
})});
Template.showTermine.helpers({
"getAddress": function() {
return Session.get("location");
}
});
The set is called inside the callback which will execute after the helper has returned the get already, this mean when it sets the variable the helper gets invalidated because of the change in the session value and reruns.
Couple of possible fixes:
Move code to set the var into the onCreated or onRendered methods and remove everything bar the return from your helper
Or
Ensure the session var is created as a null value initially and then add an if check to look at the value of the var before attempting to call the location service or set the var.
Reactive vars are defo the right way to go over sessions for repeated templates here to avoid namespace collisions in session and the same advice should work for reactive vars or sessions vars. Just avoid setting and getting anything reactive in the same helper without some checks to see if it is needed

AJAX created SELECT "invisible" inside FORM data

I'm new here but I found many help in the passt reading Q&A.
My problem is this:
in a HTML page I have a form with many fields, one of these is a SELECT with name and id "f_container"; I pose it inside a element (<div id=f_cont_ins></div>) ad use an AJAX function to populate it dynamically.
function ele_con(id,cod_cont) {
var http = createRequestObject();
http.open('get', 'fler.php?id='+id+'&art='+document.getElementById('stp').value);
http.onreadystatechange = function() {
if(http.readyState == 4){
var response = http.responseText;
document.getElementById('f_cont_ins').innerHTML = response;
}
}
http.send(null);
}
The "response" is right formatted an display right in the browser (Firefox); if I inspect the element or analyze the DOM structure it seems all ok, and I can get the value of a selection with document.getElementById('f_container').value.
But when I post the form, the field "f_cointainer" isn't present in the print_r($_POST); array, but all the other yes.
I navigate the net but don't found a clear ansver that these is not possible or a workaround to solve the problem.
I have an idea that is to use the document.location='page.php?par1=a&par2=b&f_container=5'; created reading value with document.getElementById, but I prefer a POST solution if it exist.

Resources