Skip to content

Commit d4ba181

Browse files
committed
coverage it
1 parent 6b6d0d5 commit d4ba181

File tree

11 files changed

+296
-12
lines changed

11 files changed

+296
-12
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ React Mentions
2525

2626
## Screenshots
2727

28-
<img src="https://user-images.githubusercontent.com/5378891/57270721-17b03880-70bf-11e9-9a5d-67ebf501aef1.png" />
28+
<img src="https://user-images.githubusercontent.com/5378891/57270992-2fd48780-70c0-11e9-91ae-c614d0b49a45.png" />
2929

3030
## Feature
3131

@@ -72,6 +72,7 @@ React.render(<Demo />, container);
7272
| split | Set split string before and after selected mention | string | ' ' |
7373
| validateSearch | Customize trigger search logic | (text: string, props: MentionsProps) => void | - |
7474
| filterOption | Customize filter option logic | false \| (input: string, option: OptionProps) => boolean | - |
75+
| notFoundContent | Set mentions content when not match | ReactNode | 'Not Found' |
7576
| onChange | Trigger when value changed |(text: string) => void | - |
7677
| onSelect | Trigger when user select the option | (option: OptionProps, prefix: string) => void | - |
7778
| onSearch | Trigger when prefix hit | (text: string, prefix: string) => void | - |

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@
6868
"@ant-design/create-react-context": "^0.2.4",
6969
"babel-runtime": "^6.23.0",
7070
"classnames": "^2.2.6",
71-
"omit.js": "^1.0.2",
7271
"rc-menu": "^7.4.22",
7372
"rc-trigger": "^2.6.2",
7473
"rc-util": "^4.6.0",

src/DropdownMenu.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ interface DropdownMenuProps {
1414
*/
1515
class DropdownMenu extends React.Component<DropdownMenuProps, {}> {
1616
public renderDropdown = ({
17+
notFoundContent,
1718
activeIndex,
1819
setActiveIndex,
1920
selectOption,
@@ -49,7 +50,7 @@ class DropdownMenu extends React.Component<DropdownMenuProps, {}> {
4950
);
5051
})}
5152

52-
{!options.length && <MenuItem disabled={true}>Nothing match!</MenuItem>}
53+
{!options.length && <MenuItem disabled={true}>{notFoundContent}</MenuItem>}
5354
</Menu>
5455
);
5556
};

