reactjsreact-testing-libraryreact-intlfluentui-reactreact-test-renderer

@testing-library/react render Error: `target` and `targetRef` props are `undefined`, it' required to use one of them


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:


Solution

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