Why is Enzyme document.activeElement an empty object? - reactjs

I am emplying an useRef to fix focus to button next to input when change of that input is detected:
const handleUserEshopChange = (eshopId: ID) => {
setEshopIdValue(eshopId)
setEshopNameValue('')
focusRef.current.focus()
...
}
I want to test that the focus has been affixed but document.activeElement is just an empty object:
test('onNewEshopCreate is handled correctly', () => {
const mockOnUserEshopSelect = jest.fn()
const mockOnNewEshopCreate = jest.fn()
const useRefSpy = jest.spyOn(React, 'useRef').mockReturnValueOnce({ current: { focus } })
const eshopStep = mount(
<EshopStep
...
/>
, { attachTo: document.body })
act(() => {
eshopStep.find('ForwardRef(AutocompleteInput)').simulate('change', '...')
eshopStep.find('ForwardRef(ContinueButton)').at(1).simulate('click')
})
expect(mockOnNewEshopCreate).toBeCalledTimes(1)
expect(mockOnNewEshopCreate).toBeCalledWith('...')
expect(mockOnUserEshopSelect).toBeCalledTimes(0)
expect(document.activeElement).toBe(eshopStep.find('.button'))
})
Test fails on the last line, as it expects only an ampty object ( {} ). Why is the activeElement empty?

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]);

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();
});
});
});

Jest mocking a function with hooks fails

I'm using nextJs and jest enzyme for testing of component. I have a component file like below, where I get items array from props and loop to it calling setRef inside useEffect.
[itemRefs, setItemRefs] = React.useState([]);
setRef = () => {
const refArr = [];
items.forEach(item => {
const setItemRef = RefItem => {
refArr.push(RefItem)
}
if(item && item.rendItem){
item.rendItem = item.rendItem.bind(null, setItemRef);
}
}
setItemRefs(refArr);
}
React.useEffect(() => {
setRef();
},[])
My test file is like below :
const items = [
{
original: 'mock-picture-link',
thumbnail: 'mock-thumb-link',
renderItem: jest.fn().mockReturnValue({ props: { children: {} } })
}
];
beforeEach(() => {
setItemRefs = jest.fn(),
jest.spyOn(React, 'useEffect').mockImplementationOnce(fn => fn());
})
it('mocks item references', ()=>{
jest.spyOn(React, 'useState').mockImplementationOnce(() => [items, setItemRefs]);
wrapper = shallow(<Component />).dive();
expect(setItemRefs).toHaveBeenCalledWith(items);
})
The test case fails with expected as items array BUT received a blank array. The console.log inside if(item && item.rendItem) works but console.log inside const setItemRef = RefItem => {... doesn't work I doubt the .bind is not getting mocked in jest.
Any help would be fine.

React unit test with enzyme, test onBlur functionality

I have a component with functionality which validates input element onblur and adds error class if validation fails:
TableRows = (props )=>(
return <input class="inputElement" onBlur="this.validate()" />
)
validate function is as follows:
validate = async ({ target }: ChangeEvent<HTMLInputElement>) => {
try {
const element = target;
this.setState({ loading: true });
const { value } = target;
const match = value.match(/^\d+(?:\.\d{0,2})?$/g);
if (!match || match.length === 0) {
element.className += ' inputError';
} else {
element.className = target.className.replace(' inputError', '');
}
const { data: updatedValues } = await sendforSaving({inputValue: value});
this.setState({ newValues: data });
} finally {
this.setState({ loading: false });
}
};
I am trying to write a unit test with enzyme as follows:
it('should add error class on invalid input onblur', () => {
const mockVal4Test = {
localCurrency: 'USD',
value: '20.02.1',
} as any;
component = shallow(
<TableRows {...defaultProps} initialValues={mockVal4Test} currencyType={CurrencyType.LOCAL} />
);
const myInput = component.find('.inputElement');
myInput.simulate('blur');
expect(saleTarget.hasClass('inputError')).toBe(true);
});
I get the myInput element but after simulating blur I am expecting the 'validate' function to be called and error class "inputError" to be added to the element.
I used a mock event for blur event to pass to simulate. And later the same mock event is used to check the changes, as the blur event changes the passed event object. Here is my solution.
it('should add error class on invalid input for StoreTarget input on blur', () => {
component = shallow(
<TableRows {...defaultProps} initialValues={mockVal4Test} currencyType={CurrencyType.LOCAL} />
)
const mockedEvent = {
target: {
value: '1.2.1.2',
className:'inputClass'
}
} as any;
const myInput = component.find('. inputElement');
myInput.simulate('blur', mockedEvent );
expect(mockedEvent.target.className.includes('inputError')).toBe(true);
});

Simulate change for textarea Jest test

I have the following component:
render() {
return (
<textarea onChange={this.handlechange} value="initial value" />
)
}
handlechange = (e) => {
console.log(e.currentTarget.value);
}
and the corresponding test that's supposed to check if on change fired correctly or not:
const TEST_PROPS = {
OnChange: jest.fn()
}
it("Fires on change correctly", () => {
const textArea = enzyme.mount(<TextArea {...TEST_PROPS} />);
jest.resetAllMocks();
expect(textArea.find("textarea").simulate("change"));
expect(TEST_PROPS.OnChange).toHaveBeenCalledTimes(1);
expect(TEST_PROPS.OnChange).toHaveBeenLastCalledWith(//what should go here?//);
});
I want to pass in the value of the new target.value once the onchange is fired to the toHaveBeenLastCalledWith function. How can I do this?
simulate event accepts a event obj as a 2nd arg, which you can use it in your 2nd assertion.
const TEST_PROPS = {
OnChange: jest.fn()
}
it("Fires on change correctly", () => {
const textArea = enzyme.mount(<TextArea {...TEST_PROPS} />);
const event = { target: { value: "sometext" } };
jest.resetAllMocks();
expect(textArea.find("textarea").simulate("change", event));
expect(TEST_PROPS.OnChange).toHaveBeenCalledTimes(1);
expect(TEST_PROPS.OnChange).toHaveBeenLastCalledWith(event);
});

Resources