import { act, render, renderHook, screen, waitFor } from '@testing-library/react';
import { PluginContextProvider, PluginLoadingStrategy, PluginMeta, PluginType } from '@grafana/data';
import { config } from '@grafana/runtime';
import { ExtensionRegistriesProvider } from './ExtensionRegistriesContext';
import { log } from './logs/log';
import { resetLogMock } from './logs/testUtils';
import { AddedComponentsRegistry } from './registry/AddedComponentsRegistry';
import { AddedLinksRegistry } from './registry/AddedLinksRegistry';
import { ExposedComponentsRegistry } from './registry/ExposedComponentsRegistry';
import { PluginExtensionRegistries } from './registry/types';
import { useLoadAppPlugins } from './useLoadAppPlugins';
import { usePluginComponent } from './usePluginComponent';
import { isGrafanaDevMode, wrapWithPluginContext } from './utils';
jest.mock('./useLoadAppPlugins');
jest.mock('app/features/plugins/pluginSettings', () => ({
getPluginSettings: jest.fn().mockResolvedValue({
id: 'my-app-plugin',
enabled: true,
jsonData: {},
type: 'panel',
name: 'My App Plugin',
module: 'app/plugins/my-app-plugin/module',
}),
}));
jest.mock('./utils', () => ({
...jest.requireActual('./utils'),
// Manually set the dev mode to false
// (to make sure that by default we are testing a production scneario)
isGrafanaDevMode: jest.fn().mockReturnValue(false),
wrapWithPluginContext: jest.fn().mockImplementation((_, component: React.ReactNode) => component),
}));
jest.mock('./logs/log', () => {
const { createLogMock } = jest.requireActual('./logs/testUtils');
const original = jest.requireActual('./logs/log');
return {
...original,
log: createLogMock(),
};
});
describe('usePluginComponent()', () => {
let registries: PluginExtensionRegistries;
let wrapper: ({ children }: { children: React.ReactNode }) => JSX.Element;
let pluginMeta: PluginMeta;
const originalApps = config.apps;
const pluginId = 'myorg-extensions-app';
const exposedComponentId = `${pluginId}/exposed-component/v1`;
const exposedComponentConfig = {
id: exposedComponentId,
title: 'Exposed component',
description: 'Exposed component description',
component: () =>
Hello World
,
};
const appPluginConfig = {
id: pluginId,
path: '',
version: '',
preload: false,
angular: {
detected: false,
hideDeprecation: false,
},
loadingStrategy: PluginLoadingStrategy.fetch,
dependencies: {
grafanaVersion: '8.0.0',
plugins: [],
extensions: {
exposedComponents: [],
},
},
extensions: {
addedLinks: [],
addedComponents: [],
// This is necessary, so we can register exposed components to the registry during the tests
// (Otherwise the registry would reject it in the imitated production mode)
exposedComponents: [exposedComponentConfig],
extensionPoints: [],
},
};
beforeEach(() => {
registries = {
addedComponentsRegistry: new AddedComponentsRegistry(),
exposedComponentsRegistry: new ExposedComponentsRegistry(),
addedLinksRegistry: new AddedLinksRegistry(),
};
jest.mocked(useLoadAppPlugins).mockReturnValue({ isLoading: false });
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
resetLogMock(log);
jest.mocked(wrapWithPluginContext).mockClear();
pluginMeta = {
id: pluginId,
name: 'Extensions App',
type: PluginType.app,
module: '',
baseUrl: '',
info: {
author: {
name: 'MyOrg',
},
description: 'App for testing extensions',
links: [],
logos: {
large: '',
small: '',
},
screenshots: [],
updated: '2023-10-26T18:25:01Z',
version: '1.0.0',
},
extensions: {
addedLinks: [],
addedComponents: [],
exposedComponents: [],
extensionPoints: [],
},
dependencies: {
grafanaVersion: '8.0.0',
plugins: [],
extensions: {
exposedComponents: [],
},
},
};
config.apps = {
[pluginId]: appPluginConfig,
};
wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
);
});
afterEach(() => {
config.apps = originalApps;
});
it('should return null if there are no component exposed for the id', () => {
const { result } = renderHook(() => usePluginComponent('foo/bar'), { wrapper });
expect(result.current.component).toEqual(null);
expect(result.current.isLoading).toEqual(false);
});
it('should return component, that can be rendered, from the registry', async () => {
registries.exposedComponentsRegistry.register({
pluginId,
configs: [exposedComponentConfig],
});
const { result } = renderHook(() => usePluginComponent(exposedComponentId), { wrapper });
const Component = result.current.component;
act(() => {
render(Component && );
});
expect(result.current.isLoading).toEqual(false);
expect(result.current.component).not.toBeNull();
expect(await screen.findByText('Hello World')).toBeVisible();
});
it('should dynamically update when component is registered to the registry', async () => {
const { result, rerender } = renderHook(() => usePluginComponent(exposedComponentId), { wrapper });
// No extensions yet
expect(result.current.component).toBeNull();
expect(result.current.isLoading).toEqual(false);
// Add extensions to the registry
act(() => {
registries.exposedComponentsRegistry.register({
pluginId,
configs: [exposedComponentConfig],
});
});
// Check if the hook returns the new extensions
rerender();
const Component = result.current.component;
expect(result.current.isLoading).toEqual(false);
expect(result.current.component).not.toBeNull();
act(() => {
render(Component && );
});
expect(await screen.findByText('Hello World')).toBeVisible();
});
it('should only render the hook once', async () => {
// Add extensions to the registry
act(() => {
registries.exposedComponentsRegistry.register({
pluginId,
configs: [exposedComponentConfig],
});
});
expect(wrapWithPluginContext).toHaveBeenCalledTimes(0);
renderHook(() => usePluginComponent(exposedComponentId), { wrapper });
await waitFor(() => expect(wrapWithPluginContext).toHaveBeenCalledTimes(1));
});
it('should not validate the meta-info in production mode', () => {
// Empty list of exposed component ids in the plugin meta (from plugin.json)
wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
);
registries.exposedComponentsRegistry.register({
pluginId,
configs: [exposedComponentConfig],
});
// Trying to render an exposed component that is not defined in the plugin meta
// (No restrictions due to isGrafanaDevMode() = false)
let { result } = renderHook(() => usePluginComponent(exposedComponentId), { wrapper });
expect(result.current.component).not.toBe(null);
expect(log.warning).not.toHaveBeenCalled();
});
it('should not validate the meta-info in core Grafana', () => {
// Imitate running in dev mode
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
// No plugin context -> used in Grafana core
wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
);
registries.exposedComponentsRegistry.register({
pluginId,
configs: [exposedComponentConfig],
});
// Trying to render an extension point that is not defined in the plugin meta
// (No restrictions due to isGrafanaDevMode() = false)
let { result } = renderHook(() => usePluginComponent(exposedComponentId), {
wrapper,
});
expect(result.current.component).not.toBe(null);
expect(log.warning).not.toHaveBeenCalled();
});
it('should validate the meta-info in dev mode and if inside a plugin', () => {
// Imitate running in dev mode
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
// Empty list of exposed component ids in the plugin meta (from plugin.json)
wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
);
registries.exposedComponentsRegistry.register({
pluginId,
configs: [exposedComponentConfig],
});
// Shouldn't return the component, as it's not present in the plugin.json dependencies
let { result } = renderHook(() => usePluginComponent(exposedComponentId), { wrapper });
expect(result.current.component).toBe(null);
expect(log.error).toHaveBeenCalled();
});
it('should return the exposed component if the meta-info is correct and in dev mode', () => {
// Imitate running in dev mode
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
);
registries.exposedComponentsRegistry.register({
pluginId,
configs: [exposedComponentConfig],
});
let { result } = renderHook(() => usePluginComponent(exposedComponentId), { wrapper });
expect(result.current.component).not.toBe(null);
expect(log.warning).not.toHaveBeenCalled();
});
});