grommet/grommet

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

Summary

Maintainability
F
1 wk
Test Coverage
import React from 'react';
import { render, fireEvent, act, screen } from '@testing-library/react';
import { axe } from 'jest-axe';
import 'jest-axe/extend-expect';
import 'jest-styled-components';
import 'regenerator-runtime/runtime';
import '@testing-library/jest-dom';

import { CaretDown, CaretUp, FormDown } from 'grommet-icons';
import { createPortal, expectPortal } from '../../../utils/portal';

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

describe('Select', () => {
  window.scrollTo = jest.fn();
  window.HTMLElement.prototype.scrollIntoView = jest.fn();
  beforeEach(createPortal);

  test('should not have accessibility violations', async () => {
    const { container } = render(
      <Grommet>
        <Select options={['one', 'two', 'three']} a11yTitle="test" />
      </Grommet>,
    );

    const results = await axe(container, {
      rules: {
        /* This rule is flagged because Select is built using a 
        TextInput within a DropButton. According to Dequeue and 
        WCAG 4.1.2 "interactive controls must not have focusable 
        descendants". Jest-axe is assuming that the input is focusable
        and since the input is a descendant of the button the rule is 
        flagged. However, the TextInput is built so that it is read 
        only and cannot receive focus. Select is accessible 
        according to the WCAG specification, but jest-axe is flagging
        it so we are disabling this rule. */
        'nested-interactive': { enabled: false },
      },
    });
    expect(results).toHaveNoViolations();

    expect(container.firstChild).toMatchSnapshot();
  });

  test('basic', () => {
    const { container } = render(
      <Select id="test-select" options={['one', 'two']} a11yTitle="Select" />,
    );

    expect(container.firstChild).toMatchSnapshot();
  });

  test('dark', () => {
    const { container } = render(
      <Grommet>
        <Box fill background="dark-1" align="center" justify="center">
          <Select placeholder="Select" options={['one', 'two']} />
        </Box>
      </Grommet>,
    );

    expect(container.firstChild).toMatchSnapshot();
  });

  test('prop: onOpen', () => {
    jest.useFakeTimers();
    const onOpen = jest.fn();
    const { getByPlaceholderText, container } = render(
      <Select
        placeholder="test select"
        id="test-select"
        options={['one', 'two']}
        onOpen={onOpen}
      />,
    );
    expect(container.firstChild).toMatchSnapshot();
    expect(document.getElementById('test-select__drop')).toBeNull();

    fireEvent.click(getByPlaceholderText('test select'));
    expect(container.firstChild).toMatchSnapshot();
    expectPortal('test-select__drop').toMatchSnapshot();
    // advance timers so the select opens
    jest.advanceTimersByTime(100);
    // verify that select is open
    expect(document.activeElement).toMatchSnapshot();

    expect(onOpen).toHaveBeenCalledTimes(1);
  });

  test('prop: onClose', () => {
    jest.useFakeTimers();
    const onClose = jest.fn();
    const { getByPlaceholderText, container } = render(
      <Select
        placeholder="test select"
        id="test-select"
        options={['one', 'two']}
        onClose={onClose}
      />,
    );

    fireEvent.click(getByPlaceholderText('test select'));
    // closes
    fireEvent.click(getByPlaceholderText('test select'));
    expect(container.firstChild).toMatchSnapshot();
    expect(document.getElementById('test-select__drop')).toBeNull();
    // advance timers so the select closes
    jest.advanceTimersByTime(100);
    // verify that select was closed
    expect(document.activeElement).toMatchSnapshot();
    expect(onClose).toHaveBeenCalledTimes(1);
  });

  [0, null].forEach((value) =>
    test(`${value} value`, () => {
      const { asFragment } = render(
        <Select
          id="test-select"
          placeholder="test select"
          options={[0, 1]}
          value={value}
        />,
      );

      expect(asFragment()).toMatchSnapshot();
    }),
  );

  test('search', () => {
    jest.useFakeTimers();
    const onSearch = jest.fn();
    const { getByPlaceholderText, container } = render(
      <Select
        id="test-select"
        placeholder="test select"
        options={['one', 'two']}
        onSearch={onSearch}
        value="two"
      />,
    );
    expect(container.firstChild).toMatchSnapshot();

    fireEvent.click(getByPlaceholderText('test select'));

    // advance timers so select can open
    act(() => {
      jest.advanceTimersByTime(200);
    });
    // snapshot on search box
    expectPortal('test-select__drop').toMatchSnapshot();
    expect(document.activeElement).toMatchSnapshot();
    // add content to search box
    fireEvent.change(document.activeElement, { target: { value: 'o' } });
    expect(document.activeElement).toMatchSnapshot();
    expect(onSearch).toBeCalledWith('o');
  });

  test('search and select', () => {
    jest.useFakeTimers();
    const onSearch = jest.fn();
    const onChange = jest.fn();
    const Test = () => {
      const [options, setOptions] = React.useState(['one', 'two']);
      return (
        <Select
          id="test-select"
          placeholder="test select"
          options={options}
          onChange={onChange}
          onSearch={(arg) => {
            onSearch(arg);
            setOptions(['two']);
          }}
        />
      );
    };
    const { getByPlaceholderText, getByText, container } = render(<Test />);
    expect(container.firstChild).toMatchSnapshot();

    fireEvent.click(getByPlaceholderText('test select'));

    // advance timers so select can open
    act(() => jest.advanceTimersByTime(200));
    // snapshot on search box
    expectPortal('test-select__drop').toMatchSnapshot();
    expect(document.activeElement).toMatchSnapshot();
    // add content to search box
    fireEvent.change(document.activeElement, { target: { value: 't' } });
    expect(document.activeElement).toMatchSnapshot();
    expect(onSearch).toBeCalledWith('t');

    fireEvent.click(getByText('two'));
    expect(onChange).toBeCalledWith(expect.objectContaining({ value: 'two' }));
  });

  test('select an option with complex options', () => {
    const onChange = jest.fn();
    const { getByText, container } = render(
      <Select
        id="test-select"
        plain
        value={<span>one</span>}
        options={[{ test: 'one' }, { test: 'two' }]}
        onChange={onChange}
      >
        {(option) => <span>{option.test}</span>}
      </Select>,
    );
    expect(container.firstChild).toMatchSnapshot();

    fireEvent.click(getByText('one'));

    // pressing enter here nothing will happen
    fireEvent.click(
      document.getElementById('test-select__drop').querySelector('button'),
    );
    expect(onChange).toBeCalledWith(
      expect.objectContaining({ value: { test: 'one' } }),
    );
    expect(window.scrollTo).toBeCalled();
  });

  test('size', () => {
    const { container } = render(
      <Select
        id="test-select"
        size="large"
        options={['one', 'two']}
        selected={[]}
        value={[]}
        onChange={() => {}}
      />,
    );

    expect(container.firstChild).toMatchSnapshot();
  });

  ['small', 'medium', 'large'].forEach((dropHeight) => {
    test(`${dropHeight} drop container height`, () => {
      const { getByPlaceholderText } = render(
        <Select
          id="test-select"
          size="large"
          options={['one', 'two']}
          selected={[]}
          value={[]}
          onChange={() => {}}
          dropHeight={dropHeight}
          placeholder="test select"
        />,
      );
      fireEvent.click(getByPlaceholderText('test select'));
      expect(document.activeElement).toMatchSnapshot();
    });
  });

  test('onChange without valueKey', () => {
    const onChange = jest.fn();
    const Test = () => {
      const [value] = React.useState();
      return (
        <Select
          id="test-select"
          placeholder="test select"
          labelKey="name"
          value={value}
          options={[
            {
              id: 1,
              name: 'Value1',
            },
            {
              id: 2,
              name: 'Value2',
            },
          ]}
          onChange={onChange}
        />
      );
    };
    const { getByPlaceholderText, getByText, container, asFragment } = render(
      <Grommet>
        <Test />
      </Grommet>,
    );
    expect(container.firstChild).toMatchSnapshot();
    fireEvent.click(getByPlaceholderText('test select'));

    expectPortal('test-select__drop').toMatchSnapshot();

    fireEvent.click(getByText('Value1'));
    expect(onChange).toBeCalledWith(
      expect.objectContaining({
        value: {
          id: 1,
          name: 'Value1',
        },
      }),
    );

    expect(asFragment()).toMatchSnapshot();
  });

  test('onChange without labelKey', () => {
    const onChange = jest.fn();
    const Test = () => {
      const [value] = React.useState();
      return (
        <Select
          id="test-select"
          placeholder="test select"
          valueKey="id"
          value={value}
          options={[
            {
              id: 1,
              name: 'Value1',
            },
            {
              id: 2,
              name: 'Value2',
            },
          ]}
          onChange={onChange}
        />
      );
    };
    const { getByPlaceholderText, getByText, asFragment } = render(
      <Grommet>
        <Test />
      </Grommet>,
    );
    expect(asFragment()).toMatchSnapshot();
    fireEvent.click(getByPlaceholderText('test select'));

    expectPortal('test-select__drop').toMatchSnapshot();

    fireEvent.click(getByText('1'));
    expect(onChange).toBeCalledWith(
      expect.objectContaining({
        value: {
          id: 1,
          name: 'Value1',
        },
      }),
    );

    expect(asFragment()).toMatchSnapshot();
  });

  test('onChange without labelKey or valueKey', () => {
    const onChange = jest.fn();
    const Test = () => {
      const [value] = React.useState();
      return (
        <Select
          id="test-select"
          placeholder="test select"
          value={value}
          options={[
            {
              id: 1,
              name: 'Value1',
            },
            {
              id: 2,
              name: 'Value2',
            },
          ]}
          onChange={onChange}
        />
      );
    };
    const { getByPlaceholderText, getByText, container, asFragment } = render(
      <Grommet>
        <Test />
      </Grommet>,
    );
    expect(container.firstChild).toMatchSnapshot();
    fireEvent.click(getByPlaceholderText('test select'));

    expectPortal('test-select__drop').toMatchSnapshot();

    fireEvent.click(getByText('1')); // 1 matches first key
    expect(onChange).toBeCalledWith(
      expect.objectContaining({
        value: {
          id: 1,
          name: 'Value1',
        },
      }),
    );

    expect(asFragment()).toMatchSnapshot();
  });

  test('onChange with valueKey string', () => {
    const onChange = jest.fn();
    const Test = () => {
      const [value] = React.useState();
      return (
        <Select
          id="test-select"
          placeholder="test select"
          labelKey="name"
          valueKey="id"
          value={value}
          options={[
            {
              id: 1,
              name: 'Value1',
            },
            {
              id: 2,
              name: 'Value2',
            },
          ]}
          onChange={onChange}
        />
      );
    };
    const { getByPlaceholderText, getByText, container, asFragment } = render(
      <Grommet>
        <Test />
      </Grommet>,
    );
    expect(container.firstChild).toMatchSnapshot();
    fireEvent.click(getByPlaceholderText('test select'));

    expectPortal('test-select__drop').toMatchSnapshot();

    fireEvent.click(getByText('Value1'));
    expect(onChange).toBeCalledWith(
      expect.objectContaining({
        value: {
          id: 1,
          name: 'Value1',
        },
      }),
    );

    expect(asFragment()).toMatchSnapshot();
  });

  test('onChange with valueKey object', () => {
    const onChange = jest.fn();
    const Test = () => {
      const [value] = React.useState();
      return (
        <Select
          id="test-select"
          placeholder="test select"
          labelKey="name"
          valueKey={{ key: 'id', reduce: true }}
          value={value}
          options={[
            {
              id: 1,
              name: 'Value1',
            },
            {
              id: 2,
              name: 'Value2',
            },
          ]}
          onChange={onChange}
        />
      );
    };
    const { getByPlaceholderText, getByText, asFragment } = render(
      <Grommet>
        <Test />
      </Grommet>,
    );
    expect(asFragment()).toMatchSnapshot();
    fireEvent.click(getByPlaceholderText('test select'));

    expectPortal('test-select__drop').toMatchSnapshot();

    fireEvent.click(getByText('Value1'));
    expect(onChange).toBeCalledWith(
      expect.objectContaining({
        value: 1,
      }),
    );

    expect(asFragment()).toMatchSnapshot();
  });

  test('disabled key', () => {
    jest.useFakeTimers();
    const Test = () => {
      const [value] = React.useState();
      return (
        <Select
          id="test-select"
          placeholder="test select"
          labelKey="name"
          valueKey="id"
          value={value}
          disabledKey="disabled"
          options={[
            {
              id: 1,
              name: 'Value1',
              disabled: true,
            },
            {
              id: 2,
              name: 'Value2',
              disabled: false,
            },
          ]}
        />
      );
    };
    const { getByPlaceholderText, container } = render(
      <Grommet>
        <Test />
      </Grommet>,
    );
    expect(container.firstChild).toMatchSnapshot();
    fireEvent.click(getByPlaceholderText('test select'));
    jest.advanceTimersByTime(200);
    expectPortal('test-select__drop').toMatchSnapshot();
  });

  test('complex options and children', () => {
    const { getByPlaceholderText, container } = render(
      <Select
        id="test-select"
        placeholder="test select"
        options={[{ test: 'one' }, { test: 'two' }]}
      >
        {(option) => <span>{option.test}</span>}
      </Select>,
    );
    // before opening
    expect(container.firstChild).toMatchSnapshot();
    expect(document.getElementById('test-select__drop')).toBeNull();

    fireEvent.click(getByPlaceholderText('test select'));

    // after opening
    expect(container.firstChild).toMatchSnapshot();
    expectPortal('test-select__drop').toMatchSnapshot();
  });

  test('select an option', () => {
    const onChange = jest.fn();
    const { getByPlaceholderText, container } = render(
      <Select
        id="test-select"
        placeholder="test select"
        options={['one', 'two']}
        onChange={onChange}
      />,
    );
    const select = getByPlaceholderText('test select');
    expect(container.firstChild).toMatchSnapshot();
    fireEvent.click(getByPlaceholderText('test select'));

    // pressing enter here nothing will happen
    fireEvent.click(
      document.getElementById('test-select__drop').querySelector('button'),
    );

    // checks if select has a value assigned to it after option is selected
    expect(select.value).toEqual('one');
    expect(onChange).toBeCalledWith(expect.objectContaining({ value: 'one' }));
    expect(window.scrollTo).toBeCalled();
  });

  test('select an option with enter', () => {
    const onChange = jest.fn();
    const { getByPlaceholderText, container } = render(
      <Select
        id="test-select"
        placeholder="test select"
        options={['one', 'two']}
        onChange={onChange}
      />,
    );
    expect(container.firstChild).toMatchSnapshot();

    fireEvent.click(getByPlaceholderText('test select'));
    // verify that keyboard navigation is working
    fireEvent.keyDown(document.getElementById('test-select__select-drop'), {
      key: 'Down',
      keyCode: 40,
      which: 40,
    });
    fireEvent.keyDown(document.getElementById('test-select__select-drop'), {
      key: 'Up',
      keyCode: 38,
      which: 38,
    });
    fireEvent.keyDown(document.getElementById('test-select__select-drop'), {
      key: 'Enter',
      keyCode: 13,
      which: 13,
    });
    expect(onChange).toBeCalledWith(expect.objectContaining({ value: 'one' }));
    expect(window.scrollTo).toBeCalled();
  });

  test('select an option with keypress', () => {
    const onChange = jest.fn();
    const { getByPlaceholderText, container } = render(
      <Select
        id="test-select"
        placeholder="test select"
        options={['one', 'two', 'three']}
        onChange={onChange}
      />,
    );
    expect(container.firstChild).toMatchSnapshot();

    fireEvent.click(getByPlaceholderText('test select'));
    // verify that keyboard navigation is working
    fireEvent.keyDown(document.getElementById('test-select__select-drop'), {
      key: 't',
      keyCode: 84,
      which: 84,
    });
    fireEvent.keyDown(document.getElementById('test-select__select-drop'), {
      key: 'Enter',
      keyCode: 13,
      which: 13,
    });
    expect(onChange).toBeCalledWith(expect.objectContaining({ value: 'two' }));
    expect(window.scrollTo).toBeCalled();
  });

  test('select an object with label key specific with keypress', () => {
    const onChange = jest.fn();
    const options = [
      { id: 1, name: 'one' },
      { id: 2, name: 'two' },
      { id: 3, name: 'three' },
    ];
    const { getByPlaceholderText, container } = render(
      <Select
        id="test-select"
        placeholder="test select"
        options={options}
        labelKey="name"
        valueKey="id"
        onChange={onChange}
      />,
    );
    expect(container.firstChild).toMatchSnapshot();

    fireEvent.click(getByPlaceholderText('test select'));
    // verify that keyboard navigation is working
    fireEvent.keyDown(document.getElementById('test-select__select-drop'), {
      key: 't',
      keyCode: 84,
      which: 84,
    });
    fireEvent.keyDown(document.getElementById('test-select__select-drop'), {
      key: 'Enter',
      keyCode: 13,
      which: 13,
    });
    expect(onChange).toBeCalledWith(
      expect.objectContaining({
        value: { id: 2, name: 'two' },
      }),
    );
    expect(window.scrollTo).toBeCalled();
  });

  test('select on multiple keydown always picks first enabled option', () => {
    const onChange = jest.fn();
    const { getByPlaceholderText, container } = render(
      <Select
        id="test-select"
        placeholder="test select"
        options={['one', 'two', 'three']}
        onChange={onChange}
      />,
    );
    expect(container.firstChild).toMatchSnapshot();

    fireEvent.click(getByPlaceholderText('test select'));
    // verify that keyboard navigation is working
    fireEvent.keyDown(document.getElementById('test-select__select-drop'), {
      key: 't',
      keyCode: 84,
      which: 84,
    });
    fireEvent.keyDown(document.getElementById('test-select__select-drop'), {
      key: 't',
      keyCode: 84,
      which: 84,
    });
    fireEvent.keyDown(document.getElementById('test-select__select-drop'), {
      key: 'Enter',
      keyCode: 13,
      which: 13,
    });
    expect(onChange).toBeCalledWith(
      expect.objectContaining({
        value: 'two',
      }),
    );
    expect(window.scrollTo).toBeCalled();
  });

  test('disabled', () => {
    const { getByPlaceholderText, container } = render(
      <Select
        id="test-select"
        placeholder="test select"
        disabled
        options={['one', 'two']}
      />,
    );
    expect(container.firstChild).toMatchSnapshot();
    expect(document.getElementById('test-select__drop')).toBeNull();

    fireEvent.click(getByPlaceholderText('test select'));

    expect(container.firstChild).toMatchSnapshot();
    // dropdown should still be null because select is disabled
    expect(document.getElementById('test-select__drop')).toBeNull();
  });

  test('empty results search', () => {
    const { getByPlaceholderText } = render(
      <Select
        id="test-select"
        placeholder="test select"
        options={[]}
        onSearch={() => {}}
        emptySearchMessage="no results"
      />,
    );
    fireEvent.click(getByPlaceholderText('test select'));
    // advance timers so that the select drop can open
    act(() => {
      jest.advanceTimersByTime(200);
    });
    fireEvent.change(document.activeElement, { target: { value: 'o' } });

    expect(document.activeElement).toMatchSnapshot();
  });

  test('open default state', () => {
    render(
      <Select
        open
        id="test-select"
        placeholder="test select"
        options={['one', 'two']}
      />,
    );

    expect(document.getElementById('test-select__drop')).not.toBeNull();
  });

  test('renders without icon', () => {
    const { container } = render(
      <Select id="test-select" options={['one', 'two']} icon={false} />,
    );

    expect(container.firstChild).toMatchSnapshot();
  });

  test('renders custom icon', () => {
    const { container } = render(
      <Select id="test-select" options={['one', 'two']} icon={CaretDown} />,
    );

    expect(container.firstChild).toMatchSnapshot();
  });

  test('renders default icon', () => {
    const { container } = render(
      <Select id="test-select" options={['one', 'two']} icon />,
    );

    expect(container.firstChild).toMatchSnapshot();
  });

  test('modifies select control style on open', () => {
    const customTheme = {
      select: {
        control: {
          extend: {
            background: 'purple',
          },
          open: {
            background: 'lightgrey',
          },
        },
        container: {},
      },
    };

    const { container } = render(
      <Grommet theme={customTheme}>
        <Select
          data-testid="test-select-style-open"
          id="test-open-id"
          options={['morning', 'afternoon', 'evening']}
          placeholder="Select..."
        />
      </Grommet>,
    );

    expect(container.firstChild).toMatchSnapshot();

    const selectButton = document.getElementById('test-open-id');
    let style;

    style = window.getComputedStyle(selectButton);
    expect(style.background).toBe('purple');

    fireEvent.click(selectButton);
    style = window.getComputedStyle(selectButton);
    expect(style.background).toBe('lightgrey');

    fireEvent.click(selectButton);
    style = window.getComputedStyle(selectButton);
    expect(style.background).toBe('purple');
  });

  test(`renders styled select options backwards compatible with legacy
      documentation (select.options.box)`, () => {
    const customTheme = {
      select: {
        options: {
          box: {
            background: 'lightblue',
          },
        },
      },
    };

    const { getByPlaceholderText, getByText, container } = render(
      <Grommet theme={customTheme}>
        <Select
          data-testid="test-select-style-options-1"
          id="test-options-style-id"
          options={['morning', 'afternoon', 'evening']}
          placeholder="Select..."
        />
      </Grommet>,
    );

    expect(container.firstChild).toMatchSnapshot();

    const selectButton = getByPlaceholderText('Select...');
    fireEvent.click(selectButton);

    const optionButton = getByText('morning').closest('button');
    const style = window.getComputedStyle(optionButton.firstChild);
    expect(style.background).toBe('lightblue');
  });

  test('renders styled select options using select.options.container', () => {
    const customTheme = {
      select: {
        options: {
          container: {
            background: 'lightgreen',
          },
        },
      },
    };

    const { getByPlaceholderText, getByText, container } = render(
      <Grommet theme={customTheme}>
        <Select
          data-testid="test-select-style-options-2"
          id="test-options-style-id"
          options={['morning', 'afternoon', 'evening']}
          placeholder="Select..."
        />
      </Grommet>,
    );

    expect(container.firstChild).toMatchSnapshot();

    const selectButton = getByPlaceholderText('Select...');
    fireEvent.click(selectButton);

    const optionButton = getByText('morning').closest('button');
    const style = window.getComputedStyle(optionButton.firstChild);
    expect(style.background).toBe('lightgreen');
  });

  test(`renders styled select options combining select.options.box &&
    select.options.container;
    select.options.container prioritized if conflict`, () => {
    const customTheme = {
      select: {
        options: {
          container: {
            background: 'lightgreen',
          },
          box: {
            background: 'lightblue',
            border: {
              side: 'bottom',
              size: 'small',
              color: 'blue',
            },
          },
        },
      },
    };

    const { getByPlaceholderText, getByText, container } = render(
      <Grommet theme={customTheme}>
        <Select
          data-testid="test-select-style-options-3"
          id="test-options-style-id"
          options={['morning', 'afternoon', 'evening']}
          placeholder="Select..."
        />
      </Grommet>,
    );

    expect(container.firstChild).toMatchSnapshot();

    const selectButton = getByPlaceholderText('Select...');
    fireEvent.click(selectButton);

    let style;
    const optionButton = getByText('morning').closest('button');

    style = window.getComputedStyle(optionButton.firstChild);
    expect(style.background).not.toBe('lightblue');

    style = window.getComputedStyle(optionButton.firstChild);
    expect(style.background).toBe('lightgreen');
    expect(style.borderBottom).toBe('2px solid blue');
  });

  test('renders empty search with select.emptySearchMessage styling', () => {
    const customTheme = {
      select: {
        emptySearchMessage: {
          container: {
            pad: {
              horizontal: 'small', // t-shirt size
              vertical: '6px', // px value
            },
          },
          text: {
            color: 'green',
          },
        },
      },
    };

    const { getByPlaceholderText, getByText } = render(
      <Grommet theme={customTheme}>
        <Select
          id="test-select"
          data-testid
          placeholder="test select"
          options={[]}
          onSearch={() => {}}
        />
      </Grommet>,
    );

    fireEvent.click(getByPlaceholderText('test select'));
    // advance timers so that the select drop can open
    act(() => {
      jest.advanceTimersByTime(200);
    });
    fireEvent.change(document.activeElement, { target: { value: 'o' } });

    let style;
    const emptySearchContainer = getByText('No matches found').closest('div');
    style = window.getComputedStyle(emptySearchContainer);
    expect(style.paddingLeft).toBe('12px');
    expect(style.paddingRight).toBe('12px');
    expect(style.paddingTop).toBe('6px');
    expect(style.paddingBottom).toBe('6px');

    const emptySearchMessage = getByText('No matches found').closest('span');
    style = window.getComputedStyle(emptySearchMessage);
    expect(style.color).toBe('green');

    expect(document.activeElement).toMatchSnapshot();
  });

  test('applies custom global.hover theme to options', () => {
    const customTheme = {
      global: {
        hover: {
          background: {
            color: 'lightgreen',
          },
          color: {
            dark: 'lightgrey',
            light: 'brand',
          },
        },
      },
    };
    const { getByPlaceholderText, getByText, container } = render(
      <Grommet theme={customTheme}>
        <Select
          data-testid="applies-custom-hover-style"
          id="applies-custom-hover-style-id"
          options={['morning', 'afternoon', 'evening']}
          placeholder="Select..."
        />
      </Grommet>,
    );

    expect(container.firstChild).toMatchSnapshot();

    const selectButton = getByPlaceholderText('Select...');
    fireEvent.click(selectButton);

    const optionButton = getByText('afternoon').closest('button');
    fireEvent.mouseOver(optionButton);
    expect(optionButton).toMatchSnapshot();
  });

  test('renders custom up and down icons', () => {
    const customTheme = {
      select: {
        icons: {
          down: FormDown,
          up: CaretUp,
        },
      },
    };

    const { getByPlaceholderText, container } = render(
      <Grommet theme={customTheme}>
        <Select
          options={['morning', 'afternoon', 'evening']}
          placeholder="Select..."
        />
      </Grommet>,
    );

    expect(container.firstChild).toMatchSnapshot();

    const selectButton = getByPlaceholderText('Select...');
    fireEvent.click(selectButton);
    // Check that custom up icon is applied when open
    expect(container.firstChild).toMatchSnapshot();
  });

  test('select an option then select a different option', () => {
    const onChange = jest.fn();
    const { getByPlaceholderText } = render(
      <Select
        id="test-select"
        placeholder="test select"
        options={['one', 'two']}
        onChange={onChange}
      />,
    );
    const select = getByPlaceholderText('test select');
    fireEvent.click(getByPlaceholderText('test select'));
    // select first option
    fireEvent.click(
      document.getElementById('test-select__drop').querySelector('button'),
    );

    // checks if select has a value assigned to it after option is selected
    expect(select.value).toEqual('one');

    fireEvent.click(getByPlaceholderText('test select'));
    // select second option
    fireEvent.click(
      document
        .getElementById('test-select__drop')
        .querySelectorAll('button')[1],
    );

    // checks if select has a value assigned to it after option is selected
    expect(select.value).toEqual('two');
    expect(onChange).toHaveBeenCalledTimes(2);
  });

  test('keyboard navigation with disabled option', () => {
    const { getByPlaceholderText } = render(
      <Select
        id="test-select"
        placeholder="test select"
        options={['one', 'two', 'three', 'four']}
        disabled={[1]}
        open
      />,
    );
    const select = getByPlaceholderText('test select');
    // should skip over disabled option
    fireEvent.keyDown(document.getElementById('test-select__select-drop'), {
      key: 'Down',
      keyCode: 40,
      which: 40,
    });
    fireEvent.keyDown(document.getElementById('test-select__select-drop'), {
      key: 'Down',
      keyCode: 40,
      which: 40,
    });
    fireEvent.keyDown(document.getElementById('test-select__select-drop'), {
      key: 'Up',
      keyCode: 38,
      which: 38,
    });
    // should skip oer disabled option
    fireEvent.keyDown(document.getElementById('test-select__select-drop'), {
      key: 'Up',
      keyCode: 38,
      which: 38,
    });

    fireEvent.keyDown(document.getElementById('test-select__select-drop'), {
      key: 'Enter',
      keyCode: 13,
      which: 13,
    });

    expect(select.value).toEqual('one');
  });

  test('undefined option', () => {
    const { getByPlaceholderText } = render(
      <Grommet>
        <Select
          id="test-select"
          placeholder="test select"
          options={[undefined, 1, 2]}
        />
      </Grommet>,
    );
    const select = getByPlaceholderText('test select');
    fireEvent.click(getByPlaceholderText('test select'));
    fireEvent.click(
      document.getElementById('test-select__drop').querySelector('button'),
    );
    // if undefined value is selected select will have empty string value
    expect(select.value).toEqual('');
  });

  test('valueLabel', () => {
    const { container } = render(
      <Grommet>
        <Select
          id="test-select"
          placeholder="test select"
          options={[undefined, 1, 2]}
          valueLabel="test"
        />
      </Grommet>,
    );
    expect(container.firsChild).toMatchSnapshot();
  });

  test('onChange with valueLabel', () => {
    const onChange = jest.fn();
    const Test = () => {
      const [value, setValue] = React.useState();
      return (
        <Select
          id="test-select"
          value={value}
          valueLabel={<span>{value || 'none'}</span>}
          options={['one', 'two']}
          onChange={(event) => {
            setValue(event.value);
            onChange(event);
          }}
        />
      );
    };
    const { getByText, container } = render(
      <Grommet>
        <Test />
      </Grommet>,
    );
    expect(container.firstChild).toMatchSnapshot();
    fireEvent.click(getByText('none'));

    expectPortal('test-select__drop').toMatchSnapshot();

    fireEvent.click(getByText('one'));
    expect(onChange).toBeCalledWith(
      expect.objectContaining({
        value: 'one',
        option: 'one',
        target: expect.objectContaining({
          value: 'one',
        }),
      }),
    );
  });

  test('selected', () => {
    const { container, getByPlaceholderText } = render(
      <Grommet>
        <Select
          options={['one', 'two']}
          placeholder="test select"
          id="test-select"
          selected={0}
        />
      </Grommet>,
    );
    const select = getByPlaceholderText('test select');
    expect(container.firstChild).toMatchSnapshot();
    expect(select.value).toEqual('one');
  });

  test('keyboard navigation timeout', () => {
    jest.useFakeTimers();
    const { container, getByPlaceholderText } = render(
      <Grommet>
        <Select
          id="test-select"
          placeholder="test select"
          options={['one', 'two', 'three']}
          disabled={[0]}
        />
      </Grommet>,
    );
    fireEvent.click(getByPlaceholderText('test select'));
    // key event to start keyboard navigation
    fireEvent.keyDown(document.getElementById('test-select__select-drop'), {
      key: 'Down',
      keyCode: 40,
      which: 40,
    });
    // advance timers to cause keyboard nav timeout
    act(() => {
      jest.advanceTimersByTime(100);
    });
    expect(container.firstChild).toMatchSnapshot();
  });

  test('Search timeout', () => {
    jest.useFakeTimers();
    const onSearch = jest.fn();
    const { container, getByPlaceholderText } = render(
      <Grommet>
        <Select
          id="test-select"
          placeholder="test select"
          options={['one', 'two']}
          onSearch={onSearch}
        />
      </Grommet>,
    );

    fireEvent.click(getByPlaceholderText('test select'));

    // advance timers so select can open & have focus
    act(() => {
      jest.advanceTimersByTime(200);
    });
    // add content to search box
    fireEvent.change(document.activeElement, { target: { value: 'o' } });
    expect(container.firstChild).toMatchSnapshot();
    // advance timers to cause search timeout
    act(() => {
      jest.advanceTimersByTime(100);
    });
    expect(document.activeElement).toMatchSnapshot();
  });

  test('disabled option value', () => {
    jest.useFakeTimers();
    const { getByPlaceholderText } = render(
      <Grommet>
        <Select
          id="test-select"
          placeholder="test select"
          options={['one', 'two']}
          disabled={['one']}
        />
      </Grommet>,
    );
    fireEvent.click(getByPlaceholderText('test select'));
    expectPortal('test-select__drop').toMatchSnapshot();
  });

  test('Clear option renders- top', () => {
    const Test = () => {
      const [value] = React.useState();
      return (
        <Select
          id="test-select"
          placeholder="test select"
          value={value}
          options={['one', 'two']}
          clear
        />
      );
    };
    render(
      <Grommet>
        <Test />
      </Grommet>,
    );

    fireEvent.click(screen.getByPlaceholderText('test select'));
    fireEvent.click(screen.getByRole('option', { name: 'one' }));
    fireEvent.click(screen.getByPlaceholderText('test select'));
    const clearButton = screen.getByRole('button', {
      name: 'Clear selection. Or, press down arrow to move to select options',
    });
    expect(clearButton).toBeTruthy();
    expectPortal('test-select__drop').toMatchSnapshot();
  });

  test('Clear option renders - bottom', () => {
    const Test = () => {
      const [value] = React.useState();
      return (
        <Select
          id="test-select"
          placeholder="test select"
          value={value}
          options={['one', 'two']}
          clear={{ position: 'bottom' }}
        />
      );
    };
    render(
      <Grommet>
        <Test />
      </Grommet>,
    );

    fireEvent.click(screen.getByPlaceholderText('test select'));
    fireEvent.click(screen.getByRole('option', { name: 'one' }));
    fireEvent.click(screen.getByPlaceholderText('test select'));
    const clearButton = screen.getByRole('button', {
      name: 'Clear selection. Or, press shift tab to move to select options',
    });
    expect(clearButton).toBeTruthy();

    expectPortal('test-select__drop').toMatchSnapshot();
  });

  test('Clear option renders custom label', () => {
    const Test = () => {
      const [value] = React.useState();
      return (
        <Select
          id="test-select"
          placeholder="test select"
          value={value}
          options={['one', 'two']}
          clear={{ label: 'test label' }}
        />
      );
    };
    const { getByPlaceholderText } = render(
      <Grommet>
        <Test />
      </Grommet>,
    );
    fireEvent.click(getByPlaceholderText('test select'));

    expectPortal('test-select__drop').toMatchSnapshot();
  });

  test('Clear option renders correct label when wrapped in FormField', () => {
    const Test = () => {
      const [value] = React.useState();
      return (
        <FormField label="Test" name="test">
          <Select
            name="test"
            id="test-select"
            placeholder="test select"
            value={value}
            options={['one', 'two']}
            clear
          />
        </FormField>
      );
    };
    const { getByPlaceholderText } = render(
      <Grommet>
        <Test />
      </Grommet>,
    );
    fireEvent.click(getByPlaceholderText('test select'));

    expectPortal('test-select__drop').toMatchSnapshot();
  });

  test('Clear option clears value onClick', () => {
    const Test = () => {
      const [value] = React.useState();
      return (
        <FormField label="Test" name="test">
          <Select
            name="test"
            id="test-select"
            placeholder="test select"
            value={value}
            options={['one', 'two']}
            clear
          />
        </FormField>
      );
    };
    const { getByPlaceholderText } = render(
      <Grommet>
        <Test />
      </Grommet>,
    );
    const select = getByPlaceholderText('test select');
    fireEvent.click(getByPlaceholderText('test select'));
    fireEvent.click(
      document
        .getElementById('test-select__drop')
        .querySelectorAll('button')[1],
    );
    fireEvent.click(getByPlaceholderText('test select'));
    fireEvent.click(
      document
        .getElementById('test-select__drop')
        .querySelectorAll('button')[0],
    );
    expect(select.value).toEqual('');
  });

  test('default value', () => {
    const { container, getByDisplayValue } = render(
      <Grommet>
        <Select
          id="test-select"
          placeholder="test select"
          options={['one', 'two']}
          defaultValue="two"
        />
      </Grommet>,
    );
    const select = getByDisplayValue('two');
    expect(container.firstChild).toMatchSnapshot();
    expect(select.value).toEqual('two');
  });

  test('default value object options', () => {
    const { container, getByDisplayValue } = render(
      <Grommet>
        <Select
          id="test-select"
          placeholder="test select"
          options={[
            { label: 'one', value: 1 },
            { label: 'two', value: 2 },
          ]}
          defaultValue={2}
          labelKey="label"
          valueKey={{ key: 'value', reduce: true }}
        />
      </Grommet>,
    );
    const select = getByDisplayValue('two');
    expect(container.firstChild).toMatchSnapshot();
    expect(select.value).toEqual('two');
  });

  test('default value clear', () => {
    const Test = () => {
      const [value] = React.useState();
      return (
        <Select
          id="test-select"
          placeholder="test select"
          defaultValue="two"
          value={value}
          options={['one', 'two']}
          clear
        />
      );
    };
    const { getByDisplayValue } = render(
      <Grommet>
        <Test />
      </Grommet>,
    );
    const select = getByDisplayValue('two');
    fireEvent.click(select);
    expectPortal('test-select__drop').toMatchSnapshot();

    fireEvent.click(
      document
        .getElementById('test-select__drop')
        .querySelectorAll('button')[0],
    );
    expect(select.value).toEqual('');
  });

  test('select option by typing should not break if caller passes JSX', () => {
    jest.useFakeTimers();
    const onChange = jest.fn();

    const { getByPlaceholderText, container } = render(
      <Grommet>
        <Select
          id="test-select"
          placeholder="test select"
          options={[<Box>JSX Element</Box>, 'one', 'two']}
          onChange={onChange}
        />
      </Grommet>,
    );
    expect(container.firstChild).toMatchSnapshot();

    fireEvent.click(getByPlaceholderText('test select'));

    // advance timers so select can open
    act(() => jest.advanceTimersByTime(200));
    // add content to search box
    fireEvent.keyDown(document.activeElement, {
      key: 't',
      keyCode: 84,
      which: 84,
    });
    fireEvent.keyDown(document.activeElement, {
      key: 'Enter',
      keyCode: 13,
      which: 13,
    });
    expect(onChange).toBeCalledWith(expect.objectContaining({ value: 'two' }));
    expect(container.firstChild).toMatchSnapshot();
  });

  test('should apply a11yTitle or aria-label', () => {
    const { container, getByRole } = render(
      <Grommet>
        <Select options={['one', 'two', 'three']} a11yTitle="test" />
        <Select options={['one', 'two', 'three']} aria-label="test-select" />
      </Grommet>,
    );

    expect(getByRole('button', { name: 'test' })).toBeTruthy();
    expect(getByRole('button', { name: 'test-select' })).toBeTruthy();
    expect(container.firstChild).toMatchSnapshot();
  });

  test('object options and null value', () => {
    render(
      <Grommet>
        <Select
          placeholder="test select"
          valueKey="id"
          value={null}
          options={[
            {
              id: 1,
              name: 'Value1',
            },
            {
              id: 2,
              name: 'Value2',
            },
          ]}
        />
      </Grommet>,
    );
    const select = screen.getByRole('button', { name: /test select/ });
    fireEvent.click(select);
    expect(select).toHaveAttribute('aria-expanded', 'true');
  });

  test('valueLabel with value=0', () => {
    const { asFragment } = render(
      <Grommet>
        <Select
          options={[0, 1, 2]}
          value={0}
          valueLabel={(val) => `Value: ${val}`}
        />
      </Grommet>,
    );
    expect(asFragment()).toMatchSnapshot();
  });

  test('opens drop with click but focus option with keyboard', () => {
    jest.useFakeTimers();
    const onSearch = jest.fn();
    const { getAllByRole, getByPlaceholderText } = render(
      <Grommet>
        <Select
          id="test-select"
          options={['one', 'two']}
          onSearch={onSearch}
          placeholder="test select"
        />
      </Grommet>,
    );
    const select = getByPlaceholderText('test select');
    fireEvent.click(select);
    // Wait for drop to appear and auto-focus search input
    act(() => {
      jest.advanceTimersByTime(100);
    });
    const selectDrop = document.getElementById('test-select__select-drop');
    fireEvent.keyDown(selectDrop, {
      key: 'Down',
      keyCode: 40,
      which: 40,
    });
    // Allow for auto-focus to take place (not expected by test)
    act(() => {
      jest.advanceTimersByTime(100);
    });
    expect(getAllByRole('searchbox')[0]).not.toHaveFocus();
    expect(getAllByRole('option')[0]).toHaveFocus();
  });

  test('opens drop and focus option with keyboard', () => {
    jest.useFakeTimers();
    const onSearch = jest.fn();
    const { getAllByRole, getByPlaceholderText } = render(
      <Grommet>
        <Select
          id="test-select"
          options={['one', 'two']}
          onSearch={onSearch}
          placeholder="test select"
        />
      </Grommet>,
    );
    const select = getByPlaceholderText('test select');
    fireEvent.keyDown(select, {
      key: 'Down',
      keyCode: 40,
      which: 40,
    });
    // Wait for drop to appear and auto-focus search input
    act(() => {
      jest.advanceTimersByTime(100);
    });
    const selectDrop = document.getElementById('test-select__select-drop');
    fireEvent.keyDown(selectDrop, {
      key: 'Down',
      keyCode: 40,
      which: 40,
    });
    // Allow for auto-focus to take place (not expected by test)
    act(() => {
      jest.advanceTimersByTime(100);
    });
    expect(getAllByRole('searchbox')[0]).not.toHaveFocus();
    expect(getAllByRole('option')[0]).toHaveFocus();
  });

  window.scrollTo.mockRestore();
  window.HTMLElement.prototype.scrollIntoView.mockRestore();
});