Trying to click through list of links in table - Cypress - javascript

I am trying to itirate through a list of links on a table and ensure the next page has ht ecorrect url but running into issues. One problem is that there are no good class names to work with so I have been using cy.xpath.
//Loop through each element (This is a dynamic amount of elements)
cy.xpath('//span[text()="Id"]//following::a[contains(#href,"maps")]'.each($el) => {
cy.get($el).then(($btn) => {
let id_text = $btn.text()
//Check that the element is visible and click on it
cy.get($el)
.should('be.visible')
.click()
//Check that the url contains the text value of the element that was clicked on
cy.url()
.should('contain', id_text)
})
})
It works one time through and then gets tripped up saying the DOM element became detached

When you see DOM element became detached it means an action has made the page refresh and a previous query is no longer pointing at a valid element.
In you case, the action is the .click() and the list of elements selected by cy.xpath('//span[text()="Id"]//following::a[contains(#href,"maps")]') has been refreshed, so the list that Cypress is iterating over is no longer valid.
One approach to solving this is to separate the test into two loops.
const links = []; // save link info here
const selector = '//span[text()="Id"]//following::a[contains(#href,"maps")]';
cy.xpath(selector)
.each(($el, index) => {
const id_text = $el.text()
links.push(id_text)
cy.xpath(selector).eq(index)
.as(`maps${index}`) // save a unique alias for this link
})
cy.then(function() {
links.forEach((link, index) => {
// Check that the element is visible and click on it
cy.get(`#maps${index}`) // get the element from the alias
.should('be.visible')
.click()
//Check that the url contains the text value of the element that was clicked on
cy.url().should('contain', link)
cy.go('back') // return to start page
})
})

You can shorten your code like this:
cy.get('[href*="maps"]').each(($el) => {
let id_text = $el.text().trim()
//Check that the element is visible and click on it
cy.wrap($el).should('be.visible').click()
//Check that the url contains the text value of the element that was clicked on
cy.url().should('contain', id_text)
//Wait half a sec
cy.wait(500)
})