src/Mentions.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import classNames from 'classnames';
2-
import omit from 'omit.js';
32
import toArray from 'rc-util/lib/Children/toArray';
43
import KeyCode from 'rc-util/lib/KeyCode';
54
import * as React from 'react';
@@ -32,6 +31,7 @@ export interface MentionsProps {
3231
split?: string;
3332
validateSearch?: typeof defaultValidateSearch;
3433
filterOption?: false | typeof defaultFilterOption;
34+
notFoundContent?: React.ReactNode;
3535
}
3636
interface MentionsState {
3737
value: string;
@@ -51,6 +51,7 @@ class Mentions extends React.Component<MentionsProps, MentionsState> {
5151
split: ' ',
5252
validateSearch: defaultValidateSearch,
5353
filterOption: defaultFilterOption,
54+
notFoundContent: 'Not Found',
5455
};
5556

5657
public static getDerivedStateFromProps(props: MentionsProps, prevState: MentionsState) {
@@ -308,7 +309,7 @@ class Mentions extends React.Component<MentionsProps, MentionsState> {
308309

309310
public render() {
310311
const { value, measureLocation, measurePrefix, measuring, activeIndex } = this.state;
311-
const { prefixCls, className, style, autoFocus } = this.props;
312+
const { prefixCls, className, style, autoFocus, notFoundContent } = this.props;
312313

313314
const options = measuring ? this.getOptions() : [];
314315

@@ -329,6 +330,7 @@ class Mentions extends React.Component<MentionsProps, MentionsState> {
329330
{value.slice(0, measureLocation)}
330331
<MentionsContextProvider
331332
value={{
333+
notFoundContent,
332334
activeIndex,
333335
setActiveIndex: this.setActiveIndex,
334336
selectOption: this.selectOption,

src/MentionsContext.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import createReactContext, { Context } from '@ant-design/create-react-context';
2+
import * as React from 'react';
23
import { OptionProps } from './Option';
34

45
export interface MentionsContextProps {
6+
notFoundContent: React.ReactNode;
57
activeIndex: number;
68
setActiveIndex: (index: number) => void;
79
selectOption: (option: OptionProps) => void;
810
onFocus: () => void;
911
}
1012

1113
const MentionsContext: Context<MentionsContextProps> = createReactContext({
14+
notFoundContent: '' as React.ReactNode,
1215
activeIndex: 0,
1316
setActiveIndex: (_: number) => {
1417
/* Do nothing */

tests/FullProcess.spec.jsx

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { mount } from 'enzyme';
2+
import KeyCode from 'rc-util/lib/KeyCode';
3+
import React from 'react';
4+
import Mentions from '../src';
5+
import { simulateInput } from './shared/input';
6+
7+
const { Option } = Mentions;
8+
9+
describe('Full Process', () => {
10+
function createMentions(props) {
11+
return mount(
12+
<Mentions {...props}>
13+
<Option value="bamboo">Bamboo</Option>
14+
<Option value="light">Light</Option>
15+
<Option value="cat">Cat</Option>
16+
</Mentions>,
17+
);
18+
}
19+
20+
it('Keyboard selection', () => {
21+
const onChange = jest.fn();
22+
const onSelect = jest.fn();
23+
const onSearch = jest.fn();
24+
const wrapper = createMentions({ onChange, onSelect, onSearch });
25+
26+
simulateInput(wrapper, '@');
27+
expect(wrapper.find('DropdownMenu').props().options).toMatchObject([
28+
{ value: 'bamboo' },
29+
{ value: 'light' },
30+
{ value: 'cat' },
31+
]);
32+
33+
simulateInput(wrapper, '@a');
34+
expect(wrapper.find('DropdownMenu').props().options).toMatchObject([
35+
{ value: 'bamboo' },
36+
{ value: 'cat' },
37+
]);
38+
expect(onSearch).toBeCalledWith('a', '@');
39+
40+
wrapper.find('textarea').simulate('keyDown', {
41+
which: KeyCode.DOWN,
42+
});
43+
wrapper.find('textarea').simulate('keyDown', {
44+
which: KeyCode.ENTER,
45+
});
46+
47+
expect(onChange).toBeCalledWith('@cat ');
48+
expect(onSelect).toBeCalledWith(expect.objectContaining({ value: 'cat' }), '@');
49+
});
50+
51+
it('insert into half way', () => {
52+
const onChange = jest.fn();
53+
const wrapper = createMentions({ onChange });
54+
simulateInput(wrapper, '1 @ 2');
55+
56+
// Mock direct to the position
57+
wrapper.find('textarea').instance().selectionStart = 3;
58+
wrapper.find('textarea').simulate('keyUp', {});
59+
expect(wrapper.state().measuring).toBeTruthy();
60+
61+
wrapper.find('textarea').simulate('keyDown', {
62+
which: KeyCode.ENTER,
63+
});
64+
65+
expect(onChange).toBeCalledWith('1 @bamboo 2');
66+
});
67+
68+
it('reuse typed text', () => {
69+
const onChange = jest.fn();
70+
const wrapper = createMentions({ onChange });
71+
simulateInput(wrapper, '1 @bamboo 2');
72+
73+
// Mock direct to the position
74+
wrapper.find('textarea').instance().selectionStart = 3;
75+
wrapper.find('textarea').simulate('keyUp', {});
76+
expect(wrapper.state().measuring).toBeTruthy();
77+
78+
wrapper.find('textarea').simulate('keyDown', {
79+
which: KeyCode.ENTER,
80+
});
81+
82+
expect(onChange).toBeCalledWith('1 @bamboo 2');
83+
});
84+
85+
it('stop measure if match split', () => {
86+
const wrapper = createMentions();
87+
simulateInput(wrapper, '@a');
88+
expect(wrapper.state().measuring).toBeTruthy();
89+
simulateInput(wrapper, '@a ');
90+
expect(wrapper.state().measuring).toBeFalsy();
91+
});
92+
93+
it('stop measure if remove prefix', () => {
94+
const wrapper = createMentions();
95+
simulateInput(wrapper, 'test@');
96+
expect(wrapper.state().measuring).toBeTruthy();
97+
simulateInput(wrapper, 'test');
98+
expect(wrapper.state().measuring).toBeFalsy();
99+
});
100+
101+
it('ESC', () => {
102+
const wrapper = createMentions();
103+
104+
simulateInput(wrapper, '@');
105+
expect(wrapper.state().measuring).toBe(true);
106+
107+
simulateInput(wrapper, '@', {
108+
which: KeyCode.ESC,
109+
});
110+
111+
expect(wrapper.state().measuring).toBe(false);
112+
});
113+
});

tests/Mentions.spec.jsx

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { mount } from 'enzyme';
2+
import React from 'react';
3+
import Mentions from '../src';
4+
import { simulateInput } from './shared/input';
5+
6+
const { Option } = Mentions;
7+
8+
describe('Mentions', () => {
9+
function createMentions(props) {
10+
return mount(
11+
<Mentions {...props}>
12+
<Option value="bamboo">Bamboo</Option>
13+
<Option value="light">Light</Option>
14+
<Option value="cat">Cat</Option>
15+
</Mentions>,
16+
);
17+
}
18+
19+
describe('focus test', () => {
20+
beforeEach(() => {
21+
jest.useFakeTimers();
22+
});
23+
24+
afterEach(() => {
25+
jest.useRealTimers();
26+
});
27+
28+
it('autoFocus', () => {
29+
const wrapper = createMentions({ autoFocus: true });
30+
expect(document.activeElement).toBe(wrapper.find('textarea').instance());
31+
});
32+
33+
it('not lose focus if click on dropdown', () => {
34+
const onBlur = jest.fn();
35+
const wrapper = createMentions({ autoFocus: true, defaultValue: '@', onBlur });
36+
37+
// Inject to trigger measure
38+
wrapper.instance().startMeasure('b', '@', 1);
39+
jest.runAllTimers();
40+
wrapper.update();
41+
42+
wrapper.find('MenuItem').simulate('focus');
43+
wrapper.find('textarea').simulate('blur');
44+
wrapper.find('MenuItem').simulate('click');
45+
wrapper.find('textarea').simulate('focus'); // This is not good but code focus not work in simulate
46+
jest.runAllTimers();
47+
48+
expect(onBlur).not.toBeCalled();
49+
});
50+
51+
it('focus', () => {
52+
const onFocus = jest.fn();
53+
const wrapper = createMentions({ onFocus });
54+
wrapper.find('textarea').simulate('focus');
55+
expect(onFocus).toBeCalled();
56+
});
57+
58+
it('blur', () => {
59+
const onBlur = jest.fn();
60+
const wrapper = createMentions({ onBlur });
61+
wrapper.find('textarea').simulate('blur');
62+
jest.runAllTimers();
63+
expect(onBlur).toBeCalled();
64+
});
65+
66+
it('focus() & blur()', () => {
67+
const wrapper = createMentions();
68+
wrapper.instance().focus();
69+
expect(document.activeElement).toBe(wrapper.find('textarea').instance());
70+
71+
wrapper.instance().blur();
72+
expect(document.activeElement).not.toBe(wrapper.find('textarea').instance());
73+
});
74+
});
75+
76+
describe('value', () => {
77+
it('defaultValue', () => {
78+
const wrapper = createMentions({ defaultValue: 'light' });
79+
expect(wrapper.find('textarea').props().value).toBe('light');
80+
});
81+
82+
it('controlled value', () => {
83+
const wrapper = createMentions({ value: 'bamboo' });
84+
expect(wrapper.find('textarea').props().value).toBe('bamboo');
85+
86+
wrapper.setProps({ value: 'cat' });
87+
expect(wrapper.find('textarea').props().value).toBe('cat');
88+
});
89+
90+
it('onChange', () => {
91+
const onChange = jest.fn();
92+
const wrapper = createMentions({ onChange });
93+
wrapper.find('textarea').simulate('change', {
94+
target: { value: 'bamboo' },
95+
});
96+
expect(onChange).toBeCalledWith('bamboo');
97+
});
98+
});
99+
100+
describe('filterOption', () => {
101+
it('false', () => {
102+
const wrapper = createMentions({ filterOption: false });
103+
simulateInput(wrapper, '@notExist');
104+
expect(wrapper.find('DropdownMenu').props().options.length).toBe(3);
105+
});
106+
107+
it('function', () => {
108+
const wrapper = createMentions({ filterOption: (_, { value }) => value.includes('a') });
109+
simulateInput(wrapper, '@notExist');
110+
expect(wrapper.find('DropdownMenu').props().options.length).toBe(2);
111+
});
112+
});
113+
114+
it('notFoundContent', () => {
115+
const notFoundContent = 'Bamboo Light';
116+
const wrapper = createMentions({ notFoundContent });
117+
simulateInput(wrapper, '@a');
118+
simulateInput(wrapper, '@notExist');
119+
expect(wrapper.find('DropdownMenu').props().options.length).toBe(0);
120+
expect(wrapper.find('MenuItem').props().children).toBe(notFoundContent);
121+
});
122+
123+
describe('accessibility', () => {
124+
it('hover', () => {
125+
const wrapper = createMentions();
126+
simulateInput(wrapper, '@');
127+
wrapper
128+
.find('MenuItem')
129+
.last()
130+
.simulate('mouseEnter');
131+
expect(wrapper.find('Menu').props().activeKey).toBe('cat');
132+
});
133+
});
134+
});

tests/Option.spec.jsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { mount } from 'enzyme';
2+
import React from 'react';
3+
import Mentions from '../src';
4+
5+
const { Option } = Mentions;
6+
7+
describe('Option', () => {
8+
// Option is a placeholder component. Should render nothing.
9+
it('should be empty', () => {
10+
const wrapper = mount(<Option>Nothing</Option>);
11+
expect(wrapper.instance()).toBe(null);
12+
});
13+
});

tests/setup.ts

Lines changed: 0 additions & 6 deletions
This file was deleted.

tests/shared/input.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/* eslint-disable import/prefer-default-export */
2+
3+
export function simulateInput(wrapper, text = '', keyEvent) {
4+
const lastChar = text[text.length - 1];
5+
const myKeyEvent = keyEvent || {
6+
which: lastChar.charCodeAt(0),
7+
key: lastChar,
8+
};
9+
10+
wrapper.find('textarea').simulate('keyDown', myKeyEvent);
11+
12+
const textareaInstance = wrapper.find('textarea').instance();
13+
textareaInstance.value = text;
14+
textareaInstance.selectionStart = text.length;
15+
textareaInstance.selectionStart = text.length;
16+
17+
if (!keyEvent) {
18+
wrapper.find('textarea').simulate('change', {
19+
target: { value: text },
20+
});
21+
}
22+
23+
wrapper.find('textarea').simulate('keyUp', myKeyEvent);
24+
wrapper.update();
25+
}

0 commit comments

Comments
 (0)