I'm a beginner writing e2e Javascript tests using Protractor. I have been trying to identify an array of elements that have a text property associated with them. I'm able to target them using
groupedNodeNumbers : ElementArrayFinder;
this.groupedNodeNumbers = this.element.all(by.css(".side-label-background + .side-label"));
This is supposed to give me an array of elements on which I can call getText() to extract the value associated with each of them.
validateCountGraphNodes = async() => {
let count = 0;
console.log('Inside')
this.groupedNodeNumbers.each((groupedNodeElement) => {
groupedNodeElement.getText().then((num) => {
console.log('Inside loop')
count += parseInt(num, 10);
console.log(count);
});
});
}`
I am able to log ('Inside') but not ('Inside Loop') and hence my function fails to retrieve the text associated with each element.
Could someone please point where I'm going wrong here?
Since getText() applied returns string instead of an array of strings (existing issue), you can try the following:
const cssSelector = ".side-label-background + .side-label";
$$(cssSelector).getText().then(textArrNotArr => {
for(let i = 0; i< textArrNotArr.lenght; i++) {
console.log('arr[i] = ', textArrNotArr[i]);
}
});
or
$$(cssSelector).count().then(elFinderArray => {
elFinderArray.forEach(elFinder => {
elFinder.getText().then((txt, index) => {
console.log(index);
});
});
});
note: $$('cssSelector') can be used instead of element.all(by.css('cssSelector'))
Related
I'm practicing and was trying to write in a file, all names and links of 'cars' from amazon.
The following code is working but will only write one line in the txt file. How can I write the complete list? Maybe as an object?
Is there a better way to do this?
it.only("amazon cars", () => {
cy.get("input#twotabsearchtextbox").type("cars{enter}");
cy.get(".s-main-slot")
.find("div>h2>a>span")
.each((element) => {
const elname = element.text();
cy.wrap(element)
.parent()
.invoke("attr", "href")
.then((href) => {
cy.writeFile("element.txt", `${elname} and its link ${href}`);
});
});
});
You can use the append mode of cy.writefile().
cy.writeFile("element.txt", `${elname} and its link ${href}`, { flag: 'a+' });
Alternatively, ditch the .each() and use a mapping function instead. This way you only need to write once.
cy.get('.s-main-slot')
.find('div>h2>a>span')
.then($cars => {
const descriptions = [...$cars].map(car => { // car is raw element
const elname = car.innerText; // use DOM property innerText
const href = car.parentElement.href; // use DOM method parentElement
return `${elname} and its link ${href}`
})
cy.writeFile('element.txt', descriptions.join('\n'))
})
Or for a cleaner mapping function, take the parent of the span, the text will still be the same.
cy.get('.s-main-slot')
.find('div>h2>a')
.then($cars => {
const descriptions = [...$cars].map(car => {
return `${car.innerText} and its link ${car.href}`)
})
cy.writeFile('element.txt', descriptions.join('\n'))
})
Or as an object, use a reducer to map
cy.get('.s-main-slot')
.find('div>h2>a')
.then($cars => {
const asObject = [...$cars].reduce((obj, car) => {
obj[car.innerText] = car.href; // "Cars": "https://www.amazon.com...
return obj;
}, {})
cy.writeFile("element.json", asObject)
})
I'm writing code that parses HTML fetched from a webpage, and I am having an issue where my function returns undefined.
function getVideoUrl(category) {
PathLoader.load(config.category_url + category['id']).then(function(document) {
let soup = new JSSoup(document);
let tag = soup.findAll("li");
tag.forEach((tag) => {
if (tag.attrs.class) {
if (tag.attrs.class.startsWith("pcVideoListItem")) {
return config.view_video_url + tag.attrs['_vkey'];
}
}
});
});
}
let random_category = getRandomCategory();
let video_url = getVideoUrl(random_category);
console.log(video_url);
The function getVideoUrl() is the one that returns undefined, as you may have guessed. I am utterly confused.
You're not returning anything from the getVideoUrl method. The forEach is looping through the array of elements, but it doesn't return anything above it.
I think what you should use is find instead of forEach.
Try:
const tags = soup.findAll("li");
const matchedTag = tags.find(tag => tag.attrs.class && tag.attrs.class.startsWith("pcVideoListItem"));
const videoUrl = matchedTag ? `${config.view_video_url}${matchedTag.attrs['_vkey']}` : '';
Also, only ever use let if you intend the value of the variable to change. Otherwise always use const.
I'm trying to iterate and print out in order an array in Javascript that contains the title of 2 events that I obtained from doing web scraping to a website but it prints out in disorder. I know Javascript is asynchronous but I'm new in this world of asynchronism. How can I implement the loop for to print the array in order and give customized info?
agent.add('...') is like console.log('...'). I'm doing a chatbot with DialogFlow and NodeJs 8 but that's not important at this moment. I used console.log() in the return just for debug.
I tried the next:
async function printEvent(event){
agent.add(event)
}
async function runLoop(eventsTitles){
for (let i = 0; i<eventsTitles.length; i++){
aux = await printEvent(eventsTitles[i])
}
}
But i got this error error Unexpected await inside a loop no-await-in-loop
async function showEvents(agent) {
const cheerio = require('cheerio');
const rp = require('request-promise');
const options = {
uri: 'https://www.utb.edu.co/eventos',
transform: function (body) {
return cheerio.load(body);
}
}
return rp(options)
.then($ => {
//** HERE START THE PROBLEM**
var eventsTitles = [] // array of event's titles
agent.add(`This mont we have these events available: \n`)
$('.product-title').each(function (i, elem) {
var event = $(this).text()
eventsTitles.push(event)
})
agent.add(`${eventsTitles}`) // The array prints out in order but if i iterate it, it prints out in disorder.
// *** IMPLEMENT LOOP FOR ***
agent.add(`To obtain more info click on this link https://www.utb.edu.co/eventos`)
return console.log(`Show available events`);
}).catch(err => {
agent.add(`${err}`)
return console.log(err)
})
}
I would like to always print out Event's title #1 and after Event's title #2. Something like this:
events titles.forEach((index,event) => {
agent.add(`${index}. ${event}`) // remember this is like console.log(`${index}. ${event}`)
})
Thanks for any help and explanation!
There no async case here but if you still face difficultly than use this loop
for (let index = 0; index < eventsTitles.length; index++) {
const element = eventsTitles[index];
agent.add(${index}. ${element})
}
Relatively new to writing end to end tests with Protractor. Also relatively inexperienced at working with promises.
I am writing a test where in some cases I need to loop through my code b/c the record that I select does not meet certain criteria. In those cases I would like to proceed back to a previous step and try another record (and continue doing so until I find a suitable record). I am not able to get my test to enter into my loop though.
I can write regular e2e tests with Protractor, but solving this looping issue is proving difficult. I know it must be because I'm dealing with Promises, and am not handling them correctly. Although I've seen examples of looping through protractor code, they often involve a single method that needs to be done to every item in a list. Here I have multiple steps that need to be done in order to arrive at the point where I can find and set my value to break out of the loop.
Here are some of the threads I've looked at trying to resolve this:
protractor and for loops
https://www.angularjsrecipes.com/recipes/27910331/using-protractor-with-loops
Using protractor with loops
Looping through fields in an Angular form and testing input validations using Protractor?
Protractors, promises, parameters, and closures
Asynchronously working of for loop in protractor
My code as it currently stands:
it('should select a customer who has a valid serial number', () => {
const products = new HomePage();
let serialIsValid: boolean = false;
let selectedElement, serialNumber, product, recordCount, recordList;
recordList = element.all(by.css(`mat-list.desco-list`));
recordList.then((records) => {
recordCount = records.length;
console.log('records', records.length, 'recordCount', recordCount);
}
);
for (let i = 0; i < recordCount; i++) {
if (serialIsValid === false) {
const j = i + 1;
products.btnFormsSelector.click();
products.formSelectorRepossession.click();
browser.wait(EC.visibilityOf(products.itemSearch));
products.itemSearch.element(by.tagName('input')).sendKeys(browser.params.search_string);
products.itemSearch.element(by.id('btnSearch')).click();
browser.wait(EC.visibilityOf(products.itemSearch.element(by.id('list-container'))));
selectedElement = element(by.tagName(`#itemSearch mat-list:nth-child(${{j}})`));
selectedElement.click();
browser.wait(EC.visibilityOf(products.doStuffForm));
browser.sleep(1000);
element(by.css('#successful mat-radio-button:nth-child(1) label')).click();
browser.sleep(1000);
expect(element(by.css('.itemDetailsContainer'))).toBeTruthy();
product = products.productIDNumber.getText();
product.then((item) => {
serialNumber = item;
if (item !== 'Unknown') {
expect(serialNumber).not.toContain('Unknown');
serialIsValid = true;
} else {
i++
}
})
} else {
console.log('serial is valid: ' + serialIsValid);
expect(serialNumber).not.toContain('Unknown');
break;
}
}
console.log('serial number validity: ', serialIsValid);
})
I have rewritten and reorganized my code several times, including trying to break out my code into functions grouping related steps together (as recommended in one of the threads above, and then trying to chain them together them together, like this:
findValidCustomer() {
const gotoProductSearch = (function () {...})
const searchForRecord = (function () {...})
const populateForm = (function (j) {...})
for (let i = 0; i < recordCount; i++) {
const j = i + 1;
if (serialIsValid === false) {
gotoProductSearch
.then(searchForRecord)
.then(populateForm(j))
.then(findValidSerial(i))
} else {
console.log('serial number validity' + serialIsValid);
expect(serialIsValid).not.toContain('Unknown');
break;
}
}
console.log('serial number validity' + serialIsValid);
}
When I've tried to chain them like that, I received this error
- TS2345: Argument of type 'number | undefined' is not assignable to parameter of type 'number'
Have edited my code from my actual test and apologies if I've made mistakes in doing so. Would greatly appreciate comments or explanation on how to do this in general though, b/c I know I'm not doing it correctly. Thanks in advance.
I would suggest looking into async / await and migrating this test. Why migrate? Protractor 6 and moving forward will require async / await. In order to do that, you will need to have SELENIUM_PROMISE_MANAGER: false in your config and await your promises. In my answer below, I'll use async / await.
Below is my attempt to rewrite this as async / await. Also try to define your ElementFinders, numbers, and other stuff when you need them so you can define them as consts.
it('should select a customer who has a valid serial number', async () => {
const products = new HomePage();
let serialIsValid = false; // Setting the value to false is enough
// and :boolean is not needed
const recordList = element.all(by.css(`mat-list.desco-list`));
const recordCount = await recordList.count();
console.log(`recordCount ${recordCount}`);
// This could be rewritten with .each
// See https://github.com/angular/protractor/blob/master/lib/element.ts#L575
// await recordList.each(async (el: WebElement, index: number) => {
for (let i = 0; i < recordCount; i++) {
if (serialIsValid === false) {
const j = index + 1; // Not sure what j is being used for...
await products.btnFormsSelector.click();
await products.formSelectorRepossession.click();
await browser.wait(EC.visibilityOf(products.itemSearch));
await products.itemSearch.element(by.tagName('input'))
.sendKeys(browser.params.search_string);
await products.itemSearch.element(by.id('btnSearch')).click();
await browser.wait(
EC.visibilityOf(await products.itemSearch.element(
by.id('list-container')))); // Maybe use a boolean check?
const selectedElement = element(by.tagName(
`#itemSearch mat-list:nth-child(${{j}})`));
await selectedElement.click();
// not sure what doStuffForm is but I'm guessing it returns a promise.
await browser.wait(EC.visibilityOf(await products.doStuffForm));
await browser.sleep(1000); // I would avoid sleeps since this might
// cause errors (if ran on a slower machine)
// or just cause your test to run slow
await element(by.css(
'#successful mat-radio-button:nth-child(1) label')).click();
await browser.sleep(1000);
expect(await element(by.css('.itemDetailsContainer'))).toBeTruthy();
const serialNumber = await products.productIDNumber.getText();
if (item !== 'Unknown') {
expect(serialNumber).not.toContain('Unknown');
serialIsValid = true;
}
// The else statement if you were using i in a for loop, it is not
// a good idea to increment it twice.
} else {
// So according to this, if the last item is invalid, you will not break
// and not log this. This will not fail the test. It might be a good idea
// to not have this in an else statement.
console.log(`serial is valid: ${serialIsValid}`);
expect(serialNumber).not.toContain('Unknown');
break;
}
}
console.log('serial number validity: ', serialIsValid);
});
Can you check the count again after updating your code by following snippet
element.all(by.css(`mat-list.desco-list`)).then(function(records) => {
recordCount = records.length;
console.log(recordCount);
});
OR
There is count() function in ElementArrayFinder class which returns promise with count of locator
element.all(by.css(`mat-list.desco-list`)).then(function(records) => {
records.count().then(number => {
console.log(number); })
});
I was learning react and doing some axios api call with an array. I did a code on gathering data through coinmarketcap api to learn.
So, my intention was to get the prices from the api with a hardcoded array of cryptocurrency ids and push them into an array of prices. But I ran into a problem with the prices array, as the prices were all jumbled up. I was supposed to get an array in this order
[bitcoinprice, ethereumprice, stellarprice, rippleprice]
but when I ran it in the browser, the prices came randomly and not in this order, sometimes I got my order, sometimes it didn't. I used a button which onClick called the getPrice method. Does anyone know what went wrong with my code? Thanks!
constructor(){
super();
this.state = {
cryptos:["bitcoin","ethereum","stellar","ripple"],
prices:[]
};
this.getPrice = this.getPrice.bind(this);
}
getPrice(){
const cryptos = this.state.cryptos;
console.log(cryptos);
for (var i = 0; i < cryptos.length; i++){
const cryptoUrl = 'https://api.coinmarketcap.com/v1/ticker/' + cryptos[i];
axios.get(cryptoUrl)
.then((response) => {
const data = response.data[0];
console.log(data.price_usd);
this.state.prices.push(data.price_usd);
console.log(this.state.prices);
})
.catch((error) => {
console.log(error);
});
}
}
If you want to receive the data in the order of the asynchronous calls you make, you can use Promise.all, that waits until all the promises of an array get executed and are resolved, returning the values in the order they were executed.
const cryptos = ['bitcoin', 'ethereum', 'stellar', 'ripple'];
const arr = [];
for (var i = 0; i < cryptos.length; i++){
const cryptoUrl = 'https://api.coinmarketcap.com/v1/ticker/' + cryptos[i];
arr.push(axios.get(cryptoUrl));
}
Promise.all(arr).then((response) =>
response.map(res => console.log(res.data[0].name, res.data[0].price_usd))
).catch((err) => console.log(err));
You could use a closure in the for loop to capture the value of i and use it as the index once the data is returned rather than using push:
getPrice(){
const cryptos = this.state.cryptos;
console.log(cryptos);
for (var i = 0; i < cryptos.length; i++) {
const cryptoUrl = 'https://api.coinmarketcap.com/v1/ticker/' + cryptos[i];
(function (x) {
axios.get(cryptoUrl)
.then((response) => {
const data = response.data[0];
console.log(data.price_usd);
var newPrices = this.state.prices;
newPrices[x] = data.price_usd;
this.setState({prices: newPrices});
console.log(this.state.prices);
})
.catch((error) => {
console.log(error);
});
})(i);
}
}