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

1.1k Views Asked by At

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:

  • "@fluentui/react-northstar": "^0.51.4"
  • "react": "^17.0.1"
  • "react-intl": "^5.10.18"
  • "@testing-library/react": "^11.2.7"
  • "@types/react-test-renderer": "^17.0.2"
1

There are 1 best solutions below

0
Radu Rascol On BEST ANSWER

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