Repository: draft-js-plugins/draft-js-plugins
Pull request #1641 (2020-12-14 16:58:36)

Migrating of @draft-js-plugins/inline-toolbar and @draft-js-plugins/static-toolbar 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-plugins/inline-toolbar` and `@draft-js-plugins/static-toolbar` to TypeScript, jest and @testing-library/react



Patch:
diff --git a/packages/inline-toolbar/src/components/Toolbar/index.tsx b/packages/inline-toolbar/src/components/Toolbar/index.tsx
index 9a913ea0..c69db754 100644
--- a/packages/inline-toolbar/src/components/Toolbar/index.tsx
+++ b/packages/inline-toolbar/src/components/Toolbar/index.tsx
@@ -15,7 +15,9 @@ export interface ToolbarChildrenProps {
   theme: DraftJsButtonTheme;
   getEditorState: () => EditorState;
   setEditorState: (editorState: EditorState) => void;
-  onOverrideContent: (content: ComponentType<ToolbarChildrenProps>) => void;
+  onOverrideContent: (
+    content: ComponentType<ToolbarChildrenProps> | undefined
+  ) => void;
 }
 
 interface ToolbarProps {
@@ -68,7 +70,7 @@ export default class Toolbar extends React.Component<ToolbarProps> {
    * @param {Component} overrideContent
    */
   onOverrideContent = (
-    overrideContent: ComponentType<ToolbarChildrenProps>
+    overrideContent: ComponentType<ToolbarChildrenProps> | undefined
   ): void => {
     this.setState({ overrideContent });
   };
diff --git a/packages/inline-toolbar/src/index.tsx b/packages/inline-toolbar/src/index.tsx
index 328a1da4..17b2977f 100644
--- a/packages/inline-toolbar/src/index.tsx
+++ b/packages/inline-toolbar/src/index.tsx
@@ -19,7 +19,7 @@ export type InlineToolBarPlugin = EditorPlugin & {
   InlineToolbar: ComponentType<ToolbarProps>;
 };
 
-interface StoreItemMap {
+export interface StoreItemMap {
   selection?: SelectionState;
   getEditorState?(): EditorState;
   setEditorState?(state: EditorState): void;
diff --git a/packages/linkify/src/Link/Link.tsx b/packages/linkify/src/Link/Link.tsx
index b8732ac1..fdc0c36e 100644
--- a/packages/linkify/src/Link/Link.tsx
+++ b/packages/linkify/src/Link/Link.tsx
@@ -20,20 +20,20 @@ export interface LinkProps {
   theme?: LinkifyPluginTheme;
   component?: ComponentType<ComponentProps>;
   children: ReactNode;
-  target: string;
-  rel: string;
-  className: string;
+  target?: string;
+  rel?: string;
+  className?: string;
 
   // following props are not used
-  entityKey: unknown;
-  getEditorState: unknown;
-  offsetKey: unknown;
-  setEditorState: unknown;
-  contentState: unknown;
-  blockKey: unknown;
-  dir: unknown;
-  start: unknown;
-  end: unknown;
+  entityKey?: unknown;
+  getEditorState?: unknown;
+  offsetKey?: unknown;
+  setEditorState?: unknown;
+  contentState?: unknown;
+  blockKey?: unknown;
+  dir?: unknown;
+  start?: unknown;
+  end?: unknown;
 }
 
 // The component we render when we encounter a hyperlink in the text
diff --git a/packages/static-toolbar/src/components/Toolbar/index.tsx b/packages/static-toolbar/src/components/Toolbar/index.tsx
index 3ecf8c22..0f6b4561 100644
--- a/packages/static-toolbar/src/components/Toolbar/index.tsx
+++ b/packages/static-toolbar/src/components/Toolbar/index.tsx
@@ -1,23 +1,33 @@
 /* eslint-disable react/no-array-index-key */
 import React, { ComponentType, FC, ReactElement } from 'react';
+import { EditorState } from 'draft-js';
 import {
   ItalicButton,
   BoldButton,
   UnderlineButton,
   CodeButton,
-  DraftJsStyleButtonProps,
+  DraftJsButtonTheme,
 } from '@draft-js-plugins/buttons';
 import PropTypes from 'prop-types';
 import { StaticToolBarPluginStore, StaticToolbarPluginTheme } from '../../';
 
+export interface ToolbarChildrenProps {
+  theme: DraftJsButtonTheme;
+  getEditorState: () => EditorState;
+  setEditorState: (editorState: EditorState) => void;
+  onOverrideContent: (
+    content: ComponentType<ToolbarChildrenProps> | undefined
+  ) => void;
+}
+
 export interface ToolbarPubProps {
-  children?: FC<DraftJsStyleButtonProps>;
+  children?: FC<ToolbarChildrenProps>;
 }
 
 interface ToolbarProps extends ToolbarPubProps {
   store: StaticToolBarPluginStore;
   theme: StaticToolbarPluginTheme;
-  overrideContent?: ComponentType<DraftJsStyleButtonProps>;
+  overrideContent?: ComponentType<ToolbarChildrenProps>;
 }
 
 export default class Toolbar extends React.Component<ToolbarProps> {
@@ -34,14 +44,6 @@ export default class Toolbar extends React.Component<ToolbarProps> {
     overrideContent: undefined,
   } as ToolbarProps;
 
-  // UNSAFE_componentWillMount() {
-  //   this.props.store.subscribeToItem('selection', () => this.forceUpdate());
-  // }
-
-  // componentWillUnmount() {
-  //   this.props.store.unsubscribeFromItem('selection', () => this.forceUpdate());
-  // }
-
   /**
    * This can be called by a child in order to render custom content instead
    * of the regular structure. It's the responsibility of the callee to call
@@ -49,11 +51,11 @@ export default class Toolbar extends React.Component<ToolbarProps> {
    * @param {Component} overrideContent
    */
   onOverrideContent = (
-    overrideContent: ComponentType<DraftJsStyleButtonProps>
+    overrideContent: ComponentType<ToolbarChildrenProps> | undefined
   ): void => this.setState({ overrideContent });
 
   renderDefaultButtons = (
-    externalProps: DraftJsStyleButtonProps
+    externalProps: ToolbarChildrenProps
   ): ReactElement => (
     <div>
       <ItalicButton {...externalProps} />
@@ -66,7 +68,7 @@ export default class Toolbar extends React.Component<ToolbarProps> {
   render(): ReactElement {
     const { theme, store } = this.props;
     const { overrideContent: OverrideContent } = this.state;
-    const childrenProps = {
+    const childrenProps: ToolbarChildrenProps = {
       theme: theme.buttonStyles,
       getEditorState: store.getItem('getEditorState')!,
       setEditorState: store.getItem('setEditorState')!,
diff --git a/packages/static-toolbar/src/index.tsx b/packages/static-toolbar/src/index.tsx
index a74b01aa..fa80b2ab 100644
--- a/packages/static-toolbar/src/index.tsx
+++ b/packages/static-toolbar/src/index.tsx
@@ -18,7 +18,7 @@ export type StaticToolBarPlugin = EditorPlugin & {
   Toolbar: ComponentType<ToolbarPubProps>;
 };
 
-interface StoreItemMap {
+export interface StoreItemMap {
   getEditorState?(): EditorState;
   setEditorState?(state: EditorState): void;
   selection?: SelectionState;
diff --git a/packages/static-toolbar/src/theme.ts b/packages/static-toolbar/src/theme.ts
index 045c6058..859de547 100644
--- a/packages/static-toolbar/src/theme.ts
+++ b/packages/static-toolbar/src/theme.ts
@@ -4,7 +4,7 @@ import { DraftJsButtonTheme } from '@draft-js-plugins/buttons';
 export interface StaticToolbarPluginTheme {
   buttonStyles: DraftJsButtonTheme;
   toolbarStyles: {
-    toolbar: string;
+    toolbar?: string;
   };
 }
 


Test patch:
diff --git a/packages/inline-toolbar/src/components/Toolbar/__test__/Toolbar.js b/packages/inline-toolbar/src/components/Toolbar/__test__/Toolbar.js
deleted file mode 100644
index a03e167c..00000000
--- a/packages/inline-toolbar/src/components/Toolbar/__test__/Toolbar.js
+++ /dev/null
@@ -1,59 +0,0 @@
-import React, { Component } from 'react';
-import { expect } from 'chai';
-import { mount } from 'enzyme';
-import Toolbar from '../index';
-
-describe.skip('Toolbar', () => {
-  it('allows children to override the content', (done) => {
-    class Child extends Component {
-      componentDidMount() {
-        setTimeout(() => {
-          this.props.onOverrideContent(() => {
-            setTimeout(() => {
-              this.props.onOverrideContent(undefined);
-            });
-            return <span className="overridden" />;
-          });
-        });
-      }
-      render() {
-        return <span className="initial" />;
-      }
-    }
-
-    const theme = { toolbarStyles: {} };
-
-    const store = {
-      subscribeToItem() {},
-      unsubscribeFromItem() {},
-      getItem: (name) =>
-        ({
-          getEditorState: () => ({
-            getSelection: () => ({
-              isCollapsed: () => true,
-              getHasFocus: () => true,
-            }),
-          }),
-        }[name]),
-    };
-
-    const wrapper = mount(
-      <Toolbar store={store} theme={theme}>
-        {(externalProps) => <Child {...externalProps} />}
-      </Toolbar>
-    );
-
-    expect(wrapper.find('.initial').length).to.equal(1);
-
-    setTimeout(() => {
-      expect(wrapper.find('.initial').length).to.equal(0);
-      expect(wrapper.find('.overridden').length).to.equal(1);
-
-      setTimeout(() => {
-        expect(wrapper.find('.initial').length).to.equal(1);
-        expect(wrapper.find('.overridden').length).to.equal(0);
-        done();
-      });
-    });
-  });
-});
diff --git a/packages/inline-toolbar/src/components/Toolbar/__test__/Toolbar.test.tsx b/packages/inline-toolbar/src/components/Toolbar/__test__/Toolbar.test.tsx
new file mode 100644
index 00000000..8f5fbd0b
--- /dev/null
+++ b/packages/inline-toolbar/src/components/Toolbar/__test__/Toolbar.test.tsx
@@ -0,0 +1,54 @@
+import { createStore } from '@draft-js-plugins/utils';
+import { EditorState } from 'draft-js';
+import React, { ReactElement, useEffect } from 'react';
+import { render } from '@testing-library/react';
+import { StoreItemMap } from '../../../';
+import Toolbar, { ToolbarChildrenProps } from '../index';
+
+jest.useFakeTimers();
+
+function Child({ onOverrideContent }: ToolbarChildrenProps): ReactElement {
+  useEffect(() => {
+    setTimeout(() => {
+      onOverrideContent(() => {
+        setTimeout(() => {
+          onOverrideContent(undefined);
+        });
+        return <span className="overridden" />;
+      });
+    }, 0);
+  }, []);
+  return <span className="initial" />;
+}
+
+describe('Toolbar', () => {
+  test('allows children to override the content', () => {
+    const theme = { toolbarStyles: {}, buttonStyles: {} };
+
+    const store = createStore<StoreItemMap>({
+      isVisible: false,
+      getEditorState: () =>
+        ({
+          getSelection: () => ({
+            isCollapsed: () => true,
+            getHasFocus: () => true,
+          }),
+        } as EditorState),
+    });
+
+    const { container } = render(
+      <Toolbar store={store} theme={theme}>
+        {(externalProps) => <Child {...externalProps} />}
+      </Toolbar>
+    );
+    expect(container.querySelectorAll('.initial')).toHaveLength(1);
+    //activate override
+    jest.runOnlyPendingTimers();
+    expect(container.querySelectorAll('.initial')).toHaveLength(0);
+    expect(container.querySelectorAll('.overridden')).toHaveLength(1);
+    //remove override
+    jest.runOnlyPendingTimers();
+    expect(container.querySelectorAll('.initial')).toHaveLength(1);
+    expect(container.querySelectorAll('.overridden')).toHaveLength(0);
+  });
+});
diff --git a/packages/linkify/src/Link/__test__/Link.js b/packages/linkify/src/Link/__test__/Link.js
deleted file mode 100644
index 076e539c..00000000
--- a/packages/linkify/src/Link/__test__/Link.js
+++ /dev/null
@@ -1,82 +0,0 @@
-/* eslint-disable jsx-a11y/anchor-has-content,react/no-children-prop */
-
-import React from 'react';
-import { shallow } from 'enzyme';
-import { expect } from 'chai';
-import Link from '../Link';
-
-describe('Link', () => {
-  it('applies the className based on the theme property `link`', () => {
-    const theme = { link: 'custom-class-name' };
-    const result = shallow(<Link theme={theme} />);
-    expect(result).to.have.prop('className', 'custom-class-name');
-  });
-
-  it('applies any custom passed prop', () => {
-    const result = shallow(<Link data-custom="unicorn" />);
-    expect(result).to.have.prop('data-custom', 'unicorn');
-  });
-
-  it('renders the passed in children', () => {
-    const result = shallow(
-      <Link children="https://www.draft-js-plugins.com/" />
-    );
-    expect(result).to.have.prop(
-      'children',
-      'https://www.draft-js-plugins.com/'
-    );
-  });
-
-  it('applies a custom className as well as the theme', () => {
-    const theme = { link: 'custom-class-name' };
-    const result = shallow(<Link theme={theme} className="link" />);
-    expect(result).to.have.prop('className').to.contain('link');
-    expect(result).to.have.prop('className').to.contain('custom-class-name');
-  });
-
-  it('uses the decoratedText prop as href', () => {
-    const result = shallow(
-      <Link decoratedText="https://www.draft-js-plugins.com/" />
-    );
-    expect(result)
-      .to.have.prop('href')
-      .to.contain('https://www.draft-js-plugins.com/');
-  });
-
-  it('applies http prefix when none is supplied', () => {
-    const result = shallow(<Link decoratedText="draft-js-plugins.com/" />);
-    expect(result)
-      .to.have.prop('href')
-      .to.contain('http://draft-js-plugins.com/');
-  });
-
-  it('does not apply a prefix when one is already supplied', () => {
-    const result = shallow(
-      <Link decoratedText="ftp://draft-js-plugins.com/" />
-    );
-    expect(result)
-      .to.have.prop('href')
-      .to.contain('ftp://draft-js-plugins.com/');
-  });
-
-  it('generates correct href to localhost with port', () => {
-    const result = shallow(<Link decoratedText="http://localhost:8000" />);
-    expect(result).to.have.prop('href').to.contain('http://localhost:8000');
-  });
-
-  it('generates mailto href when supplied with email', () => {
-    const result = shallow(<Link decoratedText="name@example.com" />);
-    expect(result).to.have.prop('href').to.contain('mailto:name@example.com');
-  });
-
-  it('uses _self as the default target value', () => {
-    const result = shallow(<Link />);
-    expect(result).to.have.prop('target').to.contain('_self');
-  });
-
-  it('applies custom target value', () => {
-    // eslint-disable-next-line react/jsx-no-target-blank
-    const result = shallow(<Link target="_blank" />);
-    expect(result).to.have.prop('target').to.contain('_blank');
-  });
-});
diff --git a/packages/linkify/src/Link/__test__/Link.test.tsx b/packages/linkify/src/Link/__test__/Link.test.tsx
new file mode 100644
index 00000000..f81f6c6c
--- /dev/null
+++ b/packages/linkify/src/Link/__test__/Link.test.tsx
@@ -0,0 +1,86 @@
+/* eslint-disable jsx-a11y/anchor-has-content,react/no-children-prop */
+
+import React from 'react';
+import { render } from '@testing-library/react';
+import Link from '../Link';
+
+describe('Link', () => {
+  it('applies the className based on the theme property `link` and property', () => {
+    const theme = { link: 'custom-class-name' };
+    const { getByRole } = render(
+      <Link theme={theme} className="link">
+        Link
+      </Link>
+    );
+    const link = getByRole('link');
+    expect(link).toHaveClass('custom-class-name');
+    expect(link).toHaveClass('link');
+    expect(link).toHaveAttribute('target', '_self');
+  });
+
+  it('applies any custom passed prop', () => {
+    const { getByRole } = render(<Link data-custom="unicorn">Link</Link>);
+    expect(getByRole('link')).toHaveAttribute('data-custom', 'unicorn');
+  });
+
+  it('renders the passed in children', () => {
+    const { getByRole } = render(
+      <Link>https://www.draft-js-plugins.com/</Link>
+    );
+    expect(getByRole('link')).toHaveTextContent(
+      'https://www.draft-js-plugins.com/'
+    );
+  });
+
+  it('uses the decoratedText prop as href', () => {
+    const { getByRole } = render(
+      <Link decoratedText="https://www.draft-js-plugins.com/">Link</Link>
+    );
+    expect(getByRole('link')).toHaveAttribute(
+      'href',
+      'https://www.draft-js-plugins.com/'
+    );
+  });
+
+  it('applies http prefix when none is supplied', () => {
+    const { getByRole } = render(
+      <Link decoratedText="draft-js-plugins.com/">Link</Link>
+    );
+    expect(getByRole('link')).toHaveAttribute(
+      'href',
+      'http://draft-js-plugins.com/'
+    );
+  });
+
+  it('does not apply a prefix when one is already supplied', () => {
+    const { getByRole } = render(
+      <Link decoratedText="ftp://draft-js-plugins.com/">Link</Link>
+    );
+    expect(getByRole('link')).toHaveAttribute(
+      'href',
+      'ftp://draft-js-plugins.com/'
+    );
+  });
+
+  it('generates correct href to localhost with port', () => {
+    const { getByRole } = render(
+      <Link decoratedText="http://localhost:8000">Link</Link>
+    );
+    expect(getByRole('link')).toHaveAttribute('href', 'http://localhost:8000');
+  });
+
+  it('generates mailto href when supplied with email', () => {
+    const { getByRole } = render(
+      <Link decoratedText="name@example.com">Link</Link>
+    );
+    expect(getByRole('link')).toHaveAttribute(
+      'href',
+      'mailto:name@example.com'
+    );
+  });
+
+  it('applies custom target value', () => {
+    const { getByRole } = render(<Link target="_blank">Link</Link>);
+    expect(getByRole('link')).toHaveAttribute('target', '_blank');
+  });
+});
diff --git a/packages/static-toolbar/src/components/Toolbar/__test__/Toolbar.js b/packages/static-toolbar/src/components/Toolbar/__test__/Toolbar.js
deleted file mode 100644
index 0a5677a3..00000000
--- a/packages/static-toolbar/src/components/Toolbar/__test__/Toolbar.js
+++ /dev/null
@@ -1,62 +0,0 @@
-import React, { Component } from 'react';
-import { expect } from 'chai';
-import { mount } from 'enzyme';
-import Toolbar from '../index';
-
-describe.skip('Toolbar', () => {
-  it('allows children to override the content', (done) => {
-    class Child extends Component {
-      componentDidMount() {
-        setTimeout(() => {
-          this.props.onOverrideContent(() => {
-            setTimeout(() => {
-              this.props.onOverrideContent(undefined);
-            });
-            return <span className="overridden" />;
-          });
-        });
-      }
-      render() {
-        return <span className="initial" />;
-      }
-    }
-
-    const theme = { toolbarStyles: {}, buttonStyles: {} };
-
-    const store = {
-      subscribeToItem() {},
-      unsubscribeFromItem() {},
-      getItem: (name) =>
-        ({
-          getEditorState: () => ({
-            getCurrentInlineStyle: () => ({
-              has: () => false,
-            }),
-            getSelection: () => ({
-              isCollapsed: () => true,
-              getHasFocus: () => true,
-            }),
-          }),
-        }[name]),
-    };
-
-    const wrapper = mount(
-      <Toolbar store={store} theme={theme}>
-        {(props) => <Child {...props} />}
-      </Toolbar>
-    );
-
-    expect(wrapper.find('.initial').length).to.equal(1);
-
-    setTimeout(() => {
-      expect(wrapper.find('.initial').length).to.equal(0);
-      expect(wrapper.find('.overridden').length).to.equal(1);
-
-      setTimeout(() => {
-        expect(wrapper.find('.initial').length).to.equal(1);
-        expect(wrapper.find('.overridden').length).to.equal(0);
-        done();
-      });
-    });
-  });
-});
diff --git a/packages/static-toolbar/src/components/Toolbar/__test__/Toolbar.test.tsx b/packages/static-toolbar/src/components/Toolbar/__test__/Toolbar.test.tsx
new file mode 100644
index 00000000..cef198f8
--- /dev/null
+++ b/packages/static-toolbar/src/components/Toolbar/__test__/Toolbar.test.tsx
@@ -0,0 +1,53 @@
+import { createStore } from '@draft-js-plugins/utils';
+import { EditorState } from 'draft-js';
+import React, { ReactElement, useEffect } from 'react';
+import { render } from '@testing-library/react';
+import { StoreItemMap } from '../../..';
+import Toolbar, { ToolbarChildrenProps } from '../index';
+
+jest.useFakeTimers();
+
+function Child({ onOverrideContent }: ToolbarChildrenProps): ReactElement {
+  useEffect(() => {
+    setTimeout(() => {
+      onOverrideContent(() => {
+        setTimeout(() => {
+          onOverrideContent(undefined);
+        });
+        return <span className="overridden" />;
+      });
+    }, 0);
+  }, []);
+  return <span className="initial" />;
+}
+
+describe('Toolbar', () => {
+  test('allows children to override the content', () => {
+    const theme = { toolbarStyles: {}, buttonStyles: {} };
+
+    const store = createStore<StoreItemMap>({
+      getEditorState: () =>
+        ({
+          getSelection: () => ({
+            isCollapsed: () => true,
+            getHasFocus: () => true,
+          }),
+        } as EditorState),
+    });
+
+    const { container } = render(
+      <Toolbar store={store} theme={theme}>
+        {(externalProps) => <Child {...externalProps} />}
+      </Toolbar>
+    );
+    expect(container.querySelectorAll('.initial')).toHaveLength(1);
+    //activate override
+    jest.runOnlyPendingTimers();
+    expect(container.querySelectorAll('.initial')).toHaveLength(0);
+    expect(container.querySelectorAll('.overridden')).toHaveLength(1);
+    //remove override
+    jest.runOnlyPendingTimers();
+    expect(container.querySelectorAll('.initial')).toHaveLength(1);
+    expect(container.querySelectorAll('.overridden')).toHaveLength(0);
+  });
+});