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').
Related
I am trying to find an item from a collection, from the code below, in order to update my react component, the propertState object isnt empty, it contains a list which i have console logged, however I seem to get an underfined object when i console log the value returned from my findProperty function... I am trying update my localState with that value so that my component can render the right data.
const PropertyComponent = () => {
const { propertyId } = useParams();
const propertyState: IPropertiesState = useSelector(
propertiesStateSelector
);
const[property, setProperty] = useState()
const findProperty = (propertyId, properties) => {
let propertyReturn;
for (var i=0; i < properties.length; i++) {
if (properties[i].propertyId === propertyId) {
propertyToReturn = properties[i];
break;
}
}
setProperty(propertyReturn)
return propertyReturn;
}
const foundProperty = findProperty(propertyId, propertyState.properties);
return (<>{property.propertyName}</>)
}
export default PropertyComponent
There are a few things that you shall consider when you are finding data and updating states based on external sources of data --useParams--
I will try to explain the solution by dividing your code in small pieces
const PropertyComponent = () => {
const { propertyId } = useParams();
Piece A: Consider that useParams is a hook connected to the router, that means that you component might be reactive and will change every time that a param changes in the URL. Your param might be undefined or an string depending if the param is present in your URL
const propertyState: IPropertiesState = useSelector(
propertiesStateSelector
);
Piece B: useSelector is other property that will make your component reactive to changes related to that selector. Your selector might return undefined or something based on your selection logic.
const[property, setProperty] = useState()
Piece C: Your state that starts as undefined in the first render.
So far we have just discovered 3 pieces of code that might start as undefined or not.
const findProperty = (propertyId, properties) => {
let propertyReturn;
for (var i=0; i < properties.length; i++) {
if (properties[i].propertyId === propertyId) {
propertyToReturn = properties[i];
break;
}
}
setProperty(propertyReturn)
return propertyReturn;
}
const foundProperty = findProperty(propertyId, propertyState.properties);
Piece D: Here is where more problems start appearing, you are telling your code that in every render a function findProperty will be created and inside of it you are calling the setter of your state --setProperty--, generating an internal dependency.
I would suggest to think about the actions that you want to do in simple steps and then you can understand where each piece of code belongs to where.
Let's subdivide this last piece of code --Piece D-- but in steps, you want to:
Find something.
The find should happen if you have an array where to find and a property.
With the result I want to notify my component that something was found.
Step 1 and 2 can happen in a function defined outside of your component:
const findProperty = (propertyId, properties) => properties.find((property) => property.propertyId === propertyId)
NOTE: I took the liberty of modify your code by simplifying a little
bit your find function.
Now we need to do the most important step, make your component react at the right time
const findProperty = (propertyId, properties) => properties.find((property) => property.propertyId === propertyId)
const PropertyComponent = () => {
const { propertyId } = useParams();
const propertyState: IPropertiesState = useSelector(
propertiesStateSelector
);
const[property, setProperty] = useState({ propertyName: '' }); // I suggest to add default values to have more predictable returns in your component
/**
* Here is where the magic begins and we try to mix all of our values in a consistent way (thinking on the previous pieces and the potential "undefined" values) We need to tell react "do something when the data is ready", for that reason we will use an effect
*/
useEffect(() => {
// This effect will run every time that the dependencies --second argument-- changes, then you react afterwards.
if(propertyId, propertyState.properties) {
const propertyFound = findProperty(propertyId, propertyState.properties);
if(propertyFound){ // Only if we have a result we will update our state.
setProperty(propertyFound);
}
}
}, [propertyId, propertyState.properties])
return (<>{property.propertyName}</>)
}
export default PropertyComponent
I think that in this way your intention might be more direct, but for sure there are other ways to do this. Depending of your intentions your code should be different, for instance I have a question:
What is it the purpose of this component? If its just for getting the property you could do a derived state, a little bit more complex selector. E.G.
function propertySelectorById(id) {
return function(store) {
const allProperties = propertiesStateSelector(store);
const foundProperty = findProperty(id, allProperties);
if( foundProperty ) {
return foundProperty;
} else {
return null; // Or empty object, up to you
}
}
}
Then you can use it in any component that uses the useParam, or just create a simple hook. E.G.
function usePropertySelectorHook() {
const { propertyId } = useParams();
const property = useSelector(propertySelectorById(propertyId));
return property;
}
And afterwards you can use this in any component
functon AnyComponent() {
const property = usePropertySelectorHook();
return <div> Magic {property}</div>
}
NOTE: I didn't test all the code, I wrote it directly in the comment but I think that should work.
Like this I think that there are even more ways to solve this, but its enough for now, hope that this helped you.
do you try this:
const found = propertyState.properties.find(element => element.propertyId === propertyId);
setProperty(found);
instead of all function findProperty
my first experience using useRef and forwardRef. I've been given some hints on a potential solution but I'm still a little way off, however the component is small and fairly simple.
What I am trying to do is use useRef to pass the result of a handleOnKeyDown function, which basically is looking for a keyCode 13 ("Enter" on keyboard) and then "click" the findGoodsBtnRef by passing it down into the CcceAttribute component via ref={findGoodsBtn}
I know what I have at current isn't right (because it doesn't work) and my editor is complaining about my use of ForwardRef on the top line, can anyone advise where I'm potentially going wrong on trying to implement this solution? TIA
const CommodityCodeLookupAttribute = forwardRef<Props, typeof CcceAttribute>(
({ attribute: productLookupAttribute, onChange, ...props }: ref) => {
const { object } = useFormObjectContext();
const ccceAttribute = object.attributeCollection.find(isCcceResultAttribute);
if (!ccceAttribute) {
return null;
}
const findsGoodsBtnRef = React.useRef();
const findGoodsBtn = findsGoodsBtnRef.current;
const handleOnKeyDown = (event) => {
if (event.keyCode === "13") {
// event.preventDefault();
console.log("Default prevented");
findsGoodsBtnRef.current.click();
// use the ref that points to the ccce attribute button -> click()
}
};
return (
<>
<StringAttributeDefault
attribute={productLookupAttribute}
onKeyDown={handleOnKeyDown}
{...otherProps}
/>
<CcceAttribute attribute={ccceAttribute} ref={findGoodsBtn} />
</>
);
};
In my react project, I have a requirement where I have to check for a condition continuously and call a function as soon as the condition becomes true.
The condition looks like this:
var El = document.activeElement;
if(document.getElementById('myInputField').getAttribute('aria-invalid') === 'true'
&& !(document.getElementById('myInputField') === El) {
// function call
}
How can this be achieved in ReactJs?
I'm okay to use jquery/javascript too.
Additional details:
I'm using an Input field from an external project. The input field checks the provided input and alter it's own aria-invalid attribute.
If the user submits the form with invalid input, I want to check if aria-invalid is true and focus is not on input field.
The example below shows the correct and incorrect way to manipulate the DOM (in the context of React):
// -> Imports
import React, { useEffect, useState } from "react";
// -> Element Ids
const subHeading = "sub-heading";
// -> DOM Manipulator
// -> Don't do this, opt for conditional style instead
// -> Check h1 element for example
function sideEffect() {
document.getElementById(subHeading).style.color = "red";
}
// -> Time Constant
const fiveSeconds = 5000;
export default function App() {
// -> State
const [readCond, writeCond] = useState(false);
// -> Side Effects
// -> Change readCond to true after five second pause
useEffect(() => {
setTimeout(() => writeCond(true), fiveSeconds);
}, []);
// -> Trigger DOM manipulation when readCond is true
useEffect(() => {
if (readCond) sideEffect();
}, [readCond]);
return (
<div>
<h1 style={{ color: readCond ? "red" : "black" }}>React Way - Good</h1>
<h2 id={subHeading}>DOM Way - Bad</h2>
</div>
);
}
Working sandbox
I'm using react context API in component did mount to set methods. I'd also like to use the media query there and set a method to open or close the sidenav depending on screen size.
Something like this
componentDidMount() {
let context = this.context;
let path = this.props.pageContext && this.props.pageContext.path;
context.setSidenavLeaf(newPath)
// Below this is where I'd like to use the media query to set the sidenavOPen to false. Just not sure how to achieve that
const match = window.matchMedia(`(max-width: 768px)`)
if(match {
context.setSidenavOpen(false)
}
}
Kind of confused about how to achieve something like this. I want to call the method and set it at a specific media break point in my component did mount. Which is using react router path prop. So if I hit that specific url rendered by that component and the screen size is such, close the sidenav else leave it open.
You need to listen for resize event:
componentDidMount() {
let context = this.context;
let path = this.props.pageContext && this.props.pageContext.path;
context.setSidenavLeaf(newPath);
// Below this is where I'd like to use the media query to set the sidenavOPen to false. Just not sure how to achieve that
this.checkWidth = () => {
const match = window.matchMedia(`(max-width: 768px)`);
if (match) {
context.setSidenavOpen(false);
}
};
this.checkWidth();
window.addEventListener("resize", this.checkWidth);
}
componentWillUnmount() {
window.removeEventListener("resize", this.checkWidth);
}
or add listener to the media query itself:
componentDidMount() {
let context = this.context;
let path = this.props.pageContext && this.props.pageContext.path;
context.setSidenavLeaf(newPath);
// Below this is where I'd like to use the media query to set the sidenavOPen to false. Just not sure how to achieve that
this.match = window.matchMedia(`(max-width: 768px)`);
this.checkWidth = (e) => {
if (e.matches) {
context.setSidenavOpen(false);
}
};
this.checkWidth(this.match);
this.match.addListener(this.checkWidth);
}
componentWillUnmount() {
this.match.removeListener(this.checkWidth);
}
for functional components, there's a hook on github that will do this for you https://www.npmjs.com/package/react-simple-matchmedia
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');