I'm pretty new to Protractor and I'm not satisfied with the way I wrote the code below (which is actually working).I would like to simply store the count in a variable (here, "nb_before") and reuse it after, but the promise mechanism won't allow me to do it concisely, so I decided to put everything in the same function. But it's so ugly.Am I missing something?Best regards
// We count the lines, we click on a button which adds a new line,
// then we verify that the new line has a specific text.
it("Test case", function () {
browser.get(browser.baseUrl);
var button = element(by.xpath("//whatever"));
var rows = element.all(by.xpath("//whatever"));
rows.count().then(function(count){
nb_before = count;
button.click(); // adds a line
var nb_after = nb_before + 1;
var new_line = element(by.xpath("//li[" + nb_after + "]"));
expect(new_line.getText()).toEqual("yay");
});
});
It looks like you could've used .last() method here to get the last row:
it("Test case", function () {
browser.get(browser.baseUrl);
var button = element(by.xpath("//whatever"));
var rows = element.all(by.xpath("//whatever"));
button.click(); // adds a line
expect(rows.last().getText()).toEqual("yay");
});
We can, though, get the count before and after the button click and check if it was incremented by one - this will still require us to use .then() to resolve the promise returned by .count():
var button = element(by.xpath("//whatever"));
var rows = element.all(by.xpath("//whatever"));
rows.count().then(function (countBefore) {
button.click();
expect(rows.count()).toEqual(countBefore + 1);
});
Related
I have a few lines of JavaScript code that pick up heading texts from separate sections and place them into their respective input fields. They are also executed on single pages using wp_enqueue_script.
It works absolutely fine when setTimeout() is used:
function passengerElevator() {
var getProductName = document.querySelectorAll('[data-id="6657316"]');
getProductName.forEach(function(item) {
var productName = item.querySelector('.lift');
var inputArea = item.querySelector('input[name=product]');
inputArea.value = productName.innerText;
});
var getProductName = document.querySelectorAll('[data-id="e9c06d5"]');
getProductName.forEach(function(item) {
var productName = item.querySelector('.lift');
var inputArea = item.querySelector('input[name=product]');
inputArea.value = productName.innerText;
});
setTimeout(function() { passengerElevator() },3000);
However, there is problem of page size (some pages have more than 10 input fields) and I don't want to set an astronomically high ms to delay the script. So I decided to fire it on DOMContentLoaded:
document.addEventListener("DOMContentLoaded", passengerElevator);
function passengerElevator() {
var getProductName = document.querySelectorAll('[data-id="6657316"]');
getProductName.forEach(function(item) {
var productName = item.querySelector('.lift'); // heading text (ex:Panoramic Lift)
var inputArea = item.querySelector('input[name=product]');
inputArea.value = productName.innerText; //ouput here
});
var getProductName = document.querySelectorAll('[data-id="e9c06d5"]');
getProductName.forEach(function(item) {
var productName = item.querySelector('.lift'); // Heading text (ex:Home Lift)
var inputArea = item.querySelector('input[name=product]');
inputArea.value = productName.innerText; // Output here
});
}
As you may have already guessed, it is not working. Is my code too messy to be executed faster or is there any other problem I am missing?
I know similar questions have been asked previously, however, no existing answer I found was able to help me.
It seems like you try to loop through elements that are still not loaded. Perhaps they are being appended to the page via Ajax, so DOMContentLoaded can't help there.
You can create your own check for those elements using setInterval, so use something like this:
let dataIdCheck = setInterval(() => {
if (document.querySelectorAll('[data-id="6657316"]').length > 0 && document.querySelectorAll('[data-id="e9c06d5"]').length > 0) {
clearInterval(dataIdCheck);
// your code here
}
}, 500);
This code will run every 500 milliseconds and check if those two elements exists, using .length. Once they do exists, we stop the interval and run the code.
I also suggest to do console.log('in') to check that our interval stop running once the elements are found.
I am currently working with Cypress and have created a function that randomly clicks an element from a list. I am trying to also get the text from that same function for future assertions. The problem is I am unable to return the text properly. The code I currently have is:
export function selectRandomFromList(listLocator, elementLocator) {
cy.get(listLocator).within(() => {
let numberOfElements = Cypress.$(listLocator + ' ' + elementLocator).length
let selected = Cypress._.random(0, numberOfElements - 1)
cy.get(elementLocator).eq(selected).then(($text) => {
const text = $text.text()
return text
}).click()
})
}
I was hoping that I could then in my test run this function, do the click and then store the returned text in a variable for future checking. What am I doing wrong? Also tried some other stuff with promises and such, when the code was saying I am trying to mix sync and async..
Forgot to add. This is in the support file and I want to use the text variable in the test file. Something like this:
var text = function.selectRandomFromList('[class*=***]', 'li ul li button')
After which I should have the text of the clicked button in text.
Cypress commands work the same way as Promises. You need to return a Promise when inside a then() callback. See doc here.
You may use cy.wrap($text).invoke('text') inside your then() callback instead of returning a string. But then, your click() would not work because your then() would yield a text value that cannot be clicked.
I suggest not using the within() and working directly with the elements. You'll end up with the same result, but with less complexity. I use the within() command when I need to perform several actions on several elements within a div container for example.
You can perform what you need with default commands instead of a function:
let mixLocator = listLocator + ' ' + elementLocator;
cy.get(mixLocator).its('length').then(elementCount => {
let selected = Cypress._.random(elementCount - 1); // lower = 0 is default
cy.get(mixLocator).eq(selected).click().invoke('text').as('selectedText'); // saving the text as an alias to be used later
});
Here, the invoke('text') should still work even if it is after the click(), but it is impossible to do the opposite invoke('text').click() because the invoke('text') command yields a string. If it doesn't, call it once to get the text and again to click it:
cy.get(mixLocator).eq(selected).invoke('text').as('selectedText');
cy.get(mixLocator).eq(selected).click();
or:
cy.get(mixLocator).eq(selected).then(selectedElement => {
cy.wrap(selectedElement).invoke('text').as('selectedText');
cy.wrap(selectedElement).click();
});
You may then use your saved alias later:
cy.get('#selectedText').then(selectedText => {
// use selectedText here
});
I often prefer to chose random elements in our tests to have a better coverage. I came up with a custom commands:
commands.js
Cypress.Commands.add('any', { prevSubject: 'element' }, (subject, size = 1) => {
cy.wrap(subject).then(elementList => {
elementList = (elementList.jquery) ? elementList.get() : elementList;
elementList = Cypress._.sampleSize(elementList, size);
elementList = (elementList.length > 1) ? elementList : elementList[0];
cy.wrap(elementList);
});
});
I use it like this:
cy.get(elementLocator).any().click();
or
cy.get(elementLocator).any(5).each(randomElement => {
cy.wrap(randomElement).click();
});
When this function is called by my test case, I see that the value of index is undefined.
delete thumbnail clicked with context {thumbnailContext: ThumbnailContext}thumbnailContext: ThumbnailContext {divId: "thumbnail-1", imgId: "img-1", closeId: "close-button-1", imgSrc: "data:image/png;base64,Zm9vMQ==", index: 0, …}__proto__: Object
context.js:1972 deleting index undefined
But the trace just before that in the same function shows that the value is actually 0!
The function in question is
deleteThumbnail(thumbnailContext:ThumbnailContext){
console.log("delete thumbnail clicked with context ",thumbnailContext); //the print of this shows index is 0
let wasConfirmed = confirm("Are you sure you want to delete the attachment?");
if(wasConfirmed) {
console.log("deleting index ", thumbnailContext.index); //and then this becomes undefined!!
this.thumbnailContainerRef.remove(thumbnailContext.index);
return false; /*returning false cancels the click and thus cancels further navigation and prevents the browser from going to the page specified (in this case #).*/
}
}
The code run shows the correct prints if run outside the test environment though (i.e. in dev mode)
The following is the test case. It tests that a user can delete a thumbnail of a view. The thumbnail is created using Angular''sViewContainerRef`.
fit('should delete image if user deletes a thumbnail', (done) => {
let newPracticeQuestionComponent = component;
expect(newPracticeQuestionComponent.thumbnailContainerRef.length).toBe(0);
let imageThumbnailDiv = fixture.debugElement.query(By.css("#thumbnail-1"));
let imageThumbnailImg = fixture.debugElement.query(By.css('#img-1'));
let imageThumbnailClose = fixture.debugElement.query(By.css('#close-button-1'));
expect(imageThumbnailDiv).toBeFalsy();
expect(imageThumbnailImg).toBeFalsy();
expect(imageThumbnailClose).toBeFalsy();
let fileSelectControl = (fixture.debugElement.query(By.css("#question-file-upload"))).nativeElement as HTMLInputElement;
let file1 = new File(["foo1"], "foo1.txt",{type: "image/png"});
let reader = newPracticeQuestionComponent.handleFileSelect(fileSelectControl,[file1]);
setTimeout(function() {
console.log("in timeout");
fixture.detectChanges();//without this, the view will not be updated with model
expect(newPracticeQuestionComponent.thumbnailContainerRef.length).toBe(1);
let imageThumbnailDiv2 = fixture.debugElement.query(By.css("#thumbnail-1"));
let imageThumbnailImg2= fixture.debugElement.query(By.css('#img-1'));
let imageThumbnailClose2 = fixture.debugElement.query(By.css('#close-button-1'));
expect(imageThumbnailDiv2).toBeTruthy();
expect(imageThumbnailImg2).toBeTruthy();
expect(imageThumbnailClose2).toBeTruthy();
let thumbnailViewRef:EmbeddedViewRef<ThumbnailContext> = newPracticeQuestionComponent.thumbnailContainerRef.get(0) as EmbeddedViewRef<ThumbnailContext>;
newPracticeQuestionComponent.deleteThumbnail(thumbnailViewRef.context);
expect(newPracticeQuestionComponent.thumbnailContainerRef.length).toBe(0);
let imageThumbnailDiv3 = fixture.debugElement.query(By.css("#thumbnail-1"));
let imageThumbnailImg3 = fixture.debugElement.query(By.css('#img-1'));
let imageThumbnailClose3 = fixture.debugElement.query(By.css('#close-button-1'));
expect(imageThumbnailDiv3).toBeFalsy();
expect(imageThumbnailImg3).toBeFalsy();
expect(imageThumbnailClose3).toBeFalsy();
done();//without done, jasmine will finish this test spec without checking the assertions in the timeout
}, 2000);
});
The test case passes. However, I have another test case which is showing the same behavior but it hangs. the difference between that and this test case is the other one deletes multiple thumbnails.
To achieve expected result, use thumbnailContext.thumbnailContext.index to get index value
I've been battling this test most of the afternoon, I've commented out what I tried to do but it wouldn't work. I have the ability the search for users within the company, a table is then generated and I want to select the one that matches the name of the user. Currently I just use td.text-center as it is the only td in the table, but this seems really poor and prone to breaking if other users with the same name are in the system.
Attempt 2 seems like it would be the best solution but it always gives the element not found error. Has anyone found a reliable way of locating TD within a table? The method below works in other tables, just not this one....
this.editUser = function(name) {
// Original Code Block
//browser.actions().mouseMove(element(by.css('td.text-center'))).perform();
//var editUserBtn = element(by.css('.user-action-open'));
//editUserBtn.click().then(function() {
// var editDetails = element(by.cssContainingText('li > a', 'Edit Details'));
// browser.wait(EC.visibilityOf(editDetails), 3000);
// editDetails.click();
//});
// Attempt 2
// browser.actions().mouseMove(element(by.css('td.text-center'))).perform();
//browser.sleep(3000);
//var edit = element.all(by.repeater('user in users')).filter(function(rowElement){
// return rowElement.element.all(by.css('td[ng-bind="user.Firstname"]')).getText().then(function(text){
// return text.trim() == name;
// });
//}).first();
//browser.actions().mouseMove(edit).perform();
// Currently just times out and doesn't find the element.
var editUserButton = $('.user-action-open');
browser.wait(EC.visibilityOf(editUserButton), 20000).then(function() {
editUserButton.click().then(function() {
var editDetails = element(by.cssContainingText('li > a', 'Edit Details'));
browser.wait(EC.visibilityOf(editDetails), 3000);
editDetails.click();
});
});
}
You could use ExpectedConditions to do some waiting instead of arbitrarily sleeping for 3 seconds. You could instead do a browser wait. This will wait up to the timeout if the object is in a desired state.
let users = element.all(by.repeater('user in users');
// the original post shows that this is an element.all
// this would only be true if the user can have multiple first names
let firstName = element(by.css('td[ng-bind="user.Firstname"]'));
browser.actions().mouseMove(element(by.css('td.text-center'))).perform();
// wait to see if the repeater is present or timeout in 3 seconds
browser.wait(users).isPresent(), 3000);
let edit = element.all(users).filter((rowElement) => {
// wait for first name, get the first name of the row's element
// if the text matches the name, resolve the promise true.
return browser.wait(rowElement.firstName, 3000).then(() => {
return rowElement.firstName.getText().then((text) => {
return text.trim() == name;
});
});
}).first();
browser.actions().mouseMove(edit).perform();
The rowElement.firstName might not be the correct syntax.
Chaining the promises
// wait to see if the repeater is present or timeout in 3 seconds
browser.wait(users).isPresent(), 3000).then(() => {
let edit = element.all(users).filter((rowElement) => {
// wait for first name, get the first name of the row's element
// if the text matches the name, resolve the promise true.
return browser.wait(rowElement.firstName, 3000).then(() => {
return rowElement.firstName.getText().then((text) => {
return text.trim() == name;
});
});
}).first();
browser.actions().mouseMove(edit).perform();
}).catch(err => {
// we could also have asserted the length was at least 1
// maybe we want to catch on something else? maybe this isn't a
// fail case so maybe do not use `fail(`
fail("did not find users");
});
I'm trying to make an HTTP GET request using the jQuery get() function, but I'm having some trouble.
Here's what my code looks like:
// get the links on the page
var pageLinks = $.find('#pageLinks');
// loop through each of the links
$(pageLinks).find('a').each(function(){
if($(this).attr('title') !== "Next Page"){
// make a GET request to the URL of this link
$.get($(this).attr("href"), function(data) {
console.log("here");
var temp = parse_page(data);
// concatenate the return string with another
bdy = bdy+String(temp);
console.log("done");
});
}
});
There are multiple pages that I need to get data from. Since the get() function is asynchronous, I get the pages in a random order. Secondly, the concatenation does not work. Even though I get each of the pages, they're not put into bdy.
Can anyone suggest how I might deal with this?
Thanks a lot!!
Construct bdy after all pages are retrieved, i.e. store get results in a dictionary or array; wait for all gets to finish; then assemble them in the correct order.
I tried this one and it works:
// get the links on the page
var pageLinks = $('a');
var bdy
// loop through each of the links
$(pageLinks).each(function(){
console.log(this);
// make a GET request to the URL of this link
$.get($(this).attr("href"), function(data) {
// concatenate the return string with another
bdy = bdy + data.toString();
console.log(bdy);
});
});
As an example of what #muratgu has said:
var results = [];
var count = 0;
function allDone() {
var bdy = results.join("");
// do stuff with bdy
}
// get the links on the page
var pageLinks = $.find('#pageLinks');
// filter the links so we're left with the links we want
var wantedLinks = $(pageLinks).find('a').filter(function (idx) {
return $(this).attr('title') !== "Next Page";
});
// remember how many links we're working on
count = wantedLinks.length;
// loop through each of the links
wantedLinks.each(function (idx) {
// make a GET request to the URL of this link
$.get($(this).attr("href"), function (data) {
console.log("here");
var temp = parse_page(data);
results[idx] = temp;
// Decrement the count.
count--;
if (count === 0) {
// All done.
allDone();
}
});
});
You could go further and abstract this into a data type that can perform N async downloads, and then notify you when all are complete.
I just found that there are modules that allow one to manage the control flow in JS. The ones I found are:
Async
Step
For help using the above modules, see my follow up question here.