kibana/x-pack/plugins/lens/public/drag_drop/drag_drop.test.tsx
2021-03-29 14:07:39 +02:00

1313 lines
42 KiB
TypeScript

/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { render, mount, ReactWrapper } from 'enzyme';
import { DragDrop } from './drag_drop';
import {
ChildDragDropProvider,
DragContextState,
ReorderProvider,
DragDropIdentifier,
DraggingIdentifier,
DropIdentifier,
} from './providers';
import { act } from 'react-dom/test-utils';
import { DropType } from '../types';
jest.useFakeTimers();
const dataTransfer = {
setData: jest.fn(),
getData: jest.fn(),
};
describe('DragDrop', () => {
const defaultContext = {
dragging: undefined,
setDragging: jest.fn(),
setActiveDropTarget: jest.fn(),
activeDropTarget: undefined,
dropTargetsByOrder: undefined,
keyboardMode: false,
setKeyboardMode: () => {},
setA11yMessage: jest.fn(),
registerDropTarget: jest.fn(),
};
const value = {
id: '1',
humanData: { label: 'hello', groupLabel: 'X', position: 1, canSwap: true, canDuplicate: true },
};
test('renders if nothing is being dragged', () => {
const component = render(
<DragDrop value={value} draggable order={[2, 0, 1, 0]}>
<button>Hello!</button>
</DragDrop>
);
expect(component).toMatchSnapshot();
});
test('dragover calls preventDefault if dropType is defined', () => {
const preventDefault = jest.fn();
const component = mount(
<DragDrop dropTypes={['field_add']} value={value} order={[2, 0, 1, 0]}>
<button>Hello!</button>
</DragDrop>
);
component.find('[data-test-subj="lnsDragDrop"]').at(0).simulate('dragover', { preventDefault });
expect(preventDefault).toBeCalled();
});
test('dragover does not call preventDefault if dropTypes is undefined', () => {
const preventDefault = jest.fn();
const component = mount(
<DragDrop value={value} order={[2, 0, 1, 0]}>
<button>Hello!</button>
</DragDrop>
);
component.find('[data-test-subj="lnsDragDrop"]').at(0).simulate('dragover', { preventDefault });
expect(preventDefault).not.toBeCalled();
});
test('removes selection on mouse down before dragging', async () => {
const removeAllRanges = jest.fn();
global.getSelection = jest.fn(() => (({ removeAllRanges } as unknown) as Selection));
const component = mount(
<DragDrop value={value} draggable={true} order={[2, 0, 1, 0]}>
<button>Hi!</button>
</DragDrop>
);
component.find('[data-test-subj="lnsDragDrop"]').at(0).simulate('mousedown');
expect(global.getSelection).toBeCalled();
expect(removeAllRanges).toBeCalled();
});
test('dragstart sets dragging in the context and calls it with proper params', async () => {
const setDragging = jest.fn();
const setA11yMessage = jest.fn();
const component = mount(
<ChildDragDropProvider
{...defaultContext}
dragging={undefined}
setDragging={setDragging}
setA11yMessage={setA11yMessage}
>
<DragDrop value={value} draggable={true} order={[2, 0, 1, 0]}>
<button>Hi!</button>
</DragDrop>
</ChildDragDropProvider>
);
component.find('[data-test-subj="lnsDragDrop"]').at(0).simulate('dragstart', { dataTransfer });
act(() => {
jest.runAllTimers();
});
expect(dataTransfer.setData).toBeCalledWith('text', 'hello');
expect(setDragging).toBeCalledWith({ ...value });
expect(setA11yMessage).toBeCalledWith('Lifted hello');
});
test('drop resets all the things', async () => {
const preventDefault = jest.fn();
const stopPropagation = jest.fn();
const setDragging = jest.fn();
const onDrop = jest.fn();
const component = mount(
<ChildDragDropProvider
{...defaultContext}
dragging={{ id: '2', humanData: { label: 'Label1' } }}
setDragging={setDragging}
>
<DragDrop onDrop={onDrop} dropTypes={['field_add']} value={value} order={[2, 0, 1, 0]}>
<button>Hello!</button>
</DragDrop>
</ChildDragDropProvider>
);
const dragDrop = component.find('[data-test-subj="lnsDragDrop"]').at(0);
dragDrop.simulate('dragOver');
dragDrop.simulate('drop', { preventDefault, stopPropagation });
expect(preventDefault).toBeCalled();
expect(stopPropagation).toBeCalled();
expect(setDragging).toBeCalledWith(undefined);
expect(onDrop).toBeCalledWith({ id: '2', humanData: { label: 'Label1' } }, 'field_add');
});
test('drop function is not called on dropTypes undefined', async () => {
const preventDefault = jest.fn();
const stopPropagation = jest.fn();
const setDragging = jest.fn();
const onDrop = jest.fn();
const component = mount(
<ChildDragDropProvider
{...defaultContext}
dragging={{ id: 'hi', humanData: { label: 'Label1' } }}
setDragging={setDragging}
>
<DragDrop onDrop={onDrop} dropTypes={undefined} value={value} order={[2, 0, 1, 0]}>
<button>Hello!</button>
</DragDrop>
</ChildDragDropProvider>
);
const dragDrop = component.find('[data-test-subj="lnsDragDrop"]').at(0);
dragDrop.simulate('dragover');
dragDrop.simulate('drop', { preventDefault, stopPropagation });
expect(preventDefault).not.toHaveBeenCalled();
expect(stopPropagation).not.toHaveBeenCalled();
expect(setDragging).not.toHaveBeenCalled();
expect(onDrop).not.toHaveBeenCalled();
});
test('defined dropTypes is reflected in the className', () => {
const component = render(
<DragDrop
onDrop={(x: unknown) => {
throw x;
}}
dropTypes={['field_add']}
value={value}
order={[2, 0, 1, 0]}
>
<button>Hello!</button>
</DragDrop>
);
expect(component).toMatchSnapshot();
});
test('items that has dropTypes=undefined get special styling when another item is dragged', () => {
const component = mount(
<ChildDragDropProvider {...defaultContext} dragging={{ ...value }}>
<DragDrop value={value} draggable={true} order={[2, 0, 1, 0]}>
<button>Hello!</button>
</DragDrop>
<DragDrop
order={[2, 0, 1, 0]}
onDrop={(x: unknown) => {}}
dropTypes={undefined}
value={{ id: '2', humanData: { label: 'label2' } }}
>
<button>Hello!</button>
</DragDrop>
</ChildDragDropProvider>
);
expect(component.find('[data-test-subj="lnsDragDrop"]').at(1)).toMatchSnapshot();
});
test('additional styles are reflected in the className until drop', () => {
let dragging: { id: '1'; humanData: { label: 'Label1' } } | undefined;
const getAdditionalClassesOnEnter = jest.fn().mockReturnValue('additional');
const getAdditionalClassesOnDroppable = jest.fn().mockReturnValue('droppable');
const setA11yMessage = jest.fn();
const component = mount(
<ChildDragDropProvider
{...defaultContext}
dragging={dragging}
setA11yMessage={setA11yMessage}
setDragging={() => {
dragging = { id: '1', humanData: { label: 'Label1' } };
}}
>
<DragDrop
value={{ id: '3', humanData: { label: 'ignored' } }}
draggable={true}
order={[2, 0, 1, 0]}
>
<button>Hello!</button>
</DragDrop>
<DragDrop
order={[2, 0, 1, 0]}
value={value}
onDrop={(x: unknown) => {}}
dropTypes={['field_add']}
getAdditionalClassesOnEnter={getAdditionalClassesOnEnter}
getAdditionalClassesOnDroppable={getAdditionalClassesOnDroppable}
>
<button>Hello!</button>
</DragDrop>
</ChildDragDropProvider>
);
component
.find('[data-test-subj="lnsDragDrop"]')
.first()
.simulate('dragstart', { dataTransfer });
act(() => {
jest.runAllTimers();
});
expect(setA11yMessage).toBeCalledWith('Lifted ignored');
const dragDrop = component.find('[data-test-subj="lnsDragDrop"]').at(1);
dragDrop.simulate('dragOver');
dragDrop.simulate('drop');
expect(component.find('.additional')).toHaveLength(0);
});
test('additional enter styles are reflected in the className until dragleave', () => {
let dragging: { id: '1'; humanData: { label: 'Label1' } } | undefined;
const getAdditionalClasses = jest.fn().mockReturnValue('additional');
const getAdditionalClassesOnDroppable = jest.fn().mockReturnValue('droppable');
const setActiveDropTarget = jest.fn();
const component = mount(
<ChildDragDropProvider
setA11yMessage={jest.fn()}
dragging={dragging}
setDragging={() => {
dragging = { id: '1', humanData: { label: 'Label1' } };
}}
setActiveDropTarget={setActiveDropTarget}
activeDropTarget={value as DragContextState['activeDropTarget']}
keyboardMode={false}
setKeyboardMode={(keyboardMode) => true}
dropTargetsByOrder={undefined}
registerDropTarget={jest.fn()}
>
<DragDrop
value={{ id: '3', humanData: { label: 'ignored' } }}
draggable={true}
order={[2, 0, 1, 0]}
>
<button>Hello!</button>
</DragDrop>
<DragDrop
order={[2, 0, 1, 0]}
value={value}
onDrop={(x: unknown) => {}}
dropTypes={['field_add']}
getAdditionalClassesOnEnter={getAdditionalClasses}
getAdditionalClassesOnDroppable={getAdditionalClassesOnDroppable}
>
<button>Hello!</button>
</DragDrop>
</ChildDragDropProvider>
);
component
.find('[data-test-subj="lnsDragDrop"]')
.first()
.simulate('dragstart', { dataTransfer });
act(() => {
jest.runAllTimers();
});
component.find('[data-test-subj="lnsDragDrop"]').at(1).simulate('dragover');
expect(component.find('.additional')).toHaveLength(2);
component.find('[data-test-subj="lnsDragDrop"]').at(1).simulate('dragleave');
expect(setActiveDropTarget).toBeCalledWith(undefined);
});
describe('Keyboard navigation', () => {
test('User receives proper drop Targets highlighted when pressing arrow keys', () => {
const onDrop = jest.fn();
const setActiveDropTarget = jest.fn();
const setA11yMessage = jest.fn();
const items = [
{
draggable: true,
value: {
id: '1',
humanData: { label: 'Label1', position: 1 },
},
children: '1',
order: [2, 0, 0, 0],
},
{
draggable: true,
dragType: 'move' as 'copy' | 'move',
value: {
id: '2',
humanData: { label: 'label2', position: 1 },
},
onDrop,
dropTypes: ['move_compatible'] as DropType[],
order: [2, 0, 1, 0],
},
{
draggable: true,
dragType: 'move' as 'copy' | 'move',
value: {
id: '3',
humanData: {
label: 'label3',
position: 1,
groupLabel: 'Y',
canSwap: true,
canDuplicate: true,
},
},
onDrop,
dropTypes: [
'replace_compatible',
'duplicate_compatible',
'swap_compatible',
] as DropType[],
order: [2, 0, 2, 0],
},
{
draggable: true,
dragType: 'move' as 'copy' | 'move',
value: {
id: '4',
humanData: { label: 'label4', position: 2, groupLabel: 'Y' },
},
order: [2, 0, 2, 1],
},
];
const component = mount(
<ChildDragDropProvider
{...{
...defaultContext,
dragging: { ...items[0].value, ghost: { children: <div />, style: {} } },
setActiveDropTarget,
setA11yMessage,
activeDropTarget: { ...items[1].value, onDrop, dropType: 'move_compatible' },
dropTargetsByOrder: {
'2,0,1,0': { ...items[1].value, onDrop, dropType: 'move_compatible' },
'2,0,2,0,0': { ...items[2].value, onDrop, dropType: 'replace_compatible' },
'2,0,1,0,1': { ...items[1].value, onDrop, dropType: 'duplicate_compatible' },
'2,0,1,0,2': { ...items[1].value, onDrop, dropType: 'swap_compatible' },
},
keyboardMode: true,
}}
>
{items.map((props) => (
<DragDrop {...props} key={props.value.id}>
<div />
</DragDrop>
))}
</ChildDragDropProvider>
);
const keyboardHandler = component
.find('[data-test-subj="lnsDragDrop-keyboardHandler"]')
.first()
.simulate('focus');
keyboardHandler.simulate('keydown', { key: 'ArrowRight' });
expect(setActiveDropTarget).toBeCalledWith({
...items[2].value,
onDrop,
dropType: items[2].dropTypes![0],
});
keyboardHandler.simulate('keydown', { key: 'Enter' });
expect(setA11yMessage).toBeCalledWith(
`You're dragging Label1 from at position 1 over label3 from Y group at position 1. Press space or enter to replace label3 with Label1. Hold alt or option to duplicate. Hold shift to swap.`
);
expect(setActiveDropTarget).toBeCalledWith(undefined);
expect(onDrop).toBeCalledWith(
{ humanData: { label: 'Label1', position: 1 }, id: '1' },
'move_compatible'
);
});
test('dragstart sets dragging in the context and calls it with proper params', async () => {
const setDragging = jest.fn();
const setA11yMessage = jest.fn();
const component = mount(
<ChildDragDropProvider
{...defaultContext}
dragging={undefined}
setDragging={setDragging}
setA11yMessage={setA11yMessage}
keyboardMode={false}
>
<DragDrop value={value} draggable={true} order={[2, 0, 1, 0]}>
<button>Hi!</button>
</DragDrop>
</ChildDragDropProvider>
);
const keyboardHandler = component
.find('[data-test-subj="lnsDragDrop-keyboardHandler"]')
.first()
.simulate('focus');
keyboardHandler.simulate('keydown', { key: 'Enter' });
act(() => {
jest.runAllTimers();
});
expect(setDragging).toBeCalledWith({
...value,
ghost: {
children: <button>Hi!</button>,
style: {
height: 0,
width: 0,
},
},
});
expect(setA11yMessage).toBeCalledWith('Lifted hello');
});
test('ActiveDropTarget gets ghost image', () => {
const onDrop = jest.fn();
const setActiveDropTarget = jest.fn();
const setA11yMessage = jest.fn();
const items = [
{
draggable: true,
value: {
id: '1',
humanData: { label: 'Label1', position: 1 },
},
children: '1',
order: [2, 0, 0, 0],
},
{
draggable: true,
dragType: 'move' as 'copy' | 'move',
value: {
id: '2',
humanData: { label: 'label2', position: 1 },
},
onDrop,
dropTypes: ['move_compatible'] as DropType[],
order: [2, 0, 1, 0],
},
];
const component = mount(
<ChildDragDropProvider
{...{
...defaultContext,
dragging: { ...items[0].value, ghost: { children: <div>Hello</div>, style: {} } },
setActiveDropTarget,
setA11yMessage,
activeDropTarget: { ...items[1].value, onDrop, dropType: 'move_compatible' },
dropTargetsByOrder: {
'2,0,1,0': { ...items[1].value, onDrop, dropType: 'move_compatible' },
},
keyboardMode: true,
}}
>
{items.map((props) => (
<DragDrop {...props} key={props.value.id}>
<div />
</DragDrop>
))}
</ChildDragDropProvider>
);
expect(component.find(DragDrop).at(1).find('.lnsDragDrop_ghost').text()).toEqual('Hello');
});
});
describe('multiple drop targets', () => {
let activeDropTarget: DragContextState['activeDropTarget'];
const onDrop = jest.fn();
let setActiveDropTarget = jest.fn();
const setA11yMessage = jest.fn();
let component: ReactWrapper;
beforeEach(() => {
activeDropTarget = undefined;
setActiveDropTarget = jest.fn((val) => {
activeDropTarget = value as DragContextState['activeDropTarget'];
});
component = mount(
<ChildDragDropProvider
setA11yMessage={jest.fn()}
dragging={{ id: '1', humanData: { label: 'Label1' } }}
setDragging={jest.fn()}
setActiveDropTarget={setActiveDropTarget}
activeDropTarget={activeDropTarget}
keyboardMode={false}
setKeyboardMode={(keyboardMode) => true}
dropTargetsByOrder={undefined}
registerDropTarget={jest.fn()}
>
<DragDrop
value={{ id: '3', humanData: { label: 'ignored' } }}
draggable={true}
order={[2, 0, 1, 0]}
>
<button>Drag this</button>
</DragDrop>
<DragDrop
order={[2, 0, 1, 0]}
value={value}
onDrop={onDrop}
dropTypes={['move_compatible', 'duplicate_compatible', 'swap_compatible']}
getCustomDropTarget={(dropType) => <div className="extraDrop">{dropType}</div>}
>
<button>Drop here</button>
</DragDrop>
</ChildDragDropProvider>
);
});
test('extra drop targets render correctly', () => {
expect(component.find('.extraDrop').hostNodes()).toHaveLength(2);
});
test('extra drop targets appear when dragging over and disappear when activeDropTarget changes', () => {
component.find('[data-test-subj="lnsDragDropContainer"]').first().simulate('dragenter');
// customDropTargets are visible
expect(component.find('[data-test-subj="lnsDragDropContainer"]').prop('className')).toEqual(
'lnsDragDrop__container lnsDragDrop__container-active'
);
expect(
component.find('[data-test-subj="lnsDragDropExtraDrops"]').first().prop('className')
).toEqual('lnsDragDrop__extraDrops lnsDragDrop__extraDrops-visible');
// set activeDropTarget as undefined
component.find('[data-test-subj="lnsDragDrop"]').at(1).simulate('dragleave');
act(() => {
jest.runAllTimers();
});
component.update();
// customDropTargets are invisible
expect(
component.find('[data-test-subj="lnsDragDropExtraDrops"]').first().prop('className')
).toEqual('lnsDragDrop__extraDrops');
});
test('dragging over different drop types of the same value assigns correct activeDropTarget', () => {
component
.find('[data-test-subj="lnsDragDrop"]')
.first()
.simulate('dragstart', { dataTransfer });
component.find('SingleDropInner').at(0).simulate('dragover');
expect(setActiveDropTarget).toBeCalledWith({
...value,
dropType: 'move_compatible',
onDrop,
});
component.find('SingleDropInner').at(1).simulate('dragover');
expect(setActiveDropTarget).toBeCalledWith({
...value,
dropType: 'duplicate_compatible',
onDrop,
});
component.find('SingleDropInner').at(2).simulate('dragover');
expect(setActiveDropTarget).toBeCalledWith({
...value,
dropType: 'swap_compatible',
onDrop,
});
component.find('SingleDropInner').at(2).simulate('dragleave');
expect(setActiveDropTarget).toBeCalledWith(undefined);
});
test('drop on extra drop target passes correct dropType to onDrop', () => {
component
.find('[data-test-subj="lnsDragDrop"]')
.first()
.simulate('dragstart', { dataTransfer });
component.find('SingleDropInner').at(0).simulate('dragover');
component.find('SingleDropInner').at(0).simulate('drop');
expect(onDrop).toBeCalledWith({ humanData: { label: 'Label1' }, id: '1' }, 'move_compatible');
component.find('SingleDropInner').at(1).simulate('dragover');
component.find('SingleDropInner').at(1).simulate('drop');
expect(onDrop).toBeCalledWith(
{ humanData: { label: 'Label1' }, id: '1' },
'duplicate_compatible'
);
component.find('SingleDropInner').at(2).simulate('dragover');
component.find('SingleDropInner').at(2).simulate('drop');
expect(onDrop).toBeCalledWith({ humanData: { label: 'Label1' }, id: '1' }, 'swap_compatible');
});
test('pressing Alt or Shift when dragging over the main drop target sets extra drop target as active', () => {
component
.find('[data-test-subj="lnsDragDrop"]')
.first()
.simulate('dragstart', { dataTransfer });
// needed to setup activeDropType
component
.find('SingleDropInner')
.at(0)
.simulate('dragover', { altKey: true })
.simulate('dragover', { altKey: true });
expect(setActiveDropTarget).toBeCalledWith({
...value,
dropType: 'duplicate_compatible',
onDrop,
});
component
.find('SingleDropInner')
.at(0)
.simulate('dragover', { shiftKey: true })
.simulate('dragover', { shiftKey: true });
expect(setActiveDropTarget).toBeCalledWith({
...value,
dropType: 'swap_compatible',
onDrop,
});
});
test('pressing Alt or Shift when dragging over the extra drop target does nothing', () => {
component
.find('[data-test-subj="lnsDragDrop"]')
.first()
.simulate('dragstart', { dataTransfer });
const extraDrop = component.find('SingleDropInner').at(1);
extraDrop.simulate('dragover', { altKey: true });
extraDrop.simulate('dragover', { shiftKey: true });
extraDrop.simulate('dragover');
expect(
setActiveDropTarget.mock.calls.every((call) => call[0].dropType === 'duplicate_compatible')
);
});
describe('keyboard navigation', () => {
const items = [
{
draggable: true,
value: {
id: '1',
humanData: { label: 'Label1', position: 1 },
},
children: '1',
order: [2, 0, 0, 0],
},
{
draggable: true,
dragType: 'move' as const,
value: {
id: '2',
humanData: { label: 'label2', position: 1 },
},
onDrop,
dropTypes: ['move_compatible', 'duplicate_compatible', 'swap_compatible'] as DropType[],
order: [2, 0, 1, 0],
},
{
draggable: true,
dragType: 'move' as const,
value: {
id: '3',
humanData: { label: 'label3', position: 1, groupLabel: 'Y' },
},
onDrop,
dropTypes: ['replace_compatible'] as DropType[],
order: [2, 0, 2, 0],
},
];
const assignedDropTargetsByOrder: DragContextState['dropTargetsByOrder'] = {
'2,0,1,0,0': {
...items[1].value,
onDrop,
dropType: 'move_compatible',
},
'2,0,1,0,1': {
dropType: 'duplicate_compatible',
humanData: {
label: 'label2',
position: 1,
},
id: '2',
onDrop,
},
'2,0,1,0,2': {
dropType: 'swap_compatible',
humanData: {
label: 'label2',
position: 1,
},
id: '2',
onDrop,
},
'2,0,2,0,0': {
dropType: 'replace_compatible',
humanData: {
groupLabel: 'Y',
label: 'label3',
position: 1,
},
id: '3',
onDrop,
},
};
test('when pressing enter key, context receives the proper dropTargetsByOrder', () => {
let dropTargetsByOrder: DragContextState['dropTargetsByOrder'] = {};
const setKeyboardMode = jest.fn();
component = mount(
<ChildDragDropProvider
{...{
...defaultContext,
dragging: { ...items[0].value, ghost: { children: <div />, style: {} } },
setDragging: jest.fn(),
setActiveDropTarget,
setA11yMessage,
activeDropTarget,
dropTargetsByOrder,
keyboardMode: true,
setKeyboardMode,
registerDropTarget: jest.fn((order, dropTarget) => {
dropTargetsByOrder = {
...dropTargetsByOrder,
[order.join(',')]: dropTarget,
};
}),
}}
>
{items.map((props) => (
<DragDrop {...props} key={props.value.id}>
<div />
</DragDrop>
))}
</ChildDragDropProvider>
);
component.find('[data-test-subj="lnsDragDrop-keyboardHandler"]').first().simulate('focus');
act(() => {
jest.runAllTimers();
});
component.update();
expect(dropTargetsByOrder).toEqual(assignedDropTargetsByOrder);
});
test('when pressing ArrowRight key with modifier key pressed in, the extra drop target is selected', () => {
component = mount(
<ChildDragDropProvider
{...{
...defaultContext,
dragging: { ...items[0].value, ghost: { children: <div />, style: {} } },
setDragging: jest.fn(),
setActiveDropTarget,
setA11yMessage,
activeDropTarget: undefined,
dropTargetsByOrder: assignedDropTargetsByOrder,
keyboardMode: true,
setKeyboardMode: jest.fn(),
registerDropTarget: jest.fn(),
}}
>
{items.map((props) => (
<DragDrop {...props} key={props.value.id}>
<div />
</DragDrop>
))}
</ChildDragDropProvider>
);
act(() => {
component
.find('[data-test-subj="lnsDragDrop-keyboardHandler"]')
.first()
.simulate('keydown', { key: 'ArrowRight', altKey: true });
});
expect(setActiveDropTarget).toBeCalledWith({
...items[1].value,
onDrop,
dropType: 'duplicate_compatible',
});
act(() => {
component
.find('[data-test-subj="lnsDragDrop-keyboardHandler"]')
.first()
.simulate('keydown', { key: 'ArrowRight', shiftKey: true });
});
expect(setActiveDropTarget).toBeCalledWith({
...items[1].value,
onDrop,
dropType: 'swap_compatible',
});
});
test('when having a main target selected and pressing alt, the first extra drop target is selected', () => {
component = mount(
<ChildDragDropProvider
{...{
...defaultContext,
dragging: { ...items[0].value, ghost: { children: <div />, style: {} } },
setDragging: jest.fn(),
setActiveDropTarget,
setA11yMessage,
activeDropTarget: assignedDropTargetsByOrder['2,0,1,0,0'],
dropTargetsByOrder: assignedDropTargetsByOrder,
keyboardMode: true,
setKeyboardMode: jest.fn(),
registerDropTarget: jest.fn(),
}}
>
{items.map((props) => (
<DragDrop {...props} key={props.value.id}>
<div />
</DragDrop>
))}
</ChildDragDropProvider>
);
act(() => {
component
.find('[data-test-subj="lnsDragDrop-keyboardHandler"]')
.first()
.simulate('keydown', { key: 'Alt' });
});
expect(setActiveDropTarget).toBeCalledWith({
...items[1].value,
onDrop,
dropType: 'duplicate_compatible',
});
act(() => {
component
.find('[data-test-subj="lnsDragDrop-keyboardHandler"]')
.first()
.simulate('keyup', { key: 'Alt' });
});
expect(setActiveDropTarget).toBeCalledWith({
...items[1].value,
onDrop,
dropType: 'move_compatible',
});
});
test('when having a main target selected and pressing shift, the second extra drop target is selected', () => {
component = mount(
<ChildDragDropProvider
{...{
...defaultContext,
dragging: { ...items[0].value, ghost: { children: <div />, style: {} } },
setDragging: jest.fn(),
setActiveDropTarget,
setA11yMessage,
activeDropTarget: assignedDropTargetsByOrder['2,0,1,0,0'],
dropTargetsByOrder: assignedDropTargetsByOrder,
keyboardMode: true,
setKeyboardMode: jest.fn(),
registerDropTarget: jest.fn(),
}}
>
{items.map((props) => (
<DragDrop {...props} key={props.value.id}>
<div />
</DragDrop>
))}
</ChildDragDropProvider>
);
act(() => {
component
.find('[data-test-subj="lnsDragDrop-keyboardHandler"]')
.first()
.simulate('keydown', { key: 'Shift' });
});
expect(setActiveDropTarget).toBeCalledWith({
...items[1].value,
onDrop,
dropType: 'swap_compatible',
});
act(() => {
component
.find('[data-test-subj="lnsDragDrop-keyboardHandler"]')
.first()
.simulate('keyup', { key: 'Shift' });
});
expect(setActiveDropTarget).toBeCalledWith({
...items[1].value,
onDrop,
dropType: 'move_compatible',
});
});
});
});
describe('Reordering', () => {
const onDrop = jest.fn();
const items = [
{
id: '1',
humanData: { label: 'Label1', position: 1, groupLabel: 'X' },
onDrop,
draggable: true,
},
{
id: '2',
humanData: { label: 'label2', position: 2, groupLabel: 'X' },
onDrop,
},
{
id: '3',
humanData: { label: 'label3', position: 3, groupLabel: 'X' },
onDrop,
},
];
const mountComponent = (
dragContext: Partial<DragContextState> | undefined,
onDropHandler?: () => void
) => {
let dragging = dragContext?.dragging;
let keyboardMode = !!dragContext?.keyboardMode;
let activeDropTarget = dragContext?.activeDropTarget;
const setA11yMessage = jest.fn();
const registerDropTarget = jest.fn();
const baseContext = {
dragging,
setDragging: (val?: DraggingIdentifier) => {
dragging = val;
},
keyboardMode,
setKeyboardMode: jest.fn((mode) => {
keyboardMode = mode;
}),
setActiveDropTarget: (target?: DragDropIdentifier) => {
activeDropTarget = target as DropIdentifier;
},
activeDropTarget,
setA11yMessage,
registerDropTarget,
dropTargetsByOrder: undefined,
};
const dragDropSharedProps = {
draggable: true,
dragType: 'move' as 'copy' | 'move',
reorderableGroup: items.map(({ id }) => ({ id })),
onDrop: onDropHandler || onDrop,
};
return mount(
<ChildDragDropProvider {...baseContext} {...dragContext}>
<ReorderProvider id="groupId">
<DragDrop
{...dragDropSharedProps}
value={items[0]}
dropTypes={undefined}
order={[2, 0, 0]}
>
<span>1</span>
</DragDrop>
<DragDrop
{...dragDropSharedProps}
value={items[1]}
order={[2, 0, 1]}
dropTypes={['reorder']}
>
<span>2</span>
</DragDrop>
<DragDrop
{...dragDropSharedProps}
value={items[2]}
order={[2, 0, 2]}
dropTypes={['reorder']}
>
<span>3</span>
</DragDrop>
</ReorderProvider>
</ChildDragDropProvider>
);
};
test(`Inactive group renders properly`, () => {
const component = mountComponent(undefined);
act(() => {
jest.runAllTimers();
});
expect(component.find('[data-test-subj="lnsDragDrop"]')).toHaveLength(5);
});
test(`Reorderable group with lifted element renders properly`, () => {
const setA11yMessage = jest.fn();
const setDragging = jest.fn();
const component = mountComponent({
dragging: { ...items[0] },
setDragging,
setA11yMessage,
});
act(() => {
jest.runAllTimers();
});
component
.find('[data-test-subj="lnsDragDrop"]')
.first()
.simulate('dragstart', { dataTransfer });
act(() => {
jest.runAllTimers();
});
expect(setDragging).toBeCalledWith({ ...items[0] });
expect(setA11yMessage).toBeCalledWith('Lifted Label1');
});
test(`Reordered elements get extra styles to show the reorder effect when dragging`, () => {
const component = mountComponent({ dragging: { ...items[0] } });
component
.find('[data-test-subj="lnsDragDrop"]')
.first()
.simulate('dragstart', { dataTransfer });
act(() => {
jest.runAllTimers();
});
component
.find('[data-test-subj="lnsDragDrop-reorderableDropLayer"]')
.at(1)
.simulate('dragover');
expect(
component.find('[data-test-subj="lnsDragDrop-reorderableDrag"]').at(0).prop('style')
).toEqual(undefined);
expect(
component.find('[data-test-subj="lnsDragDrop-translatableDrop"]').at(0).prop('style')
).toEqual({
transform: 'translateY(-8px)',
});
expect(
component.find('[data-test-subj="lnsDragDrop-translatableDrop"]').at(1).prop('style')
).toEqual({
transform: 'translateY(-8px)',
});
component
.find('[data-test-subj="lnsDragDrop-reorderableDropLayer"]')
.at(1)
.simulate('dragleave');
expect(
component.find('[data-test-subj="lnsDragDrop-reorderableDrag"]').at(0).prop('style')
).toEqual(undefined);
expect(
component.find('[data-test-subj="lnsDragDrop-translatableDrop"]').at(1).prop('style')
).toEqual(undefined);
});
test(`Dropping an item runs onDrop function`, () => {
const preventDefault = jest.fn();
const stopPropagation = jest.fn();
const setA11yMessage = jest.fn();
const setDragging = jest.fn();
const component = mountComponent({
dragging: { ...items[0] },
setDragging,
setA11yMessage,
});
const dragDrop = component.find('[data-test-subj="lnsDragDrop-reorderableDropLayer"]').at(1);
dragDrop.simulate('dragOver');
dragDrop.simulate('drop', { preventDefault, stopPropagation });
act(() => {
jest.runAllTimers();
});
expect(setA11yMessage).toBeCalledWith(
'Reordered Label1 in X group from position 1 to position 3'
);
expect(preventDefault).toBeCalled();
expect(stopPropagation).toBeCalled();
expect(onDrop).toBeCalledWith({ ...items[0] }, 'reorder');
});
test(`Keyboard Navigation: User cannot move an element outside of the group`, () => {
const setA11yMessage = jest.fn();
const setActiveDropTarget = jest.fn();
const component = mountComponent({
dragging: { ...items[0] },
keyboardMode: true,
activeDropTarget: undefined,
dropTargetsByOrder: {
'2,0,0': undefined,
'2,0,1': { ...items[1], onDrop, dropType: 'reorder' },
'2,0,2': { ...items[2], onDrop, dropType: 'reorder' },
},
setActiveDropTarget,
setA11yMessage,
});
const keyboardHandler = component
.find('[data-test-subj="lnsDragDrop-keyboardHandler"]')
.first();
keyboardHandler.simulate('keydown', { key: 'Space' });
keyboardHandler.simulate('keydown', { key: 'ArrowUp' });
expect(setActiveDropTarget).not.toHaveBeenCalled();
keyboardHandler.simulate('keydown', { key: 'Space' });
keyboardHandler.simulate('keydown', { key: 'ArrowDown' });
expect(setActiveDropTarget).toBeCalledWith({ ...items[1], dropType: 'reorder' });
expect(setA11yMessage).toBeCalledWith(
'Reorder Label1 in X group from position 1 to position 2. Press space or enter to reorder'
);
});
test(`Keyboard navigation: user can drop element to an activeDropTarget`, () => {
const component = mountComponent({
dragging: { ...items[0] },
activeDropTarget: { ...items[2], dropType: 'reorder', onDrop },
dropTargetsByOrder: {
'2,0,0': { ...items[0], onDrop, dropType: 'reorder' },
'2,0,1': { ...items[1], onDrop, dropType: 'reorder' },
'2,0,2': { ...items[2], onDrop, dropType: 'reorder' },
},
keyboardMode: true,
});
const keyboardHandler = component
.find('[data-test-subj="lnsDragDrop-keyboardHandler"]')
.first()
.simulate('focus');
act(() => {
keyboardHandler.simulate('keydown', { key: 'ArrowDown' });
keyboardHandler.simulate('keydown', { key: 'ArrowDown' });
keyboardHandler.simulate('keydown', { key: 'Enter' });
});
expect(onDrop).toBeCalledWith(items[0], 'reorder');
});
test(`Keyboard Navigation: Doesn't call onDrop when movement is cancelled`, () => {
const setA11yMessage = jest.fn();
const onDropHandler = jest.fn();
const component = mountComponent(
{ dragging: { ...items[0] }, setA11yMessage },
onDropHandler
);
const keyboardHandler = component.find('[data-test-subj="lnsDragDrop-keyboardHandler"]');
keyboardHandler.simulate('keydown', { key: 'Space' });
keyboardHandler.simulate('keydown', { key: 'Escape' });
act(() => {
jest.runAllTimers();
});
expect(onDropHandler).not.toHaveBeenCalled();
expect(setA11yMessage).toBeCalledWith(
'Movement cancelled. Label1 returned to X group at position 1'
);
keyboardHandler.simulate('keydown', { key: 'Space' });
keyboardHandler.simulate('keydown', { key: 'ArrowDown' });
keyboardHandler.simulate('blur');
expect(onDropHandler).not.toHaveBeenCalled();
expect(setA11yMessage).toBeCalledWith(
'Movement cancelled. Label1 returned to X group at position 1'
);
});
test(`Keyboard Navigation: Reordered elements get extra styles to show the reorder effect`, () => {
const setA11yMessage = jest.fn();
const component = mountComponent({
dragging: { ...items[0] },
keyboardMode: true,
activeDropTarget: undefined,
dropTargetsByOrder: {
'2,0,0': undefined,
'2,0,1': { ...items[1], onDrop, dropType: 'reorder' },
'2,0,2': { ...items[2], onDrop, dropType: 'reorder' },
},
setA11yMessage,
});
const keyboardHandler = component.find('[data-test-subj="lnsDragDrop-keyboardHandler"]');
keyboardHandler.simulate('keydown', { key: 'Space' });
keyboardHandler.simulate('keydown', { key: 'ArrowDown' });
expect(
component.find('[data-test-subj="lnsDragDrop-reorderableDrag"]').at(0).prop('style')
).toEqual({
transform: 'translateY(+8px)',
});
expect(
component.find('[data-test-subj="lnsDragDrop-translatableDrop"]').at(0).prop('style')
).toEqual({
transform: 'translateY(-40px)',
});
expect(
component.find('[data-test-subj="lnsDragDrop-translatableDrop"]').at(1).prop('style')
).toEqual(undefined);
expect(setA11yMessage).toBeCalledWith(
'Reorder Label1 in X group from position 1 to position 2. Press space or enter to reorder'
);
component
.find('[data-test-subj="lnsDragDrop-reorderableDropLayer"]')
.at(1)
.simulate('dragleave');
expect(
component.find('[data-test-subj="lnsDragDrop-reorderableDrag"]').at(0).prop('style')
).toEqual(undefined);
expect(
component.find('[data-test-subj="lnsDragDrop-translatableDrop"]').at(1).prop('style')
).toEqual(undefined);
});
test(`Keyboard Navigation: User cannot drop element to itself`, () => {
const setA11yMessage = jest.fn();
const setActiveDropTarget = jest.fn();
const component = mount(
<ChildDragDropProvider
{...defaultContext}
keyboardMode={true}
activeDropTarget={{
...items[1],
onDrop,
dropType: 'reorder',
}}
dropTargetsByOrder={{
'2,0,1,0': undefined,
'2,0,1,1': { ...items[1], onDrop, dropType: 'reorder' },
}}
dragging={{ ...items[0] }}
setActiveDropTarget={setActiveDropTarget}
setA11yMessage={setA11yMessage}
>
<ReorderProvider id="groupId">
<DragDrop
draggable
dragType="move"
reorderableGroup={[items[0], items[1]]}
value={items[0]}
order={[2, 0, 1, 0]}
>
<span>1</span>
</DragDrop>
<DragDrop
draggable
dragType="move"
dropTypes={['reorder']}
reorderableGroup={[items[0], items[1]]}
value={items[1]}
order={[2, 0, 1, 1]}
>
<span>2</span>
</DragDrop>
</ReorderProvider>
</ChildDragDropProvider>
);
const keyboardHandler = component.find('[data-test-subj="lnsDragDrop-keyboardHandler"]');
keyboardHandler.simulate('keydown', { key: 'Space' });
keyboardHandler.simulate('keydown', { key: 'ArrowUp' });
expect(setActiveDropTarget).toBeCalledWith(undefined);
expect(setA11yMessage).toBeCalledWith('Label1 returned to its initial position 1');
});
});
});