Found a solution, it may (probably isn't) the best way but hey it works! This loops through all numbers in the column I needed, goes into the next page, checks that the url contains the number it clicked on, then goes back.
//Get length of table
cy.get('.MuiTableBody-root')
.find('tr')
.then((row) => {
let num_rows = row.length
for(let i=1; i < num_rows; i++){
cy.get(`:nth-child(${i}) > .column-id > a`).then(($btn) => {
let id_txt = $btn.text()
//Click on ID Number
cy.get(`:nth-child(${i}) > .column-id > a`)
.click()
//Check that the url has the id_txt
cy.url()
.and('contain',id_txt)
cy.go(-1) //Go back
}) //Ends the Numbers of IDs loop
} //Ends for loop
}) //Ends the then((row)) loop

Related

I have a list of buttons with the same class name but different inner text, how do I get the value of the text on click?

My list is being populated with this block of code:
function addToHistory(cityName) {
let searchHistory = JSON.parse(localStorage.getItem("Weather Search History")) || [];
searchHistory.push(cityName);
localStorage.setItem("Weather Search History", JSON.stringify(searchHistory));
};
function updateHistory() {
let searchHistory = JSON.parse(localStorage.getItem("Weather Search History")) || [];
$("#searchHistory").html(searchHistory
.map(searchHistoryList => {
return (`<li><button class="btn btn-link"> ` + searchHistoryList + `</button></li>`);
})
.join(""));
};
and that works great. It pulls from an array in local storage that is created each time the user enters a search term. Then populates the site's sidebar with said list.
However, I'm not sure how to then take the text values of the buttons out so that I may manipulate it.
Currently have:
$('#searchHistory').on('click', function () {
console.log($(???).val());
});
You want .text() or innerText (plain JavaScript). this refers to the current element. You can also use event.target.
$('#searchHistory').on('click', function () {
console.log($(this).text());
});
Try this in your function:
console.log($(this).innerHTML());
"this" refers to the specific element that triggered the click event.

click on specific sibling after matching text found

I have many of the below 'k-top' div elements, with the same inner div structure, except different unique text in two places, in 'k-in' and in my checkbox id.
<div class="k-top">
<span class="k-icon k-i-expand"></span><-------------- trigger click on this if below text is found
<span class="k-checkbox-wrapper" role="presentation">
<input type="checkbox" tabindex="-1" id="unique TEXT99" class="k-checkbox">
<span class="k-checkbox-label checkbox-span"></span>
</span>
<span class="k-in">unique TEXT99</span></div><- if this text is found in k-in trigger click on elem above
I want to iterate through all my span.k-ins until I find the innerText to match contains of 'unique' for instance, then once unique is found, I want to .click(); on it's sibling element '.k-i-expand' as seen in the mark-up above. I do not want to trigger a .click(); on all .k-i-expand just the specific one that has same parent as where my 'unique text' is found.
Thus far I have tried .closest, I have also tried sibling.parent.. both return null or undefined.. Note, I am not using jQuery.
The below works successfully to click all .k-i-expand - but I need to .click() only the one where k-in innerText contains 'unique'. Ideally I'd use starts with, or contains, but I'd specify the whole word if needed i.e. unique TEXT99
let exp = document.querySelectorAll('.k-i-expand');
let i;
for (i = 0; i < exp.length; ++i) {
exp[i].click();
};
More previous attempts can be seen here: how to run a .click on elems parent sibling selector?
I created a recursive function which checks all it's Siblings until it finds one with the specified innerHTML. If it does not find one, it does nothing:
function checkSibling(node) {
if (node.innerHTML == "unique TEXT99") {
return true;
} else if (node.nextSibling) {
return checkSibling(node.nextSibling);
} else {
return false;
}
}
async function clickOnNode() {
let exp = document.querySelectorAll(".k-i-expand");
for await (const node of exp) {
const hasText = await checkSibling(node);
if (hasText) {
console.log("Result: ", hasText);
node.click();
}
}
}
clickOnNode();
I also created a codepen with the code for you to play around. I guess the innerHTML check could be improved via a Regex.
Have you tried iterating over the .k-top elements and looking into each one to find your .k-in?
const expandItemsContaining = (text) => {
// Let's get all the .k-top divs
const kTops = document.querySelectorAll('.k-top');
// And peek into each and every one of them
kTops.forEach(kTop => {
// First we check whether there is a .k-in containing your text
const kIn = kTop.querySelector('.k-in');
const shouldClick = kIn && kIn.innerText && kIn.innerText.indexOf(text) !== -1;
// And if there is one we find the .k-i-expand and click it
if (shouldClick) {
const kExpand = kTop.querySelector('.k-i-expand');
if (kExpand) {
kExpand.click();
}
}
})
}

JavaScript issue on filtering text in html element

I've been struggling with the same piece of code for a few days by now...
So for the html part I have this :
<input type="text" id="search_immobilier_ville" name="search_immobilier[ville]">
<div class="collection" id="search-ville-collection"></div>
I have an input where I have to type any city name, then I want to filter the matching cities names into an existing array of cities like this :
let api_results = [['Le Moule',97152],['Lamentin',97189],...]
let ville_input = document.getElementById('search_immobilier_ville');
Then display the matching cities as a list of elements in the div #search-ville-collection.
On each keyup event, I need to perform this action, and update the visual list in real time.
My issue is that my filtering system is messed up, if I search "lam" for example, I can get a city called "lamentin" (pass the test) and another with just the "la" matching like "capesterre-de-marie-galante"
So far, I've done this :
// Previously filled by an API
let api_results = [[name,postalCode],[name,postalCode],...];
ville_input.addEventListener('keyup', () => {
let value = ville_input.value.toUpperCase().trim();
// User input
let input_val = ville_input.value.toUpperCase().trim();
// Filtering through var ville_input
api_results.forEach(el => {
if (el[0].toUpperCase().startsWith(input_val) || input_val.length >= 2 && el[0].toUpperCase().includes(input_val)) {
result_list.style.display = 'block';
// if city not present in the html list, add it
if (document.getElementById(el[1]) === null) {
$(result_list).append(`<a class="collection-item search-ville-results" id="${el[1]}"> ${el[0]} - ${el[1]} </a>`);
}
}
}); // End forEach
/* Looping through the collection child nodes to check
if there are cities that don't match the user input */
for (let child of result_list.children) {
console.log(child)
// if the user input doesn't match with an existing city in the node, delete the node
if (!child.text.toUpperCase().includes(input_val)) {
result_list.removeChild(child);
}
}
// Highlight first element of the list
result_list.firstElementChild.classList.add('active');
// empty results div if user input is empty
if (input_val == '') {
result_list.style.display = 'none';
result_list.innerHTML = '';
}
});
This code works PARTIALLY. For example, if I type "lam", I'm supposed to get only one result based on my result set, but check out this scenario :
Typing "l":
Typing "la":
Typing "lam":
(Here you begin to see the issue)
Typing "lame":
I'm sure there's something wrong in my code, but I can't figure out what.
Your problem is with the loop you are using to remove invalid items:
for (let child of result_list.children) {
console.log(child)
// if the user input doesn't match with an existing city in the node, delete the node
if (!child.text.toUpperCase().includes(input_val)) {
result_list.removeChild(child);
}
}
children returns a live HTMLCollection, meaning that if you modify it (eg, by removing items) it will update, which will cause issues with your loop. You need to go through the items in a way that will not be affected if the collection changes.
Wrong way:
This is an example of how the loop behaves currently. The button should remove all the items that contain "th", but note how it doesn't get them all and requires multiple clicks:
document.querySelector('#removeAll').addEventListener('click', () => {
let list = document.querySelector('#list')
for (let item of list.children) {
if (item.textContent.toLowerCase().includes('th')) {
list.removeChild(item)
}
}
})
<button type="button" id="removeAll">Remove All</button>
<ul id="list">
<li>Stuff</li>
<li>Things</li>
<li>Others</li>
<li>More</li>
</ul>
(A) correct way:
One way to loop through the collection in a way that is not affected by items being removed is to start at the last index and go backwards:
document.querySelector('#removeAll').addEventListener('click', () => {
let list = document.querySelector('#list')
let index = list.children.length
while (index--) {
let item = list.children[index]
if (item.textContent.toLowerCase().includes('th')) {
list.removeChild(item)
}
}
})
<button type="button" id="removeAll">Remove All</button>
<ul id="list">
<li>Stuff</li>
<li>Things</li>
<li>Others</li>
<li>More</li>
</ul>
Better way
As an additional note, you might be better off just clearing the list entirely and using filter to get the matching results and then update the list that way. As it is currently, you are doing a lot of checking to see if the list already contains the item, checking the current list for invalid items, etc. That will affect performance of your UI, especially on lower end devices.
Try to clear out your result_list as the first thing you do inside your keyup event.
result_list.innerHTML = '';
After that, make sure to filter your api_results.
const filteredResults = api_results.filter(result => result[0].toUpperCase().includes(input_val));
console.log(filteredResults); // Sanity check.
filteredResults.forEach(result => /* your old function. */);

Recreate Select Control Options Dynamically jQuery

I have the following function which I use to populate a Select control with options. I am grabbing values from objects on the document, and if a condition is met, throwing another value into a Select Control as an option...
function dispatchList() {
//grab list element
var list = document.getElementById("techName");
//foreach div assigned the .square class,
$('.square').each(function () {
//convert each div with .square class toString
var square = $(this).html().toString();
//grab availability value
var availability = $(this).find('tr:eq(4)').find('td').text();
//grab IP
var online = $(this).find('tr:eq(3)').find('td').text()
//if availability and IP values meet below condition...
if ((availability === "True") && (online.indexOf("10.") === 0)) {
//grab the name value from this div
var availableName = $(this).find('tr:eq(0)').find('td').text();
//create a new option element
var item = document.createElement("option");
//create a new text node containing the name of the tech
item.appendChild(document.createTextNode(availableName));
//append the new text node (option) to our select control
list.appendChild(item);
}
})
}
This function works great, but it runs when the document is ready. I need it to run when the document is ready, but also to recreate this list without refreshing the page. Ideally the select control could be emptied and recreated with a click event on a div.
This is the part I have struggled with. I have the following click event which it would make sense to chain this to, but I have not been able to work it out...
function availability() {
//for each element with a class of .square...
$('.square').each(function () {
//grab the id of each input element (button) contained in each .square div...
var btnId = $(this).find("input").attr("id");
//when .square div is clicked, also click it's associated asp button...
$(this).on('click', function (clickEvent) {
document.getElementById(btnId).click();
//****AND ALSO TRIGGER THE dispatchList() FUNCTION TO REBUILD THE #techName LIST****
})
})
}
Can this be done without AJAX or some other post back on the select control?
Does the #techName list need to be emptied first, and then rebuilt?
Thank you for any advice!
$(document).ready(function(){
$(".square").on('click', function (clickEvent) {
var el = clickEvent.target || clickEvent.srcElement
document.getElementById($(el).find('input').attr("id")).click();
dispatchList();
})
})
That's all i can do with the given question. I didn't test the code. You can give fiddle or anything to test. Also this function is written in the browser.

Target previous element with jQuery

I'm creating portfolio section where every portfolio item shows as an image and every portfolio item has its own div which is hidden and contains more information about that item. When the user clicks on some portfolio item (image) div with more information for that item is shown. Each div with more info has two classes, portf-[nid] and portf ([nid] is Node ID, I work in Drupal and this class with [nid] helps me to target portfolio item with more info div for that item).
Each of the more info divs contains arrows for item listing (next and previous) and I need to get them function, so when the user clicks on previous I need to hide current and show the previous item if it exists(when clicks on next to hide current and show next item if it exists).
My markup looks like:
<div class="portf-3 portf">
//some elements
</div>
<div class="portf-6 portf">
//some elements
</div>
<div class="portf-7 portf">
//some elements
</div>
My question is how to hide the div I'm currently on and show the previous (or next). For example: if it is currently shown div with class portf-6 and user clicks on previous arrow, this div is being hidden and div with class portf-3 is being shown.
It's not the problem to hide/show the div but how to check if there is the div above/below the current div and to target that div above or below the current div?
Here you are:
function GoToPrev()
{
var isTheLast = $('.portf:visible').prev('.portf').length === 0;
if(!isTheLast)
{
$('.portf:visible').hide().prev().show();
}
}
function GoToNext()
{
var isTheLast = $('.portf:visible').next('.portf').length === 0;
if(!isTheLast)
{
$('.portf:visible').hide().next().show();
}
}
To check if prev / next element is present or not, you can make use of .length property as shown below
if($('.portf:visible').prev('.portf').length > 0) // greater than 0 means present else not
same for next element
if($('.portf:visible').next('.portf').length > 0)
As you also need to update the next and previous buttons, I would suggest a more structured approach to the whole thing:
function update(delta) {
var $portfs = $('.portf');
var $current = $portfs.filter(':visible');
var index = $portfs.index($current) + delta;
if (index < 0) {
index = 0;
}
if (index > $portfs.length){
index = $portfs.length;
}
$current.hide();
$portfs.eq(index).show();
$('#prev').toggle(index > 0);
$('#next').toggle(index < $portfs.length-1);
}
$('#prev').click(function () {
update(-1);
});
$('#next').click(function () {
update(1);
});
// Hide all initially
$('.portf').hide();
// Show the first with appropriate logic
update(1);
JSFiddle: http://jsfiddle.net/TrueBlueAussie/xp0peoxw/
This uses a common function that takes a delta direction value and makes the decisions on range capping an when to hide/show the next/previous buttons.
The code can be shortened further, but I was aiming for readability of the logic.
If the next/prev buttons are correctly shown the range checking is not needed, so it simplifies to:
function update(delta) {
var $portfs = $('.portf');
var $current = $portfs.filter(':visible');
var index = $portfs.index($current) + delta;
$current.hide();
$portfs.eq(index).show();
$('#prev').toggle(index > 0);
$('#next').toggle(index < $portfs.length-1);
}
JSFiddle: http://jsfiddle.net/TrueBlueAussie/xp0peoxw/1/

Categories