kibana/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.test.tsx
Marco Liberati e9e7453e1d
[Lens] Improves error messages when in Dashboard (#90668)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
2021-02-15 13:00:31 +01:00

604 lines
19 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 {
Embeddable,
LensByValueInput,
LensByReferenceInput,
LensSavedObjectAttributes,
LensEmbeddableInput,
} from './embeddable';
import { ReactExpressionRendererProps } from 'src/plugins/expressions/public';
import { Query, TimeRange, Filter, IndexPatternsContract } from 'src/plugins/data/public';
import { Document } from '../../persistence';
import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks';
import { VIS_EVENT_TO_TRIGGER } from '../../../../../../src/plugins/visualizations/public/embeddable';
import { coreMock, httpServiceMock } from '../../../../../../src/core/public/mocks';
import { IBasePath } from '../../../../../../src/core/public';
import { AttributeService, ViewMode } from '../../../../../../src/plugins/embeddable/public';
import { LensAttributeService } from '../../lens_attribute_service';
import { OnSaveProps } from '../../../../../../src/plugins/saved_objects/public/save_modal';
import { act } from 'react-dom/test-utils';
jest.mock('../../../../../../src/plugins/inspector/public/', () => ({
isAvailable: false,
open: false,
}));
const savedVis: Document = {
state: {
visualization: {},
datasourceStates: {},
query: { query: '', language: 'lucene' },
filters: [],
},
references: [],
title: 'My title',
visualizationType: '',
};
const defaultSaveMethod = (
testAttributes: LensSavedObjectAttributes,
savedObjectId?: string
): Promise<{ id: string }> => {
return new Promise(() => {
return { id: '123' };
});
};
const defaultUnwrapMethod = (savedObjectId: string): Promise<LensSavedObjectAttributes> => {
return new Promise(() => {
return { ...savedVis };
});
};
const defaultCheckForDuplicateTitle = (props: OnSaveProps): Promise<true> => {
return new Promise(() => {
return true;
});
};
const options = {
saveMethod: defaultSaveMethod,
unwrapMethod: defaultUnwrapMethod,
checkForDuplicateTitle: defaultCheckForDuplicateTitle,
};
const attributeServiceMockFromSavedVis = (document: Document): LensAttributeService => {
const core = coreMock.createStart();
const service = new AttributeService<
LensSavedObjectAttributes,
LensByValueInput,
LensByReferenceInput
>('lens', jest.fn(), core.i18n.Context, core.notifications.toasts, options);
service.unwrapAttributes = jest.fn((input: LensByValueInput | LensByReferenceInput) => {
return Promise.resolve({ ...document } as LensSavedObjectAttributes);
});
service.wrapAttributes = jest.fn();
return service;
};
describe('embeddable', () => {
let mountpoint: HTMLDivElement;
let expressionRenderer: jest.Mock<null, [ReactExpressionRendererProps]>;
let getTrigger: jest.Mock;
let trigger: { exec: jest.Mock };
let basePath: IBasePath;
let attributeService: AttributeService<
LensSavedObjectAttributes,
LensByValueInput,
LensByReferenceInput
>;
beforeEach(() => {
mountpoint = document.createElement('div');
expressionRenderer = jest.fn((_props) => null);
trigger = { exec: jest.fn() };
getTrigger = jest.fn(() => trigger);
attributeService = attributeServiceMockFromSavedVis(savedVis);
const http = httpServiceMock.createSetupContract({ basePath: '/test' });
basePath = http.basePath;
});
afterEach(() => {
mountpoint.remove();
});
it('should render expression once with expression renderer', async () => {
const embeddable = new Embeddable(
{
timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter,
attributeService,
expressionRenderer,
basePath,
indexPatternService: {} as IndexPatternsContract,
editable: true,
getTrigger,
documentToExpression: () =>
Promise.resolve({
ast: {
type: 'expression',
chain: [
{ type: 'function', function: 'my', arguments: {} },
{ type: 'function', function: 'expression', arguments: {} },
],
},
errors: undefined,
}),
},
{
timeRange: {
from: 'now-15m',
to: 'now',
},
} as LensEmbeddableInput
);
embeddable.render(mountpoint);
// wait one tick to give embeddable time to initialize
await new Promise((resolve) => setTimeout(resolve, 0));
expect(expressionRenderer).toHaveBeenCalledTimes(1);
expect(expressionRenderer.mock.calls[0][0]!.expression).toEqual(`my
| expression`);
});
it('should not render the visualization if any error arises', async () => {
const embeddable = new Embeddable(
{
timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter,
attributeService,
expressionRenderer,
basePath,
indexPatternService: {} as IndexPatternsContract,
editable: true,
getTrigger,
documentToExpression: () =>
Promise.resolve({
ast: {
type: 'expression',
chain: [
{ type: 'function', function: 'my', arguments: {} },
{ type: 'function', function: 'expression', arguments: {} },
],
},
errors: [{ shortMessage: '', longMessage: 'my validation error' }],
}),
},
{} as LensEmbeddableInput
);
await embeddable.initializeSavedVis({} as LensEmbeddableInput);
embeddable.render(mountpoint);
expect(expressionRenderer).toHaveBeenCalledTimes(0);
});
it('should initialize output with deduped list of index patterns', async () => {
attributeService = attributeServiceMockFromSavedVis({
...savedVis,
references: [
{ type: 'index-pattern', id: '123', name: 'abc' },
{ type: 'index-pattern', id: '123', name: 'def' },
{ type: 'index-pattern', id: '456', name: 'ghi' },
],
});
const embeddable = new Embeddable(
{
timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter,
attributeService,
expressionRenderer,
basePath,
indexPatternService: ({
get: (id: string) => Promise.resolve({ id }),
} as unknown) as IndexPatternsContract,
editable: true,
getTrigger,
documentToExpression: () =>
Promise.resolve({
ast: {
type: 'expression',
chain: [
{ type: 'function', function: 'my', arguments: {} },
{ type: 'function', function: 'expression', arguments: {} },
],
},
errors: undefined,
}),
},
{} as LensEmbeddableInput
);
await embeddable.initializeSavedVis({} as LensEmbeddableInput);
const outputIndexPatterns = embeddable.getOutput().indexPatterns!;
expect(outputIndexPatterns.length).toEqual(2);
expect(outputIndexPatterns[0].id).toEqual('123');
expect(outputIndexPatterns[1].id).toEqual('456');
});
it('should re-render if new input is pushed', async () => {
const timeRange: TimeRange = { from: 'now-15d', to: 'now' };
const query: Query = { language: 'kquery', query: '' };
const filters: Filter[] = [{ meta: { alias: 'test', negate: false, disabled: false } }];
const embeddable = new Embeddable(
{
timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter,
attributeService,
expressionRenderer,
basePath,
indexPatternService: {} as IndexPatternsContract,
editable: true,
getTrigger,
documentToExpression: () =>
Promise.resolve({
ast: {
type: 'expression',
chain: [
{ type: 'function', function: 'my', arguments: {} },
{ type: 'function', function: 'expression', arguments: {} },
],
},
errors: undefined,
}),
},
{ id: '123' } as LensEmbeddableInput
);
await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput);
embeddable.render(mountpoint);
expect(expressionRenderer).toHaveBeenCalledTimes(1);
embeddable.updateInput({
timeRange,
query,
filters,
searchSessionId: 'searchSessionId',
});
await new Promise((resolve) => setTimeout(resolve, 0));
expect(expressionRenderer).toHaveBeenCalledTimes(2);
});
it('should re-render when dashboard view/edit mode changes', async () => {
const embeddable = new Embeddable(
{
timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter,
attributeService,
expressionRenderer,
basePath,
indexPatternService: {} as IndexPatternsContract,
editable: true,
getTrigger,
documentToExpression: () =>
Promise.resolve({
ast: {
type: 'expression',
chain: [
{ type: 'function', function: 'my', arguments: {} },
{ type: 'function', function: 'expression', arguments: {} },
],
},
errors: undefined,
}),
},
{ id: '123' } as LensEmbeddableInput
);
await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput);
embeddable.render(mountpoint);
expect(expressionRenderer).toHaveBeenCalledTimes(1);
embeddable.updateInput({
viewMode: ViewMode.VIEW,
});
expect(expressionRenderer).toHaveBeenCalledTimes(2);
});
it('should re-render when dynamic actions input changes', async () => {
const embeddable = new Embeddable(
{
timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter,
attributeService,
expressionRenderer,
basePath,
indexPatternService: {} as IndexPatternsContract,
editable: true,
getTrigger,
documentToExpression: () =>
Promise.resolve({
ast: {
type: 'expression',
chain: [
{ type: 'function', function: 'my', arguments: {} },
{ type: 'function', function: 'expression', arguments: {} },
],
},
errors: undefined,
}),
},
{ id: '123' } as LensEmbeddableInput
);
await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput);
embeddable.render(mountpoint);
expect(expressionRenderer).toHaveBeenCalledTimes(1);
embeddable.updateInput({
enhancements: {
dynamicActions: {},
},
});
expect(expressionRenderer).toHaveBeenCalledTimes(2);
});
it('should pass context to embeddable', async () => {
const timeRange: TimeRange = { from: 'now-15d', to: 'now' };
const query: Query = { language: 'kquery', query: '' };
const filters: Filter[] = [{ meta: { alias: 'test', negate: false, disabled: false } }];
const input = {
savedObjectId: '123',
timeRange,
query,
filters,
searchSessionId: 'searchSessionId',
} as LensEmbeddableInput;
const embeddable = new Embeddable(
{
timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter,
attributeService,
expressionRenderer,
basePath,
indexPatternService: {} as IndexPatternsContract,
editable: true,
getTrigger,
documentToExpression: () =>
Promise.resolve({
ast: {
type: 'expression',
chain: [
{ type: 'function', function: 'my', arguments: {} },
{ type: 'function', function: 'expression', arguments: {} },
],
},
errors: undefined,
}),
},
input
);
await embeddable.initializeSavedVis(input);
embeddable.render(mountpoint);
expect(expressionRenderer.mock.calls[0][0].searchContext).toEqual(
expect.objectContaining({
timeRange,
query: [query, savedVis.state.query],
filters,
})
);
expect(expressionRenderer.mock.calls[0][0].searchSessionId).toBe(input.searchSessionId);
});
it('should pass render mode to expression', async () => {
const timeRange: TimeRange = { from: 'now-15d', to: 'now' };
const query: Query = { language: 'kquery', query: '' };
const filters: Filter[] = [{ meta: { alias: 'test', negate: false, disabled: false } }];
const input = {
savedObjectId: '123',
timeRange,
query,
filters,
renderMode: 'noInteractivity',
} as LensEmbeddableInput;
const embeddable = new Embeddable(
{
timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter,
attributeService,
expressionRenderer,
basePath,
indexPatternService: {} as IndexPatternsContract,
editable: true,
getTrigger,
documentToExpression: () =>
Promise.resolve({
ast: {
type: 'expression',
chain: [
{ type: 'function', function: 'my', arguments: {} },
{ type: 'function', function: 'expression', arguments: {} },
],
},
errors: undefined,
}),
},
input
);
await embeddable.initializeSavedVis(input);
embeddable.render(mountpoint);
expect(expressionRenderer.mock.calls[0][0].renderMode).toEqual('noInteractivity');
});
it('should merge external context with query and filters of the saved object', async () => {
const timeRange: TimeRange = { from: 'now-15d', to: 'now' };
const query: Query = { language: 'kquery', query: 'external filter' };
const filters: Filter[] = [{ meta: { alias: 'test', negate: false, disabled: false } }];
const newSavedVis = {
...savedVis,
state: {
...savedVis.state,
query: { language: 'kquery', query: 'saved filter' },
filters: [
{ meta: { alias: 'test', negate: false, disabled: false, indexRefName: 'filter-0' } },
],
},
references: [{ type: 'index-pattern', name: 'filter-0', id: 'my-index-pattern-id' }],
};
attributeService = attributeServiceMockFromSavedVis(newSavedVis);
const input = { savedObjectId: '123', timeRange, query, filters } as LensEmbeddableInput;
const embeddable = new Embeddable(
{
timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter,
attributeService,
expressionRenderer,
basePath,
indexPatternService: {} as IndexPatternsContract,
editable: true,
getTrigger,
documentToExpression: () =>
Promise.resolve({
ast: {
type: 'expression',
chain: [
{ type: 'function', function: 'my', arguments: {} },
{ type: 'function', function: 'expression', arguments: {} },
],
},
errors: undefined,
}),
},
input
);
await embeddable.initializeSavedVis(input);
embeddable.render(mountpoint);
expect(expressionRenderer.mock.calls[0][0].searchContext).toEqual({
timeRange,
query: [query, { language: 'kquery', query: 'saved filter' }],
filters: [
filters[0],
// actual index pattern id gets injected
{ meta: { alias: 'test', negate: false, disabled: false, index: 'my-index-pattern-id' } },
],
});
});
it('should execute trigger on event from expression renderer', async () => {
const embeddable = new Embeddable(
{
timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter,
attributeService,
expressionRenderer,
basePath,
indexPatternService: {} as IndexPatternsContract,
editable: true,
getTrigger,
documentToExpression: () =>
Promise.resolve({
ast: {
type: 'expression',
chain: [
{ type: 'function', function: 'my', arguments: {} },
{ type: 'function', function: 'expression', arguments: {} },
],
},
errors: undefined,
}),
},
{ id: '123' } as LensEmbeddableInput
);
await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput);
embeddable.render(mountpoint);
const onEvent = expressionRenderer.mock.calls[0][0].onEvent!;
const eventData = {};
onEvent({ name: 'brush', data: eventData });
expect(getTrigger).toHaveBeenCalledWith(VIS_EVENT_TO_TRIGGER.brush);
expect(trigger.exec).toHaveBeenCalledWith(
expect.objectContaining({ data: eventData, embeddable: expect.anything() })
);
});
it('should execute trigger on row click event from expression renderer', async () => {
const embeddable = new Embeddable(
{
timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter,
attributeService,
expressionRenderer,
basePath,
indexPatternService: {} as IndexPatternsContract,
editable: true,
getTrigger,
documentToExpression: () =>
Promise.resolve({
ast: {
type: 'expression',
chain: [
{ type: 'function', function: 'my', arguments: {} },
{ type: 'function', function: 'expression', arguments: {} },
],
},
errors: undefined,
}),
},
{ id: '123' } as LensEmbeddableInput
);
await embeddable.initializeSavedVis({ id: '123' } as LensEmbeddableInput);
embeddable.render(mountpoint);
const onEvent = expressionRenderer.mock.calls[0][0].onEvent!;
onEvent({ name: 'tableRowContextMenuClick', data: {} });
expect(getTrigger).toHaveBeenCalledWith(VIS_EVENT_TO_TRIGGER.tableRowContextMenuClick);
});
it('should not re-render if only change is in disabled filter', async () => {
const timeRange: TimeRange = { from: 'now-15d', to: 'now' };
const query: Query = { language: 'kquery', query: '' };
const filters: Filter[] = [{ meta: { alias: 'test', negate: false, disabled: true } }];
const embeddable = new Embeddable(
{
timefilter: dataPluginMock.createSetupContract().query.timefilter.timefilter,
attributeService,
expressionRenderer,
basePath,
indexPatternService: {} as IndexPatternsContract,
editable: true,
getTrigger,
documentToExpression: () =>
Promise.resolve({
ast: {
type: 'expression',
chain: [
{ type: 'function', function: 'my', arguments: {} },
{ type: 'function', function: 'expression', arguments: {} },
],
},
errors: undefined,
}),
},
{ id: '123', timeRange, query, filters } as LensEmbeddableInput
);
await embeddable.initializeSavedVis({
id: '123',
timeRange,
query,
filters,
} as LensEmbeddableInput);
embeddable.render(mountpoint);
act(() => {
embeddable.updateInput({
timeRange,
query,
filters: [{ meta: { alias: 'test', negate: true, disabled: true } }],
});
});
expect(expressionRenderer).toHaveBeenCalledTimes(1);
});
});