grafana/public/app/features/explore/extensions/ToolbarExtensionPoint.test.tsx
Marcus Andersson 804c726413
PluginExtensions: Make the extensions registry reactive (#83085)
* feat: add a reactive extension registry

Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com>

* feat: add hooks to work with the reactive registry

Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com>

* feat: start using the reactive registry

Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com>

* feat: update the "command palette" extension point to use the hook

* feat: update the "alerting" extension point to use the hooks

Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com>

* feat: update the "explore" extension point to use the hooks

Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com>

* feat: update the "datasources config" extension point to use the hooks

Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com>

* feat: update the "panel menu" extension point to use the hooks

Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com>

* feat: update the "pyroscope datasource" extension point to use the hooks

Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com>

* feat: update the "user profile page" extension point to use the hooks

* chore: update betterer

* fix: update the hooks to not re-render unnecessarily

* chore: remove the old `createPluginExtensionRegistry` impementation

* chore: add "TODO" for `PanelMenuBehaviour` extension point

* feat: update the return value of the hooks to contain a `{ isLoading }` param

* tests: add more tests for the usePluginExtensions() hook

* fix: exclude the cloud-home-app from being non-awaited

* refactor: use uuidv4() for random ID generation (for the registry object)

* fix: linting issue

* feat: use the hooks for the new alerting extension point

* feat: use `useMemo()` for `AlertInstanceAction` extension point context

---------

Co-authored-by: Levente Balogh <balogh.levente.hu@gmail.com>
2024-04-24 09:33:16 +02:00

246 lines
8.5 KiB
TypeScript

import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React, { ReactNode } from 'react';
import { Provider } from 'react-redux';
import { PluginExtensionPoints, PluginExtensionTypes } from '@grafana/data';
import { usePluginLinkExtensions } from '@grafana/runtime';
import { DataQuery } from '@grafana/schema';
import { contextSrv } from 'app/core/services/context_srv';
import { configureStore } from 'app/store/configureStore';
import { ExplorePanelData, ExploreState } from 'app/types';
import { createEmptyQueryResponse } from '../state/utils';
import { ToolbarExtensionPoint } from './ToolbarExtensionPoint';
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
usePluginLinkExtensions: jest.fn(),
}));
jest.mock('app/core/services/context_srv');
const contextSrvMock = jest.mocked(contextSrv);
const usePluginLinkExtensionsMock = jest.mocked(usePluginLinkExtensions);
type storeOptions = {
targets: DataQuery[];
data: ExplorePanelData;
};
function renderWithExploreStore(
children: ReactNode,
options: storeOptions = { targets: [{ refId: 'A' }], data: createEmptyQueryResponse() }
) {
const { targets, data } = options;
const store = configureStore({
explore: {
panes: {
left: {
queries: targets,
queryResponse: data,
range: {
raw: { from: 'now-1h', to: 'now' },
},
},
},
} as unknown as ExploreState,
});
render(<Provider store={store}>{children}</Provider>, {});
}
describe('ToolbarExtensionPoint', () => {
describe('with extension points', () => {
beforeAll(() => {
usePluginLinkExtensionsMock.mockReturnValue({
extensions: [
{
pluginId: 'grafana',
id: '1',
type: PluginExtensionTypes.link,
title: 'Add to dashboard',
category: 'Dashboards',
description: 'Add the current query as a panel to a dashboard',
onClick: jest.fn(),
},
{
pluginId: 'grafana-ml-app',
id: '2',
type: PluginExtensionTypes.link,
title: 'ML: Forecast',
description: 'Add the query as a ML forecast',
path: '/a/grafana-ml-ap/forecast',
},
],
isLoading: false,
});
});
it('should render "Add" extension point menu button', () => {
renderWithExploreStore(<ToolbarExtensionPoint exploreId="left" timeZone="browser" />);
expect(screen.getByRole('button', { name: 'Add' })).toBeVisible();
});
it('should render menu with extensions when "Add" is clicked', async () => {
renderWithExploreStore(<ToolbarExtensionPoint exploreId="left" timeZone="browser" />);
await userEvent.click(screen.getByRole('button', { name: 'Add' }));
expect(screen.getByRole('group', { name: 'Dashboards' })).toBeVisible();
expect(screen.getByRole('menuitem', { name: 'Add to dashboard' })).toBeVisible();
expect(screen.getByRole('menuitem', { name: 'ML: Forecast' })).toBeVisible();
});
it('should call onClick from extension when menu item is clicked', async () => {
renderWithExploreStore(<ToolbarExtensionPoint exploreId="left" timeZone="browser" />);
await userEvent.click(screen.getByRole('button', { name: 'Add' }));
await userEvent.click(screen.getByRole('menuitem', { name: 'Add to dashboard' }));
const { extensions } = usePluginLinkExtensionsMock({
extensionPointId: PluginExtensionPoints.ExploreToolbarAction,
});
const [extension] = extensions;
expect(jest.mocked(extension.onClick)).toBeCalledTimes(1);
});
it('should render confirm navigation modal when extension with path is clicked', async () => {
renderWithExploreStore(<ToolbarExtensionPoint exploreId="left" timeZone="browser" />);
await userEvent.click(screen.getByRole('button', { name: 'Add' }));
await userEvent.click(screen.getByRole('menuitem', { name: 'ML: Forecast' }));
expect(screen.getByRole('button', { name: 'Open in new tab' })).toBeVisible();
expect(screen.getByRole('button', { name: 'Open' })).toBeVisible();
expect(screen.getByRole('button', { name: 'Cancel' })).toBeVisible();
});
it('should pass a correct constructed context when fetching extensions', async () => {
const targets = [{ refId: 'A' }];
const data = createEmptyQueryResponse();
renderWithExploreStore(<ToolbarExtensionPoint exploreId="left" timeZone="browser" />, {
targets,
data,
});
const [options] = usePluginLinkExtensionsMock.mock.calls[0];
const { context } = options;
expect(context).toEqual({
exploreId: 'left',
targets,
data: expect.objectContaining({
...data,
timeRange: expect.any(Object),
}),
timeZone: 'browser',
timeRange: { from: 'now-1h', to: 'now' },
shouldShowAddCorrelation: false,
});
});
it('should pass a context with correct timeZone when fetching extensions', async () => {
const targets = [{ refId: 'A' }];
const data = createEmptyQueryResponse();
renderWithExploreStore(<ToolbarExtensionPoint exploreId="left" timeZone="" />, {
targets,
data,
});
const [options] = usePluginLinkExtensionsMock.mock.calls[0];
const { context } = options;
expect(context).toHaveProperty('timeZone', 'browser');
});
it('should correct extension point id when fetching extensions', async () => {
renderWithExploreStore(<ToolbarExtensionPoint exploreId="left" timeZone="browser" />);
const [options] = usePluginLinkExtensionsMock.mock.calls[0];
const { extensionPointId } = options;
expect(extensionPointId).toBe(PluginExtensionPoints.ExploreToolbarAction);
});
});
describe('with extension points without categories', () => {
beforeAll(() => {
usePluginLinkExtensionsMock.mockReturnValue({
extensions: [
{
pluginId: 'grafana',
id: '1',
type: PluginExtensionTypes.link,
title: 'Dashboard',
description: 'Add the current query as a panel to a dashboard',
onClick: jest.fn(),
},
{
pluginId: 'grafana-ml-app',
id: '2',
type: PluginExtensionTypes.link,
title: 'ML: Forecast',
description: 'Add the query as a ML forecast',
path: '/a/grafana-ml-ap/forecast',
},
],
isLoading: false,
});
});
it('should render "Add" extension point menu button', () => {
renderWithExploreStore(<ToolbarExtensionPoint exploreId="left" timeZone="browser" />);
expect(screen.getByRole('button', { name: 'Add' })).toBeVisible();
});
it('should render menu with extensions when "Add" is clicked', async () => {
renderWithExploreStore(<ToolbarExtensionPoint exploreId="left" timeZone="browser" />);
await userEvent.click(screen.getByRole('button', { name: 'Add' }));
// Make sure we don't have anything related to categories rendered
expect(screen.queryAllByRole('group').length).toBe(0);
expect(screen.getByRole('menuitem', { name: 'Dashboard' })).toBeVisible();
expect(screen.getByRole('menuitem', { name: 'ML: Forecast' })).toBeVisible();
});
});
describe('without extension points', () => {
beforeAll(() => {
contextSrvMock.hasPermission.mockReturnValue(true);
usePluginLinkExtensionsMock.mockReturnValue({ extensions: [], isLoading: false });
});
it('should render "add to dashboard" action button if one pane is visible', async () => {
renderWithExploreStore(<ToolbarExtensionPoint exploreId="left" timeZone="browser" />);
await waitFor(() => {
const button = screen.getByRole('button', { name: /add to dashboard/i });
expect(button).toBeVisible();
expect(button).toBeEnabled();
});
});
});
describe('with insufficient permissions', () => {
beforeAll(() => {
contextSrvMock.hasPermission.mockReturnValue(false);
usePluginLinkExtensionsMock.mockReturnValue({ extensions: [], isLoading: false });
});
it('should not render "add to dashboard" action button', async () => {
renderWithExploreStore(<ToolbarExtensionPoint exploreId="left" timeZone="browser" />);
expect(screen.queryByRole('button', { name: /add to dashboard/i })).not.toBeInTheDocument();
});
});
});