I am testing a component with which you can add sub-components by pressing on a '+' icon.
The rendered HTML is somewhere in the lines of:
<div>
<div>
<div>
From <input type="text" />
</div>
<div>
To <input type="text" />
</div>
<div>+</div>
</div>
</div>
So in the initial test, I test for the text to be there:
// test setup
test('From and to occur only once', () => {
const { getByText } = setup();
expect(getByText('From')).toBeInTheDocument();
expect(getByText('To')).toBeInTheDocument();
});
This all works perfectly fine. But I want to make sure initially the content is shown only once.
So my next test would be something in the lines of:
// test setup
test('When add button is clicked there From and To exist two times', () => {
const { getByText } = setup();
const addButton = getByText("+")
// first row
expect(getByText('From')).toBeInTheDocument();
expect(getByText('To')).toBeInTheDocument();
fireEvent.click(addButton);
// second row
expect(getByText('From')).toBeInTheDocument();
expect(getByText('To')).toBeInTheDocument();
});
How would I make a differentiation between the first and second time the elements occur?
Consider using getAllBy (after the click) instead of getBy. That can produce an array of all the elements found and you can expect() the total length of that to be 2.
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'm using React to view my pages.
I came across this problem if I try to call a function from a .js file nothing happens.
Basically, I have a small program that has two columns. Each column has a <p> tag that contains Column 1 and Column 2. There is a button below that once you click on it, both Columns should switch.
index.js
import "../style.css";
//import "./java.js";
class index extends React.Component {
render() {
return(
<div className="container">
<div className="columns" id="columnsToSwitch">
<div className="column1" id="column1_id">
<p>Column1</p>
</div>
<div className="column2" id="column2_id">
<p>Column 2</p>
</div>
</div>
<div className="switch" id="switch_id" onClick={this.switchColumns}>Switch</div>
</div>
);
}
}
export default index;
java.js
const switchCol = document.querySelectorAll("div.columns");
const el = document.getElementById("switch_id");
if(el) {
el.addEventListener("click", switchColumns, false);
}
switchCol.forEach(switches => switches.addEventListener('click', switchColumns));
function switchColumns(){
const makeSwitch1 = document.getElementById('column1_id');
document.getElementById('column2_id').appendChild(makeSwitch1);
const makeSwitch2 = document.getElementById('column2_id');
document.getElementById('column1_id').appendChild(makeSwitch2);
}
Method 1:
I tried to import the .js file that contains the function.
Nothing is happening after clicking "Switch".
Method 2:
Using onClick within a tag.
<div className="switch" id="switch_id" onClick={this.switchColumns}>Switch</div>
I get a couple of errors,
Uncaught TypeError: Cannot read properties of undefined (reading 'switchColumns')
The above error occurred in the <index> component:
On This line:
const switchCol = document.querySelectorAll(".switchCol");
There's no elements with the class of 'switchCol' so you're going to get an empty NodeList which causes the forEach loop to not execute so there are no click events on the columns themselves.
In the forEach block:
switchCol.forEach(switches => {
switches.addEventListener("column1", switchColumns);
switches.addEventListener("column2", switchColumns);
});
"column1" and "column2" are not valid event listeners, and there doesn't need to be two event listeners for one element. I think you mean to write the following:
switchCol.forEach(switch => switch.addEventListener('click', switchColumns))
Now onto your main switching column function:
function switchColumns(){
const makeSwitch1 = document.getElementById('column1');
document.getElementById('column2').appendChild(makeSwitch1);
const makeSwitch2 = document.getElementById('column2');
document.getElementById('column1').appendChild(makeSwitch2);
}
Variables makeSwitch1 and makeSwitch2 are going to be undefined as you do not have any elements with an id of column1 and column2 respectfully. Which is causing your issue with the second fix you tried.
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')
Edit: forgot an important part - this is noticeable if you click the button next to Jeff A. Menges and check the console log.
The important part of the code is the "setFullResults(cardResults.data.concat(cardResultsPageTwo.data))" line in the onClick of the button code. I think it SHOULD set fullResults to whatever I tell it to... except it doesn't work the first time you click it. Every time after, it works, but not the first time. That's going to be trouble for the next set, because I can't map over an undefined array, and I don't want to tell users to just click on the button twice for the actual search results to come up.
I'm guessing useEffect would work, but I don't know how to write it or where to put it. It's clearly not working at the top of the App functional component, but anywhere else I try to put it gives me an error.
I've tried "this.forceUpdate()" which a lot of places recommend as a quick fix (but recommend against using - but I've been trying to figure this out for hours), but "this.forceUpdate()" isn't a function no matter where I put it.
Please help me get this button working the first time it's clicked on.
import React, { useState, useEffect } from "react";
const App = () => {
let artistData = require("./mass-artists.json");
const [showTheCards, setShowTheCards] = useState();
const [fullResults, setFullResults] = useState([]);
useEffect(() => {
setFullResults();
}, []);
let artistDataMap = artistData.map(artistName => {
//console.log(artistName);
return (
<aside className="artist-section">
<span>{artistName}</span>
<button
className="astbutton"
onClick={ function GetCardList() {
fetch(
`https://api.scryfall.com/cards/search?unique=prints&q=a:"${artistName}"`
)
.then(response => {
return response.json();
})
.then((cardResults) => {
console.log(cardResults.has_more)
if (cardResults.has_more === true) {
fetch (`https://api.scryfall.com/cards/search?unique=prints&q=a:"${artistName}"&page=2`)
.then((responsepagetwo) => {
return responsepagetwo.json();
})
.then(cardResultsPageTwo => {
console.log(`First Results Page: ${cardResults}`)
console.log(`Second Results Page: ${cardResultsPageTwo}`)
setFullResults(cardResults.data.concat(cardResultsPageTwo.data))
console.log(`Full Results: ${fullResults}`)
})
}
setShowTheCards(
cardResults.data
.filter(({ digital }) => digital === false)
.map(cardData => {
if (cardData.layout === "transform") {
return (
//TODO : Transform card code
<span>Transform Card (Needs special return)</span>
)
}
else if (cardData.layout === "double_faced_token") {
return (
//TODO: Double Faced Token card code
<span>Double Faced Token (Needs special return)</span>
)
}
else {
return (
<div className="card-object">
<span className="card-object-name">
{cardData.name}
</span>
<span className="card-object-set">
{cardData.set_name}
</span>
<img
className="card-object-img-sm"
alt={cardData.name}
src={cardData.image_uris.small}
/>
</div>
)
}
})
)
});
}}
>
Show Cards
</button>
</aside>
);
});
return (
<aside>
<aside className="artist-group">
{artistDataMap}
</aside>
<aside className="card-wrapper">
{showTheCards}
</aside>
</aside>
);
};
export default App;
CodesAndBox: https://codesandbox.io/embed/compassionate-satoshi-iq3nc?fontsize=14
You can try refactoring the code like for onClick handler have a synthetic event. Add this event Listener as part of a class. Use arrow function so that you need not bind this function handler inside the constructor. After fetching the data try to set the state to the result and use the state to render the HTML mark up inside render method. And when I run this code, I have also seen one error in console that child elements require key attribute. I have seen you are using Array.prototype.map inside render method, but when you return the span element inside that try to add a key attribute so that when React diffing algorithm encounters a new element it reduces the time complexity to check certain nodes with this key attribute.
useEffect(() => {
// call the functions which depend on fullResults here
setFullResults();
}, [fullResults])
// now it will check whether fullResults changed, if changed than call functions inside useEffect which are depending on fullResults
I have something like this in view:
<div>
<div class="header-title">Example title 1</div>
</div>
<div>
<div class="header-title">Example title 2</div>
</div>
In my karma test I would like to investigate all divs by class name and check if inner text is correct so I have following code in test:
[...]
debugTest = fixture.debugElement.query(By.css('.header-title'));
elementTest = debugTest.nativeElement;
[...]
it('should component div has a correct value', () => {
fixture.detectChanges();
const content = elementTest.textContent;
expect(content).toContain('Example Title 1');
});
Following code works but I always get the first dom with .header-title class. How to extract next one? What if I have 20 divs with the same class name how to test them all?
Use queryAll() instead of query(), which returns an array.
query() returns single DebugElement which is always the first matching element, whereas queryAll() returns DebugElement[].
debugTest = fixture.debugElement.queryAll(By.css('.header-title'));
So that you can access like
elementTest1 = debugTest[0].nativeElement;
elementTest2 = debugTest[1].nativeElement;