grommet/grommet

View on GitHub
src/js/components/Layer/__tests__/Layer-test.js

Summary

Maintainability
D
1 day
Test Coverage
import React from 'react';
import 'jest-styled-components';
import { render, fireEvent } from '@testing-library/react';
import { getByTestId, queryByTestId } from '@testing-library/dom';
import 'regenerator-runtime/runtime';
import { createPortal, expectPortal } from '../../../utils/portal';

import { Grommet, Box, Layer, Select } from '../..';
import { LayerContainer } from '../LayerContainer';

const SimpleLayer = () => {
  const [showLayer, setShowLayer] = React.useState(true);

  React.useEffect(() => setShowLayer(false), []);

  let layer;
  if (showLayer) {
    layer = <Layer data-testid="test-dom-removal">This is a test</Layer>;
  }
  return <Box>{layer}</Box>;
};

const FakeLayer = ({ children, dataTestid, ...rest }) => {
  const [showLayer, setShowLayer] = React.useState(false);

  React.useEffect(() => setShowLayer(true), []);

  let layer;
  if (showLayer) {
    layer = (
      <Layer onEsc={() => setShowLayer(false)} {...rest}>
        <div data-testid={dataTestid}>
          This is a layer
          <input data-testid="test-input" />
        </div>
      </Layer>
    );
  }
  return (
    <Box>
      {layer}
      {children}
    </Box>
  );
};

const TargetLayer = (props) => {
  const [target, setTarget] = React.useState();
  let layer;
  if (target) {
    layer = (
      <Layer {...props} target={target}>
        this is a test layer
      </Layer>
    );
  }
  return (
    <Grommet>
      <div ref={setTarget} />
      {layer}
    </Grommet>
  );
};

