I am trying to write a unit test for a custom react component that use the Dialog
from @fluentui/react-northstar
when I try to render the component from the test I get a error:
Error: `target` and `targetRef` props are `undefined`, it' required to use one of them.
at ...\node_modules\@fluentui\react-component-event-listener\dist\commonjs\useEventListener.ts:30:15
at invokePassiveEffectCreate (...\node_modules\react-dom\cjs\react-dom.development.js:23487:20)
at HTMLUnknownElement.callCallback (...\node_modules\react-dom\cjs\react-dom.development.js:3945:14)
at HTMLUnknownElement.callTheUserObjectsOperation (...\node_modules\jsdom\lib\jsdom\living\generated\EventListener.js:26:30)
at innerInvokeEventListeners (...\node_modules\jsdom\lib\jsdom\living\events\EventTarget-impl.js:338:25)
at invokeEventListeners (...\node_modules\jsdom\lib\jsdom\living\events\EventTarget-impl.js:274:3)
at HTMLUnknownElementImpl._dispatch (...\node_modules\jsdom\lib\jsdom\living\events\EventTarget-impl.js:221:9)
at HTMLUnknownElementImpl.dispatchEvent (...\node_modules\jsdom\lib\jsdom\living\events\EventTarget-impl.js:94:17)
at HTMLUnknownElement.dispatchEvent (...\node_modules\jsdom\lib\jsdom\living\generated\EventTarget.js:231:34)
at Object.invokeGuardedCallbackDev (...\node_modules\react-dom\cjs\react-dom.development.js:3994:16)
at invokeGuardedCallback (...\node_modules\react-dom\cjs\react-dom.development.js:4056:31)
at flushPassiveEffectsImpl (...\node_modules\react-dom\cjs\react-dom.development.js:23574:9)
at unstable_runWithPriority (...\node_modules\scheduler\cjs\scheduler.development.js:468:12)
at runWithPriority$1 (...\node_modules\react-dom\cjs\react-dom.development.js:11276:10)
at flushPassiveEffects (...\node_modules\react-dom\cjs\react-dom.development.js:23447:14)
at Object.<anonymous>.flushWork (...\node_modules\react-dom\cjs\react-dom-test-utils.development.js:992:10)
at act (...\node_modules\react-dom\cjs\react-dom-test-utils.development.js:1107:9)
at render (...\node_modules\@testing-library\react\dist\pure.js:97:26)
at Object.<anonymous> (...\src\features\ManageUsers\__tests__\SyncModal.test.tsx:35:24)
at Promise.then.completed (...\node_modules\jest-circus\build\utils.js:276:28)
at new Promise (<anonymous>)
at callAsyncCircusFn (...\node_modules\jest-circus\build\utils.js:216:10)
at _callCircusTest (...\node_modules\jest-circus\build\run.js:212:40)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
at _runTest (...\node_modules\jest-circus\build\run.js:149:3)
at _runTestsForDescribeBlock (...\node_modules\jest-circus\build\run.js:63:9)
at _runTestsForDescribeBlock (...\node_modules\jest-circus\build\run.js:57:9)
at run (...\node_modules\jest-circus\build\run.js:25:3)
at runAndTransformResultsToJestFormat (...\node_modules\jest-circus\build\legacy-code-todo-rewrite\jestAdapterInit.js:176:21)
at jestAdapter (...\node_modules\jest-circus\build\legacy-code-todo-rewrite\jestAdapter.js:109:19)
at runTestInternal (...\node_modules\jest-runner\build\runTest.js:380:16)
at runTest (...\node_modules\jest-runner\build\runTest.js:472:34)
The code:
//testutils.tsx
import { render } from '@testing-library/react';
import { IntlProvider } from 'react-intl';
import { QueryClient, QueryClientProvider } from 'react-query';
import { FC, ReactElement } from 'react';
import renderer from 'react-test-renderer';
const createTestQueryClient = () =>
new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
export function renderWithClient(ui: React.ReactElement) {
const testQueryClient = createTestQueryClient();
const WrapperIntlProvider: FC = ({children}) => {
return (
<QueryClientProvider client={testQueryClient}>
<IntlProvider locale={'en'}>{children}</IntlProvider>
</QueryClientProvider>
);
};
const { rerender, ...result } = render(ui, { wrapper: WrapperIntlProvider });
return {
...result,
rerender: (rerenderUi: React.ReactElement) =>
rerender(
<WrapperIntlProvider>{rerenderUi}</WrapperIntlProvider>
),
};
}
export function rendererWithClient(ui: ReactElement) {
return renderer.create(<IntlProvider locale={'en'}>{ui}</IntlProvider>);
}
//SyncModal.test.tsx
import React, { ReactPortal } from 'react';
import { cleanup } from '@testing-library/react';
import SyncModal from '../SyncModal';
import { rendererWithClient, renderWithClient } from '../../../utils/__test__/testutils';
import ReactDOM from 'react-dom';
jest.mock('react-dom', () => ({
// eslint-disable-next-line
// @ts-ignore
...jest.requireActual('react-dom'),
createPortal: (node) => node as ReactPortal,
}));
afterEach(() => {
cleanup();
});
describe('SyncModal', () => {
test('match snapshot', () => {
const oldPortal = ReactDOM.createPortal;
const ref = React.createRef();
const props = {
register: jest.fn(() => ref),
onClose: () => console.log('closed'),
onSave: () => console.log('saved'),
isOpen: true,
};
ReactDOM.createPortal = (node) => node as ReactPortal; // for components that use Portal
renderWithClient(<SyncModal {...props} />);
const tree = rendererWithClient(<SyncModal {...props} />).toJSON();
console.debug(tree);
expect(tree).toMatchSnapshot();
ReactDOM.createPortal = oldPortal;
});
});
//SyncModal.tsx
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { CloseIcon, Dialog, Flex, Text } from '@fluentui/react-northstar';
import messages from './messages';
const SyncModal = (props: any) => {
const { isOpen, onClose, onSave, hasFails, failsReason } = props;
return (
<Dialog
id={'sync-modal'}
open={isOpen}
headerAction={{
icon: <CloseIcon />,
title: 'Close',
fluid: true,
onClick: () => {
onClose();
},
}}
header={{
align: 'start',
content: (
<Text align="start">
{hasFails ? (
<FormattedMessage {...messages.syncHeaderTimeout} />
) : (
<FormattedMessage {...messages.syncHeader} />
)}
</Text>
),
}}
styles={{ width: '434px', height: 188 }}
content={
<Flex column={true} styles={{ width: '100%' }}>
<Flex
gap="gap.medium"
column={true}
style={{ marginTop: '21px', marginBottom: '16px' }}
hAlign={'center'}
>
{hasFails ? (
<FormattedMessage
{...messages.syncContentTimeout}
values={{ reason: failsReason ?? 'unknown reason' }}
/>
) : (
<FormattedMessage {...messages.syncContentPart1} />
)}
</Flex>
<Text weight={'semibold'} styles={{ marginBottom: '8px' }}>
<FormattedMessage {...messages.syncContentPart2} />
</Text>
<ul style={{ paddingLeft: 20, margin: 0 }}>
<li style={{ marginBottom: '8px' }}>
<Text>
<FormattedMessage {...messages.syncContentPart3} />
</Text>
</li>
<li style={{ marginBottom: '8px' }}>
<Text>
<FormattedMessage {...messages.syncContentPart4} />
</Text>
</li>
</ul>
</Flex>
}
onCancel={() => onClose()}
cancelButton={
<Flex>
<FormattedMessage {...messages.syncCancel} />
</Flex>
}
onConfirm={() => onSave()}
confirmButton={
<Flex>
<FormattedMessage {...messages.syncConfirm} />
</Flex>
}
style={{ width: '600px' }}
{...props}
/>
);
};
export default SyncModal;
I use react-intl
for multi-language support and have to wrap my component with IntlProvider
, I cannot understand why node_modules\@fluentui\react-component-event-listener\dist\commonjs\useEventListener.ts
throws Error: 'target' and 'targetRef' props are 'undefined', it' required to use one of them.
Library versions:
We had the same error with JEST, React and Fluent UI. The solution was to wrap the render of the component with the provider from fluentui/react-northstar like this:
beforeEach(() => {
// setup a DOM element as a render target
container = document.createElement("div");
container.id = "root";
document.body.appendChild(container);
});
test("Notification Alert", () => {
const { result } = renderHook(() => useAtom(infoDialogState));
act(() => {
result.current[1](INFO_DIALOG_TYPES.MAX_FILE_SIZE);
});
render(<Provider><InfoDialog /></Provider>, container);
const linkElement = screen.getByText("dialogComponent.fileTooLargeHeader");
expect(linkElement).toBeInTheDocument();
}); ```