I have a React component that renders a <Link/>.
render: function () {
var record = this.props.record;
return (
<Link to="record.detail" params={{id:record.id}}>
<div>ID: {record.id}</div>
<div>Name: {record.name}</div>
<div>Status: {record.status}</div>
</Link>
);
}
I can easily obtain the rendered <a/>, but I'm not sure how to test that the href was built properly.
function mockRecordListItem(record) {
return stubRouterContext(require('./RecordListItem.jsx'), {record: record});
}
it('should handle click', function () {
let record = {id: 2, name: 'test', status: 'completed'};
var RecordListItem = mockRecordListItem(record);
let item = TestUtils.renderIntoDocument(<RecordListItem/>);
let a = TestUtils.findRenderedDOMComponentWithTag(item, 'a');
expect(a);
// TODO: inspect href?
expect(/* something */).to.equal('/records/2');
});
Notes: The stubRouterContext is necessary in React-Router v0.13.3 to mock the <Link/> correctly.
Edit:
Thanks to Jordan for suggesting a.getDOMNode().getAttribute('href'). Unfortunately when I run the test, the result is null. I expect this has to do with the way stubRouterContext is mocking the <Link/>, but how to 'fix' is still TBD...
I use jest and enzyme for testing. For Link from Route I use Memory Router from their official documentation https://reacttraining.com/react-router/web/guides/testing
I needed to check href of the final constructed link. This is my suggestion:
MovieCard.js:
export function MovieCard(props) {
const { id, type } = props;
return (
<Link to={`/${type}/${id}`} className={css.card} />
)
};
MovieCard.test.js (I skip imports here):
const id= 111;
const type= "movie";
test("constructs link for router", () => {
const wrapper = mount(
<MemoryRouter>
<MovieCard type={type} id={id}/>
</MemoryRouter>
);
expect(wrapper.find('[href="/movie/111"]').length).toBe(1);
});
Ok. This simply took some digging into the stubRouterContext that I already had.
The third constructor argument, stubs, is what I needed to pass in, overriding the default makeHref function.
Working example:
function mockRecordListItem(record, stubs) {
return stubRouterContext(require('./RecordListItem.jsx'), {record: record}, stubs);
}
it('should handle click', function () {
let record = {id: 2, name: 'test', status: 'completed'};
let expectedRoute = '/records/2';
let RecordListItem = mockRecordListItem(record, {
makeHref: function () {
return expectedRoute;
}
});
let item = TestUtils.renderIntoDocument(<RecordListItem/>);
let a = TestUtils.findRenderedDOMComponentWithTag(item, 'a');
expect(a);
let href = a.getDOMNode().getAttribute('href');
expect(href).to.equal(expectedRoute);
});
It was right there in front of me the whole time.
You can use a.getDOMNode() to get the a component's DOM node and then use regular DOM node methods on it. In this case, getAttribute('href') will return the value of the href attribute:
let a = TestUtils.findRenderedDOMComponentWithTag(item, 'a');
let domNode = a.getDOMNode();
expect(domNode.getAttribute('href')).to.equal('/records/2');
Related
Currently, doing setSearchParams(/* an object */) will replace the existing search params. If I would like to extend the search params instead of replacing them, e.g. adding a new param. What will be the most elegant way to achieve that?
I know I can modify the searchParams directly, then do something like setSearchParams(newSearchParamsToObject). Yet, since searchParams is not an object but a URLSearchParams. The code will be long and ugly.
Edit: Since some one appears don't understand what I have posted
Currently if we do something like this:
const [searchParams, setSearchParams] = useSearchParams({ a: 1 });
setSearchParams({ b: 2 });
It will replace the searchParams with ?b=2.
What I would like to achieve is adding b=2 to existing search params, i.e. the final search params should be ?a=1&b=2.
I suppose it would/should be trivial to set the search param value you want to add.
const [searchParams, setSearchParams] = useSearchParams();
...
searchParams.set(newParamKey, newParamValue);
setSearchParams(searchParams);
const [searchParams, setSearchParams] = useSearchParams();
const clickHandler = () => {
searchParams.set("b", 2);
setSearchParams(searchParams);
};
...
<button type="button" onClick={clickHandler}>
Add "b=2" param
</button>
Introduced in react-router#6.4 the setSearchParams now also takes a function, similar to the useState hook, to access the previous query params object to update from.
setSearchParams(searchParams => {
searchParams.set("b", 3);
return searchParams;
});
This is handy if you are using the setSearchParams in a useEffect hook as you won't need to list the current searchParams object as an external dependency.
Router v6 solution here (don't know if works with previous version too): setSearchParams accept a function that should return new search params, actually giving you the previous value:
For example:
setSearchParams(prev => ([...prev.entries(), ['foo', 'bar']]);
This is basically like #Drew Reese answer, but using the previous "state", like you would do with useState from React.
You could also use the URLSearchParams.append(name, value) method, in case you want several 'b'.
setSearchParams(searchParams.append('b', 2));
It's not like your modifying the state, here. You're just manipulating an URLSearchParams object. You don't have to worry about mutating it.
This version ->
setSearchParams(prev => ([...prev.entries(), ['foo', 'bar']]);
will add the same param without replacing, like this ->
?s=5&p=0&p=1
param p was added the second time, instead of ?s=5&p=1
React Router v6 have the useSearchParams() method that return an array that has, as a first value, a way to get the actual search params, and as a second, a method to set new values, the return value type definition is this: [URLSearchParams, SetURLSearchParams] and you can find more on the official documentation.
You can see a full working example to solve your problem on this codesandbox.
And this is a wrap up:
import React, { useCallback, useMemo } from "react";
import { useSearchParams } from "react-router-dom";
function getRandomNumber() {
return Math.floor(Math.random() * 1000) + 1;
}
function getAllParamsFromEntries(searchParamsEntity) {
const finalValues = {};
const entries = searchParamsEntity.entries();
let isDone = false;
while (!isDone) {
const iteratorElement = entries.next();
if (iteratorElement.value) {
finalValues[iteratorElement.value[0]] = iteratorElement.value[1];
}
isDone = iteratorElement.done;
}
return finalValues;
}
const MainScreen = () => {
const [searchParamsEntity, setSearchParams] = useSearchParams();
const allSearchParams = useMemo(() => {
return getAllParamsFromEntries(searchParamsEntity);
}, [searchParamsEntity]);
console.log(allSearchParams);
const appendRandomSearchParam = useCallback(() => {
setSearchParams((previousParams) => {
const searchParams = getAllParamsFromEntries(previousParams);
return {
...searchParams,
[`randomParam${getRandomNumber()}`]: getRandomNumber()
};
});
}, [setSearchParams]);
return (
<div>
<p>Search params now: {JSON.stringify(allSearchParams, undefined, 2)}</p>
<button onClick={appendRandomSearchParam}>Append</button>
</div>
);
};
So I have this Display() function which fetches events from the Google Calendar via an API and store each event's name via element.summary into the events set. And then once the events set is populated, I iterate through the set via for (let item of events) and create a new <a> tag for each event/item in the set using the name as the text via <a>{item}</a> (for e.g. <a>call<a>, then I push each <a> tag into a new array called tabs and then finally return the tabs array. The events set contains three items and when I console.log, I see the correct items ("call", "kist", & "go") in the set. However, once I console.log the tabs array, it only contains one <a> tag whose value is null whereas it is supposed to contain three <a> tags since it iterates through the events set which has three items and is supposed to create an <a> tag for each. Also, I get the error that item is not defined for the line for (let item of events), somehow I cannot iterate through the events set. See console output here.
function Display() {
let events = new Set()
let tabs = []
ApiCalendar.listUpcomingEvents(10)
.then(({result}: any) => {
result.items.forEach(element => {
console.log(element.summary)
events.add(element.summary)
}
);
console.log(events)
for (let item of events)
console.log(item)
tabs.push(<a>{item}</a>)
console.log(tabs)
return tabs
});
}
This is the class that I made in the same file as the above function, which basically renders a 'Log In' button if user is not logged in to their calendar, or renders the array of <a> tags returned by the Display() function if user is already logged in. However, even though the Display() function above does return something (i.e. an array of <a> tags) and the render() function inside the class also returns a <div> element with the corresponding component inside the div, I get the error Uncaught Error: Display(...): Nothing was returned from render. This usually means a return statement is missing. Or, to render nothing, return null. I am new to JavaScript and have no idea what I'm doing wrong. Any help is greatly appreciated and thank you in advance.
export default class LoginControl extends React.Component {
constructor(props) {
super(props);
this.state = {
sign: ApiCalendar.sign,
};
}
render() {
const isLoggedIn = this.state.sign;
let ele;
if (isLoggedIn) {
ele = <Display/>;
} else {
ele = <Button>'Sign In'</Button>;
}
return (
<div>
{ele}
</div>
);
}
}
Your Display function calls an async method and returns nothing. You will need to utilize state and effect inside Display to render returned data. But then, you will encounter errors if user navigates away from page before data is fetched.
Best solution for this problem would be to utilize redux and redux-thunk
Caution, untested code below
If you feel like you don't need redux, try this approach
async function fetchItems() {
const result = await ApiCalendar.listUpcomingEvents(10);
return result.result.items.map(({summary}) => summary);
}
function Display() {
const [items, saveItems] = useState([]);
const isMounted = useRef(true);
useEffect(() => {
return () => {
isMounted.current = false;
};
}, []);
useEffect(() => {
(async () => {
const items = await fetchItems();
//Do not update state if component is unmounted
if (isMounted.current) {
saveItems(items);
}
})();
}, []);
return <>{items.map(item => <a key={item}>{item}</a>)}</>
}
If you want to render more than summary, you can do it like this
async function fetchItems() {
const result = await ApiCalendar.listUpcomingEvents(10);
return result.result.items.map(({summary, somethingElse}) => ({summary, somethingElse}));
//can be replaced with return [...result.result.items]; to get all props
}
function Display() {
//... Main logic of Display component is the same,
//so I wouldn't duplicate it here
return <>{items.map(item => <div key={item.summary}>{item.summary} {item.somethingElse}</div>)}</>
}
It seems that you are not returning anything on Display component.
You can't return a promise on a component so you need to make it inside useEffect using react hooks or component lifecycle - and no, you don't need redux just to achieve this.
function Display() {
let events = new Set()
let tabs = [];
const [items, setItems] = useState([]);
const getList = async () => {
const res = await ApiCalendar.listUpcomingEvents(10);
setItems(res.items);
}
useEffect(async () => {
getList();
}, []);
return items.map(item => <div>{item}</div>);
}
I want to write a test for a utils method. In that method I get a html element by id and then change the color of the element. The problem is that element is only available after a button click. How can I mock the element?
UtilListItem.js
import variables from '../stylesheets/Variables.scss';
export function activeListItem(props){
let listItem = document.getElementById(props.id);
listItem.style.backgroundColor = variables.whiteGray;
return listItem;
}
UtilListeItem.test.js
it('check if the correct color is set for the acitve list item', () => {
let props = {id:'123'}
const listItem = activeListItem(props);
expect(listItem.style.backgroundColor).toBe('#ededed');
});
error
TypeError: Cannot read property 'style' of null
I'd suggest you to you jest.spyOn. It's a really handy way to spy on a function and/or attach some mock behaviour.
You can use it like this:
imoprt { activeListItem } from './utils';
let spy;
beforeAll(() => {
spy = jest.spyOn(document, 'getElementById');
});
describe('activeListItem', () => {
describe('with found element', () => {
let mockElement;
beforeAll(() => {
// here you create the element that the document.createElement will return
// it might be even without an id
mockElement = document.createElement(....);
spy.mockReturnValue(mockElement);
});
// and then you could expect it to have the background
it('should have the background applied', () => {
expect(mockElement.style.backgroundColor).toBe('#ededed');
});
});
describe('without found element', () => {
// and here you can create a scenario
// when document.createElement returns null
beforeAll(() => {
spy.mockReturnValue(null);
});
// and expect you function not to throw an error
it('should not throw an error', () => {
expect(() => activeListItem({id:'123'})).not.toThrow();
});
});
});
It's also a good idea to mock the .scss file, since it's a dependency of your utility file, so that when it's change it won't affect your unit test.
There are two options I can think of, you can opt either of them:
Put a check on listItem of function activeListItem
export function activeListItem(props) {
let listItem = document.getElementById(props.id);
if (listItem === null) {
return;
}
listItem.style.backgroundColor = variables.whiteGray;
return listItem;
}
Add dummy element in your test case
it('check if the correct color is set for the acitve list item', () => {
/** Create and add dummy div **/
let testId = "dummy-testId";
let newDiv = document.createElement("div");
newDiv.setAttribute("id", testId);
document.body.appendChild(newDiv);
let props = {id: testId}
const listItem = activeListItem(props);
expect(listItem.style.backgroundColor).toBe('#ededed');
});
This line have problem:
let listItem = document.getElementById(props.id);
Create element in the first place for mocking in jest. Be sure to wait for document and inject it.
What you doing is getting element when isn't ready to test / non exist in this context.
--- EDITED TO ADD EXAMPLE ---
What need to be added:
https://jestjs.io/docs/en/configuration#setupfiles-array
Others response to similar problem with example solution:
https://stackoverflow.com/a/41186342/5768332
A combination of the top two answers worked well for me:
...
beforeAll(() => {
const modalDiv = document.createElement("div");
modalDiv.setAttribute("id", "react-modal-container");
document.body.appendChild(modalDiv);
});
...
Problem
I have read https://github.com/ReactTraining/react-router/blob/master/packages/react-router/docs/guides/testing.md
I want to test react-router-dom, I don't care about how it work, I just need to make sure the library is working into my project boilerplate.
Reproduction
I am testing this component
<Link to="/toto">
toto
</Link>
This is the test
it('it expands when the button is clicked', () => {
const renderedComponent = mount(<Wrapper>
<MemoryRouter initialEntries={['/']}>
<Demo />
</MemoryRouter>
</Wrapper>);
renderedComponent.find('a').simulate('click');
expect(location.pathname).toBe('toto');
});
Expected
to be true
Result
blank
Question
How can I test react-router-dom?
If you look at the code for Link, you see this code:
handleClick = event => {
if (this.props.onClick) this.props.onClick(event);
if (
!event.defaultPrevented && // onClick prevented default
event.button === 0 && // ignore everything but left clicks
!this.props.target && // let browser handle "target=_blank" etc.
!isModifiedEvent(event) // ignore clicks with modifier keys
) {
event.preventDefault();
const { history } = this.context.router;
const { replace, to } = this.props;
if (replace) {
history.replace(to);
} else {
history.push(to);
}
}
};
So, presumably you find Link instead of a and override this method to return a value to your own callback you can validate the path set on the <Link>, This doesn't directly test react-router but it will validate that the paths you have set in your link are correct which is what your tests seem to be validating.
So something like (untested code):
const link = renderedComponent.find(Link)
let result = null
link.handleClick = event => {
const { replace, to } = link.props;
if (replace) {
result = null //we are expecting a push
} else {
result = to
}
}
};
link.simulate('click')
expect(result).toEqual('/toto') // '/toto' or 'toto'?
UPDATE
I've realised that the above doesn't work with a shallow render, however, if you just want to check if the to property is correct, you can probably just do it with expect(link.props.to).toEqual('/toto').
Let's say I have a component that looks like this:
var React = require('react/addons');
var ExampleComponent = React.createClass({
test : function () {
return true;
},
render : function () {
var test = this.test();
return (
<div className="test-component">
Test component - {test}
</div>
);
}
});
module.exports = ExampleComponent;
In my test, I could render this component using TestUtils, then stub out the method like so:
var renderedComponent = TestUtils.renderIntoDocument(<ExampleComponent/>);
sinon.stub(renderedComponent, 'test').returns(false);
expect(renderedComponent.test).toBe(false); //passes
But is there a way I could tell Sinon to automatically stub out a component's function every time an instance of that component is created? Ex:
sinon.stubAll(ExampleComponent, 'test').returns(false); //something like this
var renderedComponent = TestUtils.renderIntoDocument(<ExampleComponent/>);
expect(renderedComponent.test).toBe(false); //I'd like this to pass
If this isn't possible, is there a potential solution that comes close to providing the functionality I'm looking for?
You will need to overwrite the ExampleComponent.prototype instead of ExampleComponent. ExampleComponent is a constructor. Local methods like test() are saved in it's prototype.
sinon.stub(ExampleComponent.prototype, 'test').returns(false);
var renderedComponent = TestUtils.renderIntoDocument(<ExampleComponent/>);
expect(renderedComponent.test).toBe(false); //passes
I found a solution to my problem.
To clarify, my problem is that I wanted to stub out functions that belong to children components that are rendered under a parent component. So something like this:
parent.js
var Child = require('./child.js');
var Parent = React.createClass({
render : function () {
return (
<div className="parent">
<Child/>
</div>
);
}
});
module.exports = Parent;
child.js
var Child = React.createClass({
test : function () {
return true;
},
render : function () {
if (this.test) {
throw('boom');
}
return (
<div className="child">
Child
</div>
);
}
});
module.exports = Child;
If I were to use TestUtils to render Parent in one of my tests, it would throw the error, which I wanted to avoid. So my problem was that I needed to stub out Child's test function before it was instantiated. Then, when I render Parent, Child won't blow up.
The answer provided did not quite work, as Parent uses require() to get Child's constructor. I'm not sure why, but because of that, I can't stub out Child's prototype in my test and expect the test to pass, like so:
var React = require('react/addons'),
TestUtils = React.addons.TestUtils,
Parent = require('./parent.js'),
Child = require('./child.js'),
sinon = require('sinon');
describe('Parent', function () {
it('does not blow up when rendering', function () {
sinon.stub(Child.prototype, 'test').returns(false);
var parentInstance = TestUtils.renderIntoDocument(<Parent/>); //blows up
expect(parentInstance).toBeTruthy();
});
});
I was able to find a solution that fit my needs though. I switched my testing framework from Mocha to Jasmine, and I started using jasmine-react, which provided several benefits, including the ability to stub out a function of a class before it is instantiated. Here is an example of a working solution:
var React = require('react/addons'),
Parent = require('./parent.js'),
Child = require('./child.js'),
jasmineReact = require('jasmine-react-helpers');
describe('Parent', function () {
it('does not blow up when rendering', function () {
jasmineReact.spyOnClass(Child, 'test').and.returnValue(false);
var parentInstance = jasmineReact.render(<Parent/>, document.body); //does not blow up
expect(parentInstance).toBeTruthy(); //passes
});
});
I hope this helps someone else with a similar issue. If anyone has any questions I would be glad to help.