How to ensure useState works when mocking custom react hook - reactjs

I have a component which imports a custom hook. I want to mock returned values of this hook but ensure the useState still works when I fire and event.
component.tsx
export const Component = () => {
const { expanded, text, handleClick, listOfCards } = useComponent();
return (
<div>
<button id="component" aria-controls="content" aria-expanded={expanded}>
{text}
</button>
{expanded && (
<div role="region" aria-labelledby="component" id="content">
{listOfCards.map((card) => (
<p>{card.name}</p>
))}
</div>
)}
</div>
);
};
useComponent.tsx
const useComponent = () => {
const [expanded, setExpanded] = useState(false);
const { listOfCards } = useAnotherCustomHook();
const { translate } = useTranslationTool();
return {
text: translate("id123"),
expanded,
handleClick: () => setExpanded(!expanded),
listOfCards,
};
};
component.test.tsx
jest.mock("./component.hook");
const mockuseComponent = useComponent as jest.Mock<any>;
test("Checks correct attributes are used, and onClick is called when button is clicked", () => {
mockuseComponent.mockImplementation(() => ({
text: "Click to expand",
listOfCards: [{ name: "name1" }, { name: "name2" }],
}));
render(<Component />);
const button = screen.getByRole("button", { name: "Click to expand" });
expect(button).toHaveAttribute('aria-expanded', 'false');
fireEvent.click(button);
expect(button).toHaveAttribute('aria-expanded', 'true');
});
With the above test aria-expanded doesnt get set to true after we fire the event because im mocking the whole hook. So my question is, is there a way to only mock part of the hook and keep the useState functionality?

Related

How can test component rendering JSX element only when boolean flag is true

