I want to find a clickable element using cypress. The clickable element always includes the text "Login" and is inside the container div. However, the catch is that I don't know if the clickable element is modeled using an <button>, <a>, or <input type="submit">.
My HTML sometimes look like this:
<nav>
<button>Wrong Login Button</button>
</nav>
<div class='container'>
...
<div>
<h2>Login</h2>
...
<button>Login</button>
</div>
</div>
And sometimes like this example:
<div class='container'>
...
<div>
<h2>Login</h2>
...
<div>
<div>
<a>Login</a>
</div>
</div>
</div>
</div>
It is sometimes a <button>, sometimes a <a> or <input type="submit">. And sometimes there are also more nested divs in there.
How can I find the clickable element (button, a or submit) element using cypress?
I only want to find the clickable element, so if there is a heading with the text "Login", it should be ignored.
This is nearly the answer:
cy.get('.container').contains('Login')
However, I somehow need to ignore all elements other than (button, a, and input[type=submit]). Because otherwise it would return the Login
I need something like:
cy.get('.container').find('button OR a OR input[type=submit]').contains('Login')
I thought of writing a custom command that can be used as the example below:
cy.get('.container').findClickable('Login')
The implementation could look similar as the example below:
Cypress.Commands.add('findClickable', {prevSubject: ['element']}, (
subject: Cypress.Chainable<HTMLElement>,
text: string,
options?: Partial<Cypress.Loggable & Cypress.Timeoutable & Cypress.Withinable & Cypress.Shadow>,
) => {
const buttons = cy.wrap(subject).find('button').contains(text)
const links = cy.wrap(subject).find('a').contains(text)
const submits = cy.wrap(subject).find('input[type=submit]').contains(text)
if (buttons.length + links.length + submits.length !== 1) {
throw new DOMException(
`Didn't find exactly one element. Found ${buttons.length} buttons, ${links.length} links, and ${submits.length} submits.`)
}
if (buttons.length === 1)
return buttons[0]
else if (links.length === 1)
return links[0]
else
return submits[0]
})
I know that the code above does not work because of many reasons. First, there is no .length attribute and second, cypress would fail because of the timeout of the first find('buttons') method.
Does anyone know how to implement the custom command the right way?
You can use the within and contains command with a combination of selector and text.
cy.get('.container').within(() => {
cy.contains('button', 'Login').click()
cy.get('input[type=submit]').click()
cy.contains('Login').click()
})
For Multiple Login, First we are checking the button is enabled or disabled, then we are clicking on it.
cy.contains('Login').each(($ele) => {
if($ele.is(":enabled")){
cy.wrap($ele).click()
}
})
You can also do, this is a more optimized code for the above snippet.
cy.contains('Login').filter(':enabled').click()
It is possible to search for multiple selectors when separating them with a comma:
cy.get('button, a, input[type=submit]')
So, search for all clickable elements and then filter for the clickable elements containing the required text.
Cypress.Commands.add('findClickable', {prevSubject: ['element']}, (
subject: Cypress.Chainable<HTMLElement>,
text: string,
_options?: Partial<Cypress.Loggable & Cypress.Timeoutable & Cypress.Withinable & Cypress.Shadow>,
) =>
cy.wrap(subject)
.find('button, a, input[type=submit]')
.filter(':contains(' + text + ')'),
)
Usage:
cy.get('.container').findClickable('Login')
Related
As a novice Javascript programmer, I'd like to create an html document presenting a feature very similar to the "reveal spoiler" used extensively in the Stack Exchange sites.
My document therefore has a few <div> elements, each of which has an onClick event listner which, when clicked, should reveal a hiddent text.
I already know that this can be accomplished, e.g., by
<div onclick="this.innerHTML='Revealed text'"> Click to reveal </div>
However, I would like the text to be revealed to be initially stored in a variable, say txt, which will be used when the element is clicked, as in:
<div onclick="this.innerHTML=txt"> Click to reveal </div>
Since there will be many such <div> elements, I certainly cannot store the text to be revealed in a global variable. My question is then:
Can I declare a variable that is local to a specific html element?
Yes you can. HTML elements are essentially just Javascript Objects with properties/keys and values. So you could add a key and a value to an HTML element object.
But you have to add it to the dataset object that sits inside the element, like this:
element.dataset.txt = 'This is a value' // Just like a JS object
A working example of what you want could look like this:
function addVariable() {
const myElement = document.querySelector('div')
myElement.dataset.txt = 'This is the extended data'
}
function showExtendedText(event) {
const currentElement = event.currentTarget
currentElement.innerHTML += currentElement.dataset.txt
}
addVariable() // Calling this one immediately to add variables on initial load
<div onclick="showExtendedText(event)">Click to see more </div>
Or you could do it by adding the variable as a data-txt attribute right onto the element itself, in which case you don't even need the addVariable() function:
function showExtendedText(event) {
const currentElement = event.currentTarget
currentElement.innerHTML += currentElement.dataset.txt
}
<div onclick="showExtendedText(event)" data-txt="This is the extended data">Click to see more </div>
To access the data/variable for the specific element that you clicked on, you have to pass the event object as a function paramater. This event object is given to you automatically by the click event (or any other event).
Elements have attributes, so you can put the information into an attribute. Custom attributes should usually be data attributes. On click, check if a parent element has one of the attributes you're interested in, and if so, toggle that parent.
document.addEventListener('click', (e) => {
const parent = e.target.closest('[data-spoiler]');
if (!parent) return;
const currentMarkup = parent.innerHTML;
parent.innerHTML = parent.dataset.spoiler;
parent.dataset.spoiler = currentMarkup;
});
<div data-spoiler="foo">text 1</div>
<div data-spoiler="bar">text 2</div>
That's the closest you'll get to "a variable that is local to a specific html element". To define the text completely in the JavaScript instead, one option is to use an array, then look up the clicked index of the spoiler element in the array.
const spoilerTexts = ['foo', 'bar'];
const spoilerTags = [...document.querySelectorAll('.spoiler')];
document.addEventListener('click', (e) => {
const parent = e.target.closest('.spoiler');
if (!parent) return;
const currentMarkup = parent.innerHTML;
const index = spoilerTags.indexOf(parent);
parent.innerHTML = spoilerTexts[index];
spoilerTexts[index] = currentMarkup;
});
<div class="spoiler">text 1</div>
<div class="spoiler">text 2</div>
There are also libraries that allow for that sort of thing, by associating each element with a component (a JavaScript function/object used by the library) and somehow sending a variable to that component.
// for example, with React
const SpoilerElement = ({ originalText, spoilerText }) => {
const [spoilerShown, setSpoilerShown] = React.useState(false);
return (
<div onClick={() => setSpoilerShown(!spoilerShown)}>
{ spoilerShown ? spoilerText : originalText }
</div>
);
};
const App = () => (
<div>
<SpoilerElement originalText="text 1" spoilerText="foo" />
<SpoilerElement originalText="text 2" spoilerText="bar" />
</div>
)
ReactDOM.createRoot(document.querySelector('.react')).render(<App />);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div class='react'></div>
Thanks everybody for your answers, which helped immensely! However, as a minimalist, I took all that I learned from you and came up with what I believe is the simplest possible code achieving my goal:
<div spoiler = "foo" onclick="this.innerHTML=this.getAttribute('spoiler')">
Click for spoiler
</div>
<div spoiler = "bar" onclick="this.innerHTML=this.getAttribute('spoiler')">
Click for spoiler
</div>
I have the following (simplified) HTML in my React project:
const { title } = this.props;
if(title) {
return (
<div class="someClass" onClick={someFunction} title={title}>
//Whatever..
</div>
)
}
else {
return (
<div class="someClass" onClick={someFunction}>
//Whatever..
</div>
)
}
Note that the if and else return values are identical except for the title attribute.
My question is, how can I prevent this repetition, but only add title when required.
I know I can add title to every element and just leave it blank if not required, but I do not want title attributes all over my HTML unless they are actually doing something.
I have tried:
var element = (
<div class="someClass" onClick={someFunction} title={title}>
//Whatever..
</div>
)
if(title)
element.title = //Whatever
But this did not work.
I have also tried
element.setAttribute('title', title);
But this did not work either.
Both the attempts above crashed the page.
Any ideas? Surely it can be done....
you can use:
title = {title?title:null}
if there is title in your props this will add the attribute title with the specified value to the element, otherwise no title attribute will be added.
In the React world with JSX you should use className instead of class.HTML doesn't exist in React the syntax that look like HTML is called JSX which just a syntactic sugar.
When you code hits a return statement everything that comes after will never be executed.So instead of doing:
if(condition){
return ...
}
else{
return ...
}
do can do:
if(condition){
return (<div>...</div>);
}
return (<div>...</div>)
So you don't need the else
Stop doing manual DOM manipulation like in Vanilla Javascriptby doing element.setAttribute('title', title); you don't need that in React.Please take your time to learn React basics.
I want to scroll to a specific paragraph that is a child of a child of an element
Below is a simplified version of my html.
I want to scroll to the paragraph with the className of ticketType. There are more paragraphs in my actual code so I want to target it by className.
There will be other paragraphs with this className that I don't want to scroll to, so I need to specifically target (and scroll to) the paragraph with className of ticketType within the element that has an id of ticket0.
{this.state.tickets.map((e, i) => {
return (
<div className = "content createTicket" id={`ticket${i}`} >
<form key={i}>
<p className="warning ticketType">{this.state.tickets[i].errors.ticketType}</p>
<div className="group">
<input
required
type="text"
value={this.state.tickets[i].ticketType}
onChange={event => this.changeTicketDetails(event, 'ticketType', i)}
placeholder="Ticket Name eg. General Admission"
/>
</div>
</form>
)
})}
This code is scrolling to the parent div:
document.getElementById(`ticket0`).scrollIntoView({behavior: "smooth"})
I am trying to get further options using:
document.getElementById(`ticket0`).children
but when I console.log this it isn't recognising the paragraph. I am getting:
HTMLCollection(2)
0: form
0: input
In CSS I could target it as #ticket0 < .ticketType
Is there something similar in Javascript
You get that console output because parentNode.children returns an HTMLCollection and not a single element.
To achieve the desired behavior, you could try:
document.querySelector('#ticket0 > .ticketType').scrollIntoView({behavior: "smooth"})
or put another way:
document.getElementById('ticket0').getElementsByClassName('ticketType')[0].scrollIntoView({behavior: "smooth"})
or if you only care for the first child:
document.getElementById('ticket0').firstElementChild.scrollIntoView({behavior: "smooth"})
or if you care about the n-th child:
document.getElementById('ticket0').children[n].scrollIntoView({behavior: "smooth"})
I am rendering a list of students, some of which have failed their exams. For those who have failed their exams, I display a red square behind their avatars.
Whenever I hover over a student's avatar I want to display the subject that student has failed. My issue at the moment is that I display the subjects for all students, not only the one I've hovered over.
How can I display only the mainSubject for the student who's avatar I hovered on?
Here is a link to my code sandbox: Example Link
I solved it like following.
Get the id of the hovered student. Match this id from the list of students you render. if its match then show the subjects
Also, I renamed the hook
add key prop
you can check this too https://codesandbox.io/s/zealous-bhaskara-mi83k
const [hoveredStudentId, setHoveredStudentId] = useState();
return (
<>
{students.map((student, i) => {
return (
<div className="student-card" key={i}>
<p>
{student.firstName} {student.lastName}
</p>
{student.passed === false ? (
<>
<img
id={student.id}
src={student.picture}
className="student-avatar fail"
onMouseEnter={e => {
setHoveredStudentId(e.currentTarget.id);
}}
onMouseLeave={e => {
console.log(e.currentTarget.id);
setHoveredStudentId(0);
}}
alt="avatar"
/>
{hoveredStudentId === student.id && (
<div className="subject-label">{student.mainSubject}</div>
)}
</>
) : (
<img
src={student.picture}
className="student-avatar"
alt="avatar"
/>
)}
</div>
);
})}
</>
);
Issue is that you have a list of students but only 1 flag to show/hide subjects.
Solution: 1
Maintain a list of flag/student. So you will have n flags for n students. Simple way for this is to have a state in a way:
IStudentDetails { ... }
IStudentStateMap {
id: string; // uniquely identify a syudent
isSubjectVisible: boolean;
}
And based on this flag isSubjectVisible toggle visibility.
Updated code
Solution 2:
Instead of handling it using React, use CSS tricks. Note this is a patch and can be avoided.
Idea:
Wrap Student in a container element and add a class onHover on elements on elements that needs to be shown on hover.
Then use CSS to show/hide those elements.
.student-container .onHover {
display:none;
}
.student-container:hover .onHover{
display: block;
}
This way there wont be rerenders and no need for flags.
Updated Code
However, solution 1 is better as you have more control and when you are using a UI library, its better to let it do all mutation and you should follow its ways.
I'm newbie to protractor framework, and I've been trying for a while to figure out how to get the outerHTML/InnerHTML/getText() (child elements) so that I can test if an element <img> is rendered onto a view. Heads up, we've an ng-grid and I'm trying to look up in its first column to see if it contains an img element also check if it contains an attribute i.e. src=res/someImg.png.
Here is what I got
html
<div>
<a>
<div>
<div>
<span>
<i><img src="res/someImg.png"></i>
</span>
</div>
<div>
...
</div>
<div>
...
</div>
</div>
</a>
</div>
test
it('should render an icon in agent list', function () {
var row = element.all(by.repeater('row in renderedRows')).get(3);
expect(row).not.toEqual(null); //pass
expect(row.element(by.css('img')).getAttribute('src').getText()).toMatch(/someImg.png/);//fail with null
expect(row.element(by.css('span')).outerHTML).toBe('<i><img src="res/someImg.png"></i>'); //fails
expect(row.element(by.css('i')).innerHTML).toBe('<img src="res/someImg.png">'); //fails
});
Can someone tell what am I doing wrong please?
Use getAttribute() in all 3 cases - for src, outerHTML and innerHTML:
expect(row.element(by.css('img')).getAttribute('src')).toMatch(/someImg.png/);
expect(row.element(by.css('span')).getAttribute('outerHTML')).toBe('<i><img src="res/someImg.png"></i>');
expect(row.element(by.css('i')).getAttribute('innerHTML')).toBe('<img src="res/someImg.png">');
Tested - works for me.
A little more explicitly:
expect(row.element(by.css('img')).getAttribute('src')).toMatch(/someImg.png/);
expect(row.element(by.css('span')).getOuterHtml()).toBe('<i><img src="res/someImg.png"></i>');
expect(row.element(by.css('i')).getInnerHtml()).toBe('<img src="res/someImg.png">');
As alecxe said on Aug 24 '16, "getOuterHtml() and getInnerHtml() are now deprecated in WebDriverJS and Protractor" (see comment from https://stackoverflow.com/a/27575804/3482730)
You should now use the following to get innerHTML code (as indicated here: https://github.com/angular/protractor/issues/4041#issuecomment-372022296):
let i = browser.executeScript("return arguments[0].innerHTML;", element(locator)) as Promise<string>;
Example using a helper function:
function getInnerHTML(elem: ElementFinder): Promise<string> {
return getInnerHTMLCommon(elem, elem.browser_);
}
function getInnerHTMLCommon(elem: WebElement|ElementFinder, webBrowser: ProtractorBrowser): Promise<string> {
return webBrowser.executeScript("return arguments[0].innerHTML;", elem) as Promise<string>;
}
const html = await getInnerHTML(browser.element(by.xpath("div[1]")));
console.log(html);