How do I/should I split my react integration test? - reactjs

I have this test which does 3 things:
input onChange gets updated
button onClick fires searchMovies request with input value
Results are in the document
test code:
import { searchMovies as mockSearchMovies, getPopular, getFiltered, getGenreIds } from "./tmdbAPI"
afterEach(cleanup)
jest.mock('./tmdbAPI', () => {
return {
searchMovies: jest.fn(() => Promise.resolve([{ id: 1, title: "test-1", poster_path: 'test-path' }])),
getPopular: jest.fn(() => Promise.resolve([{}])),
getFiltered: jest.fn(() => Promise.resolve([{}])),
getGenreIds: jest.fn(() => Promise.resolve([{}])),
}
})
test('1. input onChange gets updated, 2. button onClick fires searchMovies request with input value, 3. Results are in the document', async () => {
const { debug, getByPlaceholderText, getByTestId } = render(
<Provider>
<FindMovies />
</Provider>
)
const testText = 'some movie title'
const input = getByPlaceholderText('Search movies...')
fireEvent.change(input, { target: { value: testText } })
expect(input.value).toBe(testText)
const button = getByTestId('search-button')
fireEvent.click(button)
expect(mockSearchMovies).toHaveBeenCalledTimes(1)
expect(mockSearchMovies).toHaveBeenCalledWith(testText)
await wait(() => expect(getByTestId("found-movie-item")).toBeInTheDocument())
})
How do I/should I split this test so each test keeps it's "state" so I can move on next one?
I'm running useEffect function in my provider which runs getPopular, getFiltered and getGenreIds async request. If I'm not mocking them as in example above I get an error. Is there a way to get around these unnecessary mocks that I'm currently not testing?

Related

React - Jest test failing - TestingLibraryElementError: Unable to find an element with the text

