How to get the result 'toHaveStyle' in testing-library-react? - reactjs

Testing library react does not catch 'toHaveStyle'.
When I clicked on the 'Content', its children which have a blue color were changed to the red color.
However, in my test, they always have the blue color.
What should I do to solve this problem?
[...]
<Content data-testid={"list-element-content"} id={data.id} toggle={state[data.id - 1]}>
<div>{data.titleUnBold}</div>
<BoldTitle>{data.titleBold}</BoldTitle>
</Content>
[...]
const Content = styled.div`
color: ${ (props) => props.toggle ? "red" : "blue" };
`;
Below the test code:
test("color changed", () => {
const mockState = [false];
const mockSwitchGuide = jest.fn();
const { getAllByTestId, rerender } = render(
<GuideListPresenter
data={mockListData}
state={mockState}
onClick={mockSwitchGuide}
/>
);
act(() => {
fireEvent.change(getAllByTestId("list-element-content")[0],{
target: {toggle: true},
});
});
rerender(
<GuideListPresenter data={mockListData} state={mockState} onClick={mockSwitchGuide} />
);
expect(getAllByTestId("list-element-content")[0].toggle).toEqual(true); // success
expect(getAllByTestId("list-element-content")[0]).toHaveStyle("color: red"); // failed
})

To test the style of your component, you can get it directly from the html document, and see precisely what style is used for a specific element.
In your example, you would do something like below:
it('should change color to red on toggle click', () => {
const { container, getAllByTestId } = render(
<GuideListPresenter
data={mockListData}
state={mockState}
onClick={mockSwitchGuide}
/>
);
// Replace <YOUR_DIV_ID> by your component's id
let contentDiv = document.getElementById('<YOUR_DIV_ID>');
let style = window.getComputedStyle(contentDiv[0]);
expect(style.color).toBe('blue'); // Sometimes, only rgb style type is read here. See the rgb that corresponds to your color if need be.
act(() => {
fireEvent.change(getAllByTestId("list-element-content")[0],{
target: {toggle: true},
});
});
// Get the updated contentDiv
contentDiv = document.getElementsByClassName('<YOUR_DIV_CLASS_NAME>');
style = window.getComputedStyle(contentDiv[0]);
expect(style.color).toBe('red');
expect(getAllByTestId("list-element-content")[0].toggle).toEqual(true);
}
Here, to get the style of your element, I am using the element's id. However, it could also work with the element's className, and using the method document.getElementByClassName('YOUR_DIV_CLASS_NAME') instead. Note that the given name here should be unique, either with the id technique, or the className.

Related

Testing icon based on dynamically imported svg in react-testing-library

It's my first question here and I've been coding for only a year so please be patient. I looked for similar problems on the website but couldn't find anything that worked for me.
I created an Icon component where I dynamically import the requested SVG.
I first used this solution which was working well but when I tried to test this component with react-testing library and snapshot I realised that the snapshot was always the span that is returned when nothing is imported. I first thought it was linked to the use of useRef() because I saw people saying refs didn't work with Jest so I changed my Icon component to be this:
const Icon: FC<IconProps> = ({ type, onClick, tooltip, className }: IconProps) => {
const [IconProps, setIconProps] = useState({
className: className || 'icon'
});
const tooltipDelay: [number, number] = [800, 0];
useEffect(() => {
setIconProps({ ...IconProps, className: className || 'icon' });
}, [className]);
const SVG = require(`../../svg/${type}.svg`).default;
const spanClassName = "svg-icon-wrapper";
if (typeof SVG !== 'undefined') {
if (tooltip) {
return (
(<Tooltip title={tooltip.title} delay={tooltip.delay ? tooltipDelay : 0}>
<i className={spanClassName}>
<SVG {...IconProps} onClick={onClick} data-testid="icon" />
</i>
</Tooltip>)
);
}
return (
<SVG {...IconProps} onClick={onClick} data-testid="icon" />
);
}
return <span className={spanClassName} data-testid="span" />;
};
export default Icon;
Here is my test
it('matches snapshot of each icon', async (done) => {
jest.useFakeTimers();
const type = 'check';
const Component = <Icon type={type} />;
const renderedComp = render(Component);
setTimeout(() => {
const { getByTestId } = renderedComp;
expect(getByTestId('icon')).toMatchSnapshot();
done();
}, 3000);
jest.runAllTimers();
});
I added timeout because I thought it might be link to the time it takes to import the SVG but nothing I tried worked.
So the main problem is:
how can I have the svg being imported for this component to return an icon (with data-testid='icon') and not a span (with data-testid='span')
Help would be much appreciated. My app works perfectly and I'm stuck with this testing for a while now.

React ref that depends by an element's reference does not get passed to the child components

The following code creates an object ref that's called editor, but as you see it depends by the contentDiv element that's a ref to a HTMLElement. After the editor object is created it needs to be passed to the TabularRibbon. The problem is that the editor is always null in tabular component. Even if I add a conditional contentDiv?.current, in front of this, it still remains null...
Anyone has any idea?
export const Editor = () => {
let contentDiv = useRef<HTMLDivElement>(null);
let editor = useRef<Editor>();
useEffect(() => {
let options: EditorOptions = { };
editor.current = new Editor(contentDiv.current, options);
return () => {
editor.current.dispose();
}
}, [contentDiv?.current])
return (
<div >
<TabularRibbon
editor={editor.current}
/>
<div ref={contentDiv} />
..........

Draft.js adding hyperlink using applyEntity doesn't seem to work

I've been working on this problem for a while, and I hope it's not a bug.
I'm testing a text editor using Draft.js, and I'd simply like my users to add a hyperlink to their articles, so I create a function for that to happen by modifying the editor state's content.
const addLlink = (e) => {
e.preventDefault();
const contentState = editorState.getCurrentContent();
const contentStateWithEntity = contentState.createEntity(
'LINK', 'MUTABLE', {url: 'https://www.amazon.com'} // link for testing only
);
const entityKey = contentStateWithEntity.getLastCreatedEntityKey();
const contentStateWithLink = Modifier.applyEntity(
contentStateWithEntity,
editorState.getSelection(),
entityKey
);
// tried with and without styling
const styledContentStateWithLink = Modifier.applyInlineStyle(
contentStateWithLink,
editorState.getSelection(),
'HYPERLINK'
);
const newState = EditorState.set(editorState, {
currentContent: styledContentStateWithLink
});
setEditorState(newState);
// Also tried: setEditorState(RichUtils.toggleLink(newState, newState.getSelection(), entityKey));
}
When I trigger it, I just use an Evergreen-ui button:
<Button onMouseDown={addLlink} appearance="minimal">Link</Button>
The styling I implement using the Modifier object works, but I can't seem to get the link to actually work. It should be noted that the link plugin as a package, which works great, but that's only for detecting typed out/pasted URLs (not embedded into text).
Does anyone have an actual working example, or suggestions of what I may be doing wrong, for links that use React functional programming?
It turns out that I needed to add a Decorator in order for the entity to be detected. I placed this code above/outside of my Editor component:
const findLinkEntities = (contentBlock, callback, contentState) => {
contentBlock.findEntityRanges(
(character) => {
const entityKey = character.getEntity();
return (
entityKey !== null &&
contentState.getEntity(entityKey).getType() === 'LINK'
);
},
callback
);
}
const Link = (props) => {
const {url} = props.contentState.getEntity(props.entityKey).getData();
return (
<a href={url} >
{props.children}
</a>
);
};
const strategyDecorator = new CompositeDecorator([
{
strategy: findLinkEntities,
component: Link,
},
]);
Basically, it detects link entities and converts them into elements when you set the new content using EditorState:
const newState = EditorState.set(editorState, {
currentContent: styledContentStateWithLink,
decorator: strategyDecorator
});
According to the official example you need to add a decorator to find the entities and apply the Link component
const decorator = new CompositeDecorator([
{
strategy: findLinkEntities,
component: Link,
},
]);
And as you're using the linkify plugin you have to pass the decorator to plugins editor
import Editor from "draft-js-plugins-editor";
import createLinkifyPlugin from "draft-js-linkify-plugin";
import "draft-js-linkify-plugin/lib/plugin.css";
...
<Editor
decorators={[decorator]}
editorState={editorState}
onChange={setEditorState}
plugins={[linkifyPlugin]}
/>
...
you can check out the working example here https://codesandbox.io/s/test-selection-decorator-draft-js-link-0lblg?file=/src/ExtendedEditor.js

React.cloneElement clone already cloned element to add new props

I have have TestWrapper component that clones element and is supposed to make the background blue. Test is also doing the same, but instead sets color. I was expecting the rendered element to have blue background and red text, but it is only red. Here is the example:
const Test: React.FC<{ element: React.ReactElement }> = ({ element }) => {
return React.cloneElement(element, {
const isSelected = useIsSelected();
style: { ...element.props.style, color: isSelected ? 'red' : 'black' }
});
};
const TestWrapper: React.FC<{ element: React.ReactElement }> = ({
element
}) => {
// BackgroundColor is blue
const { backgroundColor } = useTheme();
return React.cloneElement(element, {
style: { ...element.props.style, background: backgroundColor }
});
};
export function App() {
return <TestWrapper element={<Test element={<h1>Heading</h1>} />} />;
}
How can I achieve this? I could do this differently, but I have to be able to access hook methods from Test and TestWrapper.
Simple codesandbox example: https://codesandbox.io/s/serene-bassi-ve1ym?file=/src/App.tsx
In TestWrapper you are cloning the Test component and applying your style props to it, which are not being passed down to the element that it's cloning itself. Just returning a cloned element doesn't create a referential equality where doing something to the component will affect the element it is cloning. You would need to give a style prop to Test and pass it down to the element being cloned:
const Test: React.FC<{
style?: React.CSSProperties;
element: React.ReactElement;
}> = ({ element, style }) => {
return React.cloneElement(element, {
style: { ...element.props.style, ...style, color: "red" }
});
};
I made a fork here. Hopefully this helps!

React.useEffect stack execution prevents parent from setting defaults

I have attached a simplified example that demonstrates my issue:
https://codesandbox.io/s/reactusehook-stack-issue-piq15
I have a parent component that receives a configuration, of which screen should be rendered. the rendered screen should have control over the parent appearance. In the example above I demonstrated it with colors. But the actual use case is flow screen that has next and back buttons which can be controlled by the child.
in the example I define common props for the screens:
type GenericScreenProps = {
setColor: (color: string) => void;
};
I create the first screen, that does not care about the color (parent should default)
const ScreenA = (props: GenericScreenProps) => {
return <div>screen A</div>;
};
I create a second screen that explicitly defines a color when mounted
const ScreenB = ({ setColor }: GenericScreenProps) => {
React.useEffect(() => {
console.log("child");
setColor("green");
}, [setColor]);
return <div>screen B</div>;
};
I create a map to be able to reference the components by an index
const map: Record<string, React.JSXElementConstructor<GenericScreenProps>> = {
0: ScreenA,
1: ScreenB
};
and finally I create the parent, that has a button that swaps the component and sets the default whenever the component changes
const App = () => {
const [screenId, setScreenId] = useState(0);
const ComponentToRender = map[screenId];
const [color, setColor] = useState("red");
React.useEffect(() => {
console.log("parent");
setColor("red"); // default when not set should be red
}, [screenId]);
const onButtonClick = () => setScreenId((screenId + 1) % Object.keys(map).length)
return (
<div>
<button style={{ color }} onClick={onButtonClick}>
Button
</button>
<ComponentToRender setColor={setColor} />
</div>
);
};
In this example, the default color should be red, for screen A. and green for the second screen.
However, the color stays red because useEffect is using a stack to execute the code. if you run the code you will see that once the button clicked there will be child followed by parent in log.
I have considered the following solution, but they are not ideal:
each child has to explicitly define the color, no way to enforce it without custom lint rules
convert the parent into a react class component, there has to be a hooks solution
This might be an anti-pattern where child component controls how its parent behave, by I could not identify a way of doing that without replicating the parent container for each screen. the reason I want to keep a single parent is to enable transition between the screens.
If I understood the problem correctly, there is no need to pass down setColor to the children. Making expressions more explicit might make a bit longer code, but I think it helps in readability. As what you shared is a simplified example, please let me know if it fits your real case:
const ScreenA = () => {
return <div>screen A</div>;
};
const ScreenB = () => {
return <div>screen B</div>;
};
const App = () => {
const [screen, setScreen] = useState<"a" | "b">("a");
const [color, setColor] = useState<"red" | "green">("red");
const onButtonClick = () => {
if (screen === "a") {
setScreen("b");
setColor("green");
} else {
setScreen("a");
setColor("red");
}
};
return (
<div>
<button style={{ color }} onClick={onButtonClick}>
Button
</button>
{screen === "a" ? <ScreenA /> : <ScreenB />}
</div>
);
};
render(<App />, document.getElementById("root"));

Resources