react-tags/react-tags

View on GitHub
__tests__/ReactTags.test.tsx

Summary

Maintainability
F
4 days
Test Coverage
import React from 'react';
import { expect } from 'chai';
import { spy, stub, createSandbox } from 'sinon';

import { WithContext as ReactTags } from '../src/index';

import { KEYS, SEPARATORS } from '../src/components/constants';
import { fireEvent, render, screen } from '@testing-library/react';
import type { Tag } from '../src/components/SingleTag';

/* eslint-disable no-console */

let defaults;
const sandbox = createSandbox();
let handleDragStub;

const Keys = {
  TAB: 9,
  SPACE: 32,
  COMMA: 188,
};

beforeAll(() => {
  handleDragStub = sandbox.stub();
  defaults = {
    tags: [{ className: '', id: 'Apple', text: 'Apple' }],
    suggestions: [
      { className: '', id: 'Banana', text: 'Banana' },
      { className: '', id: 'Apple', text: 'Apple' },
      { className: '', id: 'Apricot', text: 'Apricot' },
      { className: '', id: 'Pear', text: 'Pear' },
      { className: '', id: 'Peach', text: 'Peach' },
    ],
    handleDrag: handleDragStub,
  };
});

beforeEach(() => {
  sandbox.resetHistory();
});

afterAll(() => {
  sandbox.restore();
});
const DOWN_ARROW_KEY_CODE = 40;
const ENTER_ARROW_KEY_CODE = 13;

function mockItem(overrides?: any) {
  const props = Object.assign({}, defaults, overrides);
  return <ReactTags {...props} />;
}

describe('Test ReactTags', () => {
  it.skip('should render with expected props', function () {
    const wrapper = render(mockItem());
    expect(wrapper).to.have.length(1);
    expect(wrapper.props()).to.deep.equal(defaults);
  });

  it('should update the class when the prop classNames changes', () => {
    const { rerender } = render(
      mockItem({
        classNames: {
          tag: 'tag',
        },
      })
    );

    expect(screen.getAllByTestId('tag').length).to.equal(1);
    rerender(
      mockItem({
        classNames: {
          tag: 'changed',
        },
      })
    );

    expect(screen.getAllByTestId('tag').length).to.equal(1);
  });

  it('focus on input by default', () => {
    const wrapper = render(mockItem());
    expect(document.activeElement?.tagName).to.equal('INPUT');
    expect(document.activeElement?.className).to.equal(
      'ReactTags__tagInputField'
    );
    wrapper.unmount();
  });

  it('should not focus on input if autofocus is false', () => {
    const { unmount } = render(mockItem({ autofocus: false }));
    expect(document.activeElement?.tagName).to.equal('BODY');
    unmount();
  });

  it('should not focus on input if autoFocus is false', () => {
    const { unmount } = render(mockItem({ autoFocus: false }));
    expect(document.activeElement?.tagName).to.equal('BODY');
    unmount();
  });

  describe('When readOnly is true', () => {
    it('should not render input', () => {
      const wrapper = render(mockItem({ readOnly: true }));

      expect(
        wrapper.container.querySelector('.ReactTags__tagInputField')
      ).to.equal(null);
    });

    it('should render tags without remove button', () => {
      const wrapper = render(
        mockItem({
          readOnly: true,
          tags: [
            { id: 'Mango', text: 'Mango' },
            { id: 'Lichi', text: 'Litchi' },
          ],
        })
      );
      expect(screen.getAllByTestId('tag').length).to.equal(2);
      expect(
        wrapper.container.querySelector('.ReactTags__tag__remove')
      ).to.equal(null);
    });

    it('should not edit tags', () => {
      const container = render(
        mockItem({
          readOnly: true,
          editable: true,
          tags: [
            { id: 'Mango', text: 'Mango' },
            { id: 'Lichi', text: 'Litchi' },
          ],
        })
      );
      fireEvent.click(container.queryByText('Litchi')!);
      expect(container.queryByTestId('tag-edit')).to.be.null;
    });

    it('should not be draggable', () => {
      const container = render(
        mockItem({
          readOnly: true,
          tags: [
            ...defaults.tags,
            { id: 'Litchi', text: 'Litchi' },
            { id: 'Mango', text: 'Mango' },
          ],
        })
      );

      const src = container.getByText('Apple');
      const dest = container.getByText('Mango');

      let styles = getComputedStyle(src);
      expect(styles.cursor).to.equal('auto');
      expect(styles.opacity).to.equal('1');

      fireEvent.dragStart(src);
      styles = getComputedStyle(src);
      // The cursor and opacity should not be updated when attempting to drag in readOnly mode
      expect(styles.cursor).to.equal('auto');
      expect(styles.opacity).to.equal('1');

      fireEvent.dragEnter(dest);
      fireEvent.dragLeave(dest);
      fireEvent.dragEnd(dest);

      expect(handleDragStub.called).to.be.false;
    });
  });

  it('shows the classnames of children properly', () => {
    const { container } = render(mockItem());
    expect(container.querySelectorAll('.ReactTags__tags').length).to.equal(1);
    expect(container.querySelectorAll('.ReactTags__selected').length).to.equal(
      1
    );
    expect(container.querySelectorAll('.ReactTags__tagInput').length).to.equal(
      1
    );
    expect(
      container.querySelectorAll('.ReactTags__tagInputField').length
    ).to.equal(1);
  });

  it('renders preselected tags properly', () => {
    render(mockItem());
    expect(screen.getByText('Apple')).to.exist;
  });

  it('invokes the onBlur event', () => {
    const handleInputBlur = spy();
    const { rerender } = render(mockItem());

    // Won't be invoked as there's no `handleInputBlur` event yet.
    fireEvent.blur(screen.getByTestId('input'));
    expect(handleInputBlur.callCount).to.equal(0);

    // Will be invoked despite the input being empty.
    rerender(mockItem({ handleInputBlur }));
    fireEvent.blur(screen.getByTestId('input'));
    expect(handleInputBlur.callCount).to.equal(1);
    expect(handleInputBlur.calledWith('')).to.be.true;
    expect(screen.getByTestId('input').textContent).to.be.empty;
  });

  it('invokes the onFocus event', () => {
    const handleInputFocus = spy();
    const { rerender } = render(mockItem({ inputValue: 'Example' }));
    rerender(mockItem({ handleInputFocus }));
    fireEvent.focus(screen.getByTestId('input'));
    expect(handleInputFocus.callCount).to.equal(1);
    expect(handleInputFocus.args[0].length).to.equal(2);
    expect(handleInputFocus.calledWith('Example')).to.be.true;
  });

  it('invokes the onBlur event when input has value', () => {
    const handleInputBlur = spy();
    const { rerender } = render(mockItem({ inputValue: 'Example' }));
    rerender(mockItem({ handleInputBlur }));

    // Will also be invoked for when the input has a value.
    fireEvent.blur(screen.getByTestId('input'));
    expect(handleInputBlur.callCount).to.equal(1);
    expect(handleInputBlur.args[0].length).to.equal(2);
    expect(handleInputBlur.calledWith('Example')).to.be.true;
    expect(screen.getByTestId('input').textContent).to.be.empty;
  });

  describe('tests handlePaste', () => {
    it('should not add new tag when allowAdditionFromPaste is false', () => {
      const actual = [];
      const wrapper = render(
        mockItem({
          allowAdditionFromPaste: false,
          handleAddition(tag) {
            actual.push(tag);
          },
        })
      );

      fireEvent.paste(screen.getByTestId('input'), {
        clipboardData: {
          getData: () => 'Banana',
        },
      });

      expect(actual).to.have.length(0);
      expect(actual).to.not.have.members(['Banana']);
    });

    it('should split the clipboard on delimiters', () => {
      const Keys = {
        TAB: 9,
        SPACE: 32,
        COMMA: 188,
      };

      const tags = [];
      render(
        mockItem({
          delimiters: [Keys.TAB, Keys.SPACE, Keys.COMMA],
          handleAddition(tag) {
            tags.push(tag);
          },
          tags,
        })
      );

      fireEvent.paste(screen.getByTestId('input'), {
        clipboardData: {
          getData: () =>
            'Banana,Apple,Apricot\nOrange Blueberry,Pear,Peach\tKiwi',
        },
      });

      const expected = [
        'Banana',
        'Apple',
        'Apricot\nOrange',
        'Blueberry',
        'Pear',
        'Peach',
        'Kiwi',
      ].map((value) => ({ id: value, text: value }));

      jestExpect(tags).toMatchInlineSnapshot(`
        [
          {
            "className": "",
            "id": "Banana",
            "text": "Banana",
          },
          {
            "className": "",
            "id": "Apple",
            "text": "Apple",
          },
          {
            "className": "",
            "id": "Apricot
        Orange",
            "text": "Apricot
        Orange",
          },
          {
            "className": "",
            "id": "Blueberry",
            "text": "Blueberry",
          },
          {
            "className": "",
            "id": "Pear",
            "text": "Pear",
          },
          {
            "className": "",
            "id": "Peach",
            "text": "Peach",
          },
          {
            "className": "",
            "id": "Kiwi",
            "text": "Kiwi",
          },
        ]
      `);
    });

    it('should split the clipboard when delimited with new lines', () => {
      const Keys = {
        ENTER: [10, 13],
      };

      const tags = [];
      render(
        mockItem({
          delimiters: [...Keys.ENTER],
          handleAddition(tag) {
            tags.push(tag);
          },
          tags,
        })
      );

      fireEvent.paste(screen.getByTestId('input'), {
        clipboardData: {
          getData: () => 'Banana\nApple\rApricot\r\n\r\nOrange',
        },
      });

      const expected = ['Banana', 'Apple', 'Apricot', 'Orange'].map(
        (value) => ({ id: value, text: value })
      );

      jestExpect(tags).toMatchInlineSnapshot(`
        [
          {
            "className": "",
            "id": "Banana",
            "text": "Banana",
          },
          {
            "className": "",
            "id": "Apple",
            "text": "Apple",
          },
          {
            "className": "",
            "id": "Apricot",
            "text": "Apricot",
          },
          {
            "className": "",
            "id": "Orange",
            "text": "Orange",
          },
        ]
      `);
    });

    it('should split the clipboard on separators', () => {
      const tags = [];
      render(
        mockItem({
          separators: [SEPARATORS.TAB, SEPARATORS.SPACE, SEPARATORS.COMMA],
          handleAddition(tag) {
            tags.push(tag);
          },
          tags,
        })
      );

      fireEvent.paste(screen.getByTestId('input'), {
        clipboardData: {
          getData: () =>
            'Banana,Apple,Apricot\nOrange Blueberry,Pear,Peach\tKiwi',
        },
      });

      const expected = [
        'Banana',
        'Apple',
        'Apricot\nOrange',
        'Blueberry',
        'Pear',
        'Peach',
        'Kiwi',
      ].map((value) => ({ id: value, text: value }));

      jestExpect(tags).toMatchInlineSnapshot(`
        [
          {
            "className": "",
            "id": "Banana",
            "text": "Banana",
          },
          {
            "className": "",
            "id": "Apple",
            "text": "Apple",
          },
          {
            "className": "",
            "id": "Apricot
        Orange",
            "text": "Apricot
        Orange",
          },
          {
            "className": "",
            "id": "Blueberry",
            "text": "Blueberry",
          },
          {
            "className": "",
            "id": "Pear",
            "text": "Pear",
          },
          {
            "className": "",
            "id": "Peach",
            "text": "Peach",
          },
          {
            "className": "",
            "id": "Kiwi",
            "text": "Kiwi",
          },
        ]
      `);
    });

    it('should split the clipboard when separated with new lines', () => {
      const tags = [];
      render(
        mockItem({
          separators: [SEPARATORS.ENTER],
          handleAddition(tag) {
            tags.push(tag);
          },
          tags,
        })
      );

      fireEvent.paste(screen.getByTestId('input'), {
        clipboardData: {
          getData: () => 'Banana\nApple\rApricot\r\n\r\nOrange',
        },
      });

      const expected = ['Banana', 'Apple', 'Apricot', 'Orange'].map(
        (value) => ({ id: value, text: value })
      );

      jestExpect(tags).toMatchInlineSnapshot(`
        [
          {
            "className": "",
            "id": "Banana",
            "text": "Banana",
          },
          {
            "className": "",
            "id": "Apple",
            "text": "Apple",
          },
          {
            "className": "",
            "id": "Apricot",
            "text": "Apricot",
          },
          {
            "className": "",
            "id": "Orange",
            "text": "Orange",
          },
        ]
      `);
    });

    it('should not allow duplicate tags', () => {
      const tags = [...defaults.tags];
      const wrapper = render(
        mockItem({
          delimiters: [Keys.TAB, Keys.SPACE, Keys.COMMA],
          handleAddition(tag) {
            tags.push(tag);
          },
          tags,
        })
      );

      fireEvent.paste(screen.getByTestId('input'), {
        clipboardData: {
          getData: () => 'Banana,Apple,Banana',
        },
      });

      // Note that 'Apple' and 'Banana' are only included once in the expected list
      const expected = ['Apple', 'Banana'].map((value) => ({
        className: '',
        id: value,
        text: value,
      }));

      expect(tags).to.deep.have.same.members(expected);
    });

    it('should allow pasting text only up to maxLength characters', () => {
      const tags = [];
      const maxLength = 5;
      const inputValue = 'Thimbleberry';

      const wrapper = render(
        mockItem({
          handleAddition(tag) {
            tags.push(tag);
          },
          maxLength,
        })
      );

      fireEvent.paste(screen.getByTestId('input'), {
        clipboardData: {
          getData: () => inputValue,
        },
      });

      const clipboardText = inputValue.substr(0, maxLength);

      jestExpect(tags).toMatchInlineSnapshot(`
        [
          {
            "className": "",
            "id": "Thimb",
            "text": "Thimb",
          },
        ]
      `);
    });

    it('should trim the tags', () => {
      const tags = [...defaults.tags];
      render(
        mockItem({
          delimiters: [Keys.COMMA],
          handleAddition(tag) {
            tags.push(tag);
          },
        })
      );

      fireEvent.paste(screen.getByTestId('input'), {
        clipboardData: {
          getData: () => ' Orange,Orange , Orange ',
        },
      });

      jestExpect(tags).toMatchInlineSnapshot(`
        [
          {
            "className": "",
            "id": "Apple",
            "text": "Apple",
          },
          {
            "className": "",
            "id": "Orange",
            "text": "Orange",
          },
        ]
      `);
    });
  });

  it('should not allow duplicate tags', () => {
    const tags = [];
    render(
      mockItem({
        delimiters: [Keys.TAB, Keys.SPACE, Keys.COMMA],
        handleAddition(tag) {
          tags.push(tag);
        },
        tags,
      })
    );

    fireEvent.paste(screen.getByTestId('input'), {
      clipboardData: {
        getData: () => 'Banana,Apple,Banana',
      },
    });
    expect(tags.length).to.equal(2);

    // Note that 'Apple' and 'Banana' are only included once in the expected list
    const expected = ['Apple', 'Banana'].map((value) => ({
      className: '',
      id: value,
      text: value,
    }));
    expect(tags.length).to.equal(2);
    expect(tags).to.deep.have.same.members(expected);
  });

  it('should not add empty tag when down arrow is clicked followed by enter key', () => {
    const tags = [];
    render(
      mockItem({
        handleAddition(tag) {
          tags.push(tag);
        },
        suggestions: [],
      })
    );

    expect(tags.length).to.equal(0);
    const input = screen.getByTestId('input');
    fireEvent.keyDown(input, {
      keyCode: DOWN_ARROW_KEY_CODE,
    });
    fireEvent.keyDown(input, {
      keyCode: ENTER_ARROW_KEY_CODE,
    });

    expect(tags).to.have.length(0);
  });

  describe('render tags correctly when html passed in  text attribute, fix #267', () => {
    let modifiedTags: Tag[] = [];
    let handleAddition;
    let actual: Tag[];
    beforeEach(() => {
      actual = [];
      modifiedTags = [
        ...defaults.tags,
        {
          className: '',
          id: '1',
          text: <span style={{ color: 'red' }}> NewYork</span>,
        },
        {
          className: '',
          id: '2',
          text: <span style={{ color: 'blue' }}> Austria</span>,
        },
      ];
      handleAddition = ({ id, text }) => {
        actual.push({
          id,
          text: <span style={{ color: 'yellow' }}>{text}</span>,
        });
      };
    });

    it('should render tags correctly', () => {
      render(
        mockItem({
          tags: modifiedTags,
          handleAddition,
        })
      );
      const tags = ['Apple', 'NewYork', 'Austria'];
      screen.getAllByTestId('tag').forEach((tag, index) => {
        expect(tag.textContent).to.contains(tags[index]);
      });
    });

    it('should allow adding a tag outside the suggestions list', () => {
      const actual: Tag[] = [];
      const wrapper = render(
        mockItem({
          handleAddition(tag) {
            actual.push(tag);
          },
          suggestions: [],
        })
      );
      const input = screen.getByTestId('input');
      fireEvent.change(input, { target: { value: 'Austria' } });
      fireEvent.keyDown(input, { keyCode: ENTER_ARROW_KEY_CODE });
      expect(actual).to.have.deep.members([
        { id: 'Austria', text: 'Austria', className: '' },
      ]);
    });
  });

  describe('autocomplete/suggestions filtering', () => {
    let consoleWarnStub;
    beforeAll(() => {
      consoleWarnStub = stub(console, 'warn');
    });
    afterAll(() => {
      consoleWarnStub.restore();
    });

    it('should update suggestions if the suggestions prop changes', () => {
      const { rerender } = render(mockItem());

      const input = screen.getByTestId('input');
      fireEvent.change(input, { target: { value: 'ap' } });
      fireEvent.focus(input);

      jestExpect(screen.getByTestId('suggestions')).toMatchInlineSnapshot(`
        <div
          class="ReactTags__suggestions"
          data-testid="suggestions"
        >
          <ul>
             
            <li
              class=""
            >
              <span>
                <mark>
                  Ap
                </mark>
                ricot
              </span>
            </li>
             
          </ul>
        </div>
      `);

      // Rerender with new suggestions
      rerender(
        mockItem({
          suggestions: [
            { className: '', id: 'Papaya', text: 'Papaya' },
            { className: '', id: 'Paprika', text: 'Paprika' },
          ],
        })
      );

      jestExpect(screen.getByTestId('suggestions')).toMatchInlineSnapshot(`
        <div
          class="ReactTags__suggestions"
          data-testid="suggestions"
        >
          <ul>
             
            <li
              class=""
            >
              <span>
                P
                <mark>
                  ap
                </mark>
                aya
              </span>
            </li>
            <li
              class=""
            >
              <span>
                P
                <mark>
                  ap
                </mark>
                rika
              </span>
            </li>
             
          </ul>
        </div>
      `);
    });

    it('should update suggestions if the handleFilterSuggestions prop changes', () => {
      const { rerender } = render(
        mockItem({
          suggestions: [
            { className: '', id: 'Papaya', text: 'Papaya' },
            { className: '', id: 'Paprika', text: 'Paprika' },
          ],
        })
      );

      const input = screen.getByTestId('input');
      fireEvent.change(input, { target: { value: 'ap' } });
      fireEvent.focus(input);

      jestExpect(screen.getByTestId('suggestions')).toMatchInlineSnapshot(`
        <div
          class="ReactTags__suggestions"
          data-testid="suggestions"
        >
          <ul>
             
            <li
              class=""
            >
              <span>
                P
                <mark>
                  ap
                </mark>
                aya
              </span>
            </li>
            <li
              class=""
            >
              <span>
                P
                <mark>
                  ap
                </mark>
                rika
              </span>
            </li>
             
          </ul>
        </div>
      `);

      // Rerender with new handleFilterSuggestions
      rerender(
        mockItem({
          handleFilterSuggestions: (query, suggestions) => {
            const matchingSuggestions = suggestions.filter((suggestion) => {
              return (
                suggestion.text.toLowerCase().indexOf(query.toLowerCase()) >= 0
              );
            });
            // Return first matching suggestion only
            return [matchingSuggestions[0]];
          },
        })
      );

      jestExpect(screen.getByTestId('suggestions')).toMatchInlineSnapshot(`
        <div
          class="ReactTags__suggestions"
          data-testid="suggestions"
        >
          <ul>
             
            <li
              class=""
            >
              <span>
                <mark>
                  Ap
                </mark>
                ricot
              </span>
            </li>
             
          </ul>
        </div>
      `);
    });

    it('should select the correct suggestion using the keyboard when minQueryLength is 0', () => {
      const { container } = render(
        mockItem({
          minQueryLength: 0,
        })
      );
      const input = screen.getByTestId('input');
      fireEvent.change(input, { target: { value: 'Ea' } });
      fireEvent.keyDown(input, { keyCode: DOWN_ARROW_KEY_CODE });
      fireEvent.keyDown(input, { keyCode: DOWN_ARROW_KEY_CODE });

      jestExpect(container.querySelector('.ReactTags__activeSuggestion'))
        .toMatchInlineSnapshot(`
        <li
          class="ReactTags__activeSuggestion"
        >
          <span>
            P
            <mark>
              ea
            </mark>
            ch
          </span>
        </li>
      `);

      fireEvent.change(input, { target: { value: 'Each' } });
      jestExpect(container.querySelector('.ReactTags__activeSuggestion'))
        .toMatchInlineSnapshot(`
        <li
          class="ReactTags__activeSuggestion"
        >
          <span>
            P
            <mark>
              each
            </mark>
          </span>
        </li>
      `);
    });

    it('should select the correct suggestion using the keyboard when label is custom', () => {
      const { container } = render(
        mockItem({
          labelField: 'name',
          suggestions: [
            { className: '', id: 'Papaya', name: 'Papaya' },
            { className: '', id: 'Paprika', name: 'Paprika' },
          ],
        })
      );
      const input = screen.getByTestId('input');
      fireEvent.change(input, { target: { value: 'ap' } });
      fireEvent.keyDown(input, { keyCode: DOWN_ARROW_KEY_CODE });
      fireEvent.keyDown(input, { keyCode: DOWN_ARROW_KEY_CODE });

      jestExpect(container.querySelector('.ReactTags__activeSuggestion'))
        .toMatchInlineSnapshot(`
        <li
          class="ReactTags__activeSuggestion"
        >
          <span>
            P
            <mark>
              ap
            </mark>
            rika
          </span>
        </li>
      `);

      fireEvent.change(input, { target: { value: 'Each' } });
      expect(container.querySelector('.ReactTags__activeSuggestion')).to.be
        .null;
    });

    it('should show suggestions for the tags which are already added when "allowUnique" is false', () => {
      const tags: Tag[] = [];
      const { container } = render(
        mockItem({
          allowUnique: false,
          tags: [],
          handleAddition: (tag) => {
            tags.push(tag);
          },
        })
      );
      const input = screen.getByTestId('input');
      fireEvent.change(input, { target: { value: 'apr' } });
      fireEvent.keyDown(input, { keyCode: DOWN_ARROW_KEY_CODE });
      fireEvent.keyDown(input, { keyCode: DOWN_ARROW_KEY_CODE });

      jestExpect(container.querySelector('.ReactTags__suggestions'))
        .toMatchInlineSnapshot(`
        <div
          class="ReactTags__suggestions"
          data-testid="suggestions"
        >
          <ul>
             
            <li
              class="ReactTags__activeSuggestion"
            >
              <span>
                <mark>
                  Apr
                </mark>
                icot
              </span>
            </li>
             
          </ul>
        </div>
      `);

      fireEvent.keyDown(input, { keyCode: ENTER_ARROW_KEY_CODE });
      expect(tags).to.deep.have.members([
        { className: '', id: 'Apricot', text: 'Apricot' },
      ]);
      fireEvent.change(input, { target: { value: 'apr' } });
      jestExpect(container.querySelector('.ReactTags__suggestions'))
        .toMatchInlineSnapshot(`
        <div
          class="ReactTags__suggestions"
          data-testid="suggestions"
        >
          <ul>
             
            <li
              class=""
            >
              <span>
                <mark>
                  Apr
                </mark>
                icot
              </span>
            </li>
             
          </ul>
        </div>
      `);
    });
  });

  it('should add tag with custom name field', () => {
    const labelField = 'name';
    const mapper = (data: Tag) => ({ id: data.id, name: data.text });
    const tags = defaults.tags.map(mapper);
    const suggestions = defaults.suggestions.map(mapper);

    let actual: Tag[] = [];
    render(
      mockItem({
        handleAddition(tag) {
          actual.push(tag);
        },
        labelField,
        tags,
        suggestions,
      })
    );

    const input = screen.getByTestId('input');
    fireEvent.change(input, { target: { value: 'Or' } });
    fireEvent.keyDown(input, { keyCode: ENTER_ARROW_KEY_CODE });
    expect(actual).to.have.deep.members([
      { className: '', id: 'Or', name: 'Or' },
    ]);
  });

  it('should allow duplicate tags when allowUnique is false', () => {
    const tags: Tag[] = [];
    render(
      mockItem({
        allowUnique: false,
        handleAddition(tag) {
          tags.push(tag);
        },
      })
    );

    const input = screen.getByTestId('input');
    fireEvent.change(input, { target: { value: 'Apple' } });
    fireEvent.keyDown(input, { keyCode: ENTER_ARROW_KEY_CODE });

    fireEvent.change(input, { target: { value: 'Banana' } });
    fireEvent.keyDown(input, { keyCode: ENTER_ARROW_KEY_CODE });

    fireEvent.change(input, { target: { value: 'Apple' } });
    fireEvent.keyDown(input, { keyCode: ENTER_ARROW_KEY_CODE });

    expect(tags.length).to.equal(3);
    expect(tags).to.deep.have.same.members([
      { className: '', id: 'Apple', text: 'Apple' },
      { className: '', id: 'Banana', text: 'Banana' },
      { className: '', id: 'Apple', text: 'Apple' },
    ]);
  });

  describe('Test inputFieldPosition', () => {
    it('should render input field above the tags when "inputFieldPosition" is "top"', () => {
      const { container } = render(
        mockItem({
          inputFieldPosition: 'top',
        })
      );
      const tagsContainer = container.querySelector('.ReactTags__tags');
      expect(tagsContainer?.children.length).to.equal(3);
      expect(tagsContainer?.children[1].firstChild).to.equal(
        screen.getByTestId('input')
      );
      expect(tagsContainer?.lastChild).to.equal(
        container.querySelector('.ReactTags__selected')
      );
    });

    it('should render input field below the tags when "inputFieldPosition" is "bottom"', () => {
      const { container } = render(
        mockItem({
          inputFieldPosition: 'bottom',
        })
      );

      const tagsContainer = container.querySelector('.ReactTags__tags');
      expect(tagsContainer?.children.length).to.equal(3);
      expect(tagsContainer?.children[1]).to.equal(
        container.querySelector('.ReactTags__selected')
      );
      expect(tagsContainer?.lastChild?.firstChild).to.equal(
        screen.getByTestId('input')
      );
    });

    it('should render input field inline with tags when "inputFieldPosition" is "inline"', () => {
      const { container } = render(
        mockItem({
          inputFieldPosition: 'inline',
        })
      );

      const tagsContainer = container.querySelector('.ReactTags__tags');
      expect(tagsContainer?.children.length).to.equal(2);
      expect(tagsContainer?.lastChild).to.equal(
        container.querySelector('.ReactTags__selected')
      );
      expect(tagsContainer?.lastChild?.lastChild?.firstChild).to.equal(
        screen.getByTestId('input')
      );
    });
  });

  it('should pass input props to the input element', () => {
    const wrapper = render(
      mockItem({
        inputProps: {
          disabled: true,
          maxlength: 10,
        },
      })
    );
    const input = screen.getByTestId('input');
    expect(input.getAttribute('disabled')).to.be.empty;
    expect(input.getAttribute('maxlength')).to.equal('10');
  });

  it('should trim the tags before adding', () => {
    const tags = [];
    render(
      mockItem({
        delimiters: [Keys.COMMA],
        tags: [],
        handleAddition(tag) {
          tags.push(tag);
        },
      })
    );

    fireEvent.paste(screen.getByTestId('input'), {
      clipboardData: {
        getData: () => ' Apple,Orange , Banana ',
      },
    });
    jestExpect(tags).toMatchInlineSnapshot(`
      [
        {
          "className": "",
          "id": "Apple",
          "text": "Apple",
        },
        {
          "className": "",
          "id": "Orange",
          "text": "Orange",
        },
        {
          "className": "",
          "id": "Banana",
          "text": "Banana",
        },
      ]
    `);
  });

  describe('Test drag and drop', () => {
    it('should be draggable', () => {
      const root = render(
        mockItem({
          tags: [
            ...defaults.tags,
            { id: 'Litchi', text: 'Litchi' },
            { id: 'Mango', text: 'Mango' },
          ],
        })
      );
      const src = root.getByText('Apple');
      const dest = root.getByText('Mango');
      fireEvent.dragStart(src);
      const styles = getComputedStyle(src);
      expect(styles.cursor).to.equal('move');
      fireEvent.dragEnter(dest);
      fireEvent.drop(dest);
      fireEvent.dragLeave(dest);
      fireEvent.dragEnd(dest);

      const dragTag = defaults.tags[0];
      const dragIndex = 0;
      const hoverIndex = 2;
      expect(handleDragStub.calledWithExactly(dragTag, dragIndex, hoverIndex))
        .to.be.true;
    });

    it('should not be draggable when drag and drop index is same', () => {
      const root = render(mockItem());
      const src = root.getByText('Apple');
      fireEvent.dragStart(src);
      fireEvent.dragEnter(src);
      fireEvent.drop(src);
      fireEvent.dragLeave(src);
      fireEvent.dragEnd(src);

      expect(handleDragStub.called).to.be.false;
    });

    [
      { overrideProps: { readOnly: true }, title: 'readOnly is true' },
      {
        overrideProps: { allowDragDrop: false },
        title: 'allowDragDrop is false',
      },
    ].forEach((data) => {
      const { title, overrideProps } = data;
      it(`should not be draggable when ${title}`, () => {
        const root = render(
          mockItem({
            ...overrideProps,
            tags: [
              ...defaults.tags,
              { id: 'Litchi', text: 'Litchi' },
              { id: 'Mango', text: 'Mango' },
            ],
          })
        );
        const src = root.getByText('Apple');
        const dest = root.getByText('Mango');
        fireEvent.dragStart(src);
        fireEvent.dragEnter(dest);
        fireEvent.dragLeave(dest);
        fireEvent.dragEnd(dest);
        expect(handleDragStub.called).to.be.false;
        const styles = getComputedStyle(src);
        expect(styles.cursor).to.equal('auto');
      });
    });
  });

  describe('When editable', () => {
    // todo fix as document.activeElement is null after upgrading
    // jest;
    it.skip('should update the tag to input and focus the tag when clicked', () => {
      const tags = render(
        mockItem({
          editable: true,
          tags: [
            ...defaults.tags,
            { id: 'Litchi', text: 'Litchi' },
            { id: 'Mango', text: 'Mango' },
          ],
        })
      );
      fireEvent.click(tags.getByText('Litchi'));
      expect(document.activeElement).to.equal(tags.queryByTestId('tag-edit'));
      jestExpect(tags.container).toMatchSnapshot();
    });
    // TODO fix, test fails after upgrading jest
    it.skip('should trigger "onTagUpdate" if present when tag is edited', () => {
      const onTagUpdateStub = sandbox.stub();
      const tags = render(
        mockItem({
          editable: true,
          tags: [
            ...defaults.tags,
            { id: 'Litchi', text: 'Litchi' },
            { id: 'Mango', text: 'Mango' },
          ],
          onTagUpdate: onTagUpdateStub,
        })
      );
      fireEvent.click(tags.getByText('Litchi'));
      const input = tags.queryByTestId('tag-edit');
      fireEvent.change(input, {
        target: { value: 'banana' },
      });
      fireEvent.keyDown(input, {
        keyCode: KEYS.ENTER[1],
      });
      expect(
        onTagUpdateStub.calledWithExactly(1, { id: 'banana', text: 'banana' })
      ).to.be.true;
    });
  });

  describe('When ClearAll is true', () => {
    it('should render a clear all button', () => {
      const tags = render(
        mockItem({
          clearAll: true,
        })
      );
      jestExpect(tags.container.querySelector('.ReactTags__clearAll'))
        .toMatchInlineSnapshot(`
        <button
          class="ReactTags__clearAll"
        >
          Clear all
        </button>
      `);
    });

    it('should trigger "onClearAll" callback if present when clear all button is clicked', () => {
      const onClearAllStub = sandbox.stub();
      const tags = render(
        mockItem({
          clearAll: true,
          onClearAll: onClearAllStub,
        })
      );

      const clearAllBtn = tags.getByText('Clear all');
      fireEvent.click(clearAllBtn);
      expect(onClearAllStub.calledOnce).to.be.true;
    });
  });

  describe('When maxTags is defined', () => {
    it('should disable adding tags when tag limit reached', () => {
      const tags = [{ id: 'A', text: 'A' }];
      render(
        mockItem({
          tags,
          handleAddition(tag) {
            tags.push(tag);
          },
          maxTags: 1,
        })
      );
      const input = screen.getByTestId('input');
      fireEvent.change(input, {
        target: { value: 'B' },
      });
      fireEvent.keyDown(input, {
        keyCode: ENTER_ARROW_KEY_CODE,
      });
      expect(tags).to.have.length(1);
      expect(screen.getByTestId('error').textContent).to.equal(
        'Tag limit reached!'
      );
    });
  });
});