import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { AnnotationEvent, FieldConfigSource, getDefaultTimeRange, LoadingState } from '@grafana/data'; import { locationService } from '@grafana/runtime'; import { silenceConsoleOutput } from '../../../../test/core/utils/silenceConsoleOutput'; import { backendSrv } from '../../../core/services/backend_srv'; import { DashboardSrv, setDashboardSrv } from '../../../features/dashboard/services/DashboardSrv'; import { AnnoListPanel, Props } from './AnnoListPanel'; import { Options } from './panelcfg.gen'; jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), getBackendSrv: () => backendSrv, })); const defaultOptions: Options = { limit: 10, navigateAfter: '10m', navigateBefore: '10m', navigateToPanel: true, onlyFromThisDashboard: true, onlyInTimeRange: false, showTags: true, showTime: true, showUser: true, tags: ['tag A', 'tag B'], }; const defaultResult = { text: 'Result text', userId: 1, login: 'Result login', email: 'Result email', avatarUrl: 'Result avatarUrl', tags: ['Result tag A', 'Result tag B'], time: Date.UTC(2021, 0, 1, 0, 0, 0, 0), panelId: 13, dashboardId: 14, // deliberately different from panelId id: '14', uid: '7MeksYbmk', dashboardUID: '7MeksYbmk', url: '/d/asdkjhajksd/some-dash', }; async function setupTestContext({ options = defaultOptions, results = [defaultResult], }: { options?: Options; results?: AnnotationEvent[] } = {}) { jest.clearAllMocks(); const getMock = jest.spyOn(backendSrv, 'get'); getMock.mockResolvedValue(results); const dash = { uid: 'srx16xR4z', formatDate: (time: number) => new Date(time).toISOString() }; const dashSrv = { getCurrent: () => dash } as DashboardSrv; setDashboardSrv(dashSrv); const pushSpy = jest.spyOn(locationService, 'push'); const partialSpy = jest.spyOn(locationService, 'partial'); const props: Props = { data: { state: LoadingState.Done, timeRange: getDefaultTimeRange(), series: [] }, eventBus: { subscribe: jest.fn(), getStream: jest.fn().mockImplementation(() => ({ subscribe: jest.fn(), })), publish: jest.fn(), removeAllListeners: jest.fn(), newScopedBus: jest.fn(), }, fieldConfig: {} as unknown as FieldConfigSource, height: 400, id: 1, onChangeTimeRange: jest.fn(), onFieldConfigChange: jest.fn(), onOptionsChange: jest.fn(), options, renderCounter: 1, replaceVariables: (str: string) => str, timeRange: getDefaultTimeRange(), timeZone: 'utc', title: 'Test Title', transparent: false, width: 320, }; const { rerender } = render(); await waitFor(() => expect(getMock).toHaveBeenCalledTimes(1)); return { props, rerender, getMock, pushSpy, partialSpy }; } describe('AnnoListPanel', () => { describe('when mounted', () => { it('then it should fetch annotations', async () => { const { getMock } = await setupTestContext(); expect(getMock).toHaveBeenCalledWith( '/api/annotations', { dashboardUID: 'srx16xR4z', limit: 10, tags: ['tag A', 'tag B'], type: 'annotation', }, 'anno-list-panel-1' ); }); }); describe('when there are no annotations', () => { it('then it should show a no annotations message', async () => { await setupTestContext({ results: [] }); expect(screen.getByText(/no annotations found/i)).toBeInTheDocument(); }); }); describe('when there are annotations', () => { it('then it renders the annotations correctly', async () => { await setupTestContext(); expect(screen.queryByText(/no annotations found/i)).not.toBeInTheDocument(); expect(screen.queryByText(/result email/i)).not.toBeInTheDocument(); expect(screen.getByText(/result text/i)).toBeInTheDocument(); expect(screen.getByRole('img')).toBeInTheDocument(); expect(screen.getByText('Result tag A')).toBeInTheDocument(); expect(screen.getByText('Result tag B')).toBeInTheDocument(); expect(screen.getByText(/2021-01-01T00:00:00.000Z/i)).toBeInTheDocument(); }); it("renders annotation item's html content", async () => { const { getMock } = await setupTestContext({ results: [{ ...defaultResult, text: 'test link ' }], }); getMock.mockClear(); expect(screen.getByRole('link')).toBeInTheDocument(); expect(getMock).not.toHaveBeenCalled(); }); describe('and login property is missing in annotation', () => { it('then it renders the annotations correctly', async () => { await setupTestContext({ results: [{ ...defaultResult, login: undefined }] }); expect(screen.queryByRole('img')).not.toBeInTheDocument(); expect(screen.getByText(/result text/i)).toBeInTheDocument(); expect(screen.getByText('Result tag A')).toBeInTheDocument(); expect(screen.getByText('Result tag B')).toBeInTheDocument(); expect(screen.getByText(/2021-01-01T00:00:00.000Z/i)).toBeInTheDocument(); }); }); describe('and property is missing in annotation', () => { it('then it renders the annotations correctly', async () => { await setupTestContext({ results: [{ ...defaultResult, time: undefined }] }); expect(screen.queryByText(/2021-01-01T00:00:00.000Z/i)).not.toBeInTheDocument(); expect(screen.getByText(/result text/i)).toBeInTheDocument(); expect(screen.getByRole('img')).toBeInTheDocument(); expect(screen.getByText('Result tag A')).toBeInTheDocument(); expect(screen.getByText('Result tag B')).toBeInTheDocument(); }); }); describe('and show user option is off', () => { it('then it renders the annotations correctly', async () => { await setupTestContext({ options: { ...defaultOptions, showUser: false }, }); expect(screen.queryByRole('img')).not.toBeInTheDocument(); expect(screen.getByText(/result text/i)).toBeInTheDocument(); expect(screen.getByText('Result tag A')).toBeInTheDocument(); expect(screen.getByText('Result tag B')).toBeInTheDocument(); expect(screen.getByText(/2021-01-01T00:00:00.000Z/i)).toBeInTheDocument(); }); }); describe('and show time option is off', () => { it('then it renders the annotations correctly', async () => { await setupTestContext({ options: { ...defaultOptions, showTime: false }, }); expect(screen.queryByText(/2021-01-01T00:00:00.000Z/i)).not.toBeInTheDocument(); expect(screen.getByText(/result text/i)).toBeInTheDocument(); expect(screen.getByRole('img')).toBeInTheDocument(); expect(screen.getByText('Result tag A')).toBeInTheDocument(); expect(screen.getByText('Result tag B')).toBeInTheDocument(); }); }); describe('and show tags option is off', () => { it('then it renders the annotations correctly', async () => { await setupTestContext({ options: { ...defaultOptions, showTags: false }, }); expect(screen.queryByText('Result tag A')).not.toBeInTheDocument(); expect(screen.queryByText('Result tag B')).not.toBeInTheDocument(); expect(screen.getByText(/result text/i)).toBeInTheDocument(); expect(screen.getByRole('img')).toBeInTheDocument(); expect(screen.getByText(/2021-01-01T00:00:00.000Z/i)).toBeInTheDocument(); }); }); describe('and the user clicks on the annotation', () => { it('then it should navigate to the dashboard connected to the annotation', async () => { const { getMock, pushSpy } = await setupTestContext(); getMock.mockClear(); expect(screen.getByRole('button', { name: /result text/i })).toBeInTheDocument(); await userEvent.click(screen.getByRole('button', { name: /result text/i })); await waitFor(() => expect(getMock).toHaveBeenCalledTimes(1)); expect(getMock).toHaveBeenCalledWith('/api/search', { dashboardUIDs: '7MeksYbmk' }); expect(pushSpy).toHaveBeenCalledTimes(1); expect(pushSpy).toHaveBeenCalledWith('/d/asdkjhajksd/some-dash?from=1609458600000&to=1609459800000'); }); it('should default to the current dashboard, if no dashboard is associated with the annotation', async () => { const { getMock, partialSpy } = await setupTestContext({ results: [{ ...defaultResult, dashboardUID: null, panelId: 0 }], }); getMock.mockClear(); expect(screen.getByRole('button', { name: /result text/i })).toBeInTheDocument(); await userEvent.click(screen.getByRole('button', { name: /result text/i })); expect(getMock).not.toHaveBeenCalled(); expect(partialSpy).toHaveBeenCalledTimes(1); expect(partialSpy).toHaveBeenCalledWith({ from: 1609458600000, to: 1609459800000, viewPanel: undefined }); }); it('should default to the current dashboard, if no dashboard is associated with the annotation and navigate to viewPanel', async () => { const { getMock, partialSpy } = await setupTestContext({ results: [{ ...defaultResult, dashboardUID: null }], }); getMock.mockClear(); expect(screen.getByRole('button', { name: /result text/i })).toBeInTheDocument(); await userEvent.click(screen.getByRole('button', { name: /result text/i })); expect(getMock).not.toHaveBeenCalled(); expect(partialSpy).toHaveBeenCalledTimes(1); expect(partialSpy).toHaveBeenCalledWith({ from: 1609458600000, to: 1609459800000, viewPanel: 13 }); }); }); describe('and the user clicks on a tag', () => { it('then it should navigate to the dashboard connected to the annotation', async () => { const { getMock } = await setupTestContext(); getMock.mockClear(); expect(screen.getByRole('button', { name: /result tag b/i })).toBeInTheDocument(); await userEvent.click(screen.getByRole('button', { name: /result tag b/i })); expect(getMock).toHaveBeenCalledTimes(1); expect(getMock).toHaveBeenCalledWith( '/api/annotations', { dashboardUID: 'srx16xR4z', limit: 10, tags: ['tag A', 'tag B', 'Result tag B'], type: 'annotation', }, 'anno-list-panel-1' ); expect(screen.getByText(/filter:/i)).toBeInTheDocument(); expect(screen.getAllByText(/result tag b/i)).toHaveLength(2); }); }); describe('and the user clicks on the user avatar', () => { it('then it should filter annotations by login and the filter should show', async () => { const { getMock } = await setupTestContext(); getMock.mockClear(); expect(screen.getByRole('img')).toBeInTheDocument(); await userEvent.click(screen.getByRole('img')); expect(getMock).toHaveBeenCalledTimes(1); expect(getMock).toHaveBeenCalledWith( '/api/annotations', { dashboardUID: 'srx16xR4z', limit: 10, tags: ['tag A', 'tag B'], type: 'annotation', userId: 1, }, 'anno-list-panel-1' ); expect(screen.getByText(/filter:/i)).toBeInTheDocument(); expect(screen.getByRole('button', { name: /result email/i })).toBeInTheDocument(); }); }); describe('and the user hovers over the user avatar', () => { silenceConsoleOutput(); // Popper throws an act error, but if we add act around the hover here it doesn't matter it('then it should filter annotations by login', async () => { const { getMock } = await setupTestContext(); getMock.mockClear(); expect(screen.getByRole('img')).toBeInTheDocument(); }); }); }); });