WeAreGenki/minna-ui

View on GitHub
components/select/src/__tests__/Select.test.ts

Summary

Maintainability
F
1 wk
Test Coverage
// TODO: Add integration/UI tests
//  ↳ Responsive CSS
//  ↳ Disabled state works as expected

/* eslint-disable @typescript-eslint/no-non-null-assertion */

import { tick } from 'svelte';
import Select from '../Select.svelte';

const items = [
  { id: 'au', text: 'Australia' },
  { id: 'cn', text: 'China' },
  { id: 'jp', text: 'Japan' },
  { id: 'kr', text: 'Korea' },
  { id: 'other', text: 'Other / Unknown' },
];
/* eslint-disable sort-keys */
const itemsDisabled = [
  { id: '0', text: 'Zero', disabled: true },
  { id: '1', text: 'One' },
  { id: '2', text: 'Two', disabled: true },
  { id: '3', text: 'Three' },
  { id: '4', text: 'Four', disabled: true },
  { id: '5', text: 'Five', disabled: true },
  { id: '6', text: 'Six', disabled: true },
  { id: '7', text: 'Seven' },
  { id: '8', text: 'Eight', disabled: true },
  { id: '9', text: 'Nine', disabled: true },
];
/* eslint-enable sort-keys */
const selectOpts = {
  id: 'test-select',
  items,
  value: '',
};

describe('Select component', () => {
  it('has default props set correctly', () => {
    expect.assertions(7);
    const target = document.createElement('div');
    const component = new Select({
      props: { items: [] },
      target,
    });
    expect(component.disabled).toBe(false);
    expect(component.filterable).toBe(true);
    expect(component.filterHelp).toBe('Filter...');
    expect(component.isOpen).toBe(false);
    expect(component.placeholder).toBe('Choose...');
    expect(component.id).toBeUndefined();
    expect(component.value).toBeUndefined();
  });

  it('renders correctly with required props set', () => {
    expect.assertions(8);
    const target = document.createElement('div');
    const component = new Select({
      props: selectOpts,
      target,
    });
    const select = target.querySelector('.select')!;
    expect(Array.isArray(component.items)).toBe(true);
    expect(component.items).not.toHaveLength(0);
    expect(select.getAttribute('tabindex')).toBe('0');
    expect(select.getAttribute('disabled')).toBeNull();
    expect(select.getAttribute('placeholder')).not.toBe(false);
    expect(document.querySelector('select-active')).toBeNull();
    expect(document.querySelector('select-disabled')).toBeNull();
    expect(target.innerHTML).toMatchSnapshot();
  });

  it("doesn't log errors or warnings with required props", () => {
    expect.assertions(2);
    const spy1 = jest.spyOn(console, 'error');
    const spy2 = jest.spyOn(console, 'warn');
    const target = document.createElement('div');
    new Select({
      props: selectOpts,
      target,
    });
    expect(spy1).not.toHaveBeenCalled();
    expect(spy2).not.toHaveBeenCalled();
    spy1.mockRestore();
    spy2.mockRestore();
  });

  it('renders with value prop set', () => {
    expect.assertions(2);
    const target = document.createElement('div');
    new Select({
      props: {
        ...selectOpts,
        value: 'jp',
      },
      target,
    });
    const active = target.querySelector('.option-active')!;
    expect(active.getAttribute('value')).toBe('jp');
    expect(target.innerHTML).toMatchSnapshot();
  });

  it('renders with filterable prop set to false', () => {
    expect.assertions(3);
    const target = document.createElement('div');
    const component = new Select({
      props: {
        ...selectOpts,
        filterable: false,
        isOpen: true,
      },
      target,
    });
    expect(component.filterable).toBe(false);
    const select = target.querySelector('.select')!;
    expect(select.getAttribute('placeholder')).toBe('Choose...'); // not "Filer..."
    expect(target.innerHTML).toMatchSnapshot();
  });

  it('renders with filterHelp prop', () => {
    expect.assertions(3);
    const target = document.createElement('div');
    const component = new Select({
      props: {
        ...selectOpts,
        filterHelp: 'Filter me',
        isOpen: true,
      },
      target,
    });
    expect(component.filterHelp).toBe('Filter me');
    const select = target.querySelector('.select')!;
    expect(select.getAttribute('placeholder')).toBe('Filter me');
    expect(target.innerHTML).toMatchSnapshot();
  });

  it('renders with placeholder prop', () => {
    expect.assertions(3);
    const target = document.createElement('div');
    const component = new Select({
      props: {
        ...selectOpts,
        placeholder: 'Hold your places',
      },
      target,
    });
    expect(component.placeholder).toBe('Hold your places');
    const select = target.querySelector('.select')!;
    expect(select.getAttribute('placeholder')).toBe('Hold your places');
    expect(target.innerHTML).toMatchSnapshot();
  });

  it('renders with disabled prop', () => {
    expect.assertions(5);
    const target = document.createElement('div');
    const component = new Select({
      props: {
        ...selectOpts,
        disabled: true,
      },
      target,
    });
    expect(component.disabled).toBe(true);
    const select = target.querySelector('.select')!;
    expect(select.getAttribute('disabled')).not.toBeNull();
    expect(select.getAttribute('tabindex')).toBe('-1');
    expect(target.querySelector('.select-disabled')).not.toBeNull();
    expect(target.innerHTML).toMatchSnapshot();
  });

  it('updates selected item on value change', async () => {
    expect.assertions(2);
    const target = document.createElement('div');
    const component = new Select({
      props: {
        ...selectOpts,
        value: 'cn',
      },
      target,
    });
    const active1 = target.querySelector('.option-active')!;
    expect(active1.getAttribute('value')).toBe('cn');
    component.value = 'kr';
    const select = target.querySelector<HTMLSelectElement>('.select')!;
    select.click(); // To set correct selected index
    await tick();
    const active2 = target.querySelector('.option-active')!;
    expect(active2.getAttribute('value')).toBe('kr');
  });

  it('shows on click', async () => {
    expect.assertions(2);
    const target = document.createElement('div');
    const component = new Select({
      props: selectOpts,
      target,
    });
    const select = target.querySelector<HTMLInputElement>('.select')!;
    expect(component.isOpen).toBe(false);
    select.click();
    await tick();
    expect(component.isOpen).toBe(true);
  });

  it('does not show on click when disabled', async () => {
    expect.assertions(2);
    const target = document.createElement('div');
    const component = new Select({
      props: {
        ...selectOpts,
        disabled: true,
      },
      target,
    });
    const select = target.querySelector<HTMLInputElement>('.select')!;
    expect(component.isOpen).toBe(false);
    select.click();
    await tick();
    expect(component.isOpen).toBe(false);
  });

  it('shows on enter key press', () => {
    expect.assertions(2);
    const target = document.createElement('div');
    const component = new Select({
      props: selectOpts,
      target,
    });
    expect(component.isOpen).toBe(false);
    const select = target.querySelector('.select')!;
    const event = new KeyboardEvent('keydown', { key: 'Enter' });
    select.dispatchEvent(event);
    expect(component.isOpen).toBe(true);
  });

  it('shows on spacebar key press', () => {
    expect.assertions(3);
    const target = document.createElement('div');
    const component = new Select({
      props: selectOpts,
      target,
    });
    expect(component.isOpen).toBe(false);
    const select = target.querySelector('.select')!;
    const event1 = new KeyboardEvent('keydown', { key: ' ' }); // spacebar
    select.dispatchEvent(event1);
    expect(component.isOpen).toBe(true);
    // eslint-disable-next-line @typescript-eslint/ban-ts-ignore
    // @ts-ignore - keyCode does actually exist!
    const event2 = new KeyboardEvent('keydown', { keyCode: 32 });
    select.dispatchEvent(event2);
    expect(component.isOpen).toBe(true);
  });

  it('shows on down key press', () => {
    expect.assertions(2);
    const target = document.createElement('div');
    const component = new Select({
      props: selectOpts,
      target,
    });
    expect(component.isOpen).toBe(false);
    const select = target.querySelector('.select')!;
    const event = new KeyboardEvent('keydown', { key: 'ArrowDown' });
    select.dispatchEvent(event);
    expect(component.isOpen).toBe(true);
  });

  it('shows on up key press', () => {
    expect.assertions(2);
    const target = document.createElement('div');
    const component = new Select({
      props: selectOpts,
      target,
    });
    expect(component.isOpen).toBe(false);
    const select = target.querySelector('.select')!;
    const event = new KeyboardEvent('keydown', { key: 'ArrowUp' });
    select.dispatchEvent(event);
    expect(component.isOpen).toBe(true);
  });

  it('shows automatically on focus', () => {
    expect.assertions(3);
    const target = document.createElement('div');
    document.body.appendChild(target);
    const component = new Select({
      props: selectOpts,
      target,
    });
    expect(component.isOpen).toBe(false);
    const select = target.querySelector<HTMLInputElement>('.select')!;
    select.focus();
    expect(component.isOpen).toBe(true);
    expect(document.activeElement).toStrictEqual(select);
  });

  it('hides on click outside the component', () => {
    expect.assertions(4);
    const target = document.createElement('div');
    document.body.appendChild(target);
    const component = new Select({
      props: selectOpts,
      target,
    });
    const select = target.querySelector<HTMLInputElement>('.select')!;
    select.focus();
    expect(component.isOpen).toBe(true);
    expect(document.activeElement).toStrictEqual(select);
    select.blur();
    expect(component.isOpen).toBe(false);
    expect(document.activeElement).toStrictEqual(document.body);
  });

  it('hides on ESC key press', () => {
    expect.assertions(2);
    const target = document.createElement('div');
    const component = new Select({
      props: {
        ...selectOpts,
        isOpen: true,
      },
      target,
    });
    expect(component.isOpen).toBe(true);
    const select = target.querySelector('.select')!;
    const event = new KeyboardEvent('keydown', { key: 'Escape' });
    select.dispatchEvent(event);
    expect(component.isOpen).toBe(false);
  });

  it('does nothing on invalid key press', () => {
    expect.assertions(2);
    const target = document.createElement('div');
    const component = new Select({
      props: selectOpts,
      target,
    });
    expect(component.isOpen).toBe(false);
    const select = target.querySelector('.select')!;
    const event = new KeyboardEvent('keydown', { key: 'xxx' });
    select.dispatchEvent(event);
    expect(component.isOpen).toBe(false);
  });

  it('selects next item on down key press', () => {
    expect.assertions(5);
    const target = document.createElement('div');
    const component = new Select({
      props: {
        ...selectOpts,
        isOpen: true,
        selected: 0,
      },
      target,
    });
    expect(component.isOpen).toBe(true);
    const active1 = target.querySelector('.option-active')!;
    expect(active1.getAttribute('value')).toBe('au');
    const select = target.querySelector('.select')!;
    const event = new KeyboardEvent('keydown', { key: 'ArrowDown' });
    select.dispatchEvent(event);
    const active2 = target.querySelector('.option-active')!;
    expect(active2.getAttribute('value')).toBe('au');
    select.dispatchEvent(event);
    const active3 = target.querySelector('.option-active')!;
    expect(active3.getAttribute('value')).toBe('au');
    expect(component.isOpen).toBe(true); // Still open
  });

  it('selects previous item on up key press', async () => {
    expect.assertions(5);
    const target = document.createElement('div');
    const component = new Select({
      props: {
        ...selectOpts,
        value: items[2].id,
      },
      target,
    });
    const select = target.querySelector<HTMLSelectElement>('.select')!;
    select.click(); // To set correct selected index
    expect(component.isOpen).toBe(true);
    const active1 = target.querySelector('.option-active')!;
    expect(active1.getAttribute('value')).toBe('jp');
    const event = new KeyboardEvent('keydown', { key: 'ArrowUp' });
    select.dispatchEvent(event);
    await tick();
    const active2 = target.querySelector('.option-active')!;
    expect(active2.getAttribute('value')).toBe('cn');
    select.dispatchEvent(event);
    await tick();
    const active3 = target.querySelector('.option-active')!;
    expect(active3.getAttribute('value')).toBe('au');
    expect(component.isOpen).toBe(true); // Still open
  });

  it('skips over disabled items on down key press', async () => {
    expect.assertions(5);
    const target = document.createElement('div');
    const component = new Select({
      props: {
        ...selectOpts,
        items: itemsDisabled,
        value: itemsDisabled[1].id,
      },
      target,
    });
    const select = target.querySelector<HTMLSelectElement>('.select')!;
    select.click(); // To set correct selected index
    const active1 = target.querySelector('.option-active')!;
    expect(active1.getAttribute('value')).toBe('1');
    const event = new KeyboardEvent('keydown', { key: 'ArrowDown' });
    select.dispatchEvent(event);
    await tick();
    const active2 = target.querySelector('.option-active')!;
    expect(active2.getAttribute('value')).toBe('3');
    select.dispatchEvent(event);
    await tick();
    const active3 = target.querySelector('.option-active')!;
    expect(active3.getAttribute('value')).toBe('7');
    select.dispatchEvent(event);
    await tick();
    const active4 = target.querySelector('.option-active')!;
    expect(active4.getAttribute('value')).toBe('7');
    expect(component.isOpen).toBe(true); // Still open
  });

  it('skips over disabled items on up key press', async () => {
    expect.assertions(5);
    const target = document.createElement('div');
    const component = new Select({
      props: {
        ...selectOpts,
        items: itemsDisabled,
        value: itemsDisabled[7].id,
      },
      target,
    });
    const select = target.querySelector<HTMLSelectElement>('.select')!;
    select.click(); // To set correct selected index
    const active1 = target.querySelector('.option-active')!;
    expect(active1.getAttribute('value')).toBe('7');
    const event = new KeyboardEvent('keydown', { key: 'ArrowUp' });
    select.dispatchEvent(event);
    await tick();
    const active2 = target.querySelector('.option-active')!;
    expect(active2.getAttribute('value')).toBe('3');
    select.dispatchEvent(event);
    await tick();
    const active3 = target.querySelector('.option-active')!;
    expect(active3.getAttribute('value')).toBe('1');
    select.dispatchEvent(event);
    await tick();
    const active4 = target.querySelector('.option-active')!;
    expect(active4.getAttribute('value')).toBe('1');
    expect(component.isOpen).toBe(true); // Still open
  });

  it("doesn't go past end of items on down key press", async () => {
    expect.assertions(4);
    const target = document.createElement('div');
    const component = new Select({
      props: {
        ...selectOpts,
        value: items[3].id,
      },
      target,
    });
    const select = target.querySelector<HTMLSelectElement>('.select')!;
    select.click(); // To set correct selected index
    const active1 = target.querySelector('.option-active')!;
    expect(active1.getAttribute('value')).toBe('kr');
    const event = new KeyboardEvent('keydown', { key: 'ArrowDown' });
    select.dispatchEvent(event);
    await tick();
    const active2 = target.querySelector('.option-active')!;
    expect(active2.getAttribute('value')).toBe('other');
    select.dispatchEvent(event);
    await tick();
    const active3 = target.querySelector('.option-active')!;
    expect(active3.getAttribute('value')).toBe('other');
    expect(component.isOpen).toBe(true); // Still open
  });

  it("doesn't go past end of items on up key press", async () => {
    expect.assertions(4);
    const target = document.createElement('div');
    const component = new Select({
      props: {
        ...selectOpts,
        value: items[1].id,
      },
      target,
    });
    const select = target.querySelector<HTMLSelectElement>('.select')!;
    select.click(); // To set correct selected index
    const active1 = target.querySelector('.option-active')!;
    expect(active1.getAttribute('value')).toBe('cn');
    const event = new KeyboardEvent('keydown', { key: 'ArrowUp' });
    select.dispatchEvent(event);
    await tick();
    const active2 = target.querySelector('.option-active')!;
    expect(active2.getAttribute('value')).toBe('au');
    select.dispatchEvent(event);
    await tick();
    const active3 = target.querySelector('.option-active')!;
    expect(active3.getAttribute('value')).toBe('au');
    expect(component.isOpen).toBe(true); // Still open
  });

  it('selects an item on click', async () => {
    expect.assertions(7);
    const target = document.createElement('div');
    const component = new Select({
      props: {
        ...selectOpts,
        isOpen: true,
      },
      target,
    });
    expect(component.isOpen).toBe(true);
    const active1 = target.querySelector('.option-active')!;
    expect(active1.getAttribute('value')).toBe('au');
    const select = target.querySelector<HTMLSelectElement>('.select')!;
    const option = target.querySelector('.option[value="jp"]');
    const listbox = target.querySelector('.select-listbox')!;
    const event = new MouseEvent('mousedown');
    Object.defineProperty(event, 'target', { enumerable: true, value: option });
    const spy1 = jest.spyOn(select, 'dispatchEvent');
    const spy2 = jest.spyOn(event, 'preventDefault'); // Only present in mouse event part of select()
    listbox.dispatchEvent(event);
    expect(spy1).toHaveBeenCalledTimes(1);
    expect(spy2).toHaveBeenCalledTimes(1);
    expect(component.isOpen).toBe(false);
    select.click(); // To set correct selected index
    await tick();
    const active2 = target.querySelector('.option-active')!;
    expect(active2.getAttribute('value')).toBe('jp');
    expect(component.isOpen).toBe(true);
    spy1.mockRestore();
    spy2.mockRestore();
  });

  it("doesn't select an item on click when option disabled", () => {
    expect.assertions(6);
    const target = document.createElement('div');
    const component = new Select({
      props: {
        ...selectOpts,
        isOpen: true,
        items: [
          { id: 'one', text: 'Opt 1' },
          // eslint-disable-next-line sort-keys
          { id: 'two', text: 'Opt 2', disabled: true },
          { id: 'three', text: 'Opt 3' },
        ],
      },
      target,
    });
    expect(component.isOpen).toBe(true);
    const active1 = target.querySelector('.option-active')!;
    expect(active1.getAttribute('value')).toBe('one');
    const select = target.querySelector('.select')!;
    const option = target.querySelector('.option[value="two"]');
    const listbox = target.querySelector('.select-listbox')!;
    const spy1 = jest.spyOn(select, 'dispatchEvent');
    const event = new MouseEvent('mousedown');
    Object.defineProperty(event, 'target', { enumerable: true, value: option });
    const spy2 = jest.spyOn(event, 'preventDefault');
    listbox.dispatchEvent(event);
    expect(spy1).not.toHaveBeenCalled(); // Doesn't emit an event
    expect(spy2).toHaveBeenCalledTimes(1);
    const active2 = target.querySelector('.option-active')!;
    expect(active2.getAttribute('value')).toBe('one');
    expect(component.isOpen).toBe(true); // Still open
    spy1.mockRestore();
    spy2.mockRestore();
  });

  it('selects item on enter key press', async () => {
    expect.assertions(7);
    const target = document.createElement('div');
    const component = new Select({
      props: {
        ...selectOpts,
        isOpen: true,
      },
      target,
    });
    expect(component.isOpen).toBe(true);
    const active1 = target.querySelector('.option-active')!;
    expect(active1.getAttribute('value')).toBe('au');
    const select = target.querySelector('.select')!;
    const event1 = new KeyboardEvent('keydown', { key: 'ArrowDown' });
    const spy1 = jest.spyOn(event1, 'preventDefault');
    select.dispatchEvent(event1);
    await tick();
    const active2 = target.querySelector('.option-active')!;
    expect(active2.getAttribute('value')).toBe('cn');
    const event2 = new KeyboardEvent('keydown', { key: 'Enter' });
    const spy2 = jest.spyOn(event2, 'preventDefault');
    select.dispatchEvent(event2);
    await tick();
    expect(spy1).toHaveBeenCalledTimes(1);
    expect(spy2).toHaveBeenCalledTimes(1);
    const active3 = target.querySelector('.option-active')!;
    expect(active3.getAttribute('value')).toBe('cn');
    expect(component.isOpen).toBe(false);
    spy1.mockRestore();
    spy2.mockRestore();
  });

  it('typing in input filters the shown items', async () => {
    expect.assertions(3);
    const target = document.createElement('div');
    new Select({
      props: {
        ...selectOpts,
        isOpen: true,
      },
      target,
    });
    const listbox1 = target.querySelector('.select-listbox')!;
    expect(listbox1.childNodes).toHaveLength(5);
    const select = target.querySelector<HTMLInputElement>('.select')!;
    select.value = 'o'; // simulate input + event
    const event = new Event('input');
    select.dispatchEvent(event);
    await tick();
    const listbox2 = target.querySelector('.select-listbox')!;
    expect(listbox2.childNodes).toHaveLength(2);
    expect(listbox2.innerHTML).toMatchSnapshot();
  });

  it('shows feedback message when filter has no match', async () => {
    expect.assertions(3);
    const target = document.createElement('div');
    new Select({
      props: {
        ...selectOpts,
        isOpen: true,
      },
      target,
    });
    const listbox1 = target.querySelector('.select-listbox')!;
    expect(listbox1.childNodes).toHaveLength(5);
    const select = target.querySelector<HTMLInputElement>('.select')!;
    select.value = 'xxxxx'; // Simulate input + event
    const event = new Event('input');
    select.dispatchEvent(event);
    await tick();
    const listbox2 = target.querySelector('.select-listbox')!;
    expect(listbox2.childNodes).toHaveLength(1);
    expect(listbox2.innerHTML).toMatchSnapshot();
  });

  it('input is reset on ESC key press', async () => {
    expect.assertions(4);
    const target = document.createElement('div');
    const component = new Select({
      props: {
        ...selectOpts,
        value: 'au',
      },
      target,
    });
    const select1 = target.querySelector<HTMLInputElement>('.select')!;
    expect(select1.value).toBe('Australia');
    select1.click();
    await tick();
    const select2 = target.querySelector<HTMLInputElement>('.select')!;
    expect(select2.value).toBe('');
    const event = new KeyboardEvent('keydown', { key: 'Escape' });
    select2.dispatchEvent(event);
    await tick();
    expect(component.isOpen).toBe(false);
    const select3 = target.querySelector<HTMLInputElement>('.select')!;
    expect(select3.value).toBe('Australia');
  });

  it('can dynamically add items', async () => {
    expect.assertions(4);
    const target = document.createElement('div');
    const component = new Select({
      props: selectOpts,
      target,
    });
    expect(component.items).toHaveLength(5);
    component.$set({ items: [...items, { id: 'new', text: 'New' }] });
    await tick();
    expect(component.items).toHaveLength(6);
    const newItem = target.querySelector('[value="new"]')!;
    expect(newItem).toBeDefined();
    expect(newItem.outerHTML).toMatchSnapshot();
  });
});