Suppose I have the following component that I would like to test:
const TestComponent = () => {
const [showModal, setShowModal] = useState(false);
return (
<>
<button
onClick={() => setShowModal(true)}
>
Show Modal
</button>
{ isOpen ? <Modal>...</Modal> : <div>No Modal</div>}
</>
)
}
Now I would like to have the component rendering the Modal component in its initial rendering and test its DOM. How can I pass showModal = true to it?
discribe("Rendered TestComponent", () => {
it("has Modal component", () => {
// Some operation needed here or after rendering the component?
render(<TestComponent />);
expect(screen.getByRole('input', {name: 'first-name' }).toBeInTheDocument;
})
})
First you have to attribute a data-testid to the button tag in order to manipulate it, keep the ID unique throughout the whole application.
Then you can use your favorite test lib to fire a click event, there are many avaliable that can do events, such as fire event:
const TestComponent = () => {
const [showModal, setShowModal] = useState(false);
return (
<>
<button
data-testid="show-modal"
onClick={() => setShowModal(true)}
>
Show Modal
</button>
{ showModal ? <Modal>...</Modal> : <div>No Modal</div>}
</>
)
}
describe("Rendered TestComponent", () => {
it("has Modal component", () => {
const { getByTestId } = render(<TestComponent />);
const showModalButton = getByTestId("show-modal");
fireEvent.click(showModalButton);
expect(screen.getByRole('input', { name: 'first-name' })).toBeInTheDocument;
});
});

React & Ant Design - Submit form before switching tabs

I have an ant design tab group with an ant design form in each tab. I would like that upon switching tabs, the user is prompted to submit their changes. If the user selects yes, then the data should be submitted. We switch tabs only if the response comes back as a success, or the user opted not to save.
The forms are all child components, which means the parent needs to somehow indicate that the tab is switching, and tell the child to submit their form.
I can achieve this with the useImperativeHandle hook and forwardRef but I'm wondering if there is some non-imperative way to achieve this?
Here is a stripped down example, not checking if the form is dirty, and just using the native confirm popup. There is also an async function to simulate submitting the form, which will randomly succeed or error.
Stackblitz: https://stackblitz.com/edit/react-ts-zw2cgi?file=my-form.tsx
The forms:
export type FormRef = { submit: (data?: Data) => Promise<boolean> };
export type Data = { someField: string };
const MyForm = (props: {}, ref: Ref<FormRef>) => {
const [form] = useForm<Data>();
useImperativeHandle(ref, () => ({ submit }));
async function submit(data?: Data): Promise<boolean> {
if (!data) data = form.getFieldsValue();
const res = await submitData(data);
return res;
}
return (
<Form form={form} onFinish={submit}>
<Form.Item name="someField">
<Input />
</Form.Item>
<Form.Item>
<Button htmlType="submit">Submit</Button>
</Form.Item>
</Form>
);
};
export default forwardRef(MyForm);
The parent component with the tabs:
const App: FC = () => {
const [activeKey, setActiveKey] = useState('tabOne');
const formOneRef = useRef<FormRef>();
const formTwoRef = useRef<FormRef>();
async function onChange(key: string) {
if (confirm('Save Changes?')) {
if (activeKey === 'tabOne' && (await formOneRef.current.submit()))
setActiveKey(key);
if (activeKey === 'tabTwo' && (await formTwoRef.current.submit()))
setActiveKey(key);
} else setActiveKey(key);
}
const tabs = [
{
label: 'Tab One',
key: 'tabOne',
children: <MyForm ref={formOneRef} />,
},
{
label: 'Tab Two',
key: 'tabTwo',
children: <MyForm ref={formTwoRef} />,
},
];
return <Tabs items={tabs} onChange={onChange} activeKey={activeKey} />;
};
The submit function
export default async function submitData(data: Data) {
console.log('Submitting...', data);
const res = await new Promise<boolean>((resolve) =>
setTimeout(
() => (Math.random() < 0.5 ? resolve(true) : resolve(false)),
1000
)
);
if (res) {
console.log('Success!', data);
return true;
}
if (!res) {
console.error('Fake Error', data);
return false;
}
}
Ant Design Tabs: https://ant.design/components/tabs/
Ant Design Form: https://ant.design/components/form/
I ended up making a state variable to store the submit function, and setting it in the child with useEffect.
A few caveats:
Had to set destroyInactiveTabPane to ensure forms were unmounted and remounted when navigating, so useEffect was called.
Had to wrap the form's submit function in useCallback as it was now a dependency of useEffect.
Had to make sure when calling setSubmitForm I had passed a function that returns the function, else the dispatch just calls submit immediately.
Stackblitz: https://stackblitz.com/edit/react-ts-n8y3kh?file=App.tsx
export type Data = { someField: string };
type Props = {
setSubmitForm: Dispatch<SetStateAction<() => Promise<boolean>>>;
};
const MyForm: FC<Props> = ({ setSubmitForm }) => {
const [form] = useForm<Data>();
const submit = useCallback(
async (data?: Data): Promise<boolean> => {
if (!data) data = form.getFieldsValue();
const res = await submitData(data);
return res;
},
[form]
);
useEffect(() => {
setSubmitForm(() => submit);
}, [setSubmitForm, submit]);
return (
<Form form={form} onFinish={submit}>
<Form.Item name="someField">
<Input />
</Form.Item>
<Form.Item>
<Button htmlType="submit">Submit</Button>
</Form.Item>
</Form>
);
};
const App: FC = () => {
const [activeKey, setActiveKey] = useState('tabOne');
const [submitForm, setSubmitForm] = useState<() => Promise<boolean>>(
async () => true
);
async function onChange(key: string) {
if (confirm('Save Changes?')) {
if (await submitForm()) setActiveKey(key);
} else setActiveKey(key);
}
const tabs = [
{
label: 'Tab One',
key: 'tabOne',
children: <MyForm setSubmitForm={setSubmitForm} />,
},
{
label: 'Tab Two',
key: 'tabTwo',
children: <MyForm setSubmitForm={setSubmitForm} />,
},
];
return (
<Tabs
items={tabs}
onChange={onChange}
activeKey={activeKey}
destroyInactiveTabPane
/>
);
};

How to test changes made by onClick event that calls a setState function, which is passed from another component and changes UI?

Basically the title.
Here is the overview of the App:
const App = () => {
const [isViewFavoriteImages, setIsViewFavoriteImages] = useState(false);
const toggleIsViewFavoriteImages = () => {
setIsViewFavoriteImages(
(prevToggleIsViewFavoriteImagesState) =>
!prevToggleIsViewFavoriteImagesState
);
};
return (
<div className="App">
<div className="container">
<ToggleImagesViewButton
toggleIsViewFavoriteImages={toggleIsViewFavoriteImages}
isViewFavoriteImages={isViewFavoriteImages}
/>
<ImageList isViewFavoriteImages={isViewFavoriteImages} />
</div>
</div>
);
};
export default App;
The button component:
export interface ToggleImageViewButtonProps {
toggleIsViewFavoriteImages: () => void;
isViewFavoriteImages: boolean;
}
const ToggleImageViewButton: React.FC<ToggleImageViewButtonProps> = ({
toggleIsViewFavoriteImages,
isViewFavoriteImages,
}) => {
return (
<button
onClick={toggleIsViewFavoriteImages}
className="btn btn_toggle-image-view"
data-testid="toggle-image-view"
>
{isViewFavoriteImages ? "view all" : "view favorites"}
</button>
);
};
export default ToggleImageViewButton;
And this is how I am testing it:
function renderToggleImagesViewButton(
props: Partial<ToggleImageViewButtonProps> = {}
) {
const defaultProps: ToggleImageViewButtonProps = {
toggleIsViewFavoriteImages: () => {
return;
},
isViewFavoriteImages: true,
};
return render(<ToggleImageViewButton {...defaultProps} {...props} />);
}
describe("<ToggleImagesViewButton />", () => {
test("button inner text should change to 'view all' when the user clicks the button", async () => {
const onToggle = jest.fn();
const { findByTestId } = renderToggleImagesViewButton({
toggleIsViewFavoriteImages: onToggle,
});
const toggleImagesViewButton = await findByTestId("toggle-image-view");
fireEvent.click(toggleImagesViewButton);
expect(toggleImagesViewButton).toHaveTextContent("view favorites");
});
});
This test fails and "view all" is still getting returned.
ToggleImageViewButton doesn't have internal state - the state was lifted to the parent, so testing state changes should happen in the parent's tests.
You could have the following integration test to verify the correct behaviour of the button when used in App.
test("App test", () => {
render(<App />);
const button = screen.getByTestId("toggle-image-view");
expect(button).toHaveTextContent("view favorites");
fireEvent.click(button);
expect(button).toHaveTextContent("view all");
});
As for the ToggleImageViewButton unit tests, you can simply test that it renders the right text based on isViewFavoriteImages value, and that the callback gets called when the button is clicked.
test("ToggleImageViewButton test", () => {
const onToggle = jest.fn();
render(<ToggleImageViewButton isViewFavoriteImages={false} toggleIsViewFavoriteImages={onToggle}/>);
expect(screen.getByTestId("toggle-image-view")).toHaveTextContent("view favorites");
fireEvent.click(screen.getByTestId("toggle-image-view"));
expect(onToggle).toHaveBeenCalled();
});

How to read the children in nodes when testing uisng enzymes

I have a component and I want to test the click method. I am using shallow but my test is failing as it cannot find the button and hence it`s click method. What is wrong with my code?
interface IProps {
label: string;
className: string;
onClick: () => void;
}
export const NewButton: React.StatelessComponent<IProps> = props => {
return (
<Button type="button" className={props.className} onClick={props.onClick}>
{props.label}
</Button>
);
};
import { shallow } from 'enzyme';
import * as React from 'react';
import { NewButton } from "../Buttons";
describe('<NewButton />', () => {
describe('onClick()', () => {
const props = {
className: "buttonSubmit",
label: "submit",
onClick: () => {},
}
test('successfully calls the onClick handler', () => {
const mockOnClick = jest.fn();
const wrapper = shallow(
<NewButton {...props} />
);
const button = wrapper.find('submit').dive();
expect(button.exists()).toEqual(true)
button.simulate('click');
expect(mockOnClick.mock.calls.length).toBe(1);
});
});
});
Since you are using shallow method, it will only render the component that we are testing. It does not render child components. So you should try to find the Button component.
const button = wrapper.find('Button');
After that you should mock the props.onClick event handler passed as props to NewButton component.
const props = {
className: "buttonSubmit",
label: "submit",
onClick: jest.fn(),
}
So you can use
describe('<NewButton />', () => {
describe('onClick()', () => {
const props = {
className: "buttonSubmit",
label: "submit",
onClick: jest.fn(),
}
test('successfully calls the onClick handler', () => {
const wrapper = shallow(
<NewButton {...props} />
);
const button = wrapper.find('Button');
expect(button.exists()).toEqual(true)
button.simulate('click');
// Since we passed "onClick" as props
// we expect it to be called when
// button is clicked
// expect(props.onClick).toBeCalled();
expect(props.onClick.mock.calls.length).toBe(1);
});
});
});

Test React confirmation window using enzyme

I've got a button in React which opens a simple confirmation window when the user clicks on it. Before I added the confirm method, the test below was green. After adding the confirm it's red. How do I need to change the test to work with the additional confirm?
React delete button:
const DeleteButton = (props) => {
const handleDelete = () => {
if(confirm("Are you sure?")) {
props.onDelete(props.id)
}
};
return (
<Button className="btn" onClick={handleDelete}>
<i className="fa fa-trash-o"></i>
</Button>
);
};
Here is the test (using enzyme):
describe('<DeleteButton />', () => {
it("deletes the entry", () => {
const onDelete = sinon.spy();
const props = {id: 1, onDelete: onDelete};
const wrapper = shallow(<DeleteButton {...props} />);
const deleteButton = wrapper.find(Button);
deleteButton.simulate("click");
expect(onDelete.calledOnce).to.equal(true);
});
});
You can stub confirm using sinon.stub.
describe('<DeleteImportButton />', () => {
it("simulates delete event", () => {
const onDeleteImport = sinon.spy();
const props = {id: 1, onDelete: onDeleteImport};
const wrapper = shallow(<DeleteImportButton {...props} />);
const deleteButton = wrapper.find(Button);
const confirmStub = sinon.stub(global, 'confirm');
confirmStub.returns(true);
deleteButton.simulate("click");
expect(confirmStub.calledOnce).to.equal(true);
expect(onDeleteImport.calledOnce).to.equal(true);
confirmStub.restore();
});
});

Resources