How to test interaction with Ant Design's Popover content?

Viewed 1695

I have a React Component that wraps an Ant Design Popover. This component gets a callback that is being called by user interaction (say click) in dynamically generated content. Something similar to this:

const { Popover, Button } = antd;
const PopoverExtended = ({ onWhatever, children }) => {
    const handleClick = (event) => {
        if (event.target.className === 'some-class') {
            onWhatever(event.target.dataset.value);
        }
    };
    const dynamic = () => '<span class="some-class" data-value="42">Click this text</span>';
    const content = () => {
        return (
            <div>
                <p>Some HTML</p>
                <div dangerouslySetInnerHTML={{ __html: dynamic() }} onClick={handleClick}></div>
            </div>
        );
    };
    return (
        <Popover content={content()} placement="right" trigger="click">
            {children}
        </Popover>
    );
};

ReactDOM.render(
    <PopoverExtended onWhatever={(x) => console.log(x)}>
        <Button>Click me</Button>
    </PopoverExtended>,
    document.getElementById('root')
);
<link href="https://cdnjs.cloudflare.com/ajax/libs/antd/3.26.20/antd.css" rel="stylesheet"/>

<div id="root" style="margin: 2em 0 0 2em"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment-with-locales.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/antd/3.26.20/antd-with-locales.js" crossorigin="anonymous"></script>

Everything works as expected, but using Jest and Enzyme I'm trying to test that the onWhatever callback is being called, but so far I haven't been able to target the dynamic content neither as a ShallowWrapper nor ReactWrapper. I've tried:

describe(`<PopoverExtended /> interaction`, () => {
  const mockChildren = <Button>Mock me</Button>;
  const mockCallback = jest.fn();
  const wrapper = mount(<PopoverExtended onWhatever={mockCallback}>{mockChildren}</PopoverExtended>);

  // Try 1
  const trigger = wrapper.find('.some-class[data-value="42"]'); // Nothing found.

  // Try 2
  const content = mount(<>{wrapper.find(Popover).prop('content')}</>); 
  console.log(content.html()); // Is apparently the correct Popover content HTML
  const trigger = wrapper.find('.some-class[data-value="42"]'); // Nothing found.

  // Try 3
  const content = mount(<>{wrapper.find(Popover).prop('content')}</>);
  const rendered = content.render();
  const trigger = wrapper.find('.some-class[data-value="42"]'); // Node found, but
    // it's a CheerioWrapper, so I cannot call trigger.simulate('click');
});

Any ideas on how to properly test that the callback is being called?

1 Answers

Enzyme does not see the dynamic content and hence there's no way for you to simulate click on the elements inside of dynamic content. You can verify this by doing console.log(wrapper.debug()) which will show you what Enzyme sees. After trying:

const mockCallback = jest.fn();
const wrapper = mount(<PopoverExtended onWhatever={mockCallback}>{<Button>Mock me</Button>}</PopoverExtended>);

const trigger = wrapper.find("button");
trigger.simulate("click");

Enzyme only goes as far as the hosting div:

...
<Content trigger={{...}} prefixCls="ant-popover" id={[undefined]} overlay={{...}}>
  <div className="ant-popover-inner" id={[undefined]} role="tooltip">
    <div>
      <div className="ant-popover-inner-content">
        <div>
          <p>
            Some HTML
          </p>
          <div dangerouslySetInnerHTML={{...}} onClick={[Function: handleClick]} />
        </div>
      </div>
    </div>
  </div>
</Content>
...

Now calling wrapper.html() actually returns the full DOM including the dynamic content, but that's pretty useless for our case as you've mentioned. On defense of Enzyme, the dynamic content uses html flavor instead of JSX (class instead of className) making it even more difficult to wrap.

With that out of the way, I don't see why you need to include the dynamic content in your test scenario. Simply simulate the click on the host div. In fact I'd argue that's the right way of doing it since you define the event handler on the host div and not inside the dynamic content:

it(`<PopoverExtended /> interaction`, () => {
  const mockCallback = jest.fn();
  const wrapper = mount(<PopoverExtended onWhatever={mockCallback}>{<Button>Mock me</Button>}</PopoverExtended>);
  const mockEvent = {
    type: "click",
    target: {
      dataset: { value: 42 },
      className: "some-class"
    }
  };

  const trigger = wrapper.find("button");
  trigger.simulate("click");

  const hostDiv = wrapper.find("div.trigger-wrapper");
  hostDiv.simulate("click", mockEvent);

  expect(mockCallback.mock.calls.length).toBe(1);
  expect(mockCallback.mock.calls[0][0]).toBe(42);
});

Option 2

Interestingly the react testing library has no problem including your dynamic content so you may want to use it instead of Enzyme:

import React from "react";
import { render, fireEvent, screen } from '@testing-library/react'
import { Button } from "antd";

import PopoverExtended from "./PopOverExtended";

it(`<PopoverExtended /> interaction`, async () => {
  const mockCallback = jest.fn();
  
  render(<PopoverExtended onWhatever={mockCallback}>{<Button>Mock me</Button>}</PopoverExtended>);

  fireEvent.click(screen.getByText('Mock me'))    
  fireEvent.click(screen.getByText('Click this text'))

  expect(mockCallback.mock.calls.length).toBe(1);
  expect(mockCallback.mock.calls[0][0]).toBe("42");
});

Argument for this is that the dataset is defined by the dynamic content and so you have to consider it in your test.

Related