Skip to content

Commit

Permalink
add jest tests, fix types, run initial unsaved changes check in initi…
Browse files Browse the repository at this point in the history
…alizer
  • Loading branch information
ThomThomson committed Feb 1, 2024
1 parent 0647056 commit 51794d1
Show file tree
Hide file tree
Showing 13 changed files with 534 additions and 70 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const getLastSavedStateSubjectForChild = <StateType extends unknown = unk
childId: string,
deserializer?: (state: SerializedPanelState) => StateType
): PublishingSubject<StateType | undefined> | undefined => {
if (!parentApi) return;
const fetchUnsavedChanges = (): StateType | undefined => {
if (!apiPublishesLastSavedState(parentApi)) return;
const rawLastSavedState = parentApi.getLastSavedStateForChild(childId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,15 +143,13 @@ test('Duplicates a non RefOrVal embeddable by value', async () => {
});

test('Gets a unique title from the dashboard', async () => {
expect(await incrementPanelTitle(byRefOrValEmbeddable, '')).toEqual('');
expect(await incrementPanelTitle(container, '')).toEqual('');

container.getPanelTitles = jest.fn().mockImplementation(() => {
return ['testDuplicateTitle', 'testDuplicateTitle (copy)', 'testUniqueTitle'];
});
expect(await incrementPanelTitle(byRefOrValEmbeddable, 'testUniqueTitle')).toEqual(
'testUniqueTitle (copy)'
);
expect(await incrementPanelTitle(byRefOrValEmbeddable, 'testDuplicateTitle')).toEqual(
expect(await incrementPanelTitle(container, 'testUniqueTitle')).toEqual('testUniqueTitle (copy)');
expect(await incrementPanelTitle(container, 'testDuplicateTitle')).toEqual(
'testDuplicateTitle (copy 1)'
);

Expand All @@ -160,20 +158,20 @@ test('Gets a unique title from the dashboard', async () => {
Array.from([...Array(39)], (_, index) => `testDuplicateTitle (copy ${index + 1})`)
);
});
expect(await incrementPanelTitle(byRefOrValEmbeddable, 'testDuplicateTitle')).toEqual(
expect(await incrementPanelTitle(container, 'testDuplicateTitle')).toEqual(
'testDuplicateTitle (copy 40)'
);
expect(await incrementPanelTitle(byRefOrValEmbeddable, 'testDuplicateTitle (copy 100)')).toEqual(
expect(await incrementPanelTitle(container, 'testDuplicateTitle (copy 100)')).toEqual(
'testDuplicateTitle (copy 40)'
);

container.getPanelTitles = jest.fn().mockImplementation(() => {
return ['testDuplicateTitle (copy 100)'];
});
expect(await incrementPanelTitle(byRefOrValEmbeddable, 'testDuplicateTitle')).toEqual(
expect(await incrementPanelTitle(container, 'testDuplicateTitle')).toEqual(
'testDuplicateTitle (copy 101)'
);
expect(await incrementPanelTitle(byRefOrValEmbeddable, 'testDuplicateTitle (copy 100)')).toEqual(
expect(await incrementPanelTitle(container, 'testDuplicateTitle (copy 100)')).toEqual(
'testDuplicateTitle (copy 101)'
);
});
1 change: 0 additions & 1 deletion src/plugins/embeddable/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@ export {
useReactEmbeddableParentApi,
useReactEmbeddableUnsavedChanges,
initializeReactEmbeddableUuid,
diffReactEmbeddableTitles,
initializeReactEmbeddableTitles,
serializeReactEmbeddableTitles,
} from './react_embeddable_system';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@
* Side Public License, v 1.
*/

export { useReactEmbeddableApiHandle, initializeReactEmbeddableUuid } from './react_embeddable_api';
export { useReactEmbeddableUnsavedChanges } from './react_embeddable_unsaved_changes';
export {
useReactEmbeddableApiHandle,
initializeReactEmbeddableUuid,
ReactEmbeddableParentContext,
useReactEmbeddableParentApi,
} from './react_embeddable_parenting';
} from './react_embeddable_api';
export { useReactEmbeddableUnsavedChanges } from './react_embeddable_unsaved_changes';
export {
reactEmbeddableRegistryHasKey,
RegisterReactEmbeddable,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { render, waitFor } from '@testing-library/react';
import { getMockPresentationContainer } from '@kbn/presentation-containers/mocks';
import { renderHook } from '@testing-library/react-hooks';
import React from 'react';
import { BehaviorSubject } from 'rxjs';
import { useReactEmbeddableApiHandle, ReactEmbeddableParentContext } from './react_embeddable_api';
import { DefaultEmbeddableApi } from './types';

describe('react embeddable api', () => {
const defaultApi = {
unsavedChanges: new BehaviorSubject<object | undefined>(undefined),
resetUnsavedChanges: jest.fn(),
serializeState: jest.fn().mockReturnValue({ bork: 'borkbork' }),
};

const parentApi = getMockPresentationContainer();

const TestComponent = React.forwardRef<DefaultEmbeddableApi>((_, ref) => {
useReactEmbeddableApiHandle(defaultApi, ref, '123');
return <div />;
});

it('returns the given API', () => {
const { result } = renderHook(() =>
useReactEmbeddableApiHandle<DefaultEmbeddableApi & { bork: () => 'bork' }>(
{
...defaultApi,
bork: jest.fn().mockReturnValue('bork'),
},
{} as any,
'superBork'
)
);

expect(result.current.thisApi.bork()).toEqual('bork');
expect(result.current.thisApi.serializeState()).toEqual({ bork: 'borkbork' });
});

it('publishes the API into the provided ref', async () => {
const ref = React.createRef<DefaultEmbeddableApi>();
renderHook(() => useReactEmbeddableApiHandle(defaultApi, ref, '123'));
await waitFor(() => expect(ref.current).toBeDefined());
expect(ref.current?.serializeState);
expect(ref.current?.serializeState()).toEqual({ bork: 'borkbork' });
});

it('publishes the API into an imperative handle', async () => {
const ref = React.createRef<DefaultEmbeddableApi>();
render(<TestComponent ref={ref} />);
await waitFor(() => expect(ref.current).toBeDefined());
expect(ref.current?.serializeState);
expect(ref.current?.serializeState()).toEqual({ bork: 'borkbork' });
});

it('returns an API with a parent when rendered inside a parent context', async () => {
const ref = React.createRef<DefaultEmbeddableApi>();
render(
<ReactEmbeddableParentContext.Provider value={{ parentApi }}>
<TestComponent ref={ref} />
</ReactEmbeddableParentContext.Provider>
);
await waitFor(() => expect(ref.current).toBeDefined());
expect(ref.current?.serializeState);
expect(ref.current?.serializeState()).toEqual({ bork: 'borkbork' });

expect(ref.current?.parentApi?.getLastSavedStateForChild).toBeDefined();
expect(ref.current?.parentApi?.registerPanelApi).toBeDefined();
});

it('calls registerPanelApi on its parent', async () => {
const ref = React.createRef<DefaultEmbeddableApi>();
render(
<ReactEmbeddableParentContext.Provider value={{ parentApi }}>
<TestComponent ref={ref} />
</ReactEmbeddableParentContext.Provider>
);
expect(parentApi?.registerPanelApi).toHaveBeenCalledWith('123', expect.any(Object));
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,20 @@
* Side Public License, v 1.
*/

import { apiIsPresentationContainer } from '@kbn/presentation-containers';
import { useImperativeHandle, useMemo } from 'react';
import { apiIsPresentationContainer, PresentationContainer } from '@kbn/presentation-containers';
import { createContext, useContext, useImperativeHandle, useMemo } from 'react';
import { v4 as generateId } from 'uuid';
import { useReactEmbeddableParentContext } from './react_embeddable_parenting';
import { DefaultEmbeddableApi } from './types';

type RegisterEmbeddableApi = Omit<DefaultEmbeddableApi, 'parent'>;

/**
* Pushes any API to the passed in ref. Note that any API passed in will not be rebuilt on
* subsequent renders, so it does not support reactive variables. Instead, pass in setter functions
* and publishing subjects to allow other components to listen to changes.
*/
export const useReactEmbeddableApiHandle = <
ApiType extends DefaultEmbeddableApi = DefaultEmbeddableApi,
RegisterApiType extends RegisterEmbeddableApi = RegisterEmbeddableApi
ApiType extends DefaultEmbeddableApi = DefaultEmbeddableApi
>(
apiToRegister: RegisterApiType,
apiToRegister: Omit<ApiType, 'parent'>,
ref: React.ForwardedRef<ApiType>,
uuid: string
) => {
Expand Down Expand Up @@ -58,3 +54,21 @@ export const useReactEmbeddableApiHandle = <
};

export const initializeReactEmbeddableUuid = (maybeId?: string) => maybeId ?? generateId();

/**
* Parenting
*/
interface ReactEmbeddableParentContext {
parentApi?: PresentationContainer;
}

export const ReactEmbeddableParentContext = createContext<ReactEmbeddableParentContext | null>(
null
);
export const useReactEmbeddableParentApi = (): unknown | null => {
return useContext<ReactEmbeddableParentContext | null>(ReactEmbeddableParentContext)?.parentApi;
};

export const useReactEmbeddableParentContext = (): ReactEmbeddableParentContext | null => {
return useContext<ReactEmbeddableParentContext | null>(ReactEmbeddableParentContext);
};

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import {
registerReactEmbeddableFactory,
reactEmbeddableRegistryHasKey,
getReactEmbeddableFactory,
} from './react_embeddable_registry';
import { ReactEmbeddableFactory } from './types';

describe('react embeddable registry', () => {
const testEmbeddableFactory: ReactEmbeddableFactory = {
deserializeState: jest.fn(),
getComponent: jest.fn(),
};

it('throws an error if requested embeddable factory type is not registered', () => {
expect(() => getReactEmbeddableFactory('notRegistered')).toThrowErrorMatchingInlineSnapshot(
`"No embeddable factory found for type: notRegistered"`
);
});

it('can register and get an embeddable factory', () => {
registerReactEmbeddableFactory('test', testEmbeddableFactory);
expect(getReactEmbeddableFactory('test')).toBe(testEmbeddableFactory);
});

it('can check if a factory is registered', () => {
registerReactEmbeddableFactory('test', testEmbeddableFactory);
expect(reactEmbeddableRegistryHasKey('test')).toBe(true);
expect(reactEmbeddableRegistryHasKey('notRegistered')).toBe(false);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ export const registerReactEmbeddableFactory = <
) => {
registry[key] = factory;
};

export const reactEmbeddableRegistryHasKey = (key: string) => registry[key] !== undefined;

export const getReactEmbeddableFactory = <
StateType extends unknown = unknown,
ApiType extends DefaultEmbeddableApi = DefaultEmbeddableApi
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { render, waitFor, screen } from '@testing-library/react';

import React from 'react';
import { registerReactEmbeddableFactory } from './react_embeddable_registry';
import { ReactEmbeddableRenderer } from './react_embeddable_renderer';
import { ReactEmbeddableFactory } from './types';

describe('react embeddable renderer', () => {
const testEmbeddableFactory: ReactEmbeddableFactory<{ name: string; bork: string }> = {
deserializeState: jest.fn(),
getComponent: jest.fn().mockResolvedValue(() => {
return <div>SUPER TEST COMPONENT</div>;
}),
};

it('deserializes given state', () => {
registerReactEmbeddableFactory('test', testEmbeddableFactory);
render(<ReactEmbeddableRenderer type={'test'} state={{ rawState: { blorp: 'blorp?' } }} />);
expect(testEmbeddableFactory.deserializeState).toHaveBeenCalledWith({
rawState: { blorp: 'blorp?' },
});
});

it('renders the given component once it resolves', () => {
registerReactEmbeddableFactory('test', testEmbeddableFactory);
render(<ReactEmbeddableRenderer type={'test'} state={{ rawState: { blorp: 'blorp?' } }} />);
waitFor(() => {
expect(screen.findByText('SUPER TEST COMPONENT')).toBeInTheDocument();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import {
initializeReactEmbeddableTitles,
SerializedReactEmbeddableTitles,
} from './react_embeddable_titles';

describe('react embeddable titles', () => {
const rawState: SerializedReactEmbeddableTitles = {
title: 'very cool title',
description: 'less cool description',
hidePanelTitles: false,
};

it('should initialize publishing subjects with the provided rawState', () => {
const { titlesApi } = initializeReactEmbeddableTitles(rawState);
expect(titlesApi.panelTitle.value).toBe(rawState.title);
expect(titlesApi.panelDescription.value).toBe(rawState.description);
expect(titlesApi.hidePanelTitle.value).toBe(rawState.hidePanelTitles);
});

it('should update publishing subject values when set functions are called', () => {
const { titlesApi } = initializeReactEmbeddableTitles(rawState);

titlesApi.setPanelTitle('even cooler title');
titlesApi.setPanelDescription('super uncool description');
titlesApi.setHidePanelTitle(true);

expect(titlesApi.panelTitle.value).toEqual('even cooler title');
expect(titlesApi.panelDescription.value).toEqual('super uncool description');
expect(titlesApi.hidePanelTitle.value).toBe(true);
});

it('should correctly serialize current state', () => {
const { serializeTitles, titlesApi } = initializeReactEmbeddableTitles(rawState);
titlesApi.setPanelTitle('UH OH, A TITLE');

const serializedTitles = serializeTitles();
expect(serializedTitles).toMatchInlineSnapshot(`
Object {
"description": "less cool description",
"hidePanelTitles": false,
"title": "UH OH, A TITLE",
}
`);
});

it('should return the correct set of comparators', () => {
const { titleComparators } = initializeReactEmbeddableTitles(rawState);

expect(titleComparators.title).toBeDefined();
expect(titleComparators.description).toBeDefined();
expect(titleComparators.hidePanelTitles).toBeDefined();
});

it('should correctly compare hidePanelTitles with custom comparator', () => {
const { titleComparators } = initializeReactEmbeddableTitles(rawState);

expect(titleComparators.hidePanelTitles![2]!(true, false)).toBe(false);
expect(titleComparators.hidePanelTitles![2]!(undefined, false)).toBe(true);
expect(titleComparators.hidePanelTitles![2]!(true, undefined)).toBe(false);
});
});
Loading

0 comments on commit 51794d1

Please sign in to comment.