describe('Layer', () => {
  beforeEach(createPortal);
  const positions = [
    'top',
    'bottom',
    'left',
    'right',
    'start',
    'end',
    'center',
    'top-left',
    'top-right',
    'bottom-left',
    'bottom-right',
  ];

  const fullOptions = [true, false, 'horizontal', 'vertical'];

  positions.forEach((position) =>
    fullOptions.forEach((full) => {
      test(`position: ${position} - full: ${full}`, () => {
        render(
          <Grommet>
            <Layer id="position-full-test" position={position} full={full}>
              This is a layer
            </Layer>
          </Grommet>,
        );
        expectPortal('position-full-test').toMatchSnapshot();
      });

      test(`should render correct border radius for position: ${position} - 
      full: ${full}`, () => {
        const theme = {
          layer: {
            border: {
              radius: 'large',
              intelligentRounding: true,
            },
          },
        };
        render(
          <Grommet theme={theme}>
            <Layer id="border-radius-test" position={position} full={full}>
              This is a layer
            </Layer>
          </Grommet>,
        );
        expectPortal('border-radius-test').toMatchSnapshot();
      });
    }),
  );

  ['none', 'xsmall', 'small', 'medium', 'large'].forEach((margin) =>
    test(`margin ${margin}`, () => {
      render(
        <Grommet>
          <Layer id="margin-test" margin={margin}>
            This is a layer
          </Layer>
        </Grommet>,
      );
      expectPortal('margin-test').toMatchSnapshot();
    }),
  );

  test(`should apply background`, () => {
    render(
      <Grommet>
        <Layer id="margin-test" background="brand">
          This is a layer
        </Layer>
      </Grommet>,
    );
    expectPortal('margin-test').toMatchSnapshot();
  });

  test(`custom margin`, () => {
    render(
      <Grommet>
        <Layer
          id="margin-test"
          margin={{ top: '50px', bottom: '40px', left: '30px', right: '20px' }}
        >
          This is a layer
        </Layer>
      </Grommet>,
    );
    expectPortal('margin-test').toMatchSnapshot();
  });

  test('hidden', () => {
    const { rerender } = render(
      <Grommet>
        <Layer id="hidden-test" position="hidden">
          This is a layer
        </Layer>
      </Grommet>,
    );
    expectPortal('hidden-test').toMatchSnapshot();

    rerender(
      <Grommet>
        <Layer id="hidden-test" position="center">
          This is a layer
        </Layer>
      </Grommet>,
    );
    expectPortal('hidden-test').toMatchSnapshot();
  });

  test('plain', () => {
    // elevation should not be applied when Layer is plain
    const theme = {
      layer: {
        container: {
          elevation: 'large',
        },
      },
    };

    render(
      <Grommet theme={theme}>
        <Layer id="plain-test" plain>
          This is a plain layer
        </Layer>
      </Grommet>,
    );
    expectPortal('plain-test').toMatchSnapshot();
  });

  test('non-modal', () => {
    render(
      <Grommet>
        <Layer id="non-modal-test" modal={false}>
          This is a non-modal layer
        </Layer>
      </Grommet>,
    );
    expectPortal('non-modal-test').toMatchSnapshot();
  });

  test('dark context', () => {
    render(
      <Grommet>
        <Box background="dark-1">
          <Layer id="non-modal-test" modal={false}>
            This is a non-modal layer
          </Layer>
        </Box>
      </Grommet>,
    );
    expectPortal('non-modal-test').toMatchSnapshot();
  });

  ['slide', 'fadeIn', false, true].forEach((animation) =>
    test(`animation ${animation}`, () => {
      render(
        <Grommet>
          <Layer id="animation-test" animation={animation}>
            This is a layer
          </Layer>
        </Grommet>,
      );
      expectPortal('animation-test').toMatchSnapshot();
    }),
  );

  test('invokes onEsc', () => {
    const onEsc = jest.fn();
    render(
      <Grommet>
        <LayerContainer onEsc={onEsc}>
          <input data-testid="test-input" />
        </LayerContainer>
      </Grommet>,
    );

    const inputNode = getByTestId(document, 'test-input');
    fireEvent.keyDown(inputNode, { key: 'Esc', keyCode: 27, which: 27 });
    expect(onEsc).toBeCalled();
  });

  test('is accessible', (done) => {
    /* eslint-disable jsx-a11y/tabindex-no-positive */
    render(
      <Grommet>
        <FakeLayer dataTestid="test-layer-node">
          <div data-testid="test-body-node">
            <input />
            <input tabIndex="10" />
          </div>
        </FakeLayer>
      </Grommet>,
    );
    /* eslint-enable jsx-a11y/tabindex-no-positive */

    let bodyNode = getByTestId(document, 'test-body-node');
    const layerNode = getByTestId(document, 'test-layer-node');
    const inputNode = getByTestId(document, 'test-input');
    expect(bodyNode).toMatchSnapshot();
    expect(layerNode).toMatchSnapshot();

    fireEvent.keyDown(inputNode, { key: 'Esc', keyCode: 27, which: 27 });
    // because of de-animation, we test both the initial and delayed states
    bodyNode = getByTestId(document, 'test-body-node');
    expect(bodyNode).toMatchSnapshot();
    setTimeout(() => {
      expect(queryByTestId(document, 'test-layer-node')).toBeNull();
      done();
    }, 300);
  });

  test('focus on layer', () => {
    /* eslint-disable jsx-a11y/no-autofocus */
    render(
      <Grommet>
        <Layer data-testid="focus-layer-test">
          <input />
        </Layer>
        <input autoFocus />
      </Grommet>,
    );
    /* eslint-disable jsx-a11y/no-autofocus */

    const layerNode = getByTestId(document, 'focus-layer-test');
    expect(layerNode).toMatchSnapshot();
    expect(document.activeElement.nodeName).toBe('A');
  });

  test('not steal focus from an autofocus focusable element', () => {
    /* eslint-disable jsx-a11y/no-autofocus */
    render(
      <Grommet>
        <Layer data-testid="focus-layer-input-test">
          <input autoFocus data-testid="focus-input" />
          <button type="button">Button</button>
        </Layer>
      </Grommet>,
    );
    /* eslint-disable jsx-a11y/no-autofocus */
    const layerNode = getByTestId(document, 'focus-layer-input-test');
    const inputNode = getByTestId(document, 'focus-input');
    expect(layerNode).toMatchSnapshot();
    expect(document.activeElement).toBe(inputNode);
  });

  test('target', () => {
    render(
      <Grommet>
        <TargetLayer id="target-test">This layer has a target</TargetLayer>
      </Grommet>,
    );
    expectPortal('target-test').toMatchSnapshot();
  });

  test('target not modal', () => {
    render(
      <Grommet>
        <TargetLayer id="target-test" modal={false}>
          This layer has a target
        </TargetLayer>
      </Grommet>,
    );
    expectPortal('target-test').toMatchSnapshot();
  });

  test('unmounts from dom', () => {
    render(
      <Grommet>
        <SimpleLayer />
      </Grommet>,
    );
    setTimeout(() => {
      expect(queryByTestId(document, 'test-dom-removal')).toBeNull();
    }, 1000);
  });

  test('default containerTarget', () => {
    render(
      <Grommet>
        <Layer data-testid="layer">Test</Layer>
      </Grommet>,
    );
    const layer = getByTestId(document, 'layer');
    const actualRoot = layer.parentNode.parentNode.parentNode.parentNode;
    expect(actualRoot).toBe(document.body);
  });

  test('custom containerTarget', () => {
    const target = document.createElement('div');
    document.body.appendChild(target);
    try {
      render(
        <Grommet containerTarget={target}>
          <Layer data-testid="layer">Test</Layer>
        </Grommet>,
      );
      const layer = getByTestId(document, 'layer');
      const actualRoot = layer.parentNode.parentNode.parentNode.parentNode;
      expect(actualRoot).toBe(target);
    } finally {
      document.body.removeChild(target);
    }
  });

  test('invoke onClickOutside when modal={true}', () => {
    const onClickOutside = jest.fn();
    render(
      <Grommet>
        <FakeLayer
          id="layer-node"
          onClickOutside={onClickOutside}
          animation={false}
        >
          <div data-testid="test-body-node" />
        </FakeLayer>
      </Grommet>,
    );
    expectPortal('layer-node').toMatchSnapshot();

    fireEvent(
      document,
      new MouseEvent('mousedown', { bubbles: true, cancelable: true }),
    );
    expect(onClickOutside).toHaveBeenCalledTimes(1);
  });

  test('invoke onClickOutside when modal={false}', () => {
    const onClickOutside = jest.fn();
    render(
      <Grommet>
        <FakeLayer
          id="layer-node"
          onClickOutside={onClickOutside}
          modal={false}
          animation={false}
        >
          <div data-testid="test-body-node" />
        </FakeLayer>
      </Grommet>,
    );
    expectPortal('layer-node').toMatchSnapshot();

    fireEvent(
      document,
      new MouseEvent('mousedown', { bubbles: true, cancelable: true }),
    );
    expect(onClickOutside).toHaveBeenCalledTimes(1);
  });

  test('invoke onClickOutside when modal={false} and layer has target', () => {
    const onClickOutside = jest.fn();
    render(
      <TargetLayer
        id="target-test"
        onClickOutside={onClickOutside}
        modal={false}
        animation={false}
      />,
    );
    expectPortal('target-test').toMatchSnapshot();

    fireEvent(
      document,
      new MouseEvent('mousedown', { bubbles: true, cancelable: true }),
    );
    expect(onClickOutside).toHaveBeenCalledTimes(1);
  });

  test('invoke onClickOutside when modal={true} and layer has target', () => {
    const onClickOutside = jest.fn();
    render(
      <TargetLayer
        id="target-test"
        onClickOutside={onClickOutside}
        animation={false}
      />,
    );
    expectPortal('target-test').toMatchSnapshot();

    fireEvent(
      document,
      new MouseEvent('mousedown', { bubbles: true, cancelable: true }),
    );
    expect(onClickOutside).toHaveBeenCalledTimes(1);
  });

  test('custom theme', () => {
    const theme = {
      layer: {
        container: {
          elevation: 'large',
        },
        overlay: {
          backdropFilter: `blur(12px)`,
        },
      },
    };

    render(
      <Grommet theme={theme}>
        <Layer id="custom-theme-test" animation={false}>
          This is a layer
        </Layer>
      </Grommet>,
    );
    expectPortal('custom-theme-test').toMatchSnapshot();
  });

  test('invokes onEsc when modal={false}', () => {
    jest.useFakeTimers();
    window.scrollTo = jest.fn();
    const onEsc = jest.fn();
    const { getByText, queryByText } = render(
      <Grommet>
        <Layer id="esc-test" onEsc={onEsc} modal={false} animation={false}>
          <Select options={['one', 'two', 'three']} data-testid="test-select" />
        </Layer>
      </Grommet>,
    );

    const selectNode = getByTestId(document, 'test-select');

    fireEvent.click(selectNode);
    // advance timers so the select opens
    jest.advanceTimersByTime(100);
    // verify that select is open
    expect(getByText('one')).toBeTruthy();

    fireEvent.keyDown(document, {
      key: 'Esc',
      keyCode: 27,
      which: 27,
    });

    // advance timers so the select closes
    jest.advanceTimersByTime(100);
    expect(queryByText('one')).toBeFalsy();
    // onEsc should not be called on the Layer yet
    expect(onEsc).toBeCalledTimes(0);

    fireEvent.keyDown(document, {
      key: 'Esc',
      keyCode: 27,
      which: 27,
    });
    expect(onEsc).toBeCalledTimes(1);
    expectPortal('esc-test').toMatchSnapshot();
  });

  test('should only place id on StyledLayer when singleId === true', () => {
    render(
      <Grommet options={{ layer: { singleId: true } }}>
        <Layer id="singleId-test" animation={false}>
          This is a layer
        </Layer>
      </Grommet>,
    );
    expectPortal('singleId-test').toMatchSnapshot();
  });
});