box-ui-elements
Version:
Box UI Elements
719 lines (607 loc) • 31.4 kB
JavaScript
/* eslint-disable jsx-a11y/media-has-caption */
/* eslint-disable react/jsx-no-comment-textnodes */
import * as React from 'react';
import { mount, shallow } from 'enzyme';
import { ContentState, EditorState, convertToRaw } from 'draft-js';
import sinon from 'sinon';
import DraftJSMentionSelector from '..';
import * as messages from '../../input-messages';
const sandbox = sinon.sandbox.create();
describe('bcomponents/form-elements/draft-js-mention-selector/DraftJSMentionSelector', () => {
afterEach(() => {
sandbox.verifyAndRestore();
});
const requiredProps = {
contacts: [],
label: 'label',
name: 'name',
onMention: () => {},
};
describe('render()', () => {
beforeEach(() => {
jest.spyOn(document, 'querySelector').mockImplementation(() => ({
querySelector: () => ({ currentTime: 70 }),
}));
});
afterEach(() => {
jest.restoreAllMocks();
});
test('should correctly render the component', () => {
const wrapper = shallow(<DraftJSMentionSelector {...requiredProps} />);
expect(wrapper.find('FormInput').length).toBe(1);
});
test('should toggle the time stamp if isRequired and timestampedCommentsEnabled is true', () => {
const wrapper = shallow(
<DraftJSMentionSelector {...requiredProps} isRequired={true} timestampLabel={'Toggle Timestamp'} />,
);
expect(wrapper.find('Toggle').length).toEqual(1);
});
test('should not toggle the time stamp if isRequired is false', () => {
const wrapper = shallow(
<DraftJSMentionSelector {...requiredProps} isRequired={false} timestampLabel={'Toggle Timestamp'} />,
);
expect(wrapper.find('Toggle').length).toEqual(0);
});
test('should not toggle the time stamp if timestampLabel is undefined', () => {
const wrapper = shallow(
<DraftJSMentionSelector {...requiredProps} isRequired={true} timestampLabel={undefined} />,
);
expect(wrapper.find('Toggle').length).toEqual(0);
});
test('should show timestamp toggle on with timestamp if timestamplabel is defined and isRequired is true', () => {
const props = { ...requiredProps };
const wrapper = shallow(<DraftJSMentionSelector {...props} />);
wrapper.setProps({ ...requiredProps, timestampLabel: 'Toggle Timestamp', isRequired: true });
const instance = wrapper.instance();
expect(instance.state.isTimestampToggledOn).toEqual(true);
expect(wrapper.find('Toggle').length).toEqual(1);
expect(wrapper.find('Toggle').prop('isOn')).toEqual(true);
expect(instance.state.internalEditorState.getCurrentContent().getPlainText()).toContain('0:01:10');
});
});
describe('getDerivedStateFromProps()', () => {
test('should return contacts from props', () => {
expect(DraftJSMentionSelector.getDerivedStateFromProps({ contacts: [] })).toEqual({ contacts: [] });
});
test('should return null if no contacts from props', () => {
expect(DraftJSMentionSelector.getDerivedStateFromProps({})).toEqual(null);
});
});
describe('componentDidUpdate()', () => {
let mockGetDerivedStateFromEditorState;
let mockCheckValidityIfAllowed;
let spySetState;
const setupInstance = props => {
const wrapper = shallow(<DraftJSMentionSelector {...props} />);
const instance = wrapper.instance();
mockGetDerivedStateFromEditorState = jest.fn();
mockCheckValidityIfAllowed = jest.fn();
spySetState = jest.spyOn(instance, 'setState');
instance.getDerivedStateFromEditorState = mockGetDerivedStateFromEditorState;
instance.checkValidityIfAllowed = mockCheckValidityIfAllowed;
return wrapper;
};
test('should set new state in internal editor state mode when it changes', () => {
const wrapper = setupInstance(requiredProps);
mockGetDerivedStateFromEditorState.mockReturnValue({});
wrapper.setState({
internalEditorState: EditorState.createWithContent(ContentState.createFromText('hello')),
});
expect(mockGetDerivedStateFromEditorState).toHaveBeenCalled();
expect(spySetState).toHaveBeenCalledWith({}, mockCheckValidityIfAllowed);
});
test('should check validity in internal editor state mode when it changes but derived state is null', () => {
const wrapper = setupInstance(requiredProps);
mockGetDerivedStateFromEditorState.mockReturnValue(null);
wrapper.setState({
internalEditorState: EditorState.createWithContent(ContentState.createFromText('hello')),
});
expect(mockGetDerivedStateFromEditorState).toHaveBeenCalled();
expect(spySetState).not.toHaveBeenCalled();
});
test('should set new state in external editor state mode when it changes', () => {
const initialProps = { ...requiredProps, editorState: EditorState.createEmpty() };
const wrapper = setupInstance(initialProps);
mockGetDerivedStateFromEditorState.mockReturnValue({});
wrapper.setProps({
editorState: EditorState.createWithContent(ContentState.createFromText('hello')),
});
expect(mockGetDerivedStateFromEditorState).toHaveBeenCalled();
expect(spySetState).toHaveBeenCalledWith({}, mockCheckValidityIfAllowed);
});
test('should check validity in external editor state mode when it changes but derived state is null', () => {
const initialProps = { ...requiredProps, editorState: EditorState.createEmpty() };
const wrapper = setupInstance(initialProps);
mockGetDerivedStateFromEditorState.mockReturnValue(null);
wrapper.setProps({
editorState: EditorState.createWithContent(ContentState.createFromText('hello')),
});
expect(mockGetDerivedStateFromEditorState).toHaveBeenCalled();
expect(spySetState).not.toHaveBeenCalled();
});
test('should not call getDerivedStateFromEditorState in internal editor state mode if same reference', () => {
const wrapper = setupInstance(requiredProps);
const constantEditorState = EditorState.createWithContent(ContentState.createFromText('hello'));
wrapper.setState({ internalEditorState: constantEditorState });
mockGetDerivedStateFromEditorState.mockClear();
wrapper.setState({ internalEditorState: constantEditorState });
expect(mockGetDerivedStateFromEditorState).not.toHaveBeenCalled();
expect(spySetState).not.toHaveBeenCalled();
});
test('should not call getDerivedStateFromEditorState in external editor state mode if same reference', () => {
const constantEditorState = EditorState.createWithContent(ContentState.createFromText('hello'));
const initialProps = { ...requiredProps, editorState: constantEditorState };
const wrapper = setupInstance(initialProps);
wrapper.setProps({
editorState: constantEditorState,
});
expect(mockGetDerivedStateFromEditorState).not.toHaveBeenCalled();
expect(spySetState).not.toHaveBeenCalled();
});
});
describe('getDerivedStateFromEditorState', () => {
let wrapper;
let instance;
let mockIsEditorStateEmpty;
beforeEach(() => {
wrapper = shallow(<DraftJSMentionSelector {...requiredProps} />);
instance = wrapper.instance();
mockIsEditorStateEmpty = jest.fn();
instance.isEditorStateEmpty = mockIsEditorStateEmpty;
});
test('should return isTouched false if is new editor state', () => {
mockIsEditorStateEmpty.mockReturnValueOnce(false).mockReturnValueOnce(true);
expect(instance.getDerivedStateFromEditorState()).toEqual({ isTouched: false, error: null });
});
test('should return isTouched true if editor state is dirty', () => {
mockIsEditorStateEmpty.mockReturnValueOnce(true).mockReturnValueOnce(false);
expect(instance.getDerivedStateFromEditorState()).toEqual({ isTouched: true });
});
test('should return null if not new editor state nor dirty editor', () => {
mockIsEditorStateEmpty.mockReturnValueOnce(true).mockReturnValueOnce(true);
expect(instance.getDerivedStateFromEditorState()).toEqual(null);
});
});
describe('getErrorFromValidityState()', () => {
const minLength = 4;
const maxLength = 9;
[
// too long optional string
{
str: 'foo bar baz woo',
isRequired: false,
expected: messages.tooLong(maxLength),
},
// too short optional string
{
str: 'foo',
isRequired: false,
expected: messages.tooShort(minLength),
},
{
str: '',
isRequired: true,
expected: messages.valueMissing(),
},
// empty required string
{
str: '',
isRequired: true,
expected: messages.valueMissing(),
},
// good lemgth required string
{
str: 'all good',
isRequired: true,
expected: null,
},
// good length optional string
{
str: 'all good',
isRequired: false,
expected: null,
},
].forEach(({ str, isRequired, expected }) => {
test('should return the correct error state', () => {
const editorState = EditorState.createWithContent(ContentState.createFromText(str));
const wrapper = shallow(
<DraftJSMentionSelector
{...requiredProps}
editorState={editorState}
isRequired={isRequired}
maxLength={maxLength}
minLength={minLength}
/>,
);
const instance = wrapper.instance();
const result = instance.getErrorFromValidityState();
expect(result).toEqual(expected);
});
});
});
describe('handleBlur()', () => {
[
{
validateOnBlur: true,
},
{
validateOnBlur: false,
},
].forEach(({ validateOnBlur }) => {
const wrapper = mount(<DraftJSMentionSelector {...requiredProps} validateOnBlur={validateOnBlur} />);
const instance = wrapper.instance();
afterEach(() => {
instance.handleBlur({
relatedTarget: document.createElement('div'),
});
});
if (validateOnBlur) {
test('should call checkValidity when called', () => {
sandbox.mock(instance).expects('checkValidity');
});
} else {
test('should not call checkValidity when called', () => {
sandbox.mock(instance).expects('checkValidity').never();
});
}
});
});
describe('handleChange()', () => {
let wrapper;
let instance;
let mockOnChange;
let spySetState;
const setup = props => {
mockOnChange = jest.fn();
wrapper = shallow(<DraftJSMentionSelector {...props} onChange={mockOnChange} />);
instance = wrapper.instance();
spySetState = jest.spyOn(instance, 'setState');
};
const setupWithTimestamp = props => {
mockOnChange = jest.fn();
wrapper = shallow(
<DraftJSMentionSelector
{...props}
timestampLabel="Toggle Timestamp"
isRequired={true}
onChange={mockOnChange}
/>,
);
instance = wrapper.instance();
};
test('should call onChange and setState if internal editor state exists', () => {
setup({ ...requiredProps });
const dummyEditorState = EditorState.createEmpty();
instance.handleChange(dummyEditorState);
expect(mockOnChange).toHaveBeenCalledWith(dummyEditorState);
expect(spySetState).toHaveBeenCalledWith({ internalEditorState: dummyEditorState });
});
test('should call onChange and not setState if no internal editor state exists', () => {
const dummyEditorState = EditorState.createEmpty();
setup({ ...requiredProps, editorState: dummyEditorState });
instance.handleChange(dummyEditorState);
expect(mockOnChange).toHaveBeenCalledWith(dummyEditorState);
expect(spySetState).not.toHaveBeenCalled();
});
test('should keep timestamp prepended state when content changes but timestamp entity is still present', () => {
const dummyEditorState = EditorState.createWithContent(ContentState.createFromText('hello'));
// add more text to the editor state
setupWithTimestamp({ ...requiredProps });
expect(instance.state.isTimestampToggledOn).toEqual(false);
// set the timestamp prepended state to true
instance.toggleTimestamp(dummyEditorState, true);
expect(instance.state.isTimestampToggledOn).toEqual(true);
const editorState = instance.state.internalEditorState;
// copy the editor state
const newEditorStateWithTimestamp = EditorState.createWithContent(editorState.getCurrentContent());
instance.handleChange(newEditorStateWithTimestamp);
expect(instance.state.isTimestampToggledOn).toEqual(true);
});
test('should update timestamp prepended state to false when content changes and timestamp entity is no longer present', () => {
const dummyEditorStateWithoutTimestamp = EditorState.createWithContent(
ContentState.createFromText('hello'),
);
setupWithTimestamp({ ...requiredProps, editorState: dummyEditorStateWithoutTimestamp });
instance.toggleTimestamp(dummyEditorStateWithoutTimestamp, true);
// set the timestamp prepended state to true
expect(instance.state.isTimestampToggledOn).toEqual(true);
instance.handleChange(dummyEditorStateWithoutTimestamp);
expect(instance.state.isTimestampToggledOn).toEqual(false);
});
test('should still set timestamp prepended state to false when content changes and no editor state is present', () => {
const dummyEditorStateWithoutTimestamp = EditorState.createWithContent(
ContentState.createFromText('hello'),
);
setupWithTimestamp({ ...requiredProps, editorState: dummyEditorStateWithoutTimestamp });
instance.toggleTimestamp(dummyEditorStateWithoutTimestamp, true);
instance.setState({ internalEditorState: null });
instance.handleChange(dummyEditorStateWithoutTimestamp);
expect(instance.state.isTimestampToggledOn).toEqual(false);
});
});
describe('handleValidityStateUpdateHandler()', () => {
[
{
isTouched: true,
},
{
isTouched: false,
},
].forEach(({ isTouched }) => {
const err = 'oh no';
const wrapper = shallow(<DraftJSMentionSelector {...requiredProps} />);
const instance = wrapper.instance();
beforeEach(() => {
wrapper.setState({ isTouched });
sandbox.stub(instance, 'getErrorFromValidityState').returns(err);
});
afterEach(() => {
instance.handleValidityStateUpdateHandler();
});
if (isTouched) {
test('should update state', () => {
sandbox.mock(instance).expects('setState').withArgs({ error: err });
});
} else {
test('should not update state', () => {
sandbox.mock(instance).expects('setState').never();
});
}
});
});
describe('checkValidity()', () => {
test('should call handleValidityStateUpdateHandler when called', () => {
const wrapper = shallow(<DraftJSMentionSelector {...requiredProps} />);
const instance = wrapper.instance();
sandbox.mock(instance).expects('handleValidityStateUpdateHandler');
instance.checkValidity();
});
});
describe('isEditorStateEmpty', () => {
const emptyEditorState = EditorState.createEmpty();
const contentState = ContentState.createFromText('');
const editorStateWithChangeType = EditorState.push(emptyEditorState, contentState, 'backspace-character');
const nonEmptyEditorState = EditorState.createWithContent(ContentState.createFromText('hello'));
test.each`
testcase | editorState | expectedResult
${'empty'} | ${emptyEditorState} | ${true}
${'not empty'} | ${nonEmptyEditorState} | ${false}
${'has change type'} | ${editorStateWithChangeType} | ${false}
`('should return whether the editor state is empty or not: $testcase', ({ editorState, expectedResult }) => {
const wrapper = shallow(<DraftJSMentionSelector {...requiredProps} />);
const instance = wrapper.instance();
expect(instance.isEditorStateEmpty(editorState)).toEqual(expectedResult);
});
});
describe('getVideoTimestamp()', () => {
afterEach(() => {
jest.restoreAllMocks();
});
test('should return the correct video timestamp', () => {
jest.spyOn(document, 'querySelector').mockImplementation(() => {
return {
querySelector: () => {
return { currentTime: 70 };
},
};
});
const wrapper = shallow(<DraftJSMentionSelector {...requiredProps} />);
const instance = wrapper.instance();
const { timestamp, timestampInMilliseconds } = instance.getVideoTimestamp();
expect(timestamp).toEqual('0:01:10');
expect(timestampInMilliseconds).toEqual(70000);
});
test('should return the correct videoe timestamp if it has not been started yet', () => {
const wrapper = shallow(<DraftJSMentionSelector {...requiredProps} />);
jest.spyOn(document, 'querySelector').mockImplementation(() => {
return {
querySelector: () => {
return <video src="http://dummy.mp4" />;
},
};
});
const instance = wrapper.instance();
const { timestamp, timestampInMilliseconds } = instance.getVideoTimestamp();
expect(timestamp).toEqual('0:00:00');
expect(timestampInMilliseconds).toEqual(0);
});
test('should return 0:00:00 if the video is not found', () => {
jest.spyOn(document, 'querySelector').mockImplementation(() => {
return {
querySelector: () => {
return null;
},
};
});
const wrapper = shallow(<DraftJSMentionSelector {...requiredProps} />);
const instance = wrapper.instance();
const { timestamp, timestampInMilliseconds } = instance.getVideoTimestamp();
expect(timestamp).toEqual('0:00:00');
expect(timestampInMilliseconds).toEqual(0);
});
test('should return the correct precision of the timestamp', () => {
jest.spyOn(document, 'querySelector').mockImplementation(() => {
return {
querySelector: () => {
return { currentTime: 176.34 };
},
};
});
const wrapper = shallow(<DraftJSMentionSelector {...requiredProps} />);
const instance = wrapper.instance();
const { timestamp, timestampInMilliseconds } = instance.getVideoTimestamp();
expect(timestamp).toEqual('0:02:56');
expect(timestampInMilliseconds).toEqual(176340);
});
});
describe('video timestamp toggle', () => {
const getTimestampedEnableComponent = () => {
const props = {
...requiredProps,
timestampLabel: 'Toggle Timestamp',
isRequired: true,
fileVersionId: '123',
};
return shallow(<DraftJSMentionSelector {...props} />);
};
beforeEach(() => {
jest.spyOn(document, 'querySelector').mockImplementation(() => {
return {
querySelector: () => {
return { currentTime: 70 };
},
};
});
});
afterEach(() => {
jest.restoreAllMocks();
});
test('should add timestamp to the editor state when the toggle is clicked', () => {
const wrapper = getTimestampedEnableComponent();
const instance = wrapper.instance();
wrapper.find('Toggle').simulate('change', { target: { checked: true } });
expect(instance.state.internalEditorState.getCurrentContent().getPlainText()).toContain('0:01:10');
expect(instance.state.isTimestampToggledOn).toEqual(true);
});
test('should remove timestamp from the editor state when the toggle is clicked off', () => {
const wrapper = getTimestampedEnableComponent();
const instance = wrapper.instance();
wrapper.find('Toggle').simulate('change', { target: { checked: true } });
expect(instance.state.internalEditorState.getCurrentContent().getPlainText()).toContain('0:01:10');
wrapper.find('Toggle').simulate('change', { target: { checked: false } });
expect(instance.state.internalEditorState.getCurrentContent().getPlainText()).not.toContain('0:01:10');
expect(instance.state.isTimestampToggledOn).toEqual(false);
});
test('should add timestamp to the beginning of the editor state when the toggle is clicked on', () => {
const wrapper = getTimestampedEnableComponent();
const instance = wrapper.instance();
instance.setState({
internalEditorState: EditorState.createWithContent(ContentState.createFromText('this is coool!!!')),
});
wrapper.find('Toggle').simulate('change', { target: { checked: true } });
expect(instance.state.internalEditorState.getCurrentContent().getPlainText()).toContain(
'0:01:10 this is coool!!!',
);
});
test('should remove timestamp from the beginning of the editor state when the toggle is clicked off', () => {
const wrapper = getTimestampedEnableComponent();
const instance = wrapper.instance();
instance.setState({
internalEditorState: EditorState.createWithContent(ContentState.createFromText('this is coool!!!')),
});
wrapper.find('Toggle').simulate('change', { target: { checked: true } });
expect(instance.state.internalEditorState.getCurrentContent().getPlainText()).toContain('0:01:10');
wrapper.find('Toggle').simulate('change', { target: { checked: false } });
expect(instance.state.internalEditorState.getCurrentContent().getPlainText()).toEqual('this is coool!!!');
});
test('should add an UNEDITABLE_TIMESTAMP_TEXT entity to the editor state when the toggle is clicked on', () => {
const wrapper = getTimestampedEnableComponent();
const instance = wrapper.instance();
instance.setState({
internalEditorState: EditorState.createWithContent(ContentState.createFromText('this is coool!!!')),
});
wrapper.find('Toggle').simulate('change', { target: { checked: true } });
const rawContentState = convertToRaw(instance.state.internalEditorState.getCurrentContent());
const entity = rawContentState.entityMap[0];
expect(entity.type).toEqual('UNEDITABLE_TIMESTAMP_TEXT');
expect(entity.data.timestampInMilliseconds).toEqual(70000);
expect(entity.data.fileVersionId).toEqual('123');
});
test('should remove the UNEDITABLE_TIMESTAMP_TEXT entity from the editor state when the toggle is clicked off', () => {
const wrapper = getTimestampedEnableComponent();
const instance = wrapper.instance();
instance.setState({
internalEditorState: EditorState.createWithContent(ContentState.createFromText('this is coool!!!')),
});
wrapper.find('Toggle').simulate('change', { target: { checked: true } });
wrapper.find('Toggle').simulate('change', { target: { checked: false } });
const rawContentState = convertToRaw(instance.state.internalEditorState.getCurrentContent());
expect(rawContentState.entityMap).toEqual({});
});
test('decorator should recognize the UNEDITABLE_TIMESTAMP_TEXT entity', () => {
const wrapper = getTimestampedEnableComponent();
const instance = wrapper.instance();
expect(instance.compositeDecorator).toBeDefined();
expect(typeof instance.compositeDecorator.getDecorations).toBe('function');
instance.setState({
internalEditorState: EditorState.createWithContent(ContentState.createFromText('this is coool!!!')),
});
wrapper.find('Toggle').simulate('change', { target: { checked: true } });
// Verify that the decorator strategy would match this entity
const contentState = instance.state.internalEditorState.getCurrentContent();
const firstBlock = contentState.getFirstBlock();
let entityFound = false;
firstBlock.findEntityRanges(
character => {
const entityKey = character.getEntity();
if (
entityKey !== null &&
contentState.getEntity(entityKey).getType() === 'UNEDITABLE_TIMESTAMP_TEXT'
) {
entityFound = true;
return true;
}
return false;
},
() => {},
);
expect(entityFound).toBe(true);
});
test('should set toggle state to off when all content is deleted from the editor and a timestamp was present', () => {
const wrapper = getTimestampedEnableComponent();
const instance = wrapper.instance();
// Set up initial content with timestamp
instance.setState({
internalEditorState: EditorState.createWithContent(ContentState.createFromText('this is some content')),
});
wrapper.find('Toggle').simulate('change', { target: { checked: true } });
expect(instance.state.isTimestampToggledOn).toEqual(true);
// Simulate user deleting all content (including timestamp)
const emptyEditorState = EditorState.createWithContent(ContentState.createFromText(''));
instance.handleChange(emptyEditorState);
// Verify that isTimestampToggledOn is set to false when content is deleted
expect(instance.state.isTimestampToggledOn).toEqual(false);
expect(wrapper.find('Toggle').prop('isOn')).toEqual(false);
});
test('should handle mantain content when timetamp is removed', () => {
const wrapper = getTimestampedEnableComponent();
const instance = wrapper.instance();
// Set up initial content with timestamp
instance.setState({
internalEditorState: EditorState.createWithContent(ContentState.createFromText('this is some content')),
});
wrapper.find('Toggle').simulate('change', { target: { checked: true } });
expect(instance.state.isTimestampToggledOn).toEqual(true);
// Simulate user deleting part of the content but keeping some
const partialContentEditorState = EditorState.createWithContent(
ContentState.createFromText('some content'),
);
instance.handleChange(partialContentEditorState);
// Verify that isTimestampToggledOn is set to false when timestamp is removed
expect(instance.state.isTimestampToggledOn).toEqual(false);
expect(wrapper.find('Toggle').prop('isOn')).toEqual(false);
});
test('should handle backspace deletion of timestamp by user', () => {
const wrapper = getTimestampedEnableComponent();
const instance = wrapper.instance();
// Set up initial content with timestamp
instance.setState({
internalEditorState: EditorState.createWithContent(ContentState.createFromText('this is some content')),
});
wrapper.find('Toggle').simulate('change', { target: { checked: true } });
expect(instance.state.isTimestampToggledOn).toEqual(true);
// Simulate user using backspace to delete the timestamp
// Create an editor state that represents the content after backspace deletion
const contentAfterBackspace = ContentState.createFromText('this is some content');
const editorStateAfterBackspace = EditorState.push(
instance.state.internalEditorState,
contentAfterBackspace,
'backspace-character',
);
instance.handleChange(editorStateAfterBackspace);
// Verify that isTimestampToggledOn is set to false when timestamp is deleted
expect(instance.state.isTimestampToggledOn).toEqual(false);
expect(wrapper.find('Toggle').prop('isOn')).toEqual(false);
});
});
});