grafana/public/app/features/plugins/extensions/registry/AddedComponentsRegistry.tes...

578 lines
18 KiB
TypeScript

import React from 'react';
import { firstValueFrom } from 'rxjs';
import { PluginLoadingStrategy } from '@grafana/data';
import { config } from '@grafana/runtime';
import { isGrafanaDevMode } from '../utils';
import { AddedComponentsRegistry } from './AddedComponentsRegistry';
import { MSG_CANNOT_REGISTER_READ_ONLY } from './Registry';
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),
}));
describe('AddedComponentsRegistry', () => {
const consoleWarn = jest.fn();
const originalApps = config.apps;
const pluginId = 'grafana-basic-app';
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: [],
exposedComponents: [],
extensionPoints: [],
},
};
beforeEach(() => {
global.console.warn = consoleWarn;
consoleWarn.mockReset();
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
config.apps = {
[pluginId]: appPluginConfig,
};
});
afterEach(() => {
config.apps = originalApps;
});
it('should return empty registry when no extensions registered', async () => {
const reactiveRegistry = new AddedComponentsRegistry();
const observable = reactiveRegistry.asObservable();
const registry = await firstValueFrom(observable);
expect(registry).toEqual({});
});
it('should be possible to register added components in the registry', async () => {
const extensionPointId = `${pluginId}/hello-world/v1`;
const reactiveRegistry = new AddedComponentsRegistry();
reactiveRegistry.register({
pluginId,
configs: [
{
targets: [extensionPointId],
title: 'not important',
description: 'not important',
component: () => React.createElement('div', null, 'Hello World'),
},
],
});
const registry = await reactiveRegistry.getState();
expect(Object.keys(registry)).toHaveLength(1);
expect(registry[extensionPointId][0]).toMatchObject({
pluginId,
title: 'not important',
description: 'not important',
});
});
it('should be possible to asynchronously register component extensions for the same extension point (different plugins)', async () => {
const pluginId1 = 'grafana-basic-app';
const pluginId2 = 'grafana-basic-app2';
const extensionPointId = 'grafana/alerting/home';
const reactiveRegistry = new AddedComponentsRegistry();
// Register extensions for the first plugin
reactiveRegistry.register({
pluginId: pluginId1,
configs: [
{
title: 'Component 1 title',
description: 'Component 1 description',
targets: ['grafana/alerting/home'],
component: () => React.createElement('div', null, 'Hello World1'),
},
],
});
const registry1 = await reactiveRegistry.getState();
expect(Object.keys(registry1)).toHaveLength(1);
expect(registry1[extensionPointId][0]).toMatchObject({
pluginId: pluginId1,
title: 'Component 1 title',
description: 'Component 1 description',
});
// Register an extension component for the second plugin to the same extension point
reactiveRegistry.register({
pluginId: pluginId2,
configs: [
{
title: 'Component 2 title',
description: 'Component 2 description',
targets: [extensionPointId],
component: () => React.createElement('div', null, 'Hello World1'),
},
],
});
const registry2 = await reactiveRegistry.getState();
expect(Object.keys(registry2)).toHaveLength(1);
expect(registry2[extensionPointId]).toEqual(
expect.arrayContaining([
expect.objectContaining({
pluginId: pluginId1,
title: 'Component 1 title',
description: 'Component 1 description',
}),
expect.objectContaining({
pluginId: pluginId2,
title: 'Component 2 title',
description: 'Component 2 description',
}),
])
);
});
it('should be possible to asynchronously register component extensions for a different extension points (different plugin)', async () => {
const pluginId1 = 'grafana-basic-app';
const pluginId2 = 'grafana-basic-app2';
const extensionPointId1 = 'grafana/alerting/home';
const extensionPointId2 = 'grafana/user/profile/tab';
const reactiveRegistry = new AddedComponentsRegistry();
// Register extensions for the first plugin
reactiveRegistry.register({
pluginId: pluginId1,
configs: [
{
title: 'Component 1 title',
description: 'Component 1 description',
targets: [extensionPointId1],
component: () => React.createElement('div', null, 'Hello World1'),
},
],
});
const registry1 = await reactiveRegistry.getState();
expect(registry1).toEqual({
[extensionPointId1]: expect.arrayContaining([
expect.objectContaining({
pluginId: pluginId1,
title: 'Component 1 title',
description: 'Component 1 description',
}),
]),
});
// Register an extension component for the second plugin to a different extension point
reactiveRegistry.register({
pluginId: pluginId2,
configs: [
{
title: 'Component 2 title',
description: 'Component 2 description',
targets: [extensionPointId2],
component: () => React.createElement('div', null, 'Hello World1'),
},
],
});
const registry2 = await reactiveRegistry.getState();
expect(registry2).toEqual({
[extensionPointId1]: expect.arrayContaining([
expect.objectContaining({
pluginId: pluginId1,
title: 'Component 1 title',
description: 'Component 1 description',
}),
]),
[extensionPointId2]: expect.arrayContaining([
expect.objectContaining({
pluginId: pluginId2,
title: 'Component 2 title',
description: 'Component 2 description',
}),
]),
});
});
it('should be possible to asynchronously register component extensions for the same extension point (same plugin)', async () => {
const reactiveRegistry = new AddedComponentsRegistry();
const extensionPointId = 'grafana/alerting/home';
// Register extensions for the first extension point
reactiveRegistry.register({
pluginId: pluginId,
configs: [
{
title: 'Component 1 title',
description: 'Component 1 description',
targets: [extensionPointId],
component: () => React.createElement('div', null, 'Hello World1'),
},
{
title: 'Component 2 title',
description: 'Component 2 description',
targets: [extensionPointId],
component: () => React.createElement('div', null, 'Hello World2'),
},
],
});
const registry1 = await reactiveRegistry.getState();
expect(registry1).toEqual({
[extensionPointId]: expect.arrayContaining([
expect.objectContaining({
pluginId: pluginId,
title: 'Component 1 title',
description: 'Component 1 description',
}),
expect.objectContaining({
pluginId: pluginId,
title: 'Component 2 title',
description: 'Component 2 description',
}),
]),
});
});
it('should be possible to register one extension component targeting multiple extension points', async () => {
const reactiveRegistry = new AddedComponentsRegistry();
const extensionPointId1 = 'grafana/alerting/home';
const extensionPointId2 = 'grafana/user/profile/tab';
reactiveRegistry.register({
pluginId: pluginId,
configs: [
{
title: 'Component 1 title',
description: 'Component 1 description',
targets: [extensionPointId1, extensionPointId2],
component: () => React.createElement('div', null, 'Hello World1'),
},
],
});
const registry1 = await reactiveRegistry.getState();
expect(registry1).toEqual({
[extensionPointId1]: expect.arrayContaining([
expect.objectContaining({
pluginId: pluginId,
title: 'Component 1 title',
description: 'Component 1 description',
}),
]),
[extensionPointId2]: expect.arrayContaining([
expect.objectContaining({
pluginId: pluginId,
title: 'Component 1 title',
description: 'Component 1 description',
}),
]),
});
});
it('should notify subscribers when the registry changes', async () => {
const pluginId1 = 'grafana-basic-app';
const pluginId2 = 'myorg-extensions-app';
const extensionPointId1 = 'grafana/alerting/home';
const extensionPointId2 = 'grafana/user/profile/tab';
const reactiveRegistry = new AddedComponentsRegistry();
const observable = reactiveRegistry.asObservable();
const subscribeCallback = jest.fn();
observable.subscribe(subscribeCallback);
reactiveRegistry.register({
pluginId: pluginId1,
configs: [
{
title: 'Component 1 title',
description: 'Component 1 description',
targets: [extensionPointId1],
component: () => React.createElement('div', null, 'Hello World1'),
},
],
});
expect(subscribeCallback).toHaveBeenCalledTimes(2);
reactiveRegistry.register({
pluginId: pluginId2,
configs: [
{
title: 'Component 2 title',
description: 'Component 2 description',
targets: [extensionPointId2],
component: () => React.createElement('div', null, 'Hello World2'),
},
],
});
expect(subscribeCallback).toHaveBeenCalledTimes(3);
const registry = subscribeCallback.mock.calls[2][0];
expect(registry).toEqual({
[extensionPointId1]: expect.arrayContaining([
expect.objectContaining({
pluginId: pluginId1,
title: 'Component 1 title',
description: 'Component 1 description',
}),
]),
[extensionPointId2]: expect.arrayContaining([
expect.objectContaining({
pluginId: pluginId2,
title: 'Component 2 title',
description: 'Component 2 description',
}),
]),
});
});
it('should log a warning when added component id is not suffixed with component version', async () => {
const registry = new AddedComponentsRegistry();
const extensionPointId = 'grafana/test/home';
registry.register({
pluginId,
configs: [
{
title: 'Component 1 title',
description: 'Component 1 description',
targets: [extensionPointId],
component: () => React.createElement('div', null, 'Hello World1'),
},
],
});
expect(consoleWarn).toHaveBeenCalledWith(
`[Plugin Extensions] Added component "Component 1 title": it's recommended to suffix the extension point id ("${extensionPointId}") with a version, e.g 'myorg-basic-app/extension-point/v1'.`
);
const currentState = await registry.getState();
expect(Object.keys(currentState)).toHaveLength(1);
});
it('should not register component when description is missing', async () => {
const registry = new AddedComponentsRegistry();
const extensionPointId = 'grafana/alerting/home';
registry.register({
pluginId,
configs: [
{
title: 'Component 1 title',
description: '',
targets: [extensionPointId],
component: () => React.createElement('div', null, 'Hello World1'),
},
],
});
expect(consoleWarn).toHaveBeenCalledWith(
"[Plugin Extensions] Could not register added component with title 'Component 1 title'. Reason: Description is missing."
);
const currentState = await registry.getState();
expect(Object.keys(currentState)).toHaveLength(0);
});
it('should not register component when title is missing', async () => {
const registry = new AddedComponentsRegistry();
const extensionPointId = 'grafana/alerting/home';
registry.register({
pluginId,
configs: [
{
title: 'Component 1 title',
description: '',
targets: [extensionPointId],
component: () => React.createElement('div', null, 'Hello World1'),
},
],
});
expect(consoleWarn).toHaveBeenCalledWith(
"[Plugin Extensions] Could not register added component with title 'Component 1 title'. Reason: Description is missing."
);
const currentState = await registry.getState();
expect(Object.keys(currentState)).toHaveLength(0);
});
it('should not be possible to register a component on a read-only registry', async () => {
const registry = new AddedComponentsRegistry();
const readOnlyRegistry = registry.readOnly();
const extensionPointId = 'grafana/alerting/home';
expect(() => {
readOnlyRegistry.register({
pluginId,
configs: [
{
title: 'Component 1 title',
description: '',
targets: [extensionPointId],
component: () => React.createElement('div', null, 'Hello World1'),
},
],
});
}).toThrow(MSG_CANNOT_REGISTER_READ_ONLY);
const currentState = await readOnlyRegistry.getState();
expect(Object.keys(currentState)).toHaveLength(0);
});
it('should pass down fresh registrations to the read-only version of the registry', async () => {
const pluginId = 'grafana-basic-app';
const registry = new AddedComponentsRegistry();
const readOnlyRegistry = registry.readOnly();
const subscribeCallback = jest.fn();
let readOnlyState;
// Should have no extensions registered in the beginning
readOnlyState = await readOnlyRegistry.getState();
expect(Object.keys(readOnlyState)).toHaveLength(0);
readOnlyRegistry.asObservable().subscribe(subscribeCallback);
// Register an extension to the original (writable) registry
registry.register({
pluginId,
configs: [
{
title: 'Component 1 title',
description: 'Component 1 description',
targets: ['grafana/alerting/home'],
component: () => React.createElement('div', null, 'Hello World1'),
},
],
});
// The read-only registry should have received the new extension
readOnlyState = await readOnlyRegistry.getState();
expect(Object.keys(readOnlyState)).toHaveLength(1);
expect(subscribeCallback).toHaveBeenCalledTimes(2);
expect(Object.keys(subscribeCallback.mock.calls[1][0])).toEqual(['grafana/alerting/home']);
});
it('should not register a component added by a plugin in dev-mode if the meta-info is missing from the plugin.json', async () => {
// Enabling dev mode
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
const registry = new AddedComponentsRegistry();
const componentConfig = {
title: 'Component title',
description: 'Component description',
targets: ['grafana/alerting/home'],
component: () => React.createElement('div', null, 'Hello World1'),
};
// Make sure that the meta-info is empty
config.apps[pluginId].extensions.addedComponents = [];
registry.register({
pluginId,
configs: [componentConfig],
});
const currentState = await registry.getState();
expect(Object.keys(currentState)).toHaveLength(0);
expect(consoleWarn).toHaveBeenCalled();
});
it('should register a component added by a core Grafana in dev-mode even if the meta-info is missing', async () => {
// Enabling dev mode
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
const registry = new AddedComponentsRegistry();
const componentConfig = {
title: 'Component title',
description: 'Component description',
targets: ['grafana/alerting/home'],
component: () => React.createElement('div', null, 'Hello World1'),
};
registry.register({
pluginId: 'grafana',
configs: [componentConfig],
});
const currentState = await registry.getState();
expect(Object.keys(currentState)).toHaveLength(1);
expect(consoleWarn).not.toHaveBeenCalled();
});
it('should register a component added by a plugin in production mode even if the meta-info is missing', async () => {
// Production mode
jest.mocked(isGrafanaDevMode).mockReturnValue(false);
const registry = new AddedComponentsRegistry();
const componentConfig = {
title: 'Component title',
description: 'Component description',
targets: ['grafana/alerting/home'],
component: () => React.createElement('div', null, 'Hello World1'),
};
// Make sure that the meta-info is empty
config.apps[pluginId].extensions.addedComponents = [];
registry.register({
pluginId,
configs: [componentConfig],
});
const currentState = await registry.getState();
expect(Object.keys(currentState)).toHaveLength(1);
expect(consoleWarn).not.toHaveBeenCalled();
});
it('should register a component added by a plugin in dev-mode if the meta-info is present', async () => {
// Enabling dev mode
jest.mocked(isGrafanaDevMode).mockReturnValue(true);
const registry = new AddedComponentsRegistry();
const componentConfig = {
title: 'Component title',
description: 'Component description',
targets: ['grafana/alerting/home'],
component: () => React.createElement('div', null, 'Hello World1'),
};
// Make sure that the meta-info is empty
config.apps[pluginId].extensions.addedComponents = [componentConfig];
registry.register({
pluginId,
configs: [componentConfig],
});
const currentState = await registry.getState();
expect(Object.keys(currentState)).toHaveLength(1);
expect(consoleWarn).not.toHaveBeenCalled();
});
});