box-ui-elements-mlh
Version:
726 lines (560 loc) • 25.7 kB
JavaScript
import React from 'react';
import { Set } from 'immutable';
import sinon from 'sinon';
import makeSelectable from '../makeSelectable';
import shiftSelect from '../shiftSelect';
const sandbox = sinon.sandbox.create();
jest.mock('../shiftSelect');
jest.useFakeTimers();
describe('components/table/makeSelectable', () => {
const Table = (props = {}) => <table {...props} />;
const SelectableTable = makeSelectable(Table);
const data = ['a', 'b', 'c', 'd', 'e'];
const getWrapper = (props = {}) =>
shallow(<SelectableTable onSelect={sandbox.stub()} data={data} selectedItems={[]} enableHotkeys {...props} />);
afterEach(() => {
jest.clearAllTimers();
jest.clearAllMocks();
sandbox.verifyAndRestore();
});
describe('componentDidMount()', () => {
test('should add keypress listener', () => {
document.addEventListener = jest.fn();
const instance = getWrapper().instance();
expect(document.addEventListener).toBeCalledWith('keypress', instance.handleKeyboardSearch);
});
});
describe('componentDidUpdate()', () => {
test('should call onFocus handler when focused index changes', () => {
const onFocus = jest.fn();
const wrapper = getWrapper({
onFocus,
});
wrapper.setState({ focusedIndex: 3 });
expect(onFocus).toBeCalledWith(3);
});
});
describe('componentWillUnmount()', () => {
test('should remove keypress listener', () => {
document.removeEventListener = jest.fn();
const wrapper = getWrapper();
const instance = wrapper.instance();
wrapper.unmount();
expect(document.removeEventListener).toBeCalledWith('keypress', instance.handleKeyboardSearch);
});
});
describe('onSelect()', () => {
test('should set previousIndex to state.focusedIndex, set state.focusedIndex, and call onSelect', () => {
const wrapper = getWrapper({
onSelect: sandbox.mock().withArgs(['c']),
});
wrapper.setState({ focusedIndex: 1 });
const instance = wrapper.instance();
instance.onSelect(new Set(['c']), 2);
expect(instance.previousIndex).toEqual(1);
expect(wrapper.state('focusedIndex')).toEqual(2);
});
test('should call onSelect with Immutable Set when selectedItems is given as a set', () => {
const wrapper = getWrapper({
selectedItems: new Set(['a']),
onSelect: items => {
expect(items.equals(new Set(['c']))).toBe(true);
},
});
wrapper.setState({ focusedIndex: 1 });
const instance = wrapper.instance();
instance.onSelect(new Set(['c']), 2);
});
});
describe('getProcessedProps()', () => {
test('should return selectedItems as Immutable object when given as plain JS', () => {
const wrapper = getWrapper({
selectedItems: ['a'],
});
const instance = wrapper.instance();
const processedProps = instance.getProcessedProps();
expect(processedProps.selectedItems.equals(new Set(['a']))).toBe(true);
});
});
describe('selectToggle()', () => {
test('should remove item from selection when it is selected', () => {
const wrapper = getWrapper({
selectedItems: ['a', 'b'],
});
const instance = wrapper.instance();
instance.onSelect = (selectedItems, focusedIndex) => {
expect(selectedItems.equals(new Set(['a']))).toBe(true);
expect(focusedIndex).toEqual(1);
};
instance.selectToggle(1);
});
test('should add item to selection when it is not selected', () => {
const wrapper = getWrapper({
selectedItems: [],
});
const instance = wrapper.instance();
instance.onSelect = (selectedItems, focusedIndex) => {
expect(selectedItems.equals(new Set(['b']))).toBe(true);
expect(focusedIndex).toEqual(1);
};
instance.selectToggle(1);
});
});
describe('selectRange()', () => {
afterEach(() => {
shiftSelect.mockReset();
});
test('should call shiftSelect with correct args', () => {
const selectedItems = ['a', 'b', 'c'];
const focusedIndex = 1;
const rowIndex = 2;
const anchorIndex = 3;
// expected computed value
const selectedRows = new Set([0, 1, 2]);
shiftSelect.mockImplementation(() => new Set([1, 2, 3]));
const wrapper = getWrapper({
selectedItems,
});
const instance = wrapper.instance();
instance.previousIndex = focusedIndex;
instance.anchorIndex = anchorIndex;
instance.onSelect = newSelectedItems => {
// newSelectedItems should be the item-mapped equivalent of
// [1, 2, 3] returned from the call to shiftSelect
expect(newSelectedItems.equals(new Set(['b', 'c', 'd']))).toBe(true);
};
instance.selectRange(rowIndex);
expect(shiftSelect).toHaveBeenCalledWith(selectedRows, focusedIndex, rowIndex, anchorIndex);
});
test('should not change selection if clicking on same row', () => {
const wrapper = getWrapper();
const instance = wrapper.instance();
instance.previousIndex = 1;
instance.selectRange(1);
expect(shiftSelect).not.toHaveBeenCalled();
});
});
describe('selectOne()', () => {
test('should not change selection when clicking on the only already-selected row', () => {
const wrapper = getWrapper({
selectedItems: ['a'],
});
const instance = wrapper.instance();
instance.onSelect = sandbox.mock().never();
instance.selectOne(0);
});
test('should set selection to contain only the target item', () => {
const wrapper = getWrapper({
selectedItems: ['a', 'b'],
});
const instance = wrapper.instance();
instance.onSelect = (selectedItems, focusedIndex) => {
expect(selectedItems.equals(new Set(['c']))).toBe(true);
expect(focusedIndex).toEqual(2);
};
instance.selectOne(2);
});
});
describe('handleRowClick()', () => {
test('should call selectToggle() when meta key pressed', () => {
const wrapper = getWrapper();
const instance = wrapper.instance();
const index = 1;
instance.selectToggle = sandbox.mock().withArgs(index);
instance.handleRowClick({ metaKey: true }, index);
});
test('should call selectToggle() when ctrl key pressed', () => {
const wrapper = getWrapper();
const instance = wrapper.instance();
const index = 1;
instance.selectToggle = sandbox.mock().withArgs(index);
instance.handleRowClick({ ctrlKey: true }, index);
});
test('should call selectRange() when shift key pressed', () => {
const wrapper = getWrapper();
const instance = wrapper.instance();
const index = 1;
instance.selectRange = sandbox.mock().withArgs(index);
instance.handleRowClick({ shiftKey: true }, index);
});
test('should call selectOne() when no modifier key pressed', () => {
const wrapper = getWrapper();
const instance = wrapper.instance();
const index = 1;
instance.selectOne = sandbox.mock().withArgs(index);
instance.handleRowClick({}, index);
});
});
describe('handleRowFocus()', () => {
test('should call onSelect() with correct args', () => {
const wrapper = getWrapper({
selectedItems: ['a'],
});
const instance = wrapper.instance();
instance.onSelect = (selectedItems, focusedIndex) => {
expect(selectedItems.equals(new Set(['a']))).toBe(true);
expect(focusedIndex).toEqual(2);
};
instance.handleRowFocus({}, 2);
});
});
describe('handleShiftKeyDown()', () => {
test('should be no-op when target is the boundary and already selected', () => {
const wrapper = getWrapper({
selectedItems: ['a'],
onSelect: sandbox.mock().never(),
});
wrapper.setState({ focusedIndex: 0 });
wrapper.instance().handleShiftKeyDown(0, 0);
});
test('should select target when it is not already selected', () => {
const wrapper = getWrapper({
selectedItems: [],
});
wrapper.setState({ focusedIndex: 1 });
const instance = wrapper.instance();
instance.onSelect = (selectedItems, focusedIndex) => {
expect(selectedItems.equals(new Set(['a']))).toBe(true);
expect(focusedIndex).toEqual(0);
};
instance.handleShiftKeyDown(0, 0);
});
test('should deselect source when both source and target are selected', () => {
const wrapper = getWrapper({
selectedItems: ['a', 'b'],
});
wrapper.setState({ focusedIndex: 0 });
const instance = wrapper.instance();
instance.onSelect = (selectedItems, focusedIndex) => {
expect(selectedItems.equals(new Set(['b']))).toBe(true);
expect(focusedIndex).toEqual(1);
};
instance.handleShiftKeyDown(1, 4);
});
test('should select source when target is selected but not source', () => {
const wrapper = getWrapper({
selectedItems: ['b'],
});
wrapper.setState({ focusedIndex: 0 });
const instance = wrapper.instance();
instance.onSelect = (selectedItems, focusedIndex) => {
expect(selectedItems.equals(new Set(['a', 'b']))).toBe(true);
expect(focusedIndex).toEqual(1);
};
instance.handleShiftKeyDown(1, 0);
});
});
describe('clearFocus()', () => {
test('should clear focus', () => {
const wrapper = getWrapper();
const instance = wrapper.instance();
wrapper.setState({ focusedIndex: 1 });
instance.clearFocus();
expect(wrapper.state('focusedIndex')).toBeUndefined();
});
});
describe('blur detection', () => {
test('should not set timer when table does not have focus', () => {
const wrapper = getWrapper();
const instance = wrapper.instance();
instance.clearFocus = jest.fn();
wrapper.setState({ focusedIndex: undefined });
instance.handleTableBlur();
jest.runAllTimers();
expect(instance.blurTimerID).toBeNull();
expect(instance.clearFocus).toBeCalledTimes(0);
});
test('should clear focus after timeout expires', () => {
const wrapper = getWrapper();
const instance = wrapper.instance();
instance.clearFocus = jest.fn();
wrapper.setState({ focusedIndex: 1 });
instance.handleTableBlur();
jest.runAllTimers();
expect(instance.clearFocus).toBeCalledTimes(1);
});
test('should not clear focus if focus is regained before timeout expires', () => {
const wrapper = getWrapper();
const instance = wrapper.instance();
instance.clearFocus = jest.fn();
wrapper.setState({ focusedIndex: 1 });
instance.handleTableBlur();
instance.handleTableFocus(); // regain focus
jest.runAllTimers();
expect(instance.clearFocus).toBeCalledTimes(0);
});
});
describe('keyboard shortcuts', () => {
test('should set and return this.hotkeys when this.hotkeys is null', () => {
const instance = getWrapper({}).instance();
// should be initially null
instance.hotkeys = null;
const shortcuts = instance.getHotkeyConfigs();
// should not be null anymore
expect(instance.hotkeys).not.toBeNull();
expect(shortcuts).toEqual(instance.hotkeys);
});
test('should use correct description and hotkey type for all shortcuts', () => {
const hotkeyType = 'item selection';
const instance = getWrapper({
hotkeyType,
}).instance();
const shortcuts = instance.getHotkeyConfigs();
shortcuts.forEach(shortcut => {
expect(shortcut.description).toBeTruthy();
expect(shortcut.type).toEqual(hotkeyType);
});
});
describe('down', () => {
const hotkeyIndex = 0;
test('should set focus to first row when no currently focused item', () => {
const wrapper = getWrapper({
selectedItems: ['a'],
});
wrapper.setState({ focusedIndex: undefined });
const instance = wrapper.instance();
const shortcut = instance.getHotkeyConfigs()[hotkeyIndex];
shortcut.handler({ preventDefault: sandbox.stub() });
expect(wrapper.state('focusedIndex')).toEqual(0);
});
test('should call event.preventDefault() and set focus to next item', () => {
const wrapper = getWrapper({
selectedItems: ['a'],
});
wrapper.setState({ focusedIndex: 0 });
const instance = wrapper.instance();
const shortcut = instance.getHotkeyConfigs()[hotkeyIndex];
shortcut.handler({ preventDefault: sandbox.mock() });
expect(wrapper.state('focusedIndex')).toEqual(1);
});
test('should not focus on an index higher than the highest index in the table', () => {
const wrapper = getWrapper({
selectedItems: ['a'],
});
wrapper.setState({ focusedIndex: 4 });
const instance = wrapper.instance();
const shortcut = instance.getHotkeyConfigs()[hotkeyIndex];
shortcut.handler({ preventDefault: sandbox.stub() });
expect(wrapper.state('focusedIndex')).toEqual(4);
});
});
describe('up', () => {
const hotkeyIndex = 1;
test('should call event.preventDefault() and call onSelect with new focused item', () => {
const wrapper = getWrapper({
selectedItems: ['a'],
});
wrapper.setState({ focusedIndex: 1 });
const instance = wrapper.instance();
const shortcut = instance.getHotkeyConfigs()[hotkeyIndex];
shortcut.handler({ preventDefault: sandbox.mock() });
expect(wrapper.state('focusedIndex')).toEqual(0);
});
test('should not focus on an index lower than 0', () => {
const wrapper = getWrapper({
selectedItems: ['a'],
});
wrapper.setState({ focusedIndex: 0 });
const instance = wrapper.instance();
const shortcut = instance.getHotkeyConfigs()[hotkeyIndex];
shortcut.handler({ preventDefault: sandbox.stub() });
expect(wrapper.state('focusedIndex')).toEqual(0);
});
});
describe('shift+x', () => {
const hotkeyIndex = 2;
test('should be no-op when focusedIndex is undefined', () => {
const wrapper = getWrapper({
onSelect: sandbox.mock().never(),
});
wrapper.setState({ focusedIndex: undefined });
const instance = wrapper.instance();
const shortcut = instance.getHotkeyConfigs()[hotkeyIndex];
shortcut.handler();
});
test('should call selectToggle on focused item', () => {
const wrapper = getWrapper({
focusedItem: 'b',
selectedItems: ['a'],
});
wrapper.setState({ focusedIndex: 1 });
const instance = wrapper.instance();
instance.selectToggle = sandbox.mock().withArgs(1);
const shortcut = instance.getHotkeyConfigs()[hotkeyIndex];
shortcut.handler();
});
});
describe('meta+a / ctrl+a', () => {
const hotkeyIndex = 3;
test('should call event.preventDefault() and select all items', () => {
const wrapper = getWrapper({
selectedItems: [],
});
wrapper.setState({ focusedIndex: 1 });
const instance = wrapper.instance();
instance.onSelect = (selectedItems, focusedIndex) => {
expect(selectedItems.equals(new Set(data))).toBe(true);
expect(focusedIndex).toEqual(1);
};
const shortcut = instance.getHotkeyConfigs()[hotkeyIndex];
shortcut.handler({ preventDefault: sandbox.mock() });
});
});
describe('shift+down', () => {
const hotkeyIndex = 4;
test('should be no-op when focusedIndex is undefined', () => {
const wrapper = getWrapper({
onSelect: sandbox.mock().never(),
});
wrapper.setState({ focusedIndex: undefined });
const instance = wrapper.instance();
const shortcut = instance.getHotkeyConfigs()[hotkeyIndex];
shortcut.handler();
});
test('should call handleShiftKeyDown() with the index of the next item in the table', () => {
const wrapper = getWrapper({
selectedItems: ['a'],
});
wrapper.setState({ focusedIndex: 0 });
const instance = wrapper.instance();
instance.handleShiftKeyDown = sandbox.mock().withArgs(1, data.length - 1);
const shortcut = instance.getHotkeyConfigs()[hotkeyIndex];
shortcut.handler();
});
test('should not call handleShiftKeyDown() with an index greater than the highest index', () => {
const wrapper = getWrapper({
selectedItems: ['a'],
});
wrapper.setState({ focusedIndex: 4 });
const instance = wrapper.instance();
instance.handleShiftKeyDown = sandbox.mock().withArgs(data.length - 1, data.length - 1);
const shortcut = instance.getHotkeyConfigs()[hotkeyIndex];
shortcut.handler();
});
});
describe('shift+up', () => {
const hotkeyIndex = 5;
test('should be no-op when focusedIndex is undefined', () => {
const wrapper = getWrapper({
onSelect: sandbox.mock().never(),
});
wrapper.setState({ focusedIndex: undefined });
const instance = wrapper.instance();
const shortcut = instance.getHotkeyConfigs()[hotkeyIndex];
shortcut.handler();
});
test('should call handleShiftKeyDown() with index of the next item in the table', () => {
const wrapper = getWrapper({
focusedItem: 'b',
selectedItems: ['a'],
});
wrapper.setState({ focusedIndex: 1 });
const instance = wrapper.instance();
instance.handleShiftKeyDown = sandbox.mock().withArgs(0, 0);
const shortcut = instance.getHotkeyConfigs()[hotkeyIndex];
shortcut.handler();
});
test('should not call handleShiftKeyDown() with an index lower than 0', () => {
const wrapper = getWrapper({
focusedItem: 'a',
selectedItems: ['a'],
});
wrapper.setState({ focusedIndex: 0 });
const instance = wrapper.instance();
instance.handleShiftKeyDown = sandbox.mock().withArgs(0, 0);
const shortcut = instance.getHotkeyConfigs()[hotkeyIndex];
shortcut.handler();
});
});
describe('esc', () => {
const hotkeyIndex = 6;
test('should set selection to empty', () => {
const wrapper = getWrapper({
selectedItems: ['a', 'b', 'c'],
});
wrapper.setState({ focusedIndex: 1 });
const instance = wrapper.instance();
instance.onSelect = (selectedItems, focusedIndex) => {
expect(selectedItems.equals(new Set([]))).toBe(true);
expect(focusedIndex).toEqual(1);
};
const shortcut = instance.getHotkeyConfigs()[hotkeyIndex];
shortcut.handler();
});
});
});
describe('handleKeyboardSearch()', () => {
const searchStrings = ['abc', 'bcd', 'cde', 'def', 'efg'];
const target = document.createElement('div');
const getWrapperWithSearchStrings = () => getWrapper({ searchStrings });
test('should set index correctly to matching string', () => {
const wrapper = getWrapperWithSearchStrings();
const instance = wrapper.instance();
instance.searchString = 'd';
instance.handleKeyboardSearch({ target, key: 'e' });
expect(wrapper.state('focusedIndex')).toEqual(3); // should focus on "def"
});
test('should reset searchString after 1 second', () => {
const wrapper = getWrapperWithSearchStrings();
const instance = wrapper.instance();
// start typing "c"
instance.handleKeyboardSearch({ target, key: 'c' });
jest.advanceTimersByTime(1001);
// type "d"
instance.handleKeyboardSearch({ target, key: 'd' });
// should match "def" rather than "cde" due to timeout
expect(wrapper.state('focusedIndex')).toEqual(3);
});
test('should not change focused index when no string match', () => {
const wrapper = getWrapperWithSearchStrings();
wrapper.setState({ focusedIndex: 3 });
const instance = wrapper.instance();
instance.handleKeyboardSearch({ target, key: 'z' });
expect(wrapper.state('focusedIndex')).toEqual(3); // should not change
});
test('should be noop when event target is contenteditable', () => {
const wrapper = getWrapperWithSearchStrings();
wrapper.setState({ focusedIndex: 3 });
const instance = wrapper.instance();
instance.handleKeyboardSearch({
target: { hasAttribute: () => true },
key: 'a',
});
expect(wrapper.state('focusedIndex')).toEqual(3); // should not change
});
[
{
nodeName: 'INPUT',
},
{
nodeName: 'TEXTAREA',
},
].forEach(({ nodeName }) => {
test('should be noop when event target is text field', () => {
const wrapper = getWrapperWithSearchStrings();
wrapper.setState({ focusedIndex: 3 });
const instance = wrapper.instance();
instance.handleKeyboardSearch({
target: { hasAttribute: () => false, nodeName },
key: 'a',
});
expect(wrapper.state('focusedIndex')).toEqual(3); // should not change
});
});
});
describe('render()', () => {
test('should add "is-selectable" class and pass props to table', () => {
const wrapper = getWrapper({});
const instance = wrapper.instance();
wrapper.setState({ focusedIndex: 1 });
const table = wrapper.find('Table');
expect(table.hasClass('is-selectable')).toBe(true);
expect(table.prop('onRowClick')).toEqual(wrapper.instance().handleRowClick);
expect(table.prop('onRowFocus')).toEqual(wrapper.instance().handleRowFocus);
expect(table.prop('focusedItem')).toEqual('b');
expect(table.prop('focusedIndex')).toEqual(1);
expect(table.prop('onTableBlur')).toBe(instance.handleTableBlur);
expect(table.prop('onTableFocus')).toBe(instance.handleTableFocus);
});
});
});