Say I have
const BaseComponent = (props) => {
const classNames = ['base-component', props.className];
return (
<div className={classNames.join(' ')}>
{props.children}
</div>
)
};
const SomeComponent = () => {
return (
<BaseComponent
className="foo-bar"
>
Hello
</BaseComponent>
);
}
The rendered dom here would be <div class="base-component foo-bar">Hello</div>
Now, if I shallow mount SomeComponent and test the classes, only foo-bar is available:
const dom = shallow(<SomeComponent/>);
console.log(dom.hassClass('base-component')); // comes out as false
I understand that only foo-bar was passed as the class to SomeComponent but how do I validate all other classes here too?
What if you use the .prop API.
expect(dom.find('div').prop('className'))
.to.be.equal('base-component foo-bar');
shallow() does only render the top level component which does not include your other class. You can use dive() to let it render the one component one level deeper. dive() renders the only one non-DOM child of the wrapper it was called on.
const dom = shallow(<SomeComponent/>);
console.log(dom.dive().hasClass('base-component')); // now comes out as true
If you want inspect the whole DOM you will have to use render() instead of shallow().
Also not that a call to html() on a ShallowWrapper will always return the markup of a full render no matter if it was a shallow render or not.
Related
I am trying to render multiple buttons in a parent component that manages all child states centrally. This means that the parent stores e.g. the click state, the disabled state for each button in his own state using useState and passes it as props to the childs. Additionally the onClick function is also defined inside of the parent component and is passed down to each child. At the moment I am doing this like the following:
const [isSelected, setIsSelected] = useState(Array(49).fill(false));
...
const onClick = useCallback((index) => {
const newIsSelected = [...prev];
newIsSelected[i] = !newIsSelected[i];
return newIsSelected;
}, []);
...
(In the render function:)
return isSelected.map((isFieldSelected, key) => {
<React.Fragment key={key}>
<TheChildComponent
isSelected={isFieldSelected}
onClick={onClick}
/>
</React.Fragment/>
})
To try to prevent the child component from rerendering I am using...
... useCallback to make react see that the onClick function always stays the same
... React.Fragment to make react find a component again because otherwise a child would not have a unique id or sth similar
The child component is exported as:
export default React.memo(TheChildComponent, compareEquality) with
const compareEquality = (prev, next) => {
console.log(prev, next);
return prev.isSelected === next.isSelected;
}
Somehow the log line in compareEquality is never executed and therefore I know that compareEquality is never executed. I don't know why this is happening either.
I have checked all blogs, previous Stackoverflow questions etc. but could not yet find a way to prevent the child components from being rerendered every time that at least one component executes the onClick function and by doing that updated the isSelected state.
I would be very happy if someone could point me in the right direction or explain where my problem is coming from.
Thanks in advance!
This code will actually generate a new onClick function every render, because useCallback isn't given a deps array:
const onClick = useCallback((index) => {
const newIsSelected = [...prev];
newIsSelected[i] = !newIsSelected[i];
return newIsSelected;
});
The following should only create one onClick function and re-use it throughout all renders:
const onClick = useCallback((index) => {
const newIsSelected = [...prev];
newIsSelected[i] = !newIsSelected[i];
return newIsSelected;
}, []);
Combined with vanilla React.memo, this should then prevent the children from re-rendering except when isSelected changes. (Your second argument to React.memo should have also fixed this -- I'm not sure why that didn't work.)
As a side note, you can simplify this code:
<React.Fragment key={key}>
<TheChildComponent
isSelected={isFieldSelected}
onClick={onClick}
/>
</React.Fragment/>
to the following:
<TheChildComponent key={key}
isSelected={isFieldSelected}
onClick={onClick}
/>
(assuming you indeed only need a single component in the body of the map).
Turns out the only problem was neither useCallback, useMemo or anything similar.
In the render function of the parent component I did not directly use
return isSelected.map(...)
I included that part from a seperate, very simple component like this:
const Fields = () => {
return isSelected.map((isFieldSelected, i) => (
<TheChildComponent
key={i}
isSelected={isFieldSelected}
onClick={onClick}
/>
));
};
That is where my problem was. When moving the code from the seperate component Fields into the return statement of the parent component the rerendering error vanished.
Still, thanks for the help.
I am using Hooks + Render Props in my Preact/React application. Instead of passing simple state to the render(), I am passing a component function that can be used by the user of this component.
Also, my component is Hybrid in a sense that that SortDropdown is not UI-Less like we typically have with Render Prop components. It provides some basic UI in the form of anchor element which triggers the actual drop down and shows the custom content via Render Props render function. This is how I have implemented it:
export function SortDropdown(props: SortDropdownProps) {
const { render, onSelect, value } = props;
const anchor = (ref: Ref<Element>, open: VoidFunction) => (
<SortButton buttonElm={ref} onClick={open} />
);
const surfaceContent = (ref: Ref<any>, select: SortSelectFn) => {
// NESTED INNER COMPONENT - IS IT RIGHT?
function SortMenuItem(props: SortMenuItemProps) {
const { children, field } = props;
// THE HOOK IN QUESTION
const [someState, setSomeState] = useState(false);
const isSelected = value.key === field.key;
const onClick = () => select(field);
return (
<SortMenuButton canTrigger={someState} selected={someState} onClick={onClick}>
{children}
</SortMenuButton>
);
}
return (
<div ref={ref}>{render(SortMenuItem, select)}</div>
);
};
return (
<BaseDropdown anchor={anchor} surface={surfaceContent} onSelect={onSelect as any} />
);
}
Here I have two questions. First, does defining an inner nested component, here - SortMenuItem which is then passed to the render function violates the rules of Hooks? The component may be called n-times or may not be used at all by the calling component.
Second, is it a right practice to define a nested Higher-Order Component, here - SortMenuItem being an abstraction over SortMenuButton?
I have some components that are rendering another component (FetchNextPageButton) that is already tested in isolation, like these ones:
const News = () => (
<div>
<h1>News</h1>
...
<FetchNextPageButton query={NEWS_QUERY} path="viewer.news" />
</div>
)
const Jobs = () => (
<div>
<h1>Jobs</h1>
...
<FetchNextPageButton query={JOBS_QUERY} path="viewer.jobs" />
</div>
)
const Posts = () => (
<div>
<h1>Posts</h1>
...
<FetchNextPageButton query={POSTS_QUERY} path="viewer.posts" />
</div>
)
The thing is that I'd not like having to add tests on each of these components for a functionality that is already tested somewhere else, so I think that should be enough just to test that the component is rendered and that I'm passing the right props to it.
I'd have been able to test this easily with Enzyme with something like this:
expect(wrapper.find('FetchNextPageButton').props()).toMatchObject({
query: NEWS_QUERY,
path: "viewer.news"
})
So I'm wondering what's the best approach to test it by using React testing library instead.
This is the approach that Kent C. Dodds (the creator of RTL) shared with me after discussing it with him:
import FetchNextPageButton from 'FetchNextPageButton'
jest.mock('FetchNextPageButton', () => {
return jest.fn(() => null)
})
// ... in your test
expect(FetchNextPageButton).toHaveBeenCalledWith(props, context)
Don't believe it's possible. RTL looks like focusing on validating against DOM not React's components tree.
The only workaround I see is to mock FetchNextPageButton to make it rendering all props into attributes.
jest.mock("../../../FetchNextPageButton.js", () =>
(props) => <div data-test-id="FetchNextPageButton" {...props} />);
....
const { getByTestId } = render(<YourComponent />);
expect(getByTestId("FetchNextPageButton")).toHaveAttribute("query", NEWS_QUERY);
expect(getByTestId("FetchNextPageButton")).toHaveAttribute("path", "viewer.news");
Sure, this is smoothly only for primitive values in props, but validating something like object or function would be harder.
Think, it's not RTL-way, but I agree it would be massive work to check that in scope of each container(and completely ignoring that would be rather a risk).
PS toHaveAttribute is from jest-dom
In my case, I wanted to test that a Higher Order Component (HOC), correctly enhances the component that is passed to the HOC.
What I needed to do, is make the actual component a mock and pass it to the HOC. Like described in the existing answer, you can then just expect the properties, added by the HOC.
// after 'Component' get's passed into withSelectionConstraint, it should have an id prop
const Component = jest.fn(() => <h1>Tag Zam</h1>);
const WithConstraint = withSelectionConstraint(Component, ["instance"], true);
render(<WithConstraint />);
// passing the jest mock to the HOC, enables asserting the actual properties passed by the HOC
expect(Component).toHaveBeenCalledWith(
expect.objectContaining({ ids: mockInstanceRows.map(x => x.id) }),
expect.anything()
)
Based on Ben's answer, I wrote a version which doesn't raise any error :
jest.mock(
'path/to/your/component',
() => {
const MockedComponent = (props: any) => {
const cleanedProps = Object.keys(props).reduce<Record<string, unknown>>(
(a, b) => {
// Needed because html attributes cannot be camel cased
a[b.toLowerCase()] = props[b].toString();
return a;
},
{}
);
return (
<div data-testid="any-test-id" {...cleanedProps} />
);
};
return MockedComponent;
}
);
Note that the attributes values (expect(getByTestId('any-test-id')).toHaveAttribute('attribute','value')) will be stringified.
I've implemented a "slot" system in React from this article: Vue Slots in React. However, I'm running into trouble when trying to test the component due to a "mismatch" between the Enzyme wrapper's children and React's children.
This is the function to get a "slot" child from React children. The function works as expected within a app component when provided with the children prop, but doesn't work during testing as the "children" isn't the same format as React.children.
const getSlot = (children, slot) => {
if (!children) return null;
if (!Array.isArray(children)) {
return children.type === slot ? children : null;
}
// Find the applicable React component representing the target slot
return children.find((child) => child.type === slot);
};
The TestComponent isn't directly used in the tests, but is intended to show an example of how the "slots" would be implemented in a component.
const TestComponent = ({ children }) => {
const slot = getSlot(children, TestComponentSlot);
return (
<div id="parent">
<div id="permanentContent">Permanent Content</div>
{slot && <div id="optionalSlot">{slot}</div>}
</div>
);
};
const TestComponentSlot = () => null;
TestComponent.Slot = TestComponentSlot;
This is the basics of the tests I am trying to write. Essentially, creating a super basic component tree and then checking if the component's children contained the expected "slot" component. However, the getSlot function always returns null as the input isn't the same as the input provided by React children when used within the app.
it("Finds slots in React children", () => {
const wrapper = mount(
<div>
<TestComponent.Slot>Test</TestComponent.Slot>
</div>
);
// Unsure how to properly get the React children to test method.
// Below are some example that don't work...
// None of these approaches returns React children like function expects.
// Some return null and other return Enzyme wrappers.
const children = wrapper.children();
const { children } = wrapper.instance();
const children = wrapper.children().instance();
// TODO: Eventually get something I can put into function
const slot = getSlot(children, TestComponentSlot);
});
Any help or insights would be greatly appreciated!
The problem here is that when you're using enzyme's children() method it returns ShallowWrapper[1]. In order to get the children as a React component you have to get them directly from the props method.
So, derive the children in this way:
const children = wrapper.props().children;
CodeSandbox example.
I'm trying to write a component that injects an ID into it's child component. Something like:
<Id id='someId'>
<div>Hello!</div>
</Id>
With an expected output of: <div id='someId'>Hello!</div>
I have it working whenever I use standard HTML components like <div />, <span />, etc. I can't, however, get it to work whenever I use a custom component.
const Foo = ({ children }) => (
<span>{children}</span>
)
<Id id='someId'><Foo>Child of Foo</Foo></Id>
The above will not work and I'm not sure why. Here's what the <Id /> component looks like:
import * as React from 'react'
export const Id = ({ children, id }) => (
React.cloneElement(children, { id })
)
export default Id
UPDATE: Answer is I can't do it.
When you are doing React.cloneElement(children, { id });, you are actually creating a react component and pass props into it. In this case { id } is the same as { id: id }, which mean you are passing the value of the variable id as a props named id to Foo. You can access the value in Foo from this.props.id.
You should retrieve the id passed as props in Foo, then pass it as to the span.
const Foo = ({ children, id }) => (
<span id={id}>{children}</span>
);
About what you asked in the comment, you can achieve it by telling Foo to pass all props, no matter what it is. Something like:
const Foo = props => {
return <span {...props}>{props.children}</span>;
};
Check this updated codesandbox.
I don't think there is any other way for you to achieve it if you don't want the component to be aware that it needs to handle id. AFAIK, most npm packages that works with HOC need to be aware about what are the names of the props that need to be handled.