Skip to content

Commit

Permalink
feat(reply): Add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Mingze Xiao committed May 14, 2020
1 parent 1d7e145 commit eb20b7f
Show file tree
Hide file tree
Showing 11 changed files with 384 additions and 130 deletions.
8 changes: 4 additions & 4 deletions src/components/Popups/ReplyField/ItemList.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import React, { SyntheticEvent } from 'react';
import noop from 'lodash/noop';
import uniqueId from 'lodash/uniqueId';
import ItemRow from './ItemRow';
import { Collaborator } from '../../../@types';
import './ItemList.scss';

export type Props = {
activeItemIndex?: number;
itemRowAs?: JSX.Element;
items: Collaborator[];
items: Record<string, unknown>[];
onSelect: (index: number, event: React.SyntheticEvent) => void;
setActiveItemIndex?: (index: number) => void;
};
Expand All @@ -21,11 +21,11 @@ const ItemList = ({
...rest
}: Props): JSX.Element => {
return (
<ul role="listbox" {...rest}>
<ul data-testid="ba-ItemList" role="listbox" {...rest}>
{items.map((item, index) =>
React.cloneElement(itemRowAs, {
...item,
key: item.id,
key: item.id ? String(item.id) : uniqueId(),
className: 'ba-ItemList-row',
isActive: index === activeItemIndex,
onClick: (event: SyntheticEvent) => {
Expand Down
7 changes: 7 additions & 0 deletions src/components/Popups/ReplyField/ItemRow.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
@import '~box-ui-elements/es/styles/variables';

.ba-DefaultItemRow-email {
color: $bdl-gray-62;
font-size: 11px;
line-height: 13px;
}
31 changes: 31 additions & 0 deletions src/components/Popups/ReplyField/ItemRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from 'react';
import DatalistItem from 'box-ui-elements/es/components/datalist-item';
import { UserMini, GroupMini } from '../../../@types';
import './ItemRow.scss';

export type Props = {
id?: string;
item?: UserMini | GroupMini;
name?: string;
};

const DefaultItemRow = ({ item, ...rest }: Props): JSX.Element | null => {
if (!item || !item.name) {
return null;
}

return (
<DatalistItem {...rest}>
<div className="ba-DefaultItemRow-name" data-testid="ba-DefaultItemRow-name">
{item.name}
</div>
{'email' in item && (
<div className="ba-DefaultItemRow-email" data-testid="ba-DefaultItemRow-email">
{item.email}
</div>
)}
</DatalistItem>
);
};

export default DefaultItemRow;
27 changes: 12 additions & 15 deletions src/components/Popups/ReplyField/PopupList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@ import { FormattedMessage } from 'react-intl';
import ItemList from './ItemList';
import messages from '../messages';
import PopupBase from '../PopupBase';
import { Collaborator } from '../../../@types';
import { Options, PopupReference } from '../Popper';
import './PopupList.scss';

export type Props = {
activeItemIndex?: number;
itemRowAs?: JSX.Element;
items: Collaborator[];
items: Record<string, unknown>[];
onSelect: (index: number, event: React.SyntheticEvent) => void;
options?: Partial<Options>;
reference: PopupReference;
Expand All @@ -36,18 +35,16 @@ const defaultOptions: Partial<Options> = {
placement: 'bottom-start',
};

const PopupList = ({ items, reference, options, ...rest }: Props): JSX.Element => {
return (
<PopupBase className="ba-PopupList" options={{ ...defaultOptions, ...options }} reference={reference}>
{isEmpty(items) ? (
<div className="ba-PopupList-prompt">
<FormattedMessage {...messages.popupListPrompt} />
</div>
) : (
<ItemList items={items} {...rest} />
)}
</PopupBase>
);
};
const PopupList = ({ items, reference, options, ...rest }: Props): JSX.Element => (
<PopupBase className="ba-PopupList" options={{ ...defaultOptions, ...options }} reference={reference}>
{isEmpty(items) ? (
<div className="ba-PopupList-prompt">
<FormattedMessage {...messages.popupListPrompt} />
</div>
) : (
<ItemList items={items} {...rest} />
)}
</PopupBase>
);

export default PopupList;
82 changes: 37 additions & 45 deletions src/components/Popups/ReplyField/ReplyField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { ContentState, DraftHandleValue, Editor, EditorState, SelectionState } f
import PopupList from './PopupList';
import withMentionDecorator from './withMentionDecorator';
import { Collaborator } from '../../../@types';
import { State as PopperState, VirtualElement } from '../Popper';
import { VirtualElement } from '../Popper';
import './ReplyField.scss';

export type Mention = {
Expand Down Expand Up @@ -98,7 +98,7 @@ export default class ReplyField extends React.Component<Props, State> {
}

const isNameMatch = fuzzySearch(trimmedQuery, item.name, 0);
const isEmailMatch = 'email' in item && fuzzySearch(trimmedQuery, item.email, 0);
const isEmailMatch = 'email' in item && fuzzySearch(trimmedQuery, item?.email, 0);

return isNameMatch || isEmailMatch;
});
Expand Down Expand Up @@ -140,18 +140,6 @@ export default class ReplyField extends React.Component<Props, State> {
this.setState({ popupReference: activeMention ? this.getVirtualElement(activeMention) : null });
};

handleFirstUpdate = (state: Partial<PopperState>): void => {
const { x, y } = state.rects?.reference || {};

// sometimes inner popper position is wrong due to parent popper position change, like zoom in/out
// the inner popper position should always be positive (bottom-right)
// so if position is negative after first positioning,
// we re-calculate virtualElement to re-position inner popper
if ((x && x < 0) || (y && y < 0)) {
this.updatePopupReference();
}
};

handleChange = (nextEditorState: EditorState): void => {
const { onChange } = this.props;

Expand All @@ -164,12 +152,13 @@ export default class ReplyField extends React.Component<Props, State> {

handleSelect = (index: number): void => {
const { editorState } = this.state;
const collaborators = this.getCollaborators(getActiveMentionForEditorState(editorState));

const activeMention = getActiveMentionForEditorState(editorState);
const collaborators = this.getCollaborators(activeMention);
const editorStateWithLink = addMention(editorState, activeMention, collaborators[index]);

this.handleChange(editorStateWithLink);
this.setPopupListActiveItem(0);
};

focusEditor = (): void => {
Expand All @@ -193,41 +182,45 @@ export default class ReplyField extends React.Component<Props, State> {

setPopupListActiveItem = (index: number): void => this.setState({ activeItemIndex: index });

handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>): DraftHandleValue => {
const { activeItemIndex, editorState, popupReference } = this.state;
const collaborators = this.getCollaborators(getActiveMentionForEditorState(editorState));
handleReturn = (event: React.KeyboardEvent): DraftHandleValue => {
const { activeItemIndex, popupReference } = this.state;

if (!popupReference) {
return 'not-handled';
}

switch (event.key) {
case 'ArrowDown':
if (!collaborators.length) {
break;
}
this.stopDefaultEvent(event);
this.setPopupListActiveItem(activeItemIndex === collaborators.length - 1 ? 0 : activeItemIndex + 1);
break;
case 'ArrowUp':
if (!collaborators.length) {
break;
}
this.stopDefaultEvent(event);
this.setPopupListActiveItem(activeItemIndex === 0 ? collaborators.length - 1 : activeItemIndex - 1);
break;
case 'Enter':
this.stopDefaultEvent(event);
this.handleSelect(activeItemIndex);
this.setPopupListActiveItem(0);
return 'handled';
default:
// don't stop default event if not above keys
this.stopDefaultEvent(event);
this.handleSelect(activeItemIndex);

return 'handled';
};

handleArrowHelper = (
event: React.KeyboardEvent,
getNextIndex: (activeItemIndex: number, itemsLength: number) => number,
): void => {
const { activeItemIndex, editorState, popupReference } = this.state;
const collaborators = this.getCollaborators(getActiveMentionForEditorState(editorState));

if (!popupReference || !collaborators.length) {
return;
}

return 'not-handled';
this.stopDefaultEvent(event);
const nextIndex = getNextIndex(activeItemIndex, collaborators.length);
this.setPopupListActiveItem(nextIndex);
};

handleDownArrow = (event: React.KeyboardEvent): void =>
this.handleArrowHelper(event, (activeItemIndex: number, itemsLength: number) =>
activeItemIndex === itemsLength - 1 ? 0 : activeItemIndex + 1,
);

handleUpArrow = (event: React.KeyboardEvent): void =>
this.handleArrowHelper(event, (activeItemIndex: number, itemsLength: number) =>
activeItemIndex === 0 ? itemsLength - 1 : activeItemIndex - 1,
);

render(): JSX.Element {
const { className, isDisabled, itemRowAs, placeholder, ...rest } = this.props;
const { activeItemIndex, editorState, popupReference } = this.state;
Expand All @@ -238,10 +231,10 @@ export default class ReplyField extends React.Component<Props, State> {
{...rest}
ref={this.editorRef}
editorState={editorState}
handleReturn={this.handleKeyDown}
handleReturn={this.handleReturn}
onChange={this.handleChange}
onDownArrow={this.handleKeyDown}
onUpArrow={this.handleKeyDown}
onDownArrow={this.handleDownArrow}
onUpArrow={this.handleUpArrow}
placeholder={placeholder}
readOnly={isDisabled}
stripPastedStyles
Expand All @@ -254,7 +247,6 @@ export default class ReplyField extends React.Component<Props, State> {
itemRowAs={itemRowAs}
items={this.getCollaborators(getActiveMentionForEditorState(editorState))}
onSelect={this.handleSelect}
options={{ onFirstUpdate: this.handleFirstUpdate }}
reference={popupReference}
setActiveItemIndex={this.setPopupListActiveItem}
/>
Expand Down
78 changes: 21 additions & 57 deletions src/components/Popups/ReplyField/__tests__/ItemList-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,74 +14,38 @@ describe('components/Popups/ReplyField/ItemList', () => {
const getWrapper = (props = {}): ShallowWrapper => shallow(<ItemList {...defaults} {...props} />);

describe('render()', () => {
test('should render ul with correct props', () => {
const wrapper = getWrapper();

expect(wrapper.find('ul').props()).toMatchObject({
role: 'listbox',
});
expect(wrapper.find('ul').children()).toHaveLength(2);
});

test('should render child with correct props', () => {
const wrapper = getWrapper();

expect(
wrapper
.find('ul')
.childAt(0)
.prop('className'),
).toEqual('ba-ItemList-row');
});

test('should activate child at activeItemIndex', () => {
test('should render elements with correct props', () => {
const wrapper = getWrapper({ activeItemIndex: 1 });

expect(
wrapper
.find('ul')
.childAt(0)
.prop('isActive'),
).toEqual(false);
expect(
wrapper
.find('ul')
.childAt(1)
.prop('isActive'),
).toEqual(true);
});

test('should trigger onSelect when click', () => {
const wrapper = getWrapper();
wrapper
.find('ul')
.childAt(0)
.simulate('click', 'event');
const itmeList = wrapper.find('[data-testid="ba-ItemList"]');
const firstChild = itmeList.childAt(0);
const secondChild = itmeList.childAt(1);

expect(defaults.onSelect).toBeCalledWith(0, 'event');
expect(itmeList.props()).toMatchObject({
role: 'listbox',
});
expect(itmeList.children()).toHaveLength(2);
expect(firstChild.prop('className')).toEqual('ba-ItemList-row');
expect(firstChild.prop('isActive')).toEqual(false);
expect(secondChild.prop('isActive')).toEqual(true);
});

test('should call preventDefault when mouse down', () => {
const wrapper = getWrapper();
test('should trigger events', () => {
const mockSetIndex = jest.fn();
const mockEvent = {
preventDefault: jest.fn(),
};
wrapper
.find('ul')
.childAt(0)
.simulate('mousedown', mockEvent);

expect(mockEvent.preventDefault).toBeCalled();
});

test('should call setActiveItemIndex when mouse enter', () => {
const mockSetIndex = jest.fn();
const wrapper = getWrapper({ setActiveItemIndex: mockSetIndex });
wrapper
.find('ul')
.childAt(0)
.simulate('mouseenter');
const firstChild = wrapper.find('[data-testid="ba-ItemList"]').childAt(0);

firstChild.simulate('click', mockEvent);
expect(defaults.onSelect).toBeCalledWith(0, mockEvent);

firstChild.simulate('mousedown', mockEvent);
expect(mockEvent.preventDefault).toBeCalled();

firstChild.simulate('mouseenter');
expect(mockSetIndex).toBeCalledWith(0);
});
});
Expand Down
Loading

0 comments on commit eb20b7f

Please sign in to comment.