I've a jest test that is failing on addition of a new component to the page. The test is about showing of an error alert once error occurs. Code works in local environment but fails during commit.
Error Text:
TestingLibraryElementError: Unable to find an element with the text:
Student is unable to perform register/unregister activities.. This could be because
the text is broken up by multiple elements. In this case, you can
provide a function for your text matcher to make your matcher more
flexible.
Test:
jest.mock('react-query', () => ({
...jest.requireActual('react-query'),
useMutation: jest.fn((_key, cb) => {
cb();
return { data: null };
})
}));
const useMutation = useMutationHook as ReturnType<typeof jest.fn>;
describe('StatusAlert', () => {
beforeEach(() => {
useMutation.mockReturnValue({});
});
afterEach(() => {
jest.restoreAllMocks();
});
it('should show error', () => {
useMutation.mockReturnValueOnce({
isError: true
});
const { getByText } = render(
<StudentRegister
students={[studentStub, studentStub]}
onSuccess={jest.fn()}
/>
);
expect(getByText(ErrorDict.ErrorRequest)).toBeInTheDocument();
});
StudentRegister:
Adding this component is causing the above mentioned error:
interface Props {
selectedStudents: Array<Student>;
onSuccessCallback: () => void;
}
export const StudentSelectionBar: FC<Props> = ({
selectedStudents,
onSuccessCallback
}) => {
const [isOpenDropCourseModal, setisOpenDropCourseModal] =
useState(false);
const [studentIds, setStudentIds] = useState<string[]>([]);
useEffect(() => {
setStudentIds(selectedStudents.map((student) =>
student.id));
}, [selectedStudents]);
const onToggleOpenDropCourseModal = useCallback(() => {
setisOpenDropCourseModal(
(state) => !state
);
}, []);
const {
isError: isDropCourseError,
isSuccess: isDropCourseSuccess,
isLoading: isDropCourseLoading,
mutateAsync: DropCourseMutation,
error: DropCourseError
} = useMutation<void, ApiError>(
() => dropCourse(selectedStudents.map((student) =>
student.id)),
{
onSuccess() {
onToggleOpenDropCourseModal();
onSuccess();
}
}
);
return (
<>
<StatusAlert
isError={isDropCourseError}
isSuccess={isDropCourseSuccess}
errorMessage={
dropCourseError?.errorMessage ||
ErrorMessages.FailedPostRequest
}
successMessage="Students successfully dropped from
course"
/>
<StatusAlert
isError={registerMutation.isError}
isSuccess={registerMutation.isSuccess}
errorMessage={
registerMutation.error?.errorMessage ||
ErrorDict.ErrorRequest
}
successMessage="Students successfully registered"
/>
<StatusAlert
isError={isError}
isSuccess={isSuccess}
errorMessage={
error?.errorMessage ||
ErrorDict.ErrorRequest
}
successMessage="Students successfully unregistered"
/>
<Permissions scope={[DropCourseUsers]}>
<LoadingButton
color="error"
variant="contained"
onClick={onToggleDropCourseUserModal}
className={styles['action-button']}
loading={isDropCourseLoading}
loadingPosition="center"
disabled={registerMutation.isLoading || isLoading}
>
drop Course
</LoadingButton>
</Permissions>
<DropCourseModal
isOpen={isOpenDropCourseModal}
onCloseModal={onToggleOpenDropCourseModal}
onArchiveUsers={DropCourseMutation}
users={studentIds}
/>
</>
);
};
Update:
I've noticed that removing useEffect() hook from the component, makes it render correctly in the test. Its function is to update the state variable holding studentIds on every selection on the list.
Is there a way to mock following useEffect hook with dependency in the test?
const [studentIds, setStudentIds] = useState<string[]>([]);
useEffect(() => {
setStudentIds(selectedStudents.map((student) => student.id));
}, [selectedStudents]);

(Jest/Enzyme/React) How to assert that throttled function inside change handler has been called?

I need to assert that SearchInputActions.onSearchActivated(value) is called when something is written in an input. It is a callback which is in change handler handleChange. I've been trying to create to mocks - one for handleChange and one for search but it did not work either. I am using jest and enzyme for tests.
const SearchInput = () => {
const search = throttle(event => {
const value = event.target.value;
SearchInputActions.onSearchActivated(value);
});
const handleChange = event => {
event.persist();
search(event);
};
return (
<div>
<SomeChildComponent />
<input type="text" onChange={handleChange} />
</div>
)
}
Test:
it('should dispatch search action', async () => {
const tree = mount(<SearchInput />);
const spySearch = jest.spyOn(SearchInputActions, 'onSearchActivated');
SearchInputActions.onSearchActivated.mockImplementation(() => {})
tree.find('input').simulate('change', {target: value: 'test'}});
expect(spySearch).toBeCalled();
}
I figured it out: Because the callback function was throttled (lodash throttle) I needed to add jest.useFakeTimers(); Final code looks like this:
jest.useFakeTimers();
it('should dispatch search action', async () => {
const tree = mount(<SearchInput />);
const spySearch = jest.spyOn(SearchInputActions, 'onSearchActivated');
SearchInputActions.onSearchActivated.mockImplementation(() => {})
tree.find('input').simulate('change', {target: value: 'test'}});
expect(spySearch).not.toBeCalled();
jest.runAllTimers();
expect(spySearch).toBeCalled();
SearchInputActions.onSearchActivated.mockRestore();
}

How to test for document being undefined with RTL?

I have the following react hook which brings focus to a given ref and on unmount returns the focus to the previously focused element.
export default function useFocusOnElement(elementRef: React.RefObject<HTMLHeadingElement>) {
const documentExists = typeof document !== 'undefined';
const [previouslyFocusedEl] = useState(documentExists && (document.activeElement as HTMLElement));
useEffect(() => {
if (documentExists) {
elementRef.current?.focus();
}
return () => {
if (previouslyFocusedEl) {
previouslyFocusedEl?.focus();
}
};
}, []);
}
Here is the test I wrote for it.
/**
* #jest-environment jsdom
*/
describe('useFocusOnElement', () => {
let ref: React.RefObject<HTMLDivElement>;
let focusMock: jest.SpyInstance;
beforeEach(() => {
ref = { current: document.createElement('div') } as React.RefObject<HTMLDivElement>;
focusMock = jest.spyOn(ref.current as HTMLDivElement, 'focus');
});
it('will call focus on passed ref after mount ', () => {
expect(focusMock).not.toHaveBeenCalled();
renderHook(() => useFocusOnElement(ref));
expect(focusMock).toHaveBeenCalled();
});
});
I would like to also test for the case where document is undefined as we also do SSR. In the hook I am checking for the existence of document and I would like to test for both cases.
JSDOM included document so I feel I'd need to remove it and some how catch an error in my test?
First of all, to simulate document as undefined, you should mock it like:
jest
.spyOn(global as any, 'document', 'get')
.mockImplementationOnce(() => undefined);
But to this work in your test, you will need to set spyOn inside renderHook because looks like it also makes use of document internally, and if you set spyOn before it, you will get an error.
Working test example:
it('will NOT call focus on passed ref after mount', () => {
expect(focusMock).not.toHaveBeenCalled();
renderHook(() => {
jest
.spyOn(global as any, 'document', 'get')
.mockImplementationOnce(() => undefined);
useFocusOnElement(ref);
});
expect(focusMock).not.toHaveBeenCalled();
});
You should be able to do this by creating a second test file with a node environment:
/**
* #jest-environment node
*/
describe('useFocusOnElement server-side', () => {
...
});
I ended up using wrapWithGlobal and wrapWithOverride from https://github.com/airbnb/jest-wrap.
describe('useFocusOnElement', () => {
let ref: React.RefObject<HTMLDivElement>;
let focusMock: jest.SpyInstance;
let activeElMock: unknown;
let activeEl: HTMLDivElement;
beforeEach(() => {
const { window } = new JSDOM();
global.document = window.document;
activeEl = document.createElement('div');
ref = { current: document.createElement('div') };
focusMock = jest.spyOn(ref.current as HTMLDivElement, 'focus');
activeElMock = jest.spyOn(activeEl, 'focus');
});
wrapWithOverride(
() => document,
'activeElement',
() => activeEl,
);
describe('when document present', () => {
it('will focus on passed ref after mount and will focus on previously active element on unmount', () => {
const hook = renderHook(() => useFocusOnElement(ref));
expect(focusMock).toHaveBeenCalled();
hook.unmount();
expect(activeElMock).toHaveBeenCalled();
});
});
describe('when no document present', () => {
wrapWithGlobal('document', () => undefined);
it('will not call focus on passed ref after mount nor on previously active element on unmount', () => {
const hook = renderHook(() => useFocusOnElement(ref));
expect(focusMock).not.toHaveBeenCalled();
hook.unmount();
expect(activeElMock).not.toHaveBeenCalled();
});
});
});

How to change hook mockImplementation result in a single test

I'm using react-intersection-observer in this component, when this element is inView, doSomething will be called. It is expected to trigger doSomething every time this element is seen by the user.
const Component = () => {
const { ref, inView } = useInView();
useEffect(() => {
if(inView) {
doSomething();
}
}, [inView])
return (
<div ref={ref}></div>
);
};
This works as expected, however, I don't know how to properly write this test case.
(useInView as jest.Mock).mockImplementation(() => ({inView: true}));
render(<Component />);
expect(doSomething).toHaveBeenCalledTimes(1); // Test passed
(useInView as jest.Mock).mockImplementation(() => ({inView: false}));
(useInView as jest.Mock).mockImplementation(() => ({inView: true}));
expect(doSomething).toHaveBeenCalledTimes(2); // Test failed

How to spy only one react hook useState

I want to isolate the test to a targeted useState.
Lets say I have 3 useStates, of which some are in my component and some are in children components in this testcase.
Currently this logs for 3 different useStates. How to target the one I want. Lets say its called setMovies.
const createMockUseState = <T extends {}>() => {
type TSetState = Dispatch<SetStateAction<T>>;
const setState: TSetState = jest.fn((prop) => {
// if setMovies ???
console.log('jest - spy mock = ', prop);
});
type TmockUseState = (prop: T) => [T, TSetState];
const mockUseState: TmockUseState = (prop) => [prop, setState];
const spyUseState = jest.spyOn(React, 'useState') as jest.SpyInstance<[T, TSetState]>;
spyUseState.mockImplementation(mockUseState);
};
interface Props {
propertyToTest: boolean
};
describe('Search Movies', () => {
describe('Onload - do first search()', () => {
beforeAll(async () => {
createMockUseState<PROPS>();
wrapper = mount(
<ProviderMovies>
<SearchMovies />
</ProviderMovies>
);
await new Promise((resolve) => setImmediate(resolve));
await act(
() =>
new Promise<void>((resolve) => {
resolve();
})
);
});
});
});
as we know react hooks depends on each initialization position. And for example if you have 3 hooks inside your component and you want to mock the 2-nd, you should mock 1 and 2 with necessary data.
Something like this
//mock for test file
jest.mock(useState); // you should mock here useState from React
//mocks for each it block
const useMockHook = jest.fn(...);
jest.spyOn(React, 'useState').mockReturnValueOnce(useMockHook);
expect(useMockHook).toHaveBeenCalled();
// after that you can check whatever you need

Resources