Repository: draft-js-plugins/draft-js-plugins
Pull request #1559 (2020-11-08 15:34:13)

Migrating of hashtag-plugin to jest/testing library
## Checklist

- [x] Fix any eslint errors that occur
- [x] Update change-log for every plugin you touch
- [x] Add/Update tests if you add/change new functionality
- [x] Add/Update docs if you add/change functionality
- [x] Enable "Allow edits from maintainers" for this PR

## Use-case/Problem

Fixes part of #1477

## Implementation

This PR migrates the `draft-js-hashtag-plugin` to TypeScript, jest and @testing-library/react



Patch:
diff --git a/draft-js-emoji-plugin/src/utils/__test__/getWordAt.test.js b/draft-js-emoji-plugin/src/utils/__test__/getWordAt.test.ts
similarity index 100%
rename from draft-js-emoji-plugin/src/utils/__test__/getWordAt.test.js
rename to draft-js-emoji-plugin/src/utils/__test__/getWordAt.test.ts
diff --git a/draft-js-hashtag-plugin/src/Hashtag.tsx b/draft-js-hashtag-plugin/src/Hashtag.tsx
index ad6e309e..49de6473 100644
--- a/draft-js-hashtag-plugin/src/Hashtag.tsx
+++ b/draft-js-hashtag-plugin/src/Hashtag.tsx
@@ -1,20 +1,21 @@
-import React, { ReactElement } from 'react';
+import React, { ReactElement, ReactNode } from 'react';
 import clsx from 'clsx';
 import { HashtagPluginTheme } from './theme';
 
 export interface HashtagProps {
   theme?: HashtagPluginTheme;
   className?: string;
+  children?: ReactNode;
 
   //removed props
-  decoratedText: unknown;
-  dir: unknown;
-  entityKey: unknown;
-  getEditorState: unknown;
-  offsetKey: unknown;
-  setEditorState: unknown;
-  contentState: unknown;
-  blockKey: unknown;
+  decoratedText?: unknown;
+  dir?: unknown;
+  entityKey?: unknown;
+  getEditorState?: unknown;
+  offsetKey?: unknown;
+  setEditorState?: unknown;
+  contentState?: unknown;
+  blockKey?: unknown;
 }
 
 export default function Hashtag(props: HashtagProps): ReactElement {


Test patch:
diff --git a/draft-js-counter-plugin/src/CharCounter/__test__/index.test.js b/draft-js-counter-plugin/src/CharCounter/__test__/index.test.tsx
similarity index 73%
rename from draft-js-counter-plugin/src/CharCounter/__test__/index.test.js
rename to draft-js-counter-plugin/src/CharCounter/__test__/index.test.tsx
index 495e89cc..1d84ef0b 100644
--- a/draft-js-counter-plugin/src/CharCounter/__test__/index.test.js
+++ b/draft-js-counter-plugin/src/CharCounter/__test__/index.test.tsx
@@ -1,25 +1,21 @@
 import React from 'react';
 import { screen, render } from '@testing-library/react';
+import { PluginFunctions } from 'draft-js-plugins-editor';
 import { EditorState, ContentState } from 'draft-js';
 import createCounterPlugin from '../../index';
 
 describe('CounterPlugin Character Counter', () => {
-  const createEditorStateFromText = text => {
+  const createEditorStateFromText = (text: string): EditorState => {
     const contentState = ContentState.createFromText(text);
     return EditorState.createWithContent(contentState);
   };
 
-  let counterPlugin;
-
-  beforeEach(() => {
-    counterPlugin = createCounterPlugin();
-  });
-
   it('instantiates plugin and counts 12 characters', () => {
+    const counterPlugin = createCounterPlugin();
     const editorState = createEditorStateFromText('Hello World!');
-    counterPlugin.initialize({
+    counterPlugin.initialize!({
       getEditorState: () => editorState,
-    });
+    } as PluginFunctions);
     const { CharCounter } = counterPlugin;
 
     render(<CharCounter />);
@@ -27,10 +23,11 @@ describe('CounterPlugin Character Counter', () => {
   });
 
   it('instantiates plugin and counts 3 unicode characters', () => {
+    const counterPlugin = createCounterPlugin();
     const editorState = createEditorStateFromText('😁😂😃');
-    counterPlugin.initialize({
+    counterPlugin.initialize!({
       getEditorState: () => editorState,
-    });
+    } as PluginFunctions);
     const { CharCounter } = counterPlugin;
 
     render(<CharCounter />);
diff --git a/draft-js-counter-plugin/src/CustomCounter/__test__/index.test.js b/draft-js-counter-plugin/src/CustomCounter/__test__/index.test.tsx
similarity index 77%
rename from draft-js-counter-plugin/src/CustomCounter/__test__/index.test.js
rename to draft-js-counter-plugin/src/CustomCounter/__test__/index.test.tsx
index 35d4d832..7b74294f 100644
--- a/draft-js-counter-plugin/src/CustomCounter/__test__/index.test.js
+++ b/draft-js-counter-plugin/src/CustomCounter/__test__/index.test.tsx
@@ -1,30 +1,26 @@
 import React from 'react';
 import { screen, render } from '@testing-library/react';
 import { EditorState, ContentState } from 'draft-js';
+import { PluginFunctions } from 'draft-js-plugins-editor';
 import createCounterPlugin from '../../index';
 
 describe('CounterPlugin Line Counter', () => {
-  const createEditorStateFromText = text => {
+  const createEditorStateFromText = (text: string): EditorState => {
     const contentState = ContentState.createFromText(text);
     return EditorState.createWithContent(contentState);
   };
 
-  let counterPlugin;
-
-  beforeEach(() => {
-    counterPlugin = createCounterPlugin();
-  });
-
   it('instantiates plugin with word counter and counts 5 words', () => {
+    const counterPlugin = createCounterPlugin();
     const text = 'Hello there, how are you?';
     const editorState = createEditorStateFromText(text);
-    counterPlugin.initialize({
+    counterPlugin.initialize!({
       getEditorState: () => editorState,
-    });
+    } as PluginFunctions);
     const { CustomCounter } = counterPlugin;
 
     // a function that takes a string and returns the number of words
-    const countFunction = str => {
+    const countFunction = (str: string): number => {
       const wordArray = str.match(/\S+/g); // matches words according to whitespace
       return wordArray ? wordArray.length : 0;
     };
@@ -34,15 +30,16 @@ describe('CounterPlugin Line Counter', () => {
   });
 
   it('instantiates plugin with number counter and counts 6 number characters', () => {
+    const counterPlugin = createCounterPlugin();
     const text = 'I am a 1337 h4x0r';
     const editorState = createEditorStateFromText(text);
-    counterPlugin.initialize({
+    counterPlugin.initialize!({
       getEditorState: () => editorState,
-    });
+    } as PluginFunctions);
     const { CustomCounter } = counterPlugin;
 
     // a function that takes a string and returns the number of number characters
-    const countFunction = str => {
+    const countFunction = (str: string): number => {
       const numArray = str.match(/\d/g); // matches only number characters
       return numArray ? numArray.length : 0;
     };
diff --git a/draft-js-counter-plugin/src/WordCounter/__test__/index.test.js b/draft-js-counter-plugin/src/WordCounter/__test__/index.test.tsx
similarity index 74%
rename from draft-js-counter-plugin/src/WordCounter/__test__/index.test.js
rename to draft-js-counter-plugin/src/WordCounter/__test__/index.test.tsx
index a8f16e0e..dd75a4dc 100644
--- a/draft-js-counter-plugin/src/WordCounter/__test__/index.test.js
+++ b/draft-js-counter-plugin/src/WordCounter/__test__/index.test.tsx
@@ -1,26 +1,22 @@
 import React from 'react';
 import { screen, render } from '@testing-library/react';
 import { EditorState, ContentState } from 'draft-js';
+import { PluginFunctions } from 'draft-js-plugins-editor';
 import createCounterPlugin from '../../index';
 
 describe('CounterPlugin Word Counter', () => {
-  const createEditorStateFromText = text => {
+  const createEditorStateFromText = (text: string): EditorState => {
     const contentState = ContentState.createFromText(text);
     return EditorState.createWithContent(contentState);
   };
 
-  let counterPlugin;
-
-  beforeEach(() => {
-    counterPlugin = createCounterPlugin();
-  });
-
   it('instantiates plugin and counts 5 words', () => {
+    const counterPlugin = createCounterPlugin();
     const text = 'Hello there, how are you?';
     const editorState = createEditorStateFromText(text);
-    counterPlugin.initialize({
+    counterPlugin.initialize!({
       getEditorState: () => editorState,
-    });
+    } as PluginFunctions);
     const { WordCounter } = counterPlugin;
 
     render(<WordCounter />);
diff --git a/draft-js-hashtag-plugin/src/__test__/Hashtag.js b/draft-js-hashtag-plugin/src/__test__/Hashtag.js
deleted file mode 100644
index b1f7de39..00000000
--- a/draft-js-hashtag-plugin/src/__test__/Hashtag.js
+++ /dev/null
@@ -1,34 +0,0 @@
-/* eslint-disable react/no-children-prop */
-import React from 'react';
-import { shallow } from 'enzyme';
-import { expect } from 'chai';
-import Hashtag from '../Hashtag';
-
-describe('Hashtag', () => {
-  it('applies the className based on the theme property `hashtag`', () => {
-    const theme = { hashtag: 'custom-class-name' };
-    const result = shallow(<Hashtag theme={theme} />);
-    expect(result).to.have.prop('className', 'custom-class-name');
-  });
-
-  it('applies any custom passed prop', () => {
-    const result = shallow(<Hashtag data-custom="unicorn" />);
-    expect(result).to.have.prop('data-custom', 'unicorn');
-  });
-
-  it('renders the passed in children', () => {
-    const result = shallow(<Hashtag children="#longRead" />);
-    expect(result).to.have.prop('children', '#longRead');
-  });
-
-  it('applies a custom className as well as the theme', () => {
-    const theme = { hashtag: 'custom-class-name' };
-    const result = shallow(<Hashtag theme={theme} className="hashtag" />);
-    expect(result)
-      .to.have.prop('className')
-      .to.contain('hashtag');
-    expect(result)
-      .to.have.prop('className')
-      .to.contain('custom-class-name');
-  });
-});
diff --git a/draft-js-hashtag-plugin/src/__test__/Hashtag.test.tsx b/draft-js-hashtag-plugin/src/__test__/Hashtag.test.tsx
new file mode 100644
index 00000000..66fa3275
--- /dev/null
+++ b/draft-js-hashtag-plugin/src/__test__/Hashtag.test.tsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import { render } from '@testing-library/react';
+import Hashtag from '../Hashtag';
+
+describe('Hashtag', () => {
+  it('applies the className based on the theme property `hashtag`', () => {
+    const theme = { hashtag: 'custom-class-name' };
+    const { container } = render(<Hashtag theme={theme} />);
+    expect(container.firstChild).toHaveAttribute('class', 'custom-class-name');
+  });
+
+  it('applies any custom passed prop', () => {
+    const { container } = render(<Hashtag data-custom="unicorn" />);
+    expect(container.firstChild).toHaveAttribute('data-custom', 'unicorn');
+  });
+
+  it('renders the passed in children', () => {
+    const { container } = render(<Hashtag>#longRead</Hashtag>);
+
+    expect(container.firstChild).toHaveTextContent('#longRead');
+  });
+
+  it('applies a custom className as well as the theme', () => {
+    const theme = { hashtag: 'custom-class-name' };
+    const { container } = render(<Hashtag theme={theme} className="hashtag" />);
+    expect(container.firstChild).toHaveAttribute(
+      'class',
+      'custom-class-name hashtag'
+    );
+  });
+});
diff --git a/draft-js-mention-plugin/src/Mention/__test__/Mention.test.js "b/draft-js-mention-plugin/src/Mention/__test__/Mention.test.tsx\\"
similarity index 100%
rename from draft-js-mention-plugin/src/Mention/__test__/Mention.test.js
rename to "draft-js-mention-plugin/src/Mention/__test__/Mention.test.tsx\\"
diff --git a/draft-js-mention-plugin/src/MentionSuggestions/__test__/MentionSuggestions.js b/draft-js-mention-plugin/src/MentionSuggestions/__test__/MentionSuggestions.test.tsx
similarity index 53%
rename from draft-js-mention-plugin/src/MentionSuggestions/__test__/MentionSuggestions.js
rename to draft-js-mention-plugin/src/MentionSuggestions/__test__/MentionSuggestions.test.tsx
index bb021234..cd19d42b 100644
--- a/draft-js-mention-plugin/src/MentionSuggestions/__test__/MentionSuggestions.js
+++ b/draft-js-mention-plugin/src/MentionSuggestions/__test__/MentionSuggestions.test.tsx
@@ -1,12 +1,9 @@
-import React from 'react';
-import { mount } from 'enzyme';
-import chai, { expect } from 'chai';
-import sinon from 'sinon';
-import sinonChai from 'sinon-chai';
-
-import { MentionSuggestions } from '../MentionSuggestions';
-
-chai.use(sinonChai);
+import { render } from '@testing-library/react';
+import React, { ReactElement, ReactNode } from 'react';
+import {
+  MentionSuggestions,
+  MentionSuggestionsProps,
+} from '../MentionSuggestions';
 
 const mentions = [
   {
@@ -44,42 +41,49 @@ const mentions = [
   },
 ];
 
-function defaultProps() {
-  return {
+function defaultProps(): MentionSuggestionsProps {
+  return ({
     open: false,
-    onOpenChange: sinon.spy(),
+    onOpenChange: jest.fn(),
     suggestions: mentions,
     callbacks: {
-      keyBindingFn: sinon.spy(),
-      handleReturn: sinon.spy(),
+      keyBindingFn: jest.fn(),
+      handleReturn: jest.fn(),
     },
     store: {
-      getAllSearches: sinon.spy(() => ({ has: () => false })),
-      getPortalClientRect: sinon.spy(),
-      isEscaped: sinon.spy(),
-      resetEscapedSearch: sinon.spy(),
-      escapeSearch: sinon.spy(),
+      getAllSearches: jest.fn(() => ({ has: () => false })),
+      getPortalClientRect: jest.fn(),
+      isEscaped: jest.fn(),
+      resetEscapedSearch: jest.fn(),
+      escapeSearch: jest.fn(),
     },
     ariaProps: {},
-    onSearchChange: sinon.spy(),
-    onAddMention: sinon.spy(),
-    positionSuggestions: sinon.stub().returns({}),
+    onSearchChange: jest.fn(),
+    onAddMention: jest.fn(),
+    positionSuggestions: jest.fn(() => ({})),
     theme: {},
-  };
+  } as unknown) as MentionSuggestionsProps;
 }
 
 describe('MentionSuggestions Component', () => {
   it('Controls open state', () => {
     const props = defaultProps();
-    const suggestions = mount(<MentionSuggestions {...props} />);
-    suggestions.instance().openDropdown();
-    expect(props.onOpenChange).has.been.calledWith(true);
-    suggestions.instance().closeDropdown();
-    expect(props.onOpenChange).has.been.calledWith(false);
+    const instanceRef = React.createRef<MentionSuggestions>();
+    render(<MentionSuggestions {...props} ref={instanceRef} />);
+
+    instanceRef.current!.openDropdown();
+    expect(props.onOpenChange).lastCalledWith(true);
+    instanceRef.current!.closeDropdown();
+    expect(props.onOpenChange).lastCalledWith(false);
   });
 
   it('The popoverComponent prop changes the popover component', () => {
-    const PopoverComponent = ({ children, ...props }) => (
+    const PopoverComponent = ({
+      children,
+      ...props
+    }: {
+      children?: ReactNode;
+    }): ReactElement => (
       <div data-test-test {...props}>
         {children}
       </div>
@@ -88,30 +92,34 @@ describe('MentionSuggestions Component', () => {
     const props = defaultProps();
     props.open = true;
     props.popoverComponent = <PopoverComponent />;
-    const suggestions = mount(<MentionSuggestions {...props} />);
-
-    expect(suggestions.find('[data-test-test]')).to.have.length(1);
+    const { container } = render(<MentionSuggestions {...props} />);
+    expect(container.querySelectorAll('[data-test-test]')).toHaveLength(1);
   });
 
   it('The popoverComponent recieves the children', () => {
     let called = false;
-    const PopoverComponent = ({ children, ...props }) => {
+    const PopoverComponent = ({
+      children,
+      ...props
+    }: {
+      children?: ReactNode;
+    }): ReactElement => {
       called = true;
-      expect(React.Children.count(children)).to.equal(mentions.length);
+      expect(React.Children.count(children)).toBe(mentions.length);
       return <div {...props}>{children}</div>;
     };
 
     const props = defaultProps();
     props.open = true;
     props.popoverComponent = <PopoverComponent />;
-    mount(<MentionSuggestions {...props} />);
-    expect(called).to.equal(true);
+    render(<MentionSuggestions {...props} />);
+    expect(called).toBeTruthy();
   });
 
   it('The popoverComponent prop uses div by default', () => {
     const props = defaultProps();
     props.open = true;
-    const suggestions = mount(<MentionSuggestions {...props} data-findme />);
-    expect(suggestions.find('div[data-findme]')).to.have.length(1);
+    const { container } = render(<MentionSuggestions {...props} data-findme />);
+    expect(container.firstChild).toHaveAttribute('data-findme');
   });
 });
diff --git a/draft-js-mention-plugin/src/MentionSuggestions/__test__/mentionSuggestionStrategy.js b/draft-js-mention-plugin/src/MentionSuggestions/__test__/mentionSuggestionStrategy.js
deleted file mode 100644
index c1b7d5d1..00000000
--- a/draft-js-mention-plugin/src/MentionSuggestions/__test__/mentionSuggestionStrategy.js
+++ /dev/null
@@ -1,125 +0,0 @@
-import { expect } from 'chai';
-import sinon from 'sinon';
-import { genKey, convertFromRaw } from 'draft-js';
-import mentionSuggestionsStrategy from '../../mentionSuggestionsStrategy';
-import defaultRegExp from '../../defaultRegExp';
-
-let callback;
-const trigger = '@';
-const nonWhitespaceStrategy = mentionSuggestionsStrategy(
-  trigger,
-  false,
-  defaultRegExp
-);
-const whitespaceStrategy = mentionSuggestionsStrategy(
-  trigger,
-  true,
-  defaultRegExp
-);
-const getBlock = (text, entityRanges = [], entityMap = {}) => {
-  const contentState = convertFromRaw({
-    blocks: [
-      {
-        key: genKey(),
-        text,
-        type: 'unstyled',
-        depth: 0,
-        inlineStyleRanges: [],
-        entityRanges,
-      },
-    ],
-    entityMap,
-  });
-  return contentState.getFirstBlock();
-};
-
-describe('mentionSuggestionsStrategy', () => {
-  beforeEach(() => {
-    callback = sinon.spy();
-  });
-
-  context('when whitespace support is disabled', () => {
-    it('should match a word', () => {
-      nonWhitespaceStrategy(getBlock('@the'), callback);
-      expect(callback.callCount).to.equal(1);
-      expect(callback.lastCall.args).to.deep.equal([0, 4]);
-    });
-
-    it('should match a word with special characters', () => {
-      nonWhitespaceStrategy(getBlock('@ęĻŌ'), callback);
-      expect(callback.callCount).to.equal(1);
-      expect(callback.lastCall.args).to.deep.equal([0, 4]);
-    });
-
-    it('should match not match spaces', () => {
-      nonWhitespaceStrategy(getBlock('@the walking dead'), callback);
-      expect(callback.callCount).to.equal(1);
-      expect(callback.lastCall.args).to.deep.equal([0, 4]);
-    });
-
-    it('should match multiple mentions', () => {
-      nonWhitespaceStrategy(getBlock('@the @walking @dead'), callback);
-      expect(callback.callCount).to.equal(3);
-      expect(callback.firstCall.args).to.deep.equal([0, 4]);
-      expect(callback.secondCall.args).to.deep.equal([4, 13]);
-      expect(callback.thirdCall.args).to.deep.equal([13, 19]);
-    });
-  });
-
-  context('when whitespace support is enabled', () => {
-    it('should match a word', () => {
-      whitespaceStrategy(getBlock('@the'), callback);
-      expect(callback.callCount).to.equal(1);
-      expect(callback.lastCall.args).to.deep.equal([0, 4]);
-    });
-
-    it('should match a word trailing space', () => {
-      whitespaceStrategy(getBlock('@the '), callback);
-      expect(callback.callCount).to.equal(1);
-      expect(callback.lastCall.args).to.deep.equal([0, 5]);
-    });
-
-    it('should match multiple mentions with spaces', () => {
-      whitespaceStrategy(
-        getBlock('@the walking dead tv @the white house'),
-        callback
-      );
-      expect(callback.callCount).to.equal(2);
-      expect(callback.firstCall.args).to.deep.equal([0, 21]);
-      expect(callback.secondCall.args).to.deep.equal([21, 37]);
-    });
-
-    it('should match multiple mentions with spaces and special characters', () => {
-      whitespaceStrategy(getBlock('@Thomas Müller @Mario Götze'), callback);
-      expect(callback.callCount).to.equal(2);
-      expect(callback.firstCall.args).to.deep.equal([0, 15]);
-      expect(callback.secondCall.args).to.deep.equal([15, 27]);
-    });
-
-    it('should not match entities', () => {
-      const key = genKey();
-      whitespaceStrategy(
-        getBlock(
-          '@the walking dead tv the white house',
-          [
-            {
-              key,
-              offset: 20,
-              length: 15,
-            },
-          ],
-          {
-            [key]: {
-              type: 'mention',
-              mutability: 'IMMUTABLE',
-              data: {},
-            },
-          }
-        ),
-        callback
-      );
-      expect(callback.callCount).to.equal(1);
-      expect(callback.lastCall.args).to.deep.equal([0, 20]);
-    });
-  });
-});
diff --git a/draft-js-mention-plugin/src/MentionSuggestions/__test__/mentionSuggestionStrategy.test.ts b/draft-js-mention-plugin/src/MentionSuggestions/__test__/mentionSuggestionStrategy.test.ts
new file mode 100644
index 00000000..6cb717d1
--- /dev/null
+++ b/draft-js-mention-plugin/src/MentionSuggestions/__test__/mentionSuggestionStrategy.test.ts
@@ -0,0 +1,135 @@
+import {
+  genKey,
+  convertFromRaw,
+  ContentBlock,
+  RawDraftEntity,
+  RawDraftEntityRange,
+} from 'draft-js';
+import mentionSuggestionsStrategy from '../../mentionSuggestionsStrategy';
+import defaultRegExp from '../../defaultRegExp';
+
+const trigger = '@';
+const nonWhitespaceStrategy = mentionSuggestionsStrategy(
+  trigger,
+  false,
+  defaultRegExp
+);
+const whitespaceStrategy = mentionSuggestionsStrategy(
+  trigger,
+  true,
+  defaultRegExp
+);
+const getBlock = (
+  text: string,
+  entityRanges: RawDraftEntityRange[] = [],
+  entityMap: { [key: string]: RawDraftEntity } = {}
+): ContentBlock => {
+  const contentState = convertFromRaw({
+    blocks: [
+      {
+        key: genKey(),
+        text,
+        type: 'unstyled',
+        depth: 0,
+        inlineStyleRanges: [],
+        entityRanges,
+      },
+    ],
+    entityMap,
+  });
+  return contentState.getFirstBlock();
+};
+
+describe('when whitespace support is disabled', () => {
+  it('should match a word', () => {
+    const callback = jest.fn();
+    nonWhitespaceStrategy(getBlock('@the'), callback);
+    expect(callback).toHaveBeenCalledTimes(1);
+    expect(callback).toHaveBeenNthCalledWith(1, 0, 4);
+  });
+
+  it('should match a word with special characters', () => {
+    const callback = jest.fn();
+    nonWhitespaceStrategy(getBlock('@ęĻŌ'), callback);
+    expect(callback).toHaveBeenCalledTimes(1);
+    expect(callback).toHaveBeenNthCalledWith(1, 0, 4);
+  });
+
+  it('should match not match spaces', () => {
+    const callback = jest.fn();
+    nonWhitespaceStrategy(getBlock('@the walking dead'), callback);
+    expect(callback).toHaveBeenCalledTimes(1);
+    expect(callback).toHaveBeenNthCalledWith(1, 0, 4);
+  });
+
+  it('should match multiple mentions', () => {
+    const callback = jest.fn();
+    nonWhitespaceStrategy(getBlock('@the @walking @dead'), callback);
+    expect(callback).toHaveBeenCalledTimes(3);
+    expect(callback).toHaveBeenNthCalledWith(1, 0, 4);
+    expect(callback).toHaveBeenNthCalledWith(2, 4, 13);
+    expect(callback).toHaveBeenNthCalledWith(3, 13, 19);
+  });
+});
+
+describe('when whitespace support is enabled', () => {
+  it('should match a word', () => {
+    const callback = jest.fn();
+    whitespaceStrategy(getBlock('@the'), callback);
+    expect(callback).toHaveBeenCalledTimes(1);
+    expect(callback).toHaveBeenNthCalledWith(1, 0, 4);
+  });
+
+  it('should match a word trailing space', () => {
+    const callback = jest.fn();
+    whitespaceStrategy(getBlock('@the '), callback);
+    expect(callback).toHaveBeenCalledTimes(1);
+    expect(callback).toHaveBeenNthCalledWith(1, 0, 5);
+  });
+
+  it('should match multiple mentions with spaces', () => {
+    const callback = jest.fn();
+    whitespaceStrategy(
+      getBlock('@the walking dead tv @the white house'),
+      callback
+    );
+    expect(callback).toHaveBeenCalledTimes(2);
+    expect(callback).toHaveBeenNthCalledWith(1, 0, 21);
+    expect(callback).toHaveBeenNthCalledWith(2, 21, 37);
+  });
+
+  it('should match multiple mentions with spaces and special characters', () => {
+    const callback = jest.fn();
+    whitespaceStrategy(getBlock('@Thomas Müller @Mario Götze'), callback);
+    expect(callback).toHaveBeenCalledTimes(2);
+    expect(callback).toHaveBeenNthCalledWith(1, 0, 15);
+    expect(callback).toHaveBeenNthCalledWith(2, 15, 27);
+  });
+
+  it('should not match entities', () => {
+    const callback = jest.fn();
+    const key = genKey();
+    whitespaceStrategy(
+      getBlock(
+        '@the walking dead tv the white house',
+        [
+          {
+            key: (key as unknown) as number,
+            offset: 20,
+            length: 15,
+          },
+        ],
+        {
+          [key]: {
+            type: 'mention',
+            mutability: 'IMMUTABLE',
+            data: {},
+          },
+        }
+      ),
+      callback
+    );
+    expect(callback).toHaveBeenCalledTimes(1);
+    expect(callback).toHaveBeenLastCalledWith(0, 20);
+  });
+});
diff --git a/draft-js-mention-plugin/src/utils/__test__/getSearchTextAt.js b/draft-js-mention-plugin/src/utils/__test__/getSearchTextAt.test.ts
similarity index 86%
rename from draft-js-mention-plugin/src/utils/__test__/getSearchTextAt.js
rename to draft-js-mention-plugin/src/utils/__test__/getSearchTextAt.test.ts
index 697c597b..2316ed86 100644
--- a/draft-js-mention-plugin/src/utils/__test__/getSearchTextAt.js
+++ b/draft-js-mention-plugin/src/utils/__test__/getSearchTextAt.test.ts
@@ -1,4 +1,3 @@
-import { expect } from 'chai';
 import getSearchTextAt from '../getSearchTextAt';
 
 const trigger = '@';
@@ -10,7 +9,7 @@ describe('getSearchTextAt', () => {
       begin: 3,
       end: 20,
     };
-    expect(getSearchTextAt('hi @The Walking Dead', 20, trigger)).to.deep.equal(
+    expect(getSearchTextAt('hi @The Walking Dead', 20, trigger)).toEqual(
       expected
     );
   });
@@ -21,7 +20,7 @@ describe('getSearchTextAt', () => {
       begin: 3,
       end: 15,
     };
-    expect(getSearchTextAt('hi @The Walking Dead', 15, trigger)).to.deep.equal(
+    expect(getSearchTextAt('hi @The Walking Dead', 15, trigger)).toEqual(
       expected
     );
   });
@@ -32,6 +31,6 @@ describe('getSearchTextAt', () => {
       begin: 0,
       end: 3,
     };
-    expect(getSearchTextAt('Max', 3, '')).to.deep.equal(expected);
+    expect(getSearchTextAt('Max', 3, '')).toEqual(expected);
   });
 });
diff --git a/draft-js-mention-plugin/src/utils/__test__/getTypeByTrigger.js b/draft-js-mention-plugin/src/utils/__test__/getTypeByTrigger.test.ts
similarity index 55%
rename from draft-js-mention-plugin/src/utils/__test__/getTypeByTrigger.js
rename to draft-js-mention-plugin/src/utils/__test__/getTypeByTrigger.test.ts
index 7b97b221..6abc6a29 100644
--- a/draft-js-mention-plugin/src/utils/__test__/getTypeByTrigger.js
+++ b/draft-js-mention-plugin/src/utils/__test__/getTypeByTrigger.test.ts
@@ -1,20 +1,19 @@
-import { expect } from 'chai';
 import getTypeByTrigger from '../getTypeByTrigger';
 
 describe('getTypeByTrigger', () => {
   it('returns "mention" for trigger "@"', () => {
-    expect(getTypeByTrigger('@')).to.equal('mention');
+    expect(getTypeByTrigger('@')).toBe('mention');
   });
 
   it('returns ":mention" for trigger ":"', () => {
-    expect(getTypeByTrigger(':')).to.equal(':mention');
+    expect(getTypeByTrigger(':')).toBe(':mention');
   });
 
   it('returns "-mention" for trigger "-"', () => {
-    expect(getTypeByTrigger('-')).to.equal('-mention');
+    expect(getTypeByTrigger('-')).toBe('-mention');
   });
 
   it('returns "mention" for trigger "<>"', () => {
-    expect(getTypeByTrigger('<>')).to.equal('<>mention');
+    expect(getTypeByTrigger('<>')).toBe('<>mention');
   });
 });