How to resolve "serializes to the same string" message with Jest? - reactjs

In my React app, I've built a function that accepts a string full of regular text and any number of URLs. It then converts these into a <span> in React with every URL inside of an <a href tag. The code works really well but I can't seem to write a Jest test for it.
Here's what I've tried so far:
expect(convertHyperlinks('http://stackoverflow.com'))
.toStrictEqual(<span><a href='http://stackoverflow.com' target='_blank'>stackoverflow.com</a></span>);
And:
expect(convertHyperlinks('http://stackoverflow.com'))
.toMatchInlineSnapshot(<span><a href='http://stackoverflow.com' target='_blank'>stackoverflow.com</a></span>);
In the former case I'm getting the "serializes to the same string" message.
In the latter case, it's showing me this:
Expected properties: <span>stackoverflow.com</span>
Received value: <span>stackoverflow.com</span>
Might anyone know how to build a passing test for this?
Robert
Update: Here's the code for the function in question:
export const convertHyperlinks = (text: string): React.Node => {
// Find all http instances
const regex = /http\S*/g;
const hyperlinkInstances = text.match(regex);
if (!hyperlinkInstances) {
return <span>{text}</span>;
}
// Break up `text` into its logical chunks of strings and hyperlinks
let items = [];
let idx1 = 0;
let idx2 = -1;
hyperlinkInstances.forEach((hyperlink) => {
idx2 = text.indexOf(hyperlink, idx1);
if (idx2 === idx1) {
items.push(hyperlink);
idx1 += hyperlink.length;
} else {
items.push(text.substring(idx1, idx2));
items.push(hyperlink);
idx1 = idx2 + hyperlink.length;
}
});
if (idx1 < text.length) {
items.push(text.substring(idx1, text.length));
}
return (
<span>
{items.map((item) => {
if (item.includes('http://')) {
const plainLink = item.replace('http://', '');
return (
<a href={item.toLowerCase()} target='_blank' key={plainLink}>
{plainLink}
</a>
);
} else {
return item;
}
})}
</span>
);
};

You are returning a ReactNode from the method, which is an object. But you are trying to assert as just a string. It would'nt work.
This is what you may be getting back from the method,
And so, you must assert against the object you got, and not the way you are doing it right now,
const result = convertHyperlinks('http://stackoverflow.com')
expect(result.props[0].key).equals('stackoverflow.com');
// similar kind of assertions.
Additionally, I would suggest you go the component route and just render the component in the test method and assert for presence of elements as opposed to diving into react objects.
A representation of the same is as follows,
Here is your component,
const ConvertToHyperlinks = ({text}: {text: string}) => {
// your logic and then returning DOM elements.
return <></>;
}
Then you use it anywhere as,
<div>
<ConvertToHyperlinks text={'https://www.test.com/'} />
</div>
In your unit test you can then,
const renderedComponent = render(<ConvertToHyperlinks text={''https://www.anytyhing.com}/>);
expect(renderdComponent.getByText('anytyhing.com')).ToBeInTheDocument();
Here I am using some Rect Testing Library method but the idea is same even if you use enzyme etc.

Related

How to get or filter a bunch of childNodes by their style class name in ReactJs

I am having trouble figuring out how to get or filter a bunch of childNodes by their style class name inside my useEffect. Using ReactJs v18.
Straight after the line with: const circleElements = launcherCircle!.childNodes; I would like to get/filter the div's with the class name 'launcherPos' so I can position them in a circle formation.
const LauncherComponent = () => {
const launcherCircleRef = useRef<HTMLDivElement>(null);
let modules: Module[] | null = GetModules();
const enableLauncher = (module: Module) => {
return !module.IsEnabled ? styles['not-active'] : null;
};
useEffect(() => {
const launcherCircle = launcherCircleRef.current;
const circleElements = launcherCircle!.childNodes;
let angle = 360 - 190;
let dangle = 360 / circleElements.length;
for (let i = 0; i < circleElements.length; i++) {
let circle = circleElements[i] as HTMLElement;
angle += dangle;
circle.style.transform = `rotate(${angle}deg) translate(${launcherCircle!.clientWidth / 2}px) rotate(-${angle}deg)`;
}
}, []);
if (modules == null){
return <Navigate replace to={'/noaccess'} />
} else {
return (
<div data-testid="Launcher" className={styles['launcherContainer']} >
<div className={styles['launcherCircle']} ref={launcherCircleRef}>
{modules.map(function (module: Module, idx) {
return (
<div key={idx} className={styles['launcherPos']} ><div className={`${styles['launcherButton']} ${enableLauncher(module)}`}><img src={module.ImagePath} alt={module.Prefix} /></div></div>
)
})}
<div className={styles['launcherTextDiv']}>
<span>TEST</span>
</div>
</div>
</div>
)
}
};
export default LauncherComponent;
From what I've read getElementsByClassName() is not advisable practise because of the nature of ReactJs and it's virtual DOM.
I tried the following filter but I think with React garburling the class name I didn't get anything back.
const launcherChildren = launcherCircle!.children;
const circleElements = [...launcherChildren].filter(element => element.classList.contains('launcherPos'));
Maybe there's a way to ref an array of the just the children with the launcherPos class???
There must be a couple of different ways, but, they are eluding me.
When you filter/map an array of HTMLElements, the results are in the form of objects, which contains properties like, props, ref etc.
Since className is a prop on the element, you should try looking for the class name by digging into the props key.
Simply put, all the props that you pass to the element, like onClick, onChange, value, className are stored under the props property.
You can filter the results by converting the class name into an array and further checking if it contains the target string (launcherPos in this case).
Your code should look something like this:
const circleElements = [...launcherChildren].filter(element=>element.props.className.split(' ').includes('launcherPos'))
The above method could be used when an array directly holds elements. E.g: [<div></div>,<div></div>...].
The approach that you've followed is correct, except for the way you are selecting the elements by their class names. I can see that you are using CSS modules in this component, meaning all the class names exist as properties on the imported object(styles in this case), so when you use contains('launcherPos') you are essentially checking for the presence of a string, but when using CSS modules, class names are available only as object properties, that's the reason you are getting an empty array. Simply update launcherPos to styles.launcherPos and that shall fix the issue.
All-in-all your useEffect function should look something like this:
useEffect(() => {
const launcherCircle = launcherCircleRef.current;
const launcherChildren = launcherCircle!.children;
const circleElements = [...launcherChildren].filter(element => element.classList.contains(styles.launcherPos)); //change launcherPos to styles.launcherPos
let angle = 360 - 190;
let dangle = 360 / circleElements.length;
for (let i = 0; i < circleElements.length; i++) {
let circle = circleElements[i] as HTMLElement;
angle += dangle;
circle.style.transform = `rotate(${angle}deg) translate(${launcherCircle!.clientWidth / 2}px) rotate(-${angle}deg)`;
}
}, []);

How can I create a parent html element by appending sub element from an object?

In my react app I need to return a line which will be created based on a list.
Here is the object,
searchCriteria: {
op_company: "039",
doc_type: "ALL"
}
and in my UI, i need to show it as a paragraph with bold values. So the hard coded code would be like below
<p>Download request for op_company: <b>{searchCriteria.op_company}</b>, doc_type: <b>{searchCriteria.doc_type}</b></p>
But the object(searchCriteria) will be changed based on the user request. So I tried like below.
const getSearchCriteria = (criteria) => {
let searchCriteria = []
searchCriteria.push('Download request for')
Object.keys(criteria).forEach((key) => {
if(criteria[key] !== '') {
searchCriteria.push(` ${key}: ${criteria[key]},`)
}
});
return searchCriteria;
}
return (
<p>
{getSearchCriteria(searchCriteria).map((item) => <span key = {item}>{item}</span>)}
</p>
);
here i'm getting the expected output. But I can't get the value as bold (highlighted). Is there another way to directly deal with html elements?

React - how to render multiple buttons with a regular for loop?

Sorry if its very basic but:
when rendering multiple buttons (0-9) in an iteration - What is the difference btw map and for loop ? Why does the for loop only renders the first element (0) while map works fine? Why do I have to first push the buttons into an array and return that then (as seen on other examples) ? Can I use regular for loop and render buttons without pushing it into an arary?
Thanks!
import React from 'react';
const Keys = () => {
const renderKeys = () => {
//works fine
var arr = [1,2,3,4,5,6,7,8,9]
return arr.map((val) => {
return <button>{val}</button>
})
};
const renderKeys = () => {
//does not work
for (var i=0; i<10; i++) {
return <button>{i}</button>
}
};
return (
<div>
{renderKeys()}
</div>
)
};
When you call return inside a for-loop it stops executing the loop. That's why you only get back the first button.
However, calling return inside a .map() will not stop the loop from iterating. Instead you use return to explicitly define what you want to have in a new array.
Note that .map() creates a brand new array by using elements from an existing array. You are free to utilize those elements any way you want which makes it suitable for rendering JSX.
Example:
const numbers = [1, 2, 3, 4, 5]
const numbersMultipledByTwo = numbers.map((number) => {
return <div>{ number * 2 }</div>
})
Theoretically, you could accomplish the same effect using a for-loop but that will also require help from a second array.
Working code:
import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
class App extends React.Component{
getButtonsUsingMap = () => {
const array = [1, 2, 3 ,4, 5]
return array.map((number) => {
return <button>{number}</button>
})
}
getButtonsUsingForLoop = (num) => {
const array = []
for(var i = 1; i <= num; i++){
array.push(<button>{i}</button>)
}
return array
}
render(){
return(
<div>
<h4>Using .map()</h4>
{this.getButtonsUsingMap()}
<h4>using for-loop</h4>
{this.getButtonsUsingForLoop(5)}
</div>
)
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
In that getButtonsUsingForLoop function, you can see there are more explicit demands to make it work. First we need to satisfy the argument. Then initialize a new array. Then define a boundary for the loop. Iterate and push JSX to the empty-array. Then finally return that array. So the logic is not very succinct.
Whereas on the other-hand, a .map() essentially handles all of that. As long as you have a pre-existing array to iterate over (which 99% of the time you will be dealing with some sort of state or props-array.)
See sandbox: https://codesandbox.io/s/pensive-leftpad-lt5ml
What is the difference btw map and for loop?
.map() iterates an array (and arrays only) while a for loop could be lazily summarized as a more "general" loop mechanism that is independent of any specific data type.
Why does the for loop only renders the first element (0) while map works fine?
Because you're returning from the function in the first iteration of the for loop with
const renderKeys = () => {
//does not work
for (var i=0; i<10; i++) {
return <button>{i}</button> // this returns from the function you're in
}
};
.map() works fine because it returns a new array from iterating the input-array, e.g.:
const renderKeys = () => {
// works fine
var arr = [1,2,3,4,5,6,7,8,9]
return arr.map((val) => { // here you return the new array created by map
return <button>{val}</button>
});
};
Why do I have to first push the buttons into an array and return that then (as seen on other examples)?
basically to "imitate" what map does, e.g. creating a new array from the iteration.
Can I use regular for loop and render buttons without pushing it into an array?
Directly return you mean? I don't think it's possible, but maybe somebody else does know a way!
Why I think it's not possible?
return (
<div>
{renderKeys()}
</div>
)
in JSX you return a function and you can't pass a for loop as a function argument directly, e.g. this:
return (
<div>
for (var i=0; i<10; i++) {
<button>{i}</button>
}
</div>
)
would most likely give you a syntax error...

Clarification needed on React's JSX syntax

I am learning to code in React and bit confused with JSX syntax.
I tried to understand but getting confused again and again.
It will be better if someone explains to me what exactly happening here with this code and what is the problem.
Here I am trying to iterate over form element array with below code:
const form = formElementArray.map(element =>{
<Input
key = {element.id}
elementType={element.config.elementType}
elementConfig={element.config.elementConfig}
value={element.config.value}
inValid = {!element.config.valid}
touched = {element.config.touched}
changed={(event)=>this.onChangeHandler(event,element.id)}
shouldValidate={element.config.validation}>
</Input>
})
Error: Expected an assignment or function call and instead saw an expression.
when using arrow function can emit the return keyword when you don't provide open/close brackets.
to fix remove { and } from your arrow function
const form = formElementArray.map(element =>
<Input
key = {element.id}
elementType={element.config.elementType}
elementConfig={element.config.elementConfig}
value={element.config.value}
inValid = {!element.config.valid}
touched = {element.config.touched}
changed={(event)=>this.onChangeHandler(event,element.id)}
shouldValidate={element.config.validation}>
</Input>
)
JSX expression:
{<div>some html tag with one root tags. wrapping brackets { and } </div>}
JSX means JavaScript XML. that says, you can write html in your JavaScript file.
Arrow function:
const randomFunc1 = (param) => {
var data = 'some data' + param;
return data;
}
OR
const randomFunc2 = param => {
var data = 'some data' + param;
return data;
}
OR
const randomFunc3 = param => 'some data' + param;
Above randomFunc1, randomFunc2, randomFunc3 doing same as one. randomFunc3 is the shortest syntax.
Your code is ok. but map function needs to return statement to create a new array. so just need a return keyword before <Input> tag

how to pass jsx as a string inside the return call

Is it possible to do something like:
const data={
star: "<h1>STAR</h1>",
moon: "<h3>moon</h3>"
}
const App = () => {
return(
<div>{data.start}</div>
);
}
what i get is the actual string of <h1>STAR</h1> not just STAR
I don't think you can. You can return an html string and possibly get it to display, but JSX isn't a string, it gets compiled into javscript code that creates those elements. it works when your app is built, I don't think you can use dynamic strings with it at run-time. You could do something like this:
const getData = (which) => {
if (which === 'star') {
return (<h1>STAR</h1>);
}
if (which === 'moon') {
return (<h3>moon</h3>);
}
return null; // nothing will display
}
const App = () => {
return (
<div>{getData('star')}</div>
);
};
Strings can be converted to JSX with third-party libraries such as h2x or react-render-html. It may be unsafe to do this with user input because of possible vulnerabilities and security problems that may exist libraries that parse DOM.
It's impossible to use components this way because component names aren't associated with functions that implement them during conversion.

Resources