boldr/boldr-ui

View on GitHub
src/Popover/Popover.test.js

Summary

Maintainability
D
2 days
Test Coverage
import React from 'react';
import { Simulate } from 'react-dom/test-utils';
import { mount } from 'enzyme';
import Button from '../Button';
import Popover from './';

/* eslint-disable */
const PopoverContent = Popover.Content;
const PopoverClickTrigger = Popover.Trigger.Click;
const PopoverHoverTrigger = Popover.Trigger.Hover;
const PopoverFocusTrigger = Popover.Trigger.Focus;
const PopoverBaseTrigger = Popover.Trigger.Base;
const withPopover = Popover.withPopover;
const createPlacement = Popover.Position.create;
/* eslint-enable */

const HoverContent = withPopover(({ popover }) => {
  return (
    <div>
      <div>popover content</div>
      <div>line two</div>
      <div>line three</div>
      <Button onClick={popover.close}>close</Button>
    </div>
  );
});

const simulateWithTimers = (node, event, ...arg) => {
  node.simulate(event, ...arg);
  jest.runAllTimers();
};

const simulateRawWithTimers = (node, event, ...arg) => {
  Simulate[event](node, ...arg);
  jest.runAllTimers();
};

const dispatchWithTimers = (node, event, ...arg) => {
  node.dispatchEvent(event, ...arg);
  jest.runAllTimers();
};

beforeAll(() => {
  jest.useFakeTimers();
});

describe('Popover', () => {
  it('Popover has its core function, popover :)', () => {
    let wrapper = mount(
      <Popover position={Popover.Position.BottomLeft} display="inline">
        <PopoverClickTrigger>
          <Button>click me</Button>
        </PopoverClickTrigger>
        <PopoverContent>
          <div>popover content</div>
          <div>line two</div>
        </PopoverContent>
      </Popover>,
    );
    expect(wrapper.find('Portal').length).toBe(0);
    simulateWithTimers(wrapper.find('button'), 'click');
    expect(wrapper.find('Portal').length).toBe(1);
    expect(document.querySelectorAll('.boldrui-popover-content div').length).toBe(2);
    expect(document.querySelectorAll('.boldrui-popover-content div')[1].textContent).toBe(
      'line two',
    );

    // HACK: branch window.resize (throttle)
    wrapper.find('PopoverContent').getNode().onWindowResize(
      {},
      {
        deltaX: 0,
        deltaY: 0,
      },
    );
    wrapper.find('PopoverContent').getNode().onWindowResize(
      {},
      {
        deltaX: 10,
        deltaY: 10,
      },
    );

    simulateWithTimers(wrapper.find('button'), 'click');
    expect(wrapper.find('Portal').length).toBe(1);

    wrapper.getNode().close();
    expect(wrapper.find('Portal').length).toBe(0);
    wrapper.unmount();

    wrapper = mount(
      <Popover position={Popover.Position.RightTop} display="inline">
        <PopoverHoverTrigger showDelay={100} hideDelay={100} isOutSide={() => true}>
          <Button>hover on me</Button>
        </PopoverHoverTrigger>
        <PopoverContent>
          <HoverContent />
        </PopoverContent>
      </Popover>,
    );

    expect(wrapper.find('Portal').length).toBe(0);

    wrapper.find('button').simulate('mouseenter');
    expect(wrapper.find('Portal').length).toBe(0);
    wrapper.find('button').simulate('mouseleave');
    expect(wrapper.find('Portal').length).toBe(0);

    simulateWithTimers(wrapper.find('button'), 'mouseenter');
    expect(wrapper.find('Portal').length).toBe(1);
    const fakeEvent = new MouseEvent('mousemove');
    dispatchWithTimers(window, fakeEvent);
    wrapper.unmount();

    wrapper = mount(
      <Popover position={Popover.Position.TopRight} display="inline" cushion={10}>
        <PopoverFocusTrigger>
          <input placeholder="focus on me" />
        </PopoverFocusTrigger>
        <PopoverContent>
          <div>popover content</div>
          <div>line two focus</div>
        </PopoverContent>
      </Popover>,
    );

    wrapper.find('input').simulate('focus');
    wrapper.find('input').simulate('focus');
    expect(wrapper.find('Portal').length).toBe(1);
    wrapper.find('input').simulate('blur');
    jest.runAllTimers();
  });

  it('Popover has children validation and position type check', () => {
    // NOTE: children.length === 2
    expect(() => {
      mount(
        <Popover position={Popover.Position.BottomLeft} display="inline">
          <span className="foo" />
        </Popover>,
      );
    }).toThrow();

    // NOTE: must have one PopoverTrigger
    expect(() => {
      mount(
        <Popover position={Popover.Position.BottomLeft} display="inline">
          <span className="foo" />
          <PopoverContent>
            <div>popover content</div>
            <div>line two</div>
          </PopoverContent>
        </Popover>,
      );
    }).toThrow();

    // NOTE: must have one PopoverContent
    expect(() => {
      mount(
        <Popover position={Popover.Position.BottomLeft} display="inline">
          <PopoverClickTrigger>
            <Button>click me</Button>
          </PopoverClickTrigger>
          <span className="foo" />
        </Popover>,
      );
    }).toThrow();
  });

  it('Popover can have custom prefix and custom className and custom placement position', () => {
    const wrapper = mount(
      <Popover
        position={Popover.Position.BottomLeft}
        display="inline"
        prefix="foo"
        wrapperClassName="foo"
        className="bar"
      >
        <PopoverClickTrigger>
          <Button>click me</Button>
        </PopoverClickTrigger>
        <PopoverContent>
          <HoverContent />
        </PopoverContent>
      </Popover>,
    );
    expect(wrapper.find('.foo-popover-wrapper').length).toBe(1);
    simulateWithTimers(wrapper.find('button'), 'click');

    // popover portal still in root tail of body..
    expect(wrapper.find('button').length).toBe(1);

    wrapper.unmount();

    // NOTE: createPlacement method need a function as arg[0] and this function need return object that contains some needed keys.
    expect(() => {
      createPlacement(() => {
        return null;
      })();
    }).toThrow();
  });

  it('Popover have series of placement position class', () => {
    const {
      BottomLeft,
      BottomCenter,
      BottomRight,
      LeftTop,
      LeftCenter,
      LeftBottom,
      RightTop,
      RightCenter,
      RightBottom,
      TopLeft,
      TopCenter,
      TopRight,
      AutoBottomLeft,
      AutoBottomCenter,
      AutoBottomRight,
      AutoTopLeft,
      AutoTopCenter,
      AutoTopRight,
    } = Popover.Position;
    const positionArr = [
      BottomLeft,
      BottomCenter,
      BottomRight,
      LeftTop,
      LeftCenter,
      LeftBottom,
      RightTop,
      RightCenter,
      RightBottom,
      TopLeft,
      TopCenter,
      TopRight,
      AutoBottomLeft,
      AutoBottomCenter,
      AutoBottomRight,
      AutoTopLeft,
      AutoTopCenter,
      AutoTopRight,
    ];

    positionArr.forEach(pos => {
      const wrapper = mount(
        <Popover position={pos} display="inline">
          <PopoverClickTrigger>
            <Button>click me</Button>
          </PopoverClickTrigger>
          <PopoverContent>
            <div>popover content</div>
            <div>line two</div>
          </PopoverContent>
        </Popover>,
      );
      wrapper
        .find('PopoverClickTrigger')
        .getNode()
        .onClickOutside({ target: <div className="outside" /> });
      expect(wrapper.find('Portal').length).toBe(0);

      simulateWithTimers(wrapper.find('button'), 'click');
      expect(wrapper.find('Portal').length).toBe(1);

      wrapper
        .find('PopoverClickTrigger')
        .getNode()
        .onClickOutside({ target: <div className="outside" /> });
      expect(wrapper.find('Portal').length).toBe(0);
      wrapper.unmount();
    });
  });

  it('Children of Trigger could not have string ref prop', () => {
    expect(() => {
      mount(
        <Popover position={Popover.Position.BottomLeft} display="inline">
          <PopoverClickTrigger>
            <Button ref="trigger">click me</Button>
          </PopoverClickTrigger>
          <PopoverContent>
            <div>popover content</div>
            <div>line two</div>
          </PopoverContent>
        </Popover>,
      );
    }).toThrow();
  });

  it('Base Trigger HACK', () => {
    const wrapper = mount(
      <Popover position={Popover.Position.BottomLeft} display="inline">
        <PopoverBaseTrigger>
          <Button>click me</Button>
        </PopoverBaseTrigger>
        <PopoverContent>
          <div>popover content</div>
          <div>line two</div>
        </PopoverContent>
      </Popover>,
    );
    simulateWithTimers(wrapper.find('button'), 'click');
    wrapper.unmount();
  });

  it('throws if only has visible', () => {
    expect(() =>
      mount(
        <Popover visible position={Popover.Position.BottomLeft} display="inline">
          <PopoverClickTrigger>
            <Button>click me</Button>
          </PopoverClickTrigger>
          <PopoverContent>
            <div>popover content</div>
            <div>line two</div>
          </PopoverContent>
        </Popover>,
      ),
    ).toThrow();

    expect(() =>
      mount(
        <Popover onVisibleChange={() => {}} position={Popover.Position.BottomLeft} display="inline">
          <PopoverClickTrigger>
            <Button>click me</Button>
          </PopoverClickTrigger>
          <PopoverContent>
            <div>popover content</div>
            <div>line two</div>
          </PopoverContent>
        </Popover>,
      ),
    ).toThrow();
  });

  it('can be controlled by visible & onVisibleChange', () => {
    let visible = true;
    const changeVisible = v => (visible = v);
    const wrapper = mount(
      <Popover
        visible={visible}
        onVisibleChange={changeVisible}
        position={Popover.Position.BottomLeft}
        display="inline"
      >
        <PopoverClickTrigger>
          <Button>click me</Button>
        </PopoverClickTrigger>
        <PopoverContent>
          <div>popover content</div>
          <div>line two</div>
        </PopoverContent>
      </Popover>,
    );
    expect(document.querySelector('.boldrui-popover')).toBeTruthy();

    wrapper.setProps({
      visible: false,
    });
    jest.runAllTimers();
    expect(document.querySelector('.boldrui-popover')).toBeFalsy();

    // console.log(wrapper.instance());
    wrapper.instance().open();
    jest.runAllTimers();
    wrapper.setProps({
      visible: true,
    });
    expect(document.querySelector('.boldrui-popover')).toBeTruthy();

    wrapper.setProps({
      visible: false,
    });
    jest.runAllTimers();
    expect(document.querySelector('.boldrui-popover')).toBeFalsy();
  });

  it('onBeforeXXX can return a Promise', () => {
    let p;
    const onBeforeShow = () => {
      p = new Promise(resolve => {
        resolve(2);
      });
      return p;
    };
    const wrapper = mount(
      <Popover onBeforeShow={onBeforeShow} position={Popover.Position.BottomLeft} display="inline">
        <PopoverClickTrigger>
          <Button>click me</Button>
        </PopoverClickTrigger>
        <PopoverContent>
          <div>popover content</div>
          <div>line two</div>
        </PopoverContent>
      </Popover>,
    );
    wrapper.find('button').simulate('click');
    jest.runAllTimers();

    return p.then(v => {
      expect(v).toBe(2);
      jest.runAllTimers();
      expect(document.querySelectorAll('.boldrui-popover-content').length).toBe(1);

      wrapper.unmount();
      dispatchWithTimers(window, new MouseEvent('click'));
      expect(document.querySelectorAll('.boldrui-popover-content').length).toBe(0);
    });
  });
  //
  // it('onBeforeXXX can have a callback', () => {
  //   const onBeforeShow = callback => {
  //     setTimeout(callback, 1000);
  //   };
  //   const wrapper = mount(
  //     <Popover onBeforeShow={onBeforeShow} position={Popover.Position.BottomLeft} display="inline">
  //       <PopoverClickTrigger>
  //         <Button>click me</Button>
  //       </PopoverClickTrigger>
  //       <PopoverContent>
  //         <div>popover content</div>
  //         <div>line two</div>
  //       </PopoverContent>
  //     </Popover>,
  //   );
  //   wrapper.find('button').simulate('click');
  //   jest.runAllTimers();
  //   expect(document.querySelectorAll('.boldrui-popover-content').length).toBe(0);
  //
  //   wrapper.unmount();
  //   dispatchWithTimers(window, new MouseEvent('click'));
  //   expect(document.querySelectorAll('.boldrui-popover-content').length).toBe(0);
  // });
  //
  // it('hover trigger closes on window blur', () => {
  //   const wrapper = mount(
  //     <Popover position={Popover.Position.BottomLeft}>
  //       <PopoverHoverTrigger>
  //         <Button>Hover on me</Button>
  //       </PopoverHoverTrigger>
  //       <PopoverContent>
  //         <div>popover content</div>
  //       </PopoverContent>
  //     </Popover>,
  //   );
  //
  //   simulateWithTimers(wrapper.find('button'), 'mouseenter');
  //   expect(wrapper.find('Portal').length).toBe(1);
  //
  //   // wont' close if target is not window
  //   let fakeEvent = new FocusEvent('blur');
  //   dispatchWithTimers(window, fakeEvent);
  //   expect(wrapper.find('Portal').length).toBe(1);
  //
  //   // it's tricky to set target manually
  //   fakeEvent = new FocusEvent('blur');
  //   const evt = fakeEvent.__proto__.__proto__.__proto__; // eslint-disable-line
  //   const descriptor = Object.assign({}, Object.getOwnPropertyDescriptor(evt, 'target'), {
  //     get() {
  //       return window;
  //     },
  //   });
  //   Object.defineProperty(evt, 'target', descriptor);
  //
  //   dispatchWithTimers(window, fakeEvent);
  //   expect(document.querySelectorAll('.boldrui-popover-content').length).toBe(0);
  //   wrapper.unmount();
  // });
  //
  // it('Click trigger supports custom isOutside', () => {
  //   const wrapper = mount(
  //     <Popover position={Popover.Position.BottomLeft} display="inline">
  //       <PopoverClickTrigger isOutside={() => false}>
  //         <Button>click me</Button>
  //       </PopoverClickTrigger>
  //       <PopoverContent>
  //         <div>popover content</div>
  //         <div>line two</div>
  //       </PopoverContent>
  //     </Popover>,
  //   );
  //   simulateWithTimers(wrapper.find('button'), 'click');
  //   expect(wrapper.find('Portal').length).toBe(1);
  //
  //   dispatchWithTimers(window, new MouseEvent('click'));
  //   expect(document.querySelectorAll('.boldrui-popover-content').length).toBe(0);
  //
  //   wrapper.unmount();
  //
  //   const popover = document.querySelector('.boldrui-popover');
  //   popover.parentNode.removeChild(popover);
  //   expect(document.querySelectorAll('.boldrui-popover-content').length).toBe(0);
  // });

  // it('can be nested', () => {
  //   const wrapper = mount(
  //     <Popover position={Popover.Position.BottomLeft} display="inline">
  //       <PopoverClickTrigger>
  //         <Button className="trigger-level-1">click me</Button>
  //       </PopoverClickTrigger>
  //       <PopoverContent>
  //         <div>popover content</div>
  //         <div className="level-1">line two</div>
  //
  //         <Popover position={Popover.Position.BottomLeft} display="inline">
  //           <PopoverClickTrigger>
  //             <Button className="trigger-level-2">click me</Button>
  //           </PopoverClickTrigger>
  //           <PopoverContent>
  //             <div>popover content</div>
  //             <div className="level-2">line two</div>
  //
  //             <Popover position={Popover.Position.BottomLeft} display="inline">
  //               <PopoverClickTrigger>
  //                 <Button className="trigger-level-3">click me</Button>
  //               </PopoverClickTrigger>
  //               <PopoverContent>
  //                 <div>popover content</div>
  //                 <div className="level-3">line two</div>
  //               </PopoverContent>
  //             </Popover>
  //           </PopoverContent>
  //         </Popover>
  //       </PopoverContent>
  //     </Popover>,
  //   );
  //
  //   simulateWithTimers(wrapper.find('.trigger-level-1'), 'click');
  //   expect(document.querySelectorAll('.boldrui-popover-content').length).toBe(1);
  //
  //   simulateRawWithTimers(document.querySelector('.trigger-level-2'), 'click');
  //   expect(document.querySelectorAll('.boldrui-popover-content').length).toBe(2);
  //
  //   simulateRawWithTimers(document.querySelector('.trigger-level-3'), 'click');
  //   expect(document.querySelectorAll('.boldrui-popover-content').length).toBe(3);
  //
  //   dispatchWithTimers(window, new MouseEvent('click'));
  //   expect(document.querySelectorAll('.boldrui-popover-content').length).toBe(0);
  //
  //   wrapper.unmount();
  // });

  it('supports auto position', () => {
    const {
      AutoBottomLeft,
      AutoBottomRight,
      AutoBottomCenter,
      AutoTopLeft,
      AutoTopRight,
      AutoTopCenter,
    } = Popover.Position;
    const viewport = {
      left: 0,
      top: 0,
      right: 1024,
      bottom: 768,
      width: 1024,
      height: 768,
    };
    const content = {
      width: 10,
      height: 10,
    };
    const anchor = {
      left: 1020,
      top: 760,
      right: 1021,
      bottom: 761,
      width: 1,
      height: 1,
    };
    const options = {
      anchorBoundingBoxViewport: anchor,
      cushion: 0,
    };
    const container = viewport;

    expect(AutoBottomLeft.locate(anchor, container, content, options).name).toBe(
      'position-top-right',
    );

    anchor.left = 5;
    anchor.right = 6;
    expect(AutoBottomRight.locate(anchor, container, content, options).name).toBe(
      'position-top-left',
    );

    anchor.left = 1020;
    anchor.right = 1021;
    expect(AutoBottomCenter.locate(anchor, container, content, options).name).toBe(
      'position-top-right',
    );

    anchor.left = 1;
    anchor.right = 2;
    expect(AutoBottomCenter.locate(anchor, container, content, options).name).toBe(
      'position-top-left',
    );

    anchor.left = 1020;
    anchor.right = 1021;
    anchor.top = 1;
    anchor.bottom = 2;
    expect(AutoTopLeft.locate(anchor, container, content, options).name).toBe(
      'position-bottom-right',
    );

    anchor.left = 5;
    anchor.right = 6;
    expect(AutoTopRight.locate(anchor, container, content, options).name).toBe(
      'position-bottom-left',
    );

    anchor.left = 1020;
    anchor.right = 1021;
    expect(AutoTopCenter.locate(anchor, container, content, options).name).toBe(
      'position-bottom-right',
    );

    anchor.left = 1;
    anchor.right = 2;
    expect(AutoTopCenter.locate(anchor, container, content, options).name).toBe(
      'position-bottom-left',
    );
  });

  it('throws if trigger has no children', () => {
    expect(() =>
      mount(
        <Popover position={Popover.Position.BottomLeft} display="inline">
          <PopoverClickTrigger />
          <PopoverContent>
            <div>popover content</div>
          </PopoverContent>
        </Popover>,
      ),
    ).toThrow(/Popover trigger requires a child/);
  });

  it('throws if trigger has more than one chilren', () => {
    expect(() =>
      mount(
        <Popover position={Popover.Position.BottomLeft} display="inline">
          <PopoverClickTrigger>
            <span>1</span>
            <span>2</span>
          </PopoverClickTrigger>
          <PopoverContent>
            <div>popover content</div>
          </PopoverContent>
        </Popover>,
      ),
    ).toThrow(/Popover trigger requires only one child/);
  });

  it('trigger wraps number/string in a span', () => {
    const wrapper = mount(
      <Popover position={Popover.Position.BottomLeft} display="inline">
        <PopoverClickTrigger>abc</PopoverClickTrigger>
        <PopoverContent>
          <span>popover content</span>
        </PopoverContent>
      </Popover>,
    );

    expect(wrapper.find('.boldrui-popover-wrapper span').node.textContent).toBe('abc');
  });

  it("won't close if click within trigger/content", () => {
    let trigger = new PopoverBaseTrigger({
      injectIsOutsideSelf() {},
      getTriggerNode() {
        return {
          contains() {
            return true;
          },
        };
      },
      getContentNode() {
        return {
          contains() {
            return true;
          },
        };
      },
    });
    expect(trigger.isOutsideSelf()).toBe(false);

    trigger = new PopoverBaseTrigger({
      injectIsOutsideSelf() {},
      getTriggerNode() {
        return {
          contains() {
            return false;
          },
        };
      },
      getContentNode() {
        return {
          contains() {
            return true;
          },
        };
      },
    });
    expect(trigger.isOutsideSelf()).toBe(false);
  });
});