mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Lens] Restore embeddable test coverage (#204986)
## Summary Fixes #198754 Restore previous removed tests when performing the refactor. The new tests take advantage of the new architecture to be more modular and close to the logic modules. The `data_loader` tests are not just covering the re-render logic but also some `expression_params` logic, who in the past have proven to be the source of some bugs: specifically the tests will check that the params are correctly passed to the params logic and then stored correctly in the observable. New mocks take advantage of the plain initializers to build some of the API, that will make it in sync with the actual implementation for future maintenance. ### Checklist Check the PR satisfies following conditions. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Marta Bondyra <4283304+mbondyra@users.noreply.github.com>
This commit is contained in:
parent
6ed214a69f
commit
41950c22df
13 changed files with 913 additions and 131 deletions
|
@ -141,7 +141,7 @@ export function mockDataPlugin(
|
|||
},
|
||||
search: createMockSearchService(),
|
||||
nowProvider: {
|
||||
get: jest.fn(),
|
||||
get: jest.fn(() => new Date()),
|
||||
},
|
||||
fieldFormats: {
|
||||
deserialize: jest.fn(),
|
||||
|
|
|
@ -0,0 +1,414 @@
|
|||
/*
|
||||
* 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 { faker } from '@faker-js/faker';
|
||||
import { loadEmbeddableData } from './data_loader';
|
||||
import {
|
||||
createUnifiedSearchApi,
|
||||
getLensApiMock,
|
||||
getLensAttributesMock,
|
||||
getLensInternalApiMock,
|
||||
makeEmbeddableServices,
|
||||
} from './mocks';
|
||||
import { BehaviorSubject, filter, firstValueFrom } from 'rxjs';
|
||||
import { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query';
|
||||
import { LensDocument } from '../persistence';
|
||||
import {
|
||||
GetStateType,
|
||||
LensApi,
|
||||
LensEmbeddableStartServices,
|
||||
LensInternalApi,
|
||||
LensOverrides,
|
||||
LensPublicCallbacks,
|
||||
LensRuntimeState,
|
||||
} from './types';
|
||||
import {
|
||||
HasParentApi,
|
||||
PublishesTimeRange,
|
||||
PublishesUnifiedSearch,
|
||||
PublishingSubject,
|
||||
ViewMode,
|
||||
} from '@kbn/presentation-publishing';
|
||||
import { PublishesSearchSession } from '@kbn/presentation-publishing/interfaces/fetch/publishes_search_session';
|
||||
import { isObject } from 'lodash';
|
||||
import { defaultDoc } from '../mocks';
|
||||
|
||||
jest.mock('@kbn/interpreter', () => ({
|
||||
toExpression: jest.fn().mockReturnValue('expression'),
|
||||
}));
|
||||
|
||||
// Mock it for now, later investigate why the real one is not triggering here on tests
|
||||
jest.mock('@kbn/presentation-publishing', () => {
|
||||
const original = jest.requireActual('@kbn/presentation-publishing');
|
||||
const rx = jest.requireActual('rxjs');
|
||||
return {
|
||||
...original,
|
||||
fetch$: jest.fn((api: unknown) => {
|
||||
const typeApi = api as PublishesTimeRange &
|
||||
PublishesUnifiedSearch &
|
||||
HasParentApi<PublishesUnifiedSearch & PublishesSearchSession>;
|
||||
const emptyObservable = rx.of(undefined);
|
||||
return rx.merge(
|
||||
typeApi.timeRange$ ?? emptyObservable,
|
||||
typeApi.filters$ ?? emptyObservable,
|
||||
typeApi.query$ ?? emptyObservable,
|
||||
typeApi.parentApi?.filters$ ?? emptyObservable,
|
||||
typeApi.parentApi?.query$ ?? emptyObservable,
|
||||
typeApi.parentApi?.searchSessionId$ ?? emptyObservable,
|
||||
typeApi.timeRange$ ?? typeApi.parentApi?.timeRange$ ?? emptyObservable,
|
||||
typeApi.parentApi?.timeslice$ ?? emptyObservable
|
||||
);
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
// In order to listen the reload function, we need to
|
||||
// monitor the internalApi dispatchRenderStart spy
|
||||
type ChangeFnType = ({
|
||||
api,
|
||||
getState,
|
||||
parentApi,
|
||||
internalApi,
|
||||
services,
|
||||
}: {
|
||||
api: LensApi;
|
||||
internalApi: LensInternalApi;
|
||||
getState: jest.MockedFunction<GetStateType>;
|
||||
parentApi: ReturnType<typeof createUnifiedSearchApi> &
|
||||
LensPublicCallbacks & {
|
||||
searchSessionId$: BehaviorSubject<string>;
|
||||
};
|
||||
services: LensEmbeddableStartServices;
|
||||
}) => Promise<void | boolean>;
|
||||
|
||||
async function expectRerenderOnDataLoder(
|
||||
changeFn: ChangeFnType,
|
||||
runtimeState: LensRuntimeState = { attributes: getLensAttributesMock() },
|
||||
parentApiOverrides?: Partial<
|
||||
{
|
||||
filters$: BehaviorSubject<Filter[] | undefined>;
|
||||
query$: BehaviorSubject<Query | AggregateQuery | undefined>;
|
||||
timeRange$: BehaviorSubject<TimeRange | undefined>;
|
||||
} & LensOverrides
|
||||
>
|
||||
): Promise<void> {
|
||||
const parentApi = {
|
||||
...createUnifiedSearchApi(),
|
||||
searchSessionId$: new BehaviorSubject<string>(''),
|
||||
onLoad: jest.fn(),
|
||||
onBeforeBadgesRender: jest.fn(),
|
||||
onBrushEnd: jest.fn(),
|
||||
onFilter: jest.fn(),
|
||||
onTableRowClick: jest.fn(),
|
||||
// Make TS happy
|
||||
removePanel: jest.fn(),
|
||||
replacePanel: jest.fn(),
|
||||
getPanelCount: jest.fn(),
|
||||
children$: new BehaviorSubject({}),
|
||||
addNewPanel: jest.fn(),
|
||||
...parentApiOverrides,
|
||||
};
|
||||
const api: LensApi = {
|
||||
...getLensApiMock(),
|
||||
parentApi,
|
||||
};
|
||||
const getState = jest.fn(() => runtimeState);
|
||||
const internalApi = getLensInternalApiMock();
|
||||
const services = makeEmbeddableServices(new BehaviorSubject<string>(''), undefined, {
|
||||
visOverrides: { id: 'lnsXY' },
|
||||
dataOverrides: { id: 'form_based' },
|
||||
});
|
||||
services.documentToExpression = jest.fn().mockResolvedValue({ ast: 'expression_string' });
|
||||
const { cleanup } = loadEmbeddableData(
|
||||
faker.string.uuid(),
|
||||
getState,
|
||||
api,
|
||||
parentApi,
|
||||
internalApi,
|
||||
services
|
||||
);
|
||||
// there's a debounce, so skip to the next tick
|
||||
jest.advanceTimersByTime(100);
|
||||
expect(internalApi.dispatchRenderStart).toHaveBeenCalledTimes(1);
|
||||
// change something
|
||||
const result = await changeFn({
|
||||
api,
|
||||
getState,
|
||||
parentApi,
|
||||
internalApi,
|
||||
services,
|
||||
});
|
||||
// fallback to true if undefined is returned
|
||||
const expectRerender = result ?? true;
|
||||
// there's a debounce, so skip to the next tick
|
||||
jest.advanceTimersByTime(200);
|
||||
// unsubscribe to all observables before checking
|
||||
cleanup();
|
||||
// now check if the re-render has been dispatched
|
||||
expect(internalApi.dispatchRenderStart).toHaveBeenCalledTimes(expectRerender ? 2 : 1);
|
||||
}
|
||||
|
||||
function waitForValue(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
observable: PublishingSubject<any>,
|
||||
predicate?: (v: NonNullable<unknown>) => boolean
|
||||
) {
|
||||
// Wait for the subject to emit the first non-null value
|
||||
return firstValueFrom(observable.pipe(filter((v) => v != null && (predicate?.(v) ?? true))));
|
||||
}
|
||||
|
||||
describe('Data Loader', () => {
|
||||
beforeAll(() => {
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
it('should re-render once on filter change', async () => {
|
||||
await expectRerenderOnDataLoder(async ({ api }) => {
|
||||
(api.filters$ as BehaviorSubject<Filter[]>).next([
|
||||
{ meta: { alias: 'test', negate: false, disabled: false } },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it('should re-render once on search session change', async () => {
|
||||
await expectRerenderOnDataLoder(async ({ api }) => {
|
||||
// dispatch a new searchSessionId
|
||||
|
||||
(
|
||||
api.parentApi as unknown as { searchSessionId$: BehaviorSubject<string | undefined> }
|
||||
).searchSessionId$.next('newSessionId');
|
||||
});
|
||||
});
|
||||
|
||||
it('should re-render once on attributes change', async () => {
|
||||
await expectRerenderOnDataLoder(async ({ internalApi }) => {
|
||||
// trigger a change by changing the title in the attributes
|
||||
(internalApi.attributes$ as BehaviorSubject<LensDocument | undefined>).next({
|
||||
...internalApi.attributes$.getValue(),
|
||||
title: faker.lorem.word(),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should re-render when dashboard view/edit mode changes if dynamic actions are set', async () => {
|
||||
await expectRerenderOnDataLoder(async ({ api, getState }) => {
|
||||
getState.mockReturnValue({
|
||||
attributes: getLensAttributesMock(),
|
||||
enhancements: {
|
||||
dynamicActions: {
|
||||
events: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
// trigger a change by changing the title in the attributes
|
||||
(api.viewMode as BehaviorSubject<ViewMode | undefined>).next('view');
|
||||
});
|
||||
});
|
||||
|
||||
it('should not re-render when dashboard view/edit mode changes if dynamic actions are not set', async () => {
|
||||
await expectRerenderOnDataLoder(async ({ api }) => {
|
||||
// the default get state does not have dynamic actions
|
||||
// trigger a change by changing the title in the attributes
|
||||
(api.viewMode as BehaviorSubject<ViewMode | undefined>).next('view');
|
||||
// should not re-render
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
it('should pass context to embeddable', async () => {
|
||||
const query: Query = { language: 'kquery', query: '' };
|
||||
const filters: Filter[] = [{ meta: { alias: 'test', negate: false, disabled: false } }];
|
||||
|
||||
await expectRerenderOnDataLoder(
|
||||
async ({ internalApi }) => {
|
||||
await waitForValue(
|
||||
internalApi.expressionParams$,
|
||||
(v: unknown) => isObject(v) && 'searchContext' in v
|
||||
);
|
||||
|
||||
const params = internalApi.expressionParams$.getValue()!;
|
||||
expect(params.searchContext).toEqual(
|
||||
expect.objectContaining({ query: [query, defaultDoc.state.query], filters })
|
||||
);
|
||||
|
||||
return false;
|
||||
},
|
||||
undefined, // use default attributes
|
||||
createUnifiedSearchApi(query, filters) // customize parentApi
|
||||
);
|
||||
});
|
||||
|
||||
it('should pass render mode to expression', async () => {
|
||||
await expectRerenderOnDataLoder(async ({ internalApi }) => {
|
||||
await waitForValue(
|
||||
internalApi.expressionParams$,
|
||||
(v: unknown) => isObject(v) && 'renderMode' in v
|
||||
);
|
||||
const params = internalApi.expressionParams$.getValue();
|
||||
expect(params?.renderMode).toEqual('view');
|
||||
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
it('should merge external context with query and filters of the saved object', async () => {
|
||||
const parentApiTimeRange: TimeRange = { from: 'now-15d', to: 'now' };
|
||||
const parentApiQuery: Query = { language: 'kquery', query: 'external query' };
|
||||
const parentApiFilters: Filter[] = [
|
||||
{ meta: { alias: 'external filter', negate: false, disabled: false } },
|
||||
];
|
||||
|
||||
const vizQuery: Query = { language: 'kquery', query: 'saved filter' };
|
||||
const vizFilters: Filter[] = [
|
||||
{ meta: { alias: 'test', negate: false, disabled: false, index: 'filter-0' } },
|
||||
];
|
||||
|
||||
let attributes = getLensAttributesMock();
|
||||
attributes = {
|
||||
...attributes,
|
||||
state: {
|
||||
...attributes.state,
|
||||
query: vizQuery,
|
||||
filters: vizFilters,
|
||||
},
|
||||
references: [
|
||||
{ type: 'index-pattern', name: vizFilters[0].meta.index!, id: 'my-index-pattern-id' },
|
||||
],
|
||||
};
|
||||
|
||||
await expectRerenderOnDataLoder(
|
||||
async ({ internalApi }) => {
|
||||
await waitForValue(
|
||||
internalApi.expressionParams$,
|
||||
(v: unknown) => isObject(v) && 'searchContext' in v
|
||||
);
|
||||
|
||||
const params = internalApi.expressionParams$.getValue()!;
|
||||
expect(params.searchContext).toEqual(
|
||||
expect.objectContaining({
|
||||
query: [parentApiQuery, vizQuery],
|
||||
filters: [
|
||||
...parentApiFilters,
|
||||
...vizFilters.map(({ meta }) => ({ meta: { ...meta, index: 'injected!' } })),
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
return false;
|
||||
},
|
||||
{ attributes },
|
||||
createUnifiedSearchApi(parentApiQuery, parentApiFilters, parentApiTimeRange)
|
||||
);
|
||||
});
|
||||
|
||||
it('should call onload after rerender and onData$ call', async () => {
|
||||
await expectRerenderOnDataLoder(async ({ parentApi, internalApi, api }) => {
|
||||
expect(parentApi.onLoad).toHaveBeenLastCalledWith(true);
|
||||
|
||||
await waitForValue(
|
||||
internalApi.expressionParams$,
|
||||
(v: unknown) => isObject(v) && 'expression' in v && typeof v.expression != null
|
||||
);
|
||||
|
||||
const params = internalApi.expressionParams$.getValue();
|
||||
// trigger a onData
|
||||
params?.onData$?.(1);
|
||||
expect(parentApi.onLoad).toHaveBeenCalledTimes(2);
|
||||
expect(parentApi.onLoad).toHaveBeenNthCalledWith(2, false, undefined, api.dataLoading);
|
||||
|
||||
// turn off the re-render check, that will be performed here
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
it('should initialize dateViews api with deduped list of index patterns', async () => {
|
||||
await expectRerenderOnDataLoder(
|
||||
async ({ internalApi }) => {
|
||||
await waitForValue(
|
||||
internalApi.dataViews,
|
||||
(v: NonNullable<unknown>) => Array.isArray(v) && v.length > 0
|
||||
);
|
||||
const outputIndexPatterns = internalApi.dataViews.getValue() || [];
|
||||
|
||||
expect(outputIndexPatterns.length).toEqual(2);
|
||||
expect(outputIndexPatterns[0].id).toEqual('123');
|
||||
expect(outputIndexPatterns[1].id).toEqual('456');
|
||||
|
||||
return false;
|
||||
},
|
||||
{
|
||||
attributes: getLensAttributesMock({
|
||||
references: [
|
||||
{ type: 'index-pattern', id: '123', name: 'abc' },
|
||||
{ type: 'index-pattern', id: '123', name: 'def' },
|
||||
{ type: 'index-pattern', id: '456', name: 'ghi' },
|
||||
],
|
||||
}),
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should override noPadding in the display options if noPadding is set in the embeddable input', async () => {
|
||||
await expectRerenderOnDataLoder(async ({ internalApi }) => {
|
||||
await waitForValue(
|
||||
internalApi.expressionParams$,
|
||||
(v: unknown) => isObject(v) && 'expression' in v && typeof v.expression != null
|
||||
);
|
||||
|
||||
const params = internalApi.expressionParams$.getValue()!;
|
||||
expect(params.noPadding).toBeUndefined();
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
it('should reload only once when the attributes or savedObjectId and the search context change at the same time', async () => {
|
||||
await expectRerenderOnDataLoder(async ({ internalApi, api }) => {
|
||||
// trigger a change by changing the title in the attributes
|
||||
(internalApi.attributes$ as BehaviorSubject<LensDocument | undefined>).next({
|
||||
...internalApi.attributes$.getValue(),
|
||||
title: faker.lorem.word(),
|
||||
});
|
||||
(api.savedObjectId as BehaviorSubject<string | undefined>).next('newSavedObjectId');
|
||||
});
|
||||
});
|
||||
|
||||
it('should pass over the overrides as variables', async () => {
|
||||
await expectRerenderOnDataLoder(
|
||||
async ({ internalApi }) => {
|
||||
await waitForValue(
|
||||
internalApi.expressionParams$,
|
||||
(v: unknown) => isObject(v) && 'variables' in v && typeof v.variables != null
|
||||
);
|
||||
|
||||
const params = internalApi.expressionParams$.getValue()!;
|
||||
expect(params.variables).toEqual(
|
||||
expect.objectContaining({
|
||||
overrides: {
|
||||
settings: {
|
||||
onBrushEnd: 'ignore',
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
return false;
|
||||
},
|
||||
// send a runtime state with the overrides
|
||||
{
|
||||
attributes: getLensAttributesMock(),
|
||||
overrides: {
|
||||
settings: {
|
||||
onBrushEnd: 'ignore',
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
|
@ -93,14 +93,7 @@ export function loadEmbeddableData(
|
|||
updateWarnings,
|
||||
resetMessages,
|
||||
updateMessages,
|
||||
} = buildUserMessagesHelpers(
|
||||
api,
|
||||
internalApi,
|
||||
services,
|
||||
onBeforeBadgesRender,
|
||||
services.spaces,
|
||||
metaInfo
|
||||
);
|
||||
} = buildUserMessagesHelpers(api, internalApi, services, onBeforeBadgesRender, metaInfo);
|
||||
|
||||
const dispatchBlockingErrorIfAny = () => {
|
||||
const blockingErrors = getUserMessages(blockingMessageDisplayLocations, {
|
||||
|
@ -136,9 +129,7 @@ export function loadEmbeddableData(
|
|||
internalApi.updateDataLoading(true);
|
||||
|
||||
// the component is ready to load
|
||||
if (apiHasLensComponentCallbacks(parentApi)) {
|
||||
parentApi.onLoad?.(true);
|
||||
}
|
||||
onLoad?.(true);
|
||||
|
||||
const currentState = getState();
|
||||
|
||||
|
@ -169,6 +160,7 @@ export function loadEmbeddableData(
|
|||
internalApi.updateVisualizationContext({
|
||||
activeData: adapters?.tables?.tables,
|
||||
});
|
||||
|
||||
// data has loaded
|
||||
internalApi.updateDataLoading(false);
|
||||
// The third argument here is an observable to let the
|
||||
|
|
|
@ -149,6 +149,7 @@ export async function getExpressionRendererParams(
|
|||
onData = noop,
|
||||
logError,
|
||||
api,
|
||||
renderMode,
|
||||
addUserMessages,
|
||||
updateBlockingErrors,
|
||||
searchContext,
|
||||
|
@ -199,6 +200,7 @@ export async function getExpressionRendererParams(
|
|||
syncTooltips,
|
||||
searchSessionId,
|
||||
onRender$: onRender,
|
||||
renderMode,
|
||||
handleEvent,
|
||||
onData$: onData,
|
||||
// Remove ES|QL query from it
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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 { faker } from '@faker-js/faker';
|
||||
import { initializeEditApi } from './initialize_edit';
|
||||
import {
|
||||
getLensApiMock,
|
||||
getLensInternalApiMock,
|
||||
getLensRuntimeStateMock,
|
||||
makeEmbeddableServices,
|
||||
} from '../mocks';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { ApplicationStart } from '@kbn/core/public';
|
||||
import { LensEmbeddableStartServices } from '../types';
|
||||
|
||||
function createEditApi(servicesOverrides: Partial<LensEmbeddableStartServices> = {}) {
|
||||
const internalApi = getLensInternalApiMock();
|
||||
const runtimeState = getLensRuntimeStateMock();
|
||||
const api = getLensApiMock();
|
||||
const services = {
|
||||
...makeEmbeddableServices(new BehaviorSubject<string>(''), undefined, {
|
||||
visOverrides: { id: 'lnsXY' },
|
||||
dataOverrides: { id: 'formBased' },
|
||||
}),
|
||||
...servicesOverrides,
|
||||
};
|
||||
return initializeEditApi(
|
||||
faker.string.uuid(),
|
||||
runtimeState,
|
||||
() => runtimeState,
|
||||
internalApi,
|
||||
api,
|
||||
api,
|
||||
api,
|
||||
() => false, // DSL based
|
||||
services,
|
||||
{ getAppContext: () => ({ currentAppId: 'lens' }), viewMode: new BehaviorSubject('edit') }
|
||||
);
|
||||
}
|
||||
|
||||
describe('edit features', () => {
|
||||
it('should be editable if visualize library privileges allow it', () => {
|
||||
const editApi = createEditApi();
|
||||
expect(editApi.api.isEditingEnabled()).toBe(true);
|
||||
});
|
||||
|
||||
it('should not be editable if visualize library privileges do not allow it', () => {
|
||||
const editApi = createEditApi({
|
||||
capabilities: {
|
||||
visualize: {
|
||||
// cannot save
|
||||
save: false,
|
||||
saveQuery: true,
|
||||
// cannot see the visualization
|
||||
show: true,
|
||||
createShortUrl: true,
|
||||
},
|
||||
dashboard: {
|
||||
// cannot edit in dashboard
|
||||
showWriteControls: false,
|
||||
},
|
||||
} as unknown as ApplicationStart['capabilities'],
|
||||
});
|
||||
|
||||
expect(editApi.api.isEditingEnabled()).toBe(false);
|
||||
});
|
||||
});
|
|
@ -248,7 +248,12 @@ export function initializeEditApi(
|
|||
* Check everything here: user/app permissions and the current inline editing state
|
||||
*/
|
||||
isEditingEnabled: () => {
|
||||
return apiHasAppContext(parentApi) && canEdit() && panelManagementApi.isEditingEnabled();
|
||||
return Boolean(
|
||||
parentApi &&
|
||||
apiHasAppContext(parentApi) &&
|
||||
canEdit() &&
|
||||
panelManagementApi.isEditingEnabled()
|
||||
);
|
||||
},
|
||||
getEditHref: async () => {
|
||||
if (!parentApi || !apiHasAppContext(parentApi)) {
|
||||
|
|
|
@ -53,6 +53,7 @@ export function initializeInternalApi(
|
|||
// the isNewPanel won't be serialized so it will be always false after the edit panel closes applying the changes
|
||||
const isNewlyCreated$ = new BehaviorSubject<boolean>(initialState.isNewPanel || false);
|
||||
|
||||
const blockingError$ = new BehaviorSubject<Error | undefined>(undefined);
|
||||
const visualizationContext$ = new BehaviorSubject<VisualizationContext>({
|
||||
// doc can point to a different set of attributes for the visualization
|
||||
// i.e. when inline editing or applying a suggestion
|
||||
|
@ -78,6 +79,7 @@ export function initializeInternalApi(
|
|||
renderCount$,
|
||||
isNewlyCreated$,
|
||||
dataViews: dataViews$,
|
||||
blockingError$,
|
||||
messages$,
|
||||
validationMessages$,
|
||||
dispatchError: () => {
|
||||
|
@ -103,6 +105,7 @@ export function initializeInternalApi(
|
|||
messages$.next([]);
|
||||
validationMessages$.next([]);
|
||||
},
|
||||
updateBlockingError: (blockingError: Error | undefined) => blockingError$.next(blockingError),
|
||||
setAsCreated: () => isNewlyCreated$.next(false),
|
||||
getDisplayOptions: () => {
|
||||
const latestAttributes = attributes$.getValue();
|
||||
|
|
|
@ -63,7 +63,7 @@ export function initializeStateManagement(
|
|||
// This is the way to communicate to the embeddable panel to render a blocking error with the
|
||||
// default panel error component - i.e. cannot find a Lens SO type of thing.
|
||||
// For Lens specific errors, we use a Lens specific error component.
|
||||
const [blockingError$] = buildObservableVariable<Error | undefined>(undefined);
|
||||
const [blockingError$] = buildObservableVariable<Error | undefined>(internalApi.blockingError$);
|
||||
return {
|
||||
api: {
|
||||
updateAttributes: internalApi.updateAttributes,
|
||||
|
|
|
@ -30,99 +30,104 @@ import {
|
|||
LensRendererProps,
|
||||
LensRuntimeState,
|
||||
LensSerializedState,
|
||||
VisualizationContext,
|
||||
} from '../types';
|
||||
import { createMockDatasource, createMockVisualization, makeDefaultServices } from '../../mocks';
|
||||
import { Datasource, DatasourceMap, Visualization, VisualizationMap } from '../../types';
|
||||
import { initializeInternalApi } from '../initializers/initialize_internal_api';
|
||||
|
||||
const LensApiMock: LensApi = {
|
||||
// Static props
|
||||
type: DOC_TYPE,
|
||||
uuid: faker.string.uuid(),
|
||||
// Shared Embeddable Observables
|
||||
panelTitle: new BehaviorSubject<string | undefined>(faker.lorem.words()),
|
||||
hidePanelTitle: new BehaviorSubject<boolean | undefined>(false),
|
||||
filters$: new BehaviorSubject<Filter[] | undefined>([]),
|
||||
query$: new BehaviorSubject<Query | AggregateQuery | undefined>({
|
||||
query: 'test',
|
||||
language: 'kuery',
|
||||
}),
|
||||
timeRange$: new BehaviorSubject<TimeRange | undefined>({ from: 'now-15m', to: 'now' }),
|
||||
dataLoading: new BehaviorSubject<boolean | undefined>(false),
|
||||
// Methods
|
||||
getSavedVis: jest.fn(),
|
||||
getFullAttributes: jest.fn(),
|
||||
canViewUnderlyingData$: new BehaviorSubject<boolean>(false),
|
||||
loadViewUnderlyingData: jest.fn(),
|
||||
getViewUnderlyingDataArgs: jest.fn(() => ({
|
||||
dataViewSpec: { id: 'index-pattern-id' },
|
||||
timeRange: { from: 'now-7d', to: 'now' },
|
||||
filters: [],
|
||||
query: undefined,
|
||||
columns: [],
|
||||
})),
|
||||
isTextBasedLanguage: jest.fn(() => true),
|
||||
getTextBasedLanguage: jest.fn(),
|
||||
getInspectorAdapters: jest.fn(() => ({})),
|
||||
inspect: jest.fn(),
|
||||
closeInspector: jest.fn(async () => {}),
|
||||
supportedTriggers: jest.fn(() => []),
|
||||
canLinkToLibrary: jest.fn(async () => false),
|
||||
canUnlinkFromLibrary: jest.fn(async () => false),
|
||||
unlinkFromLibrary: jest.fn(),
|
||||
checkForDuplicateTitle: jest.fn(),
|
||||
/** New embeddable api inherited methods */
|
||||
resetUnsavedChanges: jest.fn(),
|
||||
serializeState: jest.fn(),
|
||||
snapshotRuntimeState: jest.fn(),
|
||||
saveToLibrary: jest.fn(async () => 'saved-id'),
|
||||
getByValueRuntimeSnapshot: jest.fn(),
|
||||
onEdit: jest.fn(),
|
||||
isEditingEnabled: jest.fn(() => true),
|
||||
getTypeDisplayName: jest.fn(() => 'Lens'),
|
||||
setPanelTitle: jest.fn(),
|
||||
setHidePanelTitle: jest.fn(),
|
||||
phase$: new BehaviorSubject<PhaseEvent | undefined>({
|
||||
id: faker.string.uuid(),
|
||||
status: 'rendered',
|
||||
timeToEvent: 1000,
|
||||
}),
|
||||
unsavedChanges: new BehaviorSubject<object | undefined>(undefined),
|
||||
dataViews: new BehaviorSubject<DataView[] | undefined>(undefined),
|
||||
libraryId$: new BehaviorSubject<string | undefined>(undefined),
|
||||
savedObjectId: new BehaviorSubject<string | undefined>(undefined),
|
||||
adapters$: new BehaviorSubject<Adapters>({}),
|
||||
updateAttributes: jest.fn(),
|
||||
updateSavedObjectId: jest.fn(),
|
||||
updateOverrides: jest.fn(),
|
||||
getByReferenceState: jest.fn(),
|
||||
getByValueState: jest.fn(),
|
||||
getTriggerCompatibleActions: jest.fn(),
|
||||
blockingError: new BehaviorSubject<Error | undefined>(undefined),
|
||||
panelDescription: new BehaviorSubject<string | undefined>(undefined),
|
||||
setPanelDescription: jest.fn(),
|
||||
viewMode: new BehaviorSubject<ViewMode>('view'),
|
||||
disabledActionIds: new BehaviorSubject<string[] | undefined>(undefined),
|
||||
setDisabledActionIds: jest.fn(),
|
||||
rendered$: new BehaviorSubject<boolean>(false),
|
||||
searchSessionId$: new BehaviorSubject<string | undefined>(undefined),
|
||||
};
|
||||
function getDefaultLensApiMock() {
|
||||
const LensApiMock: LensApi = {
|
||||
// Static props
|
||||
type: DOC_TYPE,
|
||||
uuid: faker.string.uuid(),
|
||||
// Shared Embeddable Observables
|
||||
panelTitle: new BehaviorSubject<string | undefined>(faker.lorem.words()),
|
||||
hidePanelTitle: new BehaviorSubject<boolean | undefined>(false),
|
||||
filters$: new BehaviorSubject<Filter[] | undefined>([]),
|
||||
query$: new BehaviorSubject<Query | AggregateQuery | undefined>({
|
||||
query: 'test',
|
||||
language: 'kuery',
|
||||
}),
|
||||
timeRange$: new BehaviorSubject<TimeRange | undefined>({ from: 'now-15m', to: 'now' }),
|
||||
dataLoading: new BehaviorSubject<boolean | undefined>(false),
|
||||
// Methods
|
||||
getSavedVis: jest.fn(),
|
||||
getFullAttributes: jest.fn(),
|
||||
canViewUnderlyingData$: new BehaviorSubject<boolean>(false),
|
||||
loadViewUnderlyingData: jest.fn(),
|
||||
getViewUnderlyingDataArgs: jest.fn(() => ({
|
||||
dataViewSpec: { id: 'index-pattern-id' },
|
||||
timeRange: { from: 'now-7d', to: 'now' },
|
||||
filters: [],
|
||||
query: undefined,
|
||||
columns: [],
|
||||
})),
|
||||
isTextBasedLanguage: jest.fn(() => true),
|
||||
getTextBasedLanguage: jest.fn(),
|
||||
getInspectorAdapters: jest.fn(() => ({})),
|
||||
inspect: jest.fn(),
|
||||
closeInspector: jest.fn(async () => {}),
|
||||
supportedTriggers: jest.fn(() => []),
|
||||
canLinkToLibrary: jest.fn(async () => false),
|
||||
canUnlinkFromLibrary: jest.fn(async () => false),
|
||||
unlinkFromLibrary: jest.fn(),
|
||||
checkForDuplicateTitle: jest.fn(),
|
||||
/** New embeddable api inherited methods */
|
||||
resetUnsavedChanges: jest.fn(),
|
||||
serializeState: jest.fn(),
|
||||
snapshotRuntimeState: jest.fn(),
|
||||
saveToLibrary: jest.fn(async () => 'saved-id'),
|
||||
getByValueRuntimeSnapshot: jest.fn(),
|
||||
onEdit: jest.fn(),
|
||||
isEditingEnabled: jest.fn(() => true),
|
||||
getTypeDisplayName: jest.fn(() => 'Lens'),
|
||||
setPanelTitle: jest.fn(),
|
||||
setHidePanelTitle: jest.fn(),
|
||||
phase$: new BehaviorSubject<PhaseEvent | undefined>({
|
||||
id: faker.string.uuid(),
|
||||
status: 'rendered',
|
||||
timeToEvent: 1000,
|
||||
}),
|
||||
unsavedChanges: new BehaviorSubject<object | undefined>(undefined),
|
||||
dataViews: new BehaviorSubject<DataView[] | undefined>(undefined),
|
||||
libraryId$: new BehaviorSubject<string | undefined>(undefined),
|
||||
savedObjectId: new BehaviorSubject<string | undefined>(undefined),
|
||||
adapters$: new BehaviorSubject<Adapters>({}),
|
||||
updateAttributes: jest.fn(),
|
||||
updateSavedObjectId: jest.fn(),
|
||||
updateOverrides: jest.fn(),
|
||||
getByReferenceState: jest.fn(),
|
||||
getByValueState: jest.fn(),
|
||||
getTriggerCompatibleActions: jest.fn(),
|
||||
blockingError: new BehaviorSubject<Error | undefined>(undefined),
|
||||
panelDescription: new BehaviorSubject<string | undefined>(undefined),
|
||||
setPanelDescription: jest.fn(),
|
||||
viewMode: new BehaviorSubject<ViewMode>('view'),
|
||||
disabledActionIds: new BehaviorSubject<string[] | undefined>(undefined),
|
||||
setDisabledActionIds: jest.fn(),
|
||||
rendered$: new BehaviorSubject<boolean>(false),
|
||||
searchSessionId$: new BehaviorSubject<string | undefined>(undefined),
|
||||
};
|
||||
return LensApiMock;
|
||||
}
|
||||
|
||||
const LensSerializedStateMock: LensSerializedState = createEmptyLensState(
|
||||
'lnsXY',
|
||||
faker.lorem.words(),
|
||||
faker.lorem.text(),
|
||||
{ query: 'test', language: 'kuery' }
|
||||
);
|
||||
function getDefaultLensSerializedStateMock() {
|
||||
const LensSerializedStateMock: LensSerializedState = createEmptyLensState(
|
||||
'lnsXY',
|
||||
faker.lorem.words(),
|
||||
faker.lorem.text(),
|
||||
{ query: 'test', language: 'kuery' }
|
||||
);
|
||||
return LensSerializedStateMock;
|
||||
}
|
||||
|
||||
export function getLensAttributesMock(attributes?: Partial<LensRuntimeState['attributes']>) {
|
||||
return deepMerge(LensSerializedStateMock.attributes!, attributes ?? {});
|
||||
return deepMerge(getDefaultLensSerializedStateMock().attributes!, attributes ?? {});
|
||||
}
|
||||
|
||||
export function getLensApiMock(overrides: Partial<LensApi> = {}) {
|
||||
return {
|
||||
...LensApiMock,
|
||||
...getDefaultLensApiMock(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
@ -130,7 +135,7 @@ export function getLensApiMock(overrides: Partial<LensApi> = {}) {
|
|||
export function getLensSerializedStateMock(overrides: Partial<LensSerializedState> = {}) {
|
||||
return {
|
||||
savedObjectId: faker.string.uuid(),
|
||||
...LensSerializedStateMock,
|
||||
...getDefaultLensSerializedStateMock(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
@ -139,15 +144,15 @@ export function getLensRuntimeStateMock(
|
|||
overrides: Partial<LensRuntimeState> = {}
|
||||
): LensRuntimeState {
|
||||
return {
|
||||
...(LensSerializedStateMock as LensRuntimeState),
|
||||
...(getDefaultLensSerializedStateMock() as LensRuntimeState),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function getLensComponentProps(overrides: Partial<LensRendererProps> = {}) {
|
||||
return {
|
||||
...LensSerializedStateMock,
|
||||
...LensApiMock,
|
||||
...getDefaultLensSerializedStateMock(),
|
||||
...getDefaultLensApiMock(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
@ -274,38 +279,26 @@ export function getValidExpressionParams(
|
|||
};
|
||||
}
|
||||
|
||||
const LensInternalApiMock = initializeInternalApi(
|
||||
getLensRuntimeStateMock(),
|
||||
{},
|
||||
makeEmbeddableServices()
|
||||
);
|
||||
function getInternalApiWithFunctionWrappers() {
|
||||
const newApi = initializeInternalApi(getLensRuntimeStateMock(), {}, makeEmbeddableServices());
|
||||
const fns: Array<keyof LensInternalApi> = (
|
||||
Object.keys(newApi) as Array<keyof LensInternalApi>
|
||||
).filter((key) => typeof newApi[key] === 'function');
|
||||
for (const fn of fns) {
|
||||
const originalFn = newApi[fn];
|
||||
// @ts-expect-error
|
||||
newApi[fn] = jest.fn(originalFn);
|
||||
}
|
||||
return newApi;
|
||||
}
|
||||
|
||||
export function getLensInternalApiMock(overrides: Partial<LensInternalApi> = {}): LensInternalApi {
|
||||
return {
|
||||
...LensInternalApiMock,
|
||||
...getInternalApiWithFunctionWrappers(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function getVisualizationContextHelperMock(
|
||||
attributesOverrides?: Partial<LensRuntimeState['attributes']>,
|
||||
contextOverrides?: Omit<Partial<VisualizationContext>, 'doc'>
|
||||
) {
|
||||
return {
|
||||
getVisualizationContext: jest.fn(() => ({
|
||||
activeAttributes: getLensAttributesMock(attributesOverrides),
|
||||
mergedSearchContext: {},
|
||||
indexPatterns: {},
|
||||
indexPatternRefs: [],
|
||||
activeVisualizationState: undefined,
|
||||
activeDatasourceState: undefined,
|
||||
activeData: undefined,
|
||||
...contextOverrides,
|
||||
})),
|
||||
updateVisualizationContext: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
export function createUnifiedSearchApi(
|
||||
query: Query | AggregateQuery = {
|
||||
query: '',
|
||||
|
|
|
@ -29,7 +29,7 @@ function getDefaultProps({
|
|||
// provide a valid expression to render
|
||||
internalApi.updateExpressionParams(getValidExpressionParams());
|
||||
return {
|
||||
internalApi: getLensInternalApiMock(internalApiOverrides),
|
||||
internalApi,
|
||||
api: getLensApiMock(apiOverrides),
|
||||
onUnmount: jest.fn(),
|
||||
};
|
||||
|
|
|
@ -440,6 +440,8 @@ export type LensInternalApi = Simplify<
|
|||
updateMessages: (newMessages: UserMessage[]) => void;
|
||||
validationMessages$: PublishingSubject<UserMessage[]>;
|
||||
updateValidationMessages: (newMessages: UserMessage[]) => void;
|
||||
blockingError$: PublishingSubject<Error | undefined>;
|
||||
updateBlockingError: (newBlockingError: Error | undefined) => void;
|
||||
resetAllMessages: () => void;
|
||||
getDisplayOptions: () => VisualizationDisplayOptions;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,303 @@
|
|||
/*
|
||||
* 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 { buildUserMessagesHelpers } from './api'; // Adjust the import path as necessary
|
||||
import {
|
||||
getLensApiMock,
|
||||
getLensAttributesMock,
|
||||
getLensInternalApiMock,
|
||||
makeEmbeddableServices,
|
||||
} from '../mocks';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import type { Datasource, SharingSavedObjectProps, UserMessage, Visualization } from '../../types';
|
||||
import type { UserMessagesDisplayLocationId } from '../../types';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { EDITOR_MISSING_VIS_TYPE, EDITOR_UNKNOWN_DATASOURCE_TYPE } from '../../user_messages_ids';
|
||||
|
||||
const ALL_LOCATIONS: UserMessagesDisplayLocationId[] = [
|
||||
'toolbar',
|
||||
'embeddableBadge',
|
||||
'visualization', // blocks render
|
||||
'visualizationOnEmbeddable', // blocks render in embeddable only
|
||||
'visualizationInEditor', // blocks render in editor only
|
||||
'textBasedLanguagesQueryInput',
|
||||
'banner',
|
||||
'dimensionButton',
|
||||
];
|
||||
|
||||
function createUserMessage(
|
||||
locations: Array<Exclude<UserMessagesDisplayLocationId, 'dimensionButton'>> = ['embeddableBadge'],
|
||||
severity: UserMessage['severity'] = 'error'
|
||||
): UserMessage {
|
||||
return {
|
||||
uniqueId: faker.string.uuid(),
|
||||
severity: severity || 'error',
|
||||
shortMessage: faker.lorem.word(),
|
||||
longMessage: () => faker.lorem.sentence(),
|
||||
fixableInEditor: false,
|
||||
displayLocations: locations.map((location) => ({ id: location })),
|
||||
};
|
||||
}
|
||||
|
||||
function buildUserMessagesApi(
|
||||
metaInfo?: SharingSavedObjectProps,
|
||||
{
|
||||
visOverrides,
|
||||
dataOverrides,
|
||||
}: {
|
||||
visOverrides?: { id: string } & Partial<Visualization>;
|
||||
dataOverrides?: { id: string } & Partial<Datasource>;
|
||||
} = {
|
||||
visOverrides: { id: 'lnsXY' },
|
||||
dataOverrides: { id: 'formBased' },
|
||||
}
|
||||
) {
|
||||
const api = getLensApiMock();
|
||||
const internalApi = getLensInternalApiMock();
|
||||
const services = makeEmbeddableServices(new BehaviorSubject<string>(''), undefined, {
|
||||
visOverrides,
|
||||
dataOverrides,
|
||||
});
|
||||
// fill the context with some data
|
||||
internalApi.updateVisualizationContext({
|
||||
activeAttributes: getLensAttributesMock({
|
||||
state: {
|
||||
datasourceStates: { formBased: { something: {} } },
|
||||
visualization: { activeId: 'lnsXY', state: {} },
|
||||
query: { query: '', language: 'kuery' },
|
||||
filters: [],
|
||||
},
|
||||
}),
|
||||
activeVisualizationState: {},
|
||||
activeDatasourceState: {},
|
||||
});
|
||||
const onBeforeBadgesRender = jest.fn((messages) => messages);
|
||||
const userMessagesApi = buildUserMessagesHelpers(
|
||||
api,
|
||||
internalApi,
|
||||
services,
|
||||
onBeforeBadgesRender,
|
||||
metaInfo
|
||||
);
|
||||
return { api, internalApi, userMessagesApi, onBeforeBadgesRender };
|
||||
}
|
||||
|
||||
describe('User Messages API', () => {
|
||||
describe('resetMessages', () => {
|
||||
it('should reset the runtime errors', () => {
|
||||
const { userMessagesApi } = buildUserMessagesApi();
|
||||
// add runtime messages
|
||||
const userMessageError = createUserMessage();
|
||||
const userMessageWarning = createUserMessage(['embeddableBadge'], 'warning');
|
||||
const userMessageInfo = createUserMessage(['embeddableBadge'], 'info');
|
||||
userMessagesApi.addUserMessages([userMessageError, userMessageWarning, userMessageInfo]);
|
||||
expect(userMessagesApi.getUserMessages('embeddableBadge').length).toEqual(3);
|
||||
userMessagesApi.resetMessages();
|
||||
expect(userMessagesApi.getUserMessages('embeddableBadge').length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateValidationErrors', () => {
|
||||
it('should basically work', () => {
|
||||
const { userMessagesApi, internalApi } = buildUserMessagesApi();
|
||||
internalApi.updateValidationMessages = jest.fn();
|
||||
const messages = Array(3).fill(createUserMessage());
|
||||
userMessagesApi.updateValidationErrors(messages);
|
||||
expect(internalApi.updateValidationMessages).toHaveBeenCalledWith(messages);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateMessages', () => {
|
||||
it('should avoid to update duplicate messages', () => {
|
||||
const { userMessagesApi, internalApi } = buildUserMessagesApi();
|
||||
// start with these 3 messages
|
||||
const messages = Array(3)
|
||||
.fill(1)
|
||||
.map(() => createUserMessage());
|
||||
// update the messages
|
||||
userMessagesApi.updateMessages(messages);
|
||||
expect(internalApi.updateMessages).toHaveBeenCalledTimes(1);
|
||||
// now try again with the same messages
|
||||
userMessagesApi.updateMessages(messages);
|
||||
expect(internalApi.updateMessages).toHaveBeenCalledTimes(1);
|
||||
// now try with one extra message
|
||||
const messagesWithNewEntry = [...messages, createUserMessage()];
|
||||
userMessagesApi.updateMessages(messagesWithNewEntry);
|
||||
expect(internalApi.updateMessages).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should update the messages if there are new messages', () => {
|
||||
const { userMessagesApi, internalApi } = buildUserMessagesApi();
|
||||
// start with these 3 messages
|
||||
const messages = Array(3).fill(createUserMessage());
|
||||
// update the messages
|
||||
userMessagesApi.updateMessages(messages);
|
||||
expect(internalApi.updateMessages).toHaveBeenCalledWith(messages);
|
||||
// now try with one extra message
|
||||
const messagesWithNewEntry = [...messages, createUserMessage()];
|
||||
userMessagesApi.updateMessages(messagesWithNewEntry);
|
||||
expect(internalApi.updateMessages).toHaveBeenCalledWith(messagesWithNewEntry);
|
||||
});
|
||||
|
||||
it('should update the messages when changing', () => {
|
||||
const { userMessagesApi, internalApi } = buildUserMessagesApi();
|
||||
// start with these 3 messages
|
||||
const messages = Array(3).fill(createUserMessage());
|
||||
// update the messages
|
||||
userMessagesApi.updateMessages(messages);
|
||||
expect(internalApi.updateMessages).toHaveBeenCalledWith(messages);
|
||||
// update with new messages
|
||||
const newMessages = Array(3).fill(createUserMessage());
|
||||
userMessagesApi.updateMessages(newMessages);
|
||||
expect(internalApi.updateMessages).toHaveBeenCalledWith(newMessages);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateBlockingErrors', () => {
|
||||
it('should basically work with a regular Error', () => {
|
||||
const { userMessagesApi, internalApi } = buildUserMessagesApi();
|
||||
internalApi.updateBlockingError = jest.fn();
|
||||
const error = new Error('Something went wrong');
|
||||
userMessagesApi.updateBlockingErrors(error);
|
||||
expect(internalApi.updateBlockingError).toHaveBeenCalledWith(error);
|
||||
});
|
||||
|
||||
it('should work with user messages too', () => {
|
||||
const { userMessagesApi, internalApi } = buildUserMessagesApi();
|
||||
internalApi.updateBlockingError = jest.fn();
|
||||
const userMessage = createUserMessage();
|
||||
userMessagesApi.updateBlockingErrors([userMessage]);
|
||||
expect(internalApi.updateBlockingError).toHaveBeenCalledWith(
|
||||
new Error(userMessage.shortMessage)
|
||||
);
|
||||
});
|
||||
|
||||
it('should pick only the first error from a list of user messages', () => {
|
||||
const { userMessagesApi, internalApi } = buildUserMessagesApi();
|
||||
internalApi.updateBlockingError = jest.fn();
|
||||
const userMessage = createUserMessage();
|
||||
userMessagesApi.updateBlockingErrors([userMessage, createUserMessage(), createUserMessage()]);
|
||||
expect(internalApi.updateBlockingError).toHaveBeenCalledWith(
|
||||
new Error(userMessage.shortMessage)
|
||||
);
|
||||
});
|
||||
|
||||
it('should clear out the error when an empty error is passed', () => {
|
||||
const { userMessagesApi, internalApi } = buildUserMessagesApi();
|
||||
internalApi.updateBlockingError = jest.fn();
|
||||
userMessagesApi.updateBlockingErrors(new Error(''));
|
||||
expect(internalApi.updateBlockingError).toHaveBeenCalledWith(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserMessages', () => {
|
||||
it('should return empty list for no messages', () => {
|
||||
const { userMessagesApi } = buildUserMessagesApi();
|
||||
for (const locationId of ALL_LOCATIONS) {
|
||||
expect(userMessagesApi.getUserMessages(locationId)).toEqual([]);
|
||||
}
|
||||
});
|
||||
|
||||
it('should return basic validation for missing parts of the config', () => {
|
||||
const { userMessagesApi, internalApi } = buildUserMessagesApi();
|
||||
// no doc scenario
|
||||
internalApi.updateVisualizationContext({
|
||||
...internalApi.getVisualizationContext(),
|
||||
activeAttributes: undefined,
|
||||
});
|
||||
for (const locationId of ALL_LOCATIONS) {
|
||||
expect(userMessagesApi.getUserMessages(locationId).map(({ uniqueId }) => uniqueId)).toEqual(
|
||||
[EDITOR_MISSING_VIS_TYPE, EDITOR_UNKNOWN_DATASOURCE_TYPE]
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should detect a URL conflict', () => {
|
||||
const { userMessagesApi } = buildUserMessagesApi({ outcome: 'conflict' });
|
||||
|
||||
for (const locationId of ALL_LOCATIONS.filter((id) => id !== 'visualization')) {
|
||||
expect(userMessagesApi.getUserMessages(locationId)).toEqual([]);
|
||||
}
|
||||
expect(userMessagesApi.getUserMessages('visualization')).toEqual(
|
||||
expect.arrayContaining([expect.objectContaining({ uniqueId: 'url-conflict' })])
|
||||
);
|
||||
});
|
||||
|
||||
it('should filter messages based on severity criteria', () => {
|
||||
const { userMessagesApi } = buildUserMessagesApi();
|
||||
const userMessageError = createUserMessage();
|
||||
const userMessageWarning = createUserMessage(['embeddableBadge'], 'warning');
|
||||
const userMessageInfo = createUserMessage(['embeddableBadge'], 'info');
|
||||
userMessagesApi.addUserMessages([userMessageError, userMessageWarning, userMessageInfo]);
|
||||
expect(userMessagesApi.getUserMessages('embeddableBadge', { severity: 'error' })).toEqual(
|
||||
expect.arrayContaining([userMessageError])
|
||||
);
|
||||
expect(userMessagesApi.getUserMessages('embeddableBadge', { severity: 'warning' })).toEqual(
|
||||
expect.arrayContaining([userMessageWarning])
|
||||
);
|
||||
expect(userMessagesApi.getUserMessages('embeddableBadge', { severity: 'info' })).toEqual(
|
||||
expect.arrayContaining([userMessageInfo])
|
||||
);
|
||||
});
|
||||
|
||||
it('should filter messages based on locationId', () => {
|
||||
const { userMessagesApi } = buildUserMessagesApi();
|
||||
const userMessageEmbeddable = createUserMessage(['embeddableBadge']);
|
||||
const userMessageVisualization = createUserMessage(['visualization']);
|
||||
const userMessageEmbeddableVisualization = createUserMessage([
|
||||
'visualization',
|
||||
'embeddableBadge',
|
||||
]);
|
||||
userMessagesApi.addUserMessages([
|
||||
userMessageEmbeddable,
|
||||
userMessageVisualization,
|
||||
userMessageEmbeddableVisualization,
|
||||
]);
|
||||
expect(userMessagesApi.getUserMessages('embeddableBadge').length).toEqual(2);
|
||||
expect(userMessagesApi.getUserMessages('visualization').length).toEqual(2);
|
||||
expect(userMessagesApi.getUserMessages('visualizationOnEmbeddable').length).toEqual(0);
|
||||
});
|
||||
|
||||
it('should return deeper validation messages from both datasource and visualization', () => {
|
||||
const vizGetUserMessages = jest.fn();
|
||||
const datasourceGetUserMessages = jest.fn();
|
||||
const { userMessagesApi } = buildUserMessagesApi(undefined, {
|
||||
visOverrides: { id: 'lnsXY', getUserMessages: vizGetUserMessages },
|
||||
dataOverrides: { id: 'formBased', getUserMessages: datasourceGetUserMessages },
|
||||
});
|
||||
// now add a message, then check that it has been called in both the visualization and datasource
|
||||
const userMessageVisualization = createUserMessage(['visualization']);
|
||||
userMessagesApi.addUserMessages([userMessageVisualization]);
|
||||
userMessagesApi.getUserMessages('visualization');
|
||||
expect(vizGetUserMessages).toHaveBeenCalled();
|
||||
expect(datasourceGetUserMessages).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should enable consumers to filter the final list of messages', () => {
|
||||
const { userMessagesApi, onBeforeBadgesRender } = buildUserMessagesApi();
|
||||
// it should not be called when no messages are avaialble
|
||||
userMessagesApi.getUserMessages('embeddableBadge');
|
||||
expect(onBeforeBadgesRender).not.toHaveBeenCalled();
|
||||
// now add a message, then check that it has been called
|
||||
const userMessageEmbeddable = createUserMessage(['embeddableBadge']);
|
||||
userMessagesApi.addUserMessages([userMessageEmbeddable]);
|
||||
userMessagesApi.getUserMessages('embeddableBadge');
|
||||
expect(onBeforeBadgesRender).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('addUserMessages', () => {
|
||||
it('should basically work', () => {
|
||||
const { userMessagesApi } = buildUserMessagesApi();
|
||||
expect(userMessagesApi.getUserMessages('embeddableBadge').length).toEqual(0);
|
||||
// now add a message, then check that it has been called
|
||||
const userMessageEmbeddable = createUserMessage();
|
||||
userMessagesApi.addUserMessages([userMessageEmbeddable]);
|
||||
expect(userMessagesApi.getUserMessages('embeddableBadge').length).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -5,9 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SpacesApi } from '@kbn/spaces-plugin/public';
|
||||
import { Adapters } from '@kbn/inspector-plugin/common';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import {
|
||||
filterAndSortUserMessages,
|
||||
getApplicationUserMessages,
|
||||
|
@ -99,9 +97,8 @@ function getWarningMessages(
|
|||
export function buildUserMessagesHelpers(
|
||||
api: LensApi,
|
||||
internalApi: LensInternalApi,
|
||||
{ coreStart, data, visualizationMap, datasourceMap }: LensEmbeddableStartServices,
|
||||
{ coreStart, data, visualizationMap, datasourceMap, spaces }: LensEmbeddableStartServices,
|
||||
onBeforeBadgesRender: LensPublicCallbacks['onBeforeBadgesRender'],
|
||||
spaces?: SpacesApi,
|
||||
metaInfo?: SharingSavedObjectProps
|
||||
): {
|
||||
getUserMessages: UserMessagesGetter;
|
||||
|
@ -268,9 +265,9 @@ export function buildUserMessagesHelpers(
|
|||
addLog(`Blocking error: ${error?.message}`);
|
||||
}
|
||||
|
||||
if (error?.message !== api.blockingError.getValue()?.message) {
|
||||
if (error?.message !== internalApi.blockingError$.getValue()?.message) {
|
||||
const finalError = error?.message === '' ? undefined : error;
|
||||
(api.blockingError as BehaviorSubject<Error | undefined>).next(finalError);
|
||||
internalApi.updateBlockingError(finalError);
|
||||
}
|
||||
},
|
||||
updateWarnings: () => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue