mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Lens] Functional tests for drag and drop (#82796)
This commit is contained in:
parent
7d3e19801f
commit
ac150da49d
7 changed files with 258 additions and 28 deletions
|
@ -216,13 +216,17 @@ export async function BrowserProvider({ getService }: FtrProviderContext) {
|
|||
* Does a drag-and-drop action from one point to another
|
||||
* https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/input_exports_Actions.html#dragAndDrop
|
||||
*
|
||||
* @param {{element: WebElementWrapper | {x: number, y: number}, offset: {x: number, y: number}}} from
|
||||
* @param {{element: WebElementWrapper | {x: number, y: number}, offset: {x: number, y: number}}} to
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async dragAndDrop(
|
||||
from: { offset?: { x: any; y: any }; location: any },
|
||||
to: { offset?: { x: any; y: any }; location: any }
|
||||
from: {
|
||||
location: WebElementWrapper | { x?: number; y?: number };
|
||||
offset?: { x?: number; y?: number };
|
||||
},
|
||||
to: {
|
||||
location: WebElementWrapper | { x?: number; y?: number };
|
||||
offset?: { x?: number; y?: number };
|
||||
}
|
||||
) {
|
||||
// The offset should be specified in pixels relative to the center of the element's bounding box
|
||||
const getW3CPoint = (data: any) => {
|
||||
|
@ -230,7 +234,11 @@ export async function BrowserProvider({ getService }: FtrProviderContext) {
|
|||
data.offset = {};
|
||||
}
|
||||
return data.location instanceof WebElementWrapper
|
||||
? { x: data.offset.x || 0, y: data.offset.y || 0, origin: data.location._webElement }
|
||||
? {
|
||||
x: data.offset.x || 0,
|
||||
y: data.offset.y || 0,
|
||||
origin: data.location._webElement,
|
||||
}
|
||||
: { x: data.location.x, y: data.location.y, origin: Origin.POINTER };
|
||||
};
|
||||
|
||||
|
@ -240,6 +248,62 @@ export async function BrowserProvider({ getService }: FtrProviderContext) {
|
|||
return await this.getActions().move(startPoint).press().move(endPoint).release().perform();
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs drag and drop for html5 native drag and drop implementation
|
||||
* There's a bug in Chromedriver for html5 dnd that doesn't allow to use the method `dragAndDrop` defined above
|
||||
* https://github.com/SeleniumHQ/selenium/issues/6235
|
||||
* This implementation simulates user's action by calling the drag and drop specific events directly.
|
||||
*
|
||||
* @param {string} from html selector
|
||||
* @param {string} to html selector
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async html5DragAndDrop(from: string, to: string) {
|
||||
await this.execute(
|
||||
`
|
||||
function createEvent(typeOfEvent) {
|
||||
const event = document.createEvent("CustomEvent");
|
||||
event.initCustomEvent(typeOfEvent, true, true, null);
|
||||
event.dataTransfer = {
|
||||
data: {},
|
||||
setData: function (key, value) {
|
||||
this.data[key] = value;
|
||||
},
|
||||
getData: function (key) {
|
||||
return this.data[key];
|
||||
}
|
||||
};
|
||||
return event;
|
||||
}
|
||||
function dispatchEvent(element, event, transferData) {
|
||||
if (transferData !== undefined) {
|
||||
event.dataTransfer = transferData;
|
||||
}
|
||||
if (element.dispatchEvent) {
|
||||
element.dispatchEvent(event);
|
||||
} else if (element.fireEvent) {
|
||||
element.fireEvent("on" + event.type, event);
|
||||
}
|
||||
}
|
||||
|
||||
const origin = document.querySelector(arguments[0]);
|
||||
const target = document.querySelector(arguments[1]);
|
||||
|
||||
const dragStartEvent = createEvent('dragstart');
|
||||
dispatchEvent(origin, dragStartEvent);
|
||||
|
||||
setTimeout(() => {
|
||||
const dropEvent = createEvent('drop');
|
||||
dispatchEvent(target, dropEvent, dragStartEvent.dataTransfer);
|
||||
const dragEndEvent = createEvent('dragend');
|
||||
dispatchEvent(origin, dragEndEvent, dropEvent.dataTransfer);
|
||||
}, 50);
|
||||
`,
|
||||
from,
|
||||
to
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads the current browser window/frame.
|
||||
* https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/lib/webdriver_exports_Navigation.html#refresh
|
||||
|
|
|
@ -300,17 +300,27 @@ describe('DragDrop', () => {
|
|||
jest.runAllTimers();
|
||||
|
||||
component.find('[data-test-subj="lnsDragDrop-reorderableDrop"]').at(2).simulate('dragover');
|
||||
expect(component.find('[data-test-subj="lnsDragDrop"]').at(0).prop('style')).toEqual({});
|
||||
expect(component.find('[data-test-subj="lnsDragDrop"]').at(1).prop('style')).toEqual({
|
||||
transform: 'translateY(-8px)',
|
||||
expect(
|
||||
component.find('[data-test-subj="lnsDragDrop-reorderableDrag"]').at(0).prop('style')
|
||||
).toEqual({});
|
||||
expect(
|
||||
component.find('[data-test-subj="lnsDragDrop-reorderableDrag"]').at(1).prop('style')
|
||||
).toEqual({
|
||||
transform: 'translateY(-40px)',
|
||||
});
|
||||
expect(component.find('[data-test-subj="lnsDragDrop"]').at(2).prop('style')).toEqual({
|
||||
transform: 'translateY(-8px)',
|
||||
expect(
|
||||
component.find('[data-test-subj="lnsDragDrop-reorderableDrag"]').at(2).prop('style')
|
||||
).toEqual({
|
||||
transform: 'translateY(-40px)',
|
||||
});
|
||||
|
||||
component.find('[data-test-subj="lnsDragDrop-reorderableDrop"]').at(2).simulate('dragleave');
|
||||
expect(component.find('[data-test-subj="lnsDragDrop"]').at(1).prop('style')).toEqual({});
|
||||
expect(component.find('[data-test-subj="lnsDragDrop"]').at(2).prop('style')).toEqual({});
|
||||
expect(
|
||||
component.find('[data-test-subj="lnsDragDrop-reorderableDrag"]').at(1).prop('style')
|
||||
).toEqual({});
|
||||
expect(
|
||||
component.find('[data-test-subj="lnsDragDrop-reorderableDrag"]').at(2).prop('style')
|
||||
).toEqual({});
|
||||
});
|
||||
test(`Dropping an item runs onDrop function`, () => {
|
||||
const preventDefault = jest.fn();
|
||||
|
|
|
@ -271,12 +271,12 @@ const DragDropInner = React.memo(function DragDropInner(
|
|||
dropTo={dropTo}
|
||||
label={label}
|
||||
className={className}
|
||||
dataTestSubj={props['data-test-subj'] || 'lnsDragDrop'}
|
||||
draggingProps={{
|
||||
className: classNames(children.props.className, classes),
|
||||
draggable,
|
||||
onDragEnd: dragEnd,
|
||||
onDragStart: dragStart,
|
||||
dataTestSubj: props['data-test-subj'] || 'lnsDragDrop',
|
||||
isReorderDragging,
|
||||
}}
|
||||
dropProps={{
|
||||
|
@ -338,13 +338,13 @@ export const ReorderableDragDrop = ({
|
|||
label,
|
||||
dropTo,
|
||||
className,
|
||||
dataTestSubj,
|
||||
}: {
|
||||
draggingProps: {
|
||||
className: string;
|
||||
draggable: Props['draggable'];
|
||||
onDragEnd: (e: DroppableEvent) => void;
|
||||
onDragStart: (e: DroppableEvent) => void;
|
||||
dataTestSubj: string;
|
||||
isReorderDragging: boolean;
|
||||
};
|
||||
dropProps: {
|
||||
|
@ -361,6 +361,7 @@ export const ReorderableDragDrop = ({
|
|||
label: string;
|
||||
dropTo: DropToHandler;
|
||||
className?: string;
|
||||
dataTestSubj: string;
|
||||
}) => {
|
||||
const { itemsInGroup, dragging, id, droppable } = dropProps;
|
||||
const { reorderState, setReorderState } = useContext(ReorderContext);
|
||||
|
@ -378,7 +379,10 @@ export const ReorderableDragDrop = ({
|
|||
);
|
||||
|
||||
return (
|
||||
<div className={classNames('lnsDragDrop__reorderableContainer', className)}>
|
||||
<div
|
||||
className={classNames('lnsDragDrop__reorderableContainer', className)}
|
||||
data-test-subj={dataTestSubj}
|
||||
>
|
||||
<EuiScreenReaderOnly showOnFocus>
|
||||
<button
|
||||
aria-label={label}
|
||||
|
@ -442,6 +446,7 @@ export const ReorderableDragDrop = ({
|
|||
/>
|
||||
</EuiScreenReaderOnly>
|
||||
{React.cloneElement(children, {
|
||||
['data-test-subj']: 'lnsDragDrop-reorderableDrag',
|
||||
draggable: draggingProps.draggable,
|
||||
onDragEnd: draggingProps.onDragEnd,
|
||||
onDragStart: (e: DroppableEvent) => {
|
||||
|
@ -452,7 +457,6 @@ export const ReorderableDragDrop = ({
|
|||
}));
|
||||
draggingProps.onDragStart(e);
|
||||
},
|
||||
['data-test-subj']: draggingProps.dataTestSubj,
|
||||
className: classNames(
|
||||
draggingProps.className,
|
||||
{
|
||||
|
|
83
x-pack/test/functional/apps/lens/drag_and_drop.ts
Normal file
83
x-pack/test/functional/apps/lens/drag_and_drop.ts
Normal file
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function ({ getPageObjects }: FtrProviderContext) {
|
||||
const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'header']);
|
||||
|
||||
describe('lens drag and drop tests', () => {
|
||||
it('should construct the basic split xy chart', async () => {
|
||||
await PageObjects.visualize.navigateToNewVisualization();
|
||||
await PageObjects.visualize.clickVisType('lens');
|
||||
await PageObjects.lens.goToTimeRange();
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await PageObjects.lens.dragFieldToWorkspace('@timestamp');
|
||||
|
||||
expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_xDimensionPanel')).to.eql(
|
||||
'@timestamp'
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow dropping fields to existing and empty dimension triggers', async () => {
|
||||
await PageObjects.lens.switchToVisualization('lnsDatatable');
|
||||
|
||||
await PageObjects.lens.dragFieldToDimensionTrigger(
|
||||
'clientip',
|
||||
'lnsDatatable_column > lns-dimensionTrigger'
|
||||
);
|
||||
expect(await PageObjects.lens.getDimensionTriggerText('lnsDatatable_column')).to.eql(
|
||||
'Top values of clientip'
|
||||
);
|
||||
|
||||
await PageObjects.lens.dragFieldToDimensionTrigger(
|
||||
'bytes',
|
||||
'lnsDatatable_column > lns-empty-dimension'
|
||||
);
|
||||
expect(await PageObjects.lens.getDimensionTriggerText('lnsDatatable_column', 1)).to.eql(
|
||||
'bytes'
|
||||
);
|
||||
await PageObjects.lens.dragFieldToDimensionTrigger(
|
||||
'@message.raw',
|
||||
'lnsDatatable_column > lns-empty-dimension'
|
||||
);
|
||||
expect(await PageObjects.lens.getDimensionTriggerText('lnsDatatable_column', 2)).to.eql(
|
||||
'Top values of @message.raw'
|
||||
);
|
||||
});
|
||||
|
||||
it('should reorder the elements for the table', async () => {
|
||||
await PageObjects.lens.reorderDimensions('lnsDatatable_column', 2, 0);
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
expect(await PageObjects.lens.getDimensionTriggersTexts('lnsDatatable_column')).to.eql([
|
||||
'Top values of @message.raw',
|
||||
'Top values of clientip',
|
||||
'bytes',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should move the column to compatible dimension group', async () => {
|
||||
await PageObjects.lens.switchToVisualization('bar');
|
||||
expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_xDimensionPanel')).to.eql([
|
||||
'Top values of @message.raw',
|
||||
]);
|
||||
expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel')).to.eql([
|
||||
'Top values of clientip',
|
||||
]);
|
||||
|
||||
await PageObjects.lens.dragDimensionToDimension(
|
||||
'lnsXY_xDimensionPanel > lns-dimensionTrigger',
|
||||
'lnsXY_splitDimensionPanel > lns-dimensionTrigger'
|
||||
);
|
||||
|
||||
expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_xDimensionPanel')).to.eql([]);
|
||||
expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_splitDimensionPanel')).to.eql([
|
||||
'Top values of @message.raw',
|
||||
]);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -31,6 +31,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) {
|
|||
loadTestFile(require.resolve('./dashboard'));
|
||||
loadTestFile(require.resolve('./persistent_context'));
|
||||
loadTestFile(require.resolve('./colors'));
|
||||
loadTestFile(require.resolve('./drag_and_drop'));
|
||||
loadTestFile(require.resolve('./lens_reporting'));
|
||||
|
||||
// has to be last one in the suite because it overrides saved objects
|
||||
|
|
|
@ -292,11 +292,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
expect(await PageObjects.lens.hasChartSwitchWarning('treemap')).to.eql(false);
|
||||
await PageObjects.lens.switchToVisualization('treemap');
|
||||
expect(
|
||||
await PageObjects.lens.getDimensionTriggerText('lnsPie_groupByDimensionPanel', 0)
|
||||
).to.eql('Top values of geo.dest');
|
||||
expect(
|
||||
await PageObjects.lens.getDimensionTriggerText('lnsPie_groupByDimensionPanel', 1)
|
||||
).to.eql('Top values of geo.src');
|
||||
await PageObjects.lens.getDimensionTriggersTexts('lnsPie_groupByDimensionPanel')
|
||||
).to.eql(['Top values of geo.dest', 'Top values of geo.src']);
|
||||
expect(await PageObjects.lens.getDimensionTriggerText('lnsPie_sizeByDimensionPanel')).to.eql(
|
||||
'Average of bytes'
|
||||
);
|
||||
|
|
|
@ -14,7 +14,8 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
|
|||
const retry = getService('retry');
|
||||
const find = getService('find');
|
||||
const comboBox = getService('comboBox');
|
||||
const PageObjects = getPageObjects(['header', 'header', 'timePicker', 'common']);
|
||||
const browser = getService('browser');
|
||||
const PageObjects = getPageObjects(['header', 'timePicker', 'common']);
|
||||
|
||||
return logWrapper('lensPage', log, {
|
||||
/**
|
||||
|
@ -121,6 +122,64 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Drags field to workspace
|
||||
*
|
||||
* @param field - the desired field for the dimension
|
||||
* */
|
||||
async dragFieldToWorkspace(field: string) {
|
||||
await browser.html5DragAndDrop(
|
||||
testSubjects.getCssSelector(`lnsFieldListPanelField-${field}`),
|
||||
testSubjects.getCssSelector('lnsWorkspace')
|
||||
);
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
},
|
||||
|
||||
/**
|
||||
* Drags field to dimension trigger
|
||||
*
|
||||
* @param field - the desired field for the dimension
|
||||
* @param dimension - the selector of the dimension being changed
|
||||
* */
|
||||
async dragFieldToDimensionTrigger(field: string, dimension: string) {
|
||||
await browser.html5DragAndDrop(
|
||||
testSubjects.getCssSelector(`lnsFieldListPanelField-${field}`),
|
||||
testSubjects.getCssSelector(dimension)
|
||||
);
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
},
|
||||
|
||||
/**
|
||||
* Drags field to dimension trigger
|
||||
*
|
||||
* @param from - the selector of the dimension being moved
|
||||
* @param to - the selector of the dimension being dropped to
|
||||
* */
|
||||
async dragDimensionToDimension(from: string, to: string) {
|
||||
await browser.html5DragAndDrop(
|
||||
testSubjects.getCssSelector(from),
|
||||
testSubjects.getCssSelector(to)
|
||||
);
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
},
|
||||
|
||||
/**
|
||||
* Reorder elements within the group
|
||||
*
|
||||
* @param startIndex - the index of dragging element
|
||||
* @param endIndex - the index of drop
|
||||
* */
|
||||
async reorderDimensions(dimension: string, startIndex: number, endIndex: number) {
|
||||
const dragging = `[data-test-subj='${dimension}']:nth-of-type(${
|
||||
startIndex + 1
|
||||
}) .lnsDragDrop`;
|
||||
const dropping = `[data-test-subj='${dimension}']:nth-of-type(${
|
||||
endIndex + 1
|
||||
}) [data-test-subj='lnsDragDrop-reorderableDrop'`;
|
||||
await browser.html5DragAndDrop(dragging, dropping);
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
},
|
||||
|
||||
async assertPalette(palette: string) {
|
||||
await retry.try(async () => {
|
||||
await testSubjects.click('lns-palettePicker');
|
||||
|
@ -351,18 +410,30 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
|
|||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
await testSubjects.missingOrFail('lnsApp_saveAndReturnButton');
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets label of dimension trigger in dimension panel
|
||||
*
|
||||
* @param dimension - the selector of the dimension
|
||||
* @param index - the index of the dimension trigger in group
|
||||
*/
|
||||
async getDimensionTriggerText(dimension: string, index = 0) {
|
||||
const dimensionElements = await testSubjects.findAll(dimension);
|
||||
const trigger = await testSubjects.findDescendant(
|
||||
'lns-dimensionTrigger',
|
||||
dimensionElements[index]
|
||||
);
|
||||
return await trigger.getVisibleText();
|
||||
const dimensionTexts = await this.getDimensionTriggersTexts(dimension);
|
||||
return dimensionTexts[index];
|
||||
},
|
||||
/**
|
||||
* Gets label of all dimension triggers in dimension group
|
||||
*
|
||||
* @param dimension - the selector of the dimension
|
||||
*/
|
||||
async getDimensionTriggersTexts(dimension: string) {
|
||||
return retry.try(async () => {
|
||||
const dimensionElements = await testSubjects.findAll(`${dimension} > lns-dimensionTrigger`);
|
||||
const dimensionTexts = await Promise.all(
|
||||
await dimensionElements.map(async (el) => await el.getVisibleText())
|
||||
);
|
||||
return dimensionTexts;
|
||||
});
|
||||
},
|
||||
|
||||
async isShowingNoResults() {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue