/usr/share/grafana/public/app/features/query/state
const applyFieldOverridesMock = jest.fn(); // needs to be first in this file import { Subject } from 'rxjs'; // Importing this way to be able to spy on grafana/data import * as grafanaData from '@grafana/data'; import { DataSourceApi, DataTransformerID, dateTime, TypedVariableModel } from '@grafana/data'; import { FrameType, mockTransformationsRegistry } from '@grafana/data/internal'; import { DataSourceSrv, setDataSourceSrv, setEchoSrv } from '@grafana/runtime'; import { TemplateSrvMock } from 'app/features/templating/template_srv.mock'; import { Echo } from '../../../core/services/echo/Echo'; import { createDashboardModelFixture } from '../../dashboard/state/__fixtures__/dashboardFixtures'; import { createDashboardQueryRunner, DashboardQueryRunnerFactoryArgs, setDashboardQueryRunnerFactory, } from './DashboardQueryRunner/DashboardQueryRunner'; import { emptyResult } from './DashboardQueryRunner/utils'; import { PanelQueryRunner, QueryRunnerOptions } from './PanelQueryRunner'; jest.mock('@grafana/data', () => ({ __esModule: true, ...jest.requireActual('@grafana/data'), applyFieldOverrides: applyFieldOverridesMock, })); jest.mock('app/core/services/backend_srv'); jest.mock('app/core/config', () => ({ config: { featureToggles: { transformations: true } }, getConfig: () => ({ featureToggles: {}, }), })); const dashboardModel = createDashboardModelFixture({ panels: [{ id: 1, type: 'graph' }], }); jest.mock('app/features/dashboard/services/DashboardSrv', () => ({ getDashboardSrv: () => { return { getCurrent: () => dashboardModel, }; }, })); jest.mock('app/features/templating/template_srv', () => ({ ...jest.requireActual('app/features/templating/template_srv'), getTemplateSrv: () => new TemplateSrvMock([ { name: 'server', type: 'datasource', current: { text: 'Server1', value: 'server' }, options: [{ text: 'Server1', value: 'server1' }], }, //multi value variable { name: 'multi', type: 'datasource', multi: true, current: { text: 'Server1,Server2', value: ['server-1', 'server-2'] }, options: [ { text: 'Server1', value: 'server1' }, { text: 'Server2', value: 'server2' }, ], }, ] as TypedVariableModel[]), })); interface ScenarioContext { setup: (fn: () => void) => void; // Options used in setup maxDataPoints?: number | null; dsInterval?: string; minInterval?: string; scopedVars: grafanaData.ScopedVars; // Filled in by the Scenario runner events?: grafanaData.PanelData[]; res?: grafanaData.PanelData; queryCalledWith?: grafanaData.DataQueryRequest; runner: PanelQueryRunner; } type ScenarioFn = (ctx: ScenarioContext) => void; const defaultPanelConfig: grafanaData.DataConfigSource = { getFieldOverrideOptions: () => undefined, getTransformations: () => undefined, getDataSupport: () => ({ annotations: false, alertStates: false }), }; function describeQueryRunnerScenario( description: string, scenarioFn: ScenarioFn, panelConfig?: grafanaData.DataConfigSource ) { describe(description, () => { let setupFn = () => {}; const ctx: ScenarioContext = { maxDataPoints: 200, scopedVars: { server: { text: 'Server1', value: 'server-1' }, }, runner: new PanelQueryRunner(panelConfig || defaultPanelConfig), setup: (fn: () => void) => { setupFn = fn; }, }; const response = { data: [ { target: 'hello', datapoints: [ [1, 1000], [2, 2000], ], }, ], }; setDataSourceSrv({} as DataSourceSrv); setDashboardQueryRunnerFactory(() => ({ getResult: emptyResult, run: () => undefined, cancel: () => undefined, cancellations: () => new Subject(), destroy: () => undefined, })); createDashboardQueryRunner({} as DashboardQueryRunnerFactoryArgs); beforeEach(async () => { setEchoSrv(new Echo()); setupFn(); const datasource = { name: 'TestDB', uid: 'TestDB-uid', interval: ctx.dsInterval, query: (options: grafanaData.DataQueryRequest) => { ctx.queryCalledWith = options; return Promise.resolve(response); }, getRef: () => ({ type: 'test', uid: 'TestDB-uid' }), testDatasource: jest.fn(), } as unknown as DataSourceApi; const args = { datasource, scopedVars: ctx.scopedVars, minInterval: ctx.minInterval, maxDataPoints: ctx.maxDataPoints ?? Infinity, timeRange: { from: dateTime('2023-01-01T12:00:00Z'), to: dateTime('2023-01-02T12:00:00Z'), raw: { from: '1d', to: 'now' }, }, panelId: 1, panelName: 'PanelName', queries: [{ refId: 'A' }], } as QueryRunnerOptions; ctx.runner = new PanelQueryRunner(panelConfig || defaultPanelConfig); ctx.runner.getData({ withTransforms: true, withFieldConfig: true }).subscribe({ next: (data: grafanaData.PanelData) => { ctx.res = data; ctx.events?.push(data); }, }); ctx.events = []; ctx.runner.run(args); }); scenarioFn(ctx); }); } describe('PanelQueryRunner', () => { beforeAll(() => { const { convertFrameTypeTransformer } = grafanaData.standardTransformers; mockTransformationsRegistry([convertFrameTypeTransformer]); }); beforeEach(() => { jest.clearAllMocks(); }); describeQueryRunnerScenario('simple scenario', (ctx) => { it('should set requestId on request', async () => { expect(ctx.queryCalledWith?.requestId).toBe('Q100'); }); it('should set datasource uid on request', async () => { expect(ctx.queryCalledWith?.targets[0].datasource?.uid).toBe('TestDB-uid'); }); it('should pass scopedVars to datasource with interval props', async () => { expect(ctx.queryCalledWith?.scopedVars.server!.text).toBe('Server1'); expect(ctx.queryCalledWith?.scopedVars.__interval!.text).toBe('5m'); expect(ctx.queryCalledWith?.scopedVars.__interval_ms!.text).toBe('300000'); }); it('should pass the panel id and name', async () => { expect(ctx.queryCalledWith?.panelId).toBe(1); expect(ctx.queryCalledWith?.panelName).toBe('PanelName'); }); }); describeQueryRunnerScenario('with maxDataPoints', (ctx) => { ctx.setup(() => { ctx.maxDataPoints = 200; }); it('should return data', async () => { expect(ctx.res?.error).toBeUndefined(); expect(ctx.res?.series.length).toBe(1); }); it('should use widthPixels as maxDataPoints', async () => { expect(ctx.queryCalledWith?.maxDataPoints).toBe(200); }); it('should calculate interval based on width', async () => { expect(ctx.queryCalledWith?.interval).toBe('5m'); }); it('fast query should only publish 1 data events', async () => { expect(ctx.events?.length).toBe(1); }); }); describeQueryRunnerScenario('with no panel min interval but datasource min interval', (ctx) => { ctx.setup(() => { ctx.maxDataPoints = 20000; ctx.dsInterval = '15s'; }); it('should limit interval to data source min interval', async () => { expect(ctx.queryCalledWith?.interval).toBe('15s'); }); }); describeQueryRunnerScenario('with panel min interval and data source min interval', (ctx) => { ctx.setup(() => { ctx.maxDataPoints = 20000; ctx.dsInterval = '15s'; ctx.minInterval = '30s'; }); it('should limit interval to panel min interval', async () => { expect(ctx.queryCalledWith?.interval).toBe('30s'); }); }); describeQueryRunnerScenario('with maxDataPoints', (ctx) => { ctx.setup(() => { ctx.maxDataPoints = 10; }); it('should pass maxDataPoints if specified', async () => { expect(ctx.queryCalledWith?.maxDataPoints).toBe(10); }); it('should use instead of width to calculate interval', async () => { expect(ctx.queryCalledWith?.interval).toBe('2h'); }); }); describeQueryRunnerScenario( 'field overrides', (ctx) => { it('should apply when field override options are set', async () => { ctx.runner.getData({ withTransforms: true, withFieldConfig: true }).subscribe({ next: (data: grafanaData.PanelData) => { return data; }, }); expect(applyFieldOverridesMock).toBeCalled(); }); }, { getFieldOverrideOptions: () => ({ fieldConfig: { defaults: { unit: 'm/s', }, // @ts-ignore overrides: [], }, replaceVariables: (v) => v, theme: grafanaData.createTheme(), }), getTransformations: () => undefined, getDataSupport: () => ({ annotations: false, alertStates: false }), } ); describeQueryRunnerScenario( 'transformations', (ctx) => { it('should apply when transformations are set', async () => { const spy = jest.spyOn(grafanaData, 'transformDataFrame'); spy.mockClear(); ctx.runner.getData({ withTransforms: true, withFieldConfig: true }).subscribe({ next: (data: grafanaData.PanelData) => { return data; }, }); expect(spy).toBeCalled(); }); }, { getFieldOverrideOptions: () => undefined, // @ts-ignore getTransformations: () => [{} as unknown as grafanaData.DataTransformerConfig], getDataSupport: () => ({ annotations: false, alertStates: false }), } ); describeQueryRunnerScenario( 'transformations', (ctx) => { it('should re-categorize any anno frames returned by series transformations', async () => { ctx.runner.getData({ withTransforms: true, withFieldConfig: false }).subscribe({ next: (data: grafanaData.PanelData) => { try { expect(data.series).toEqual([]); expect(data.annotations).toEqual([ { name: 'exemplar', meta: { custom: { resultType: 'exemplar' }, dataTopic: 'annotations' }, length: 2, fields: [ { config: {}, name: 'Time', state: null, type: 'time', values: [1000, 2000] }, { config: {}, name: 'Value', state: null, type: 'number', values: [1, 2] }, ], }, ]); return data; } catch (e) { return Promise.reject(e instanceof Error ? e.message : e); } }, }); }); }, { getFieldOverrideOptions: () => undefined, getTransformations: () => [ { id: DataTransformerID.convertFrameType, topic: grafanaData.DataTopic.Series, options: { targetType: FrameType.Exemplar, }, }, ], getDataSupport: () => ({ annotations: true, alertStates: false }), } ); describeQueryRunnerScenario( 'getData', (ctx) => { it('should not apply transformations when transform option is false', async () => { const spy = jest.spyOn(grafanaData, 'transformDataFrame'); spy.mockClear(); ctx.runner.getData({ withTransforms: false, withFieldConfig: true }).subscribe({ next: (data: grafanaData.PanelData) => { return data; }, }); expect(spy).not.toBeCalled(); }); it('should not apply field config when applyFieldConfig option is false', async () => { ctx.runner.getData({ withFieldConfig: false, withTransforms: true }).subscribe({ next: (data: grafanaData.PanelData) => { return data; }, }); expect(applyFieldOverridesMock).not.toBeCalled(); }); }, { getFieldOverrideOptions: () => ({ fieldConfig: { defaults: { unit: 'm/s', }, // @ts-ignore overrides: [], }, replaceVariables: (v) => v, theme: grafanaData.createTheme(), }), // @ts-ignore getTransformations: () => [{} as unknown as grafanaData.DataTransformerConfig], getDataSupport: () => ({ annotations: false, alertStates: false }), } ); describeQueryRunnerScenario( 'getData', (ctx) => { it('should not apply transformations when transform option is false', async () => { const spy = jest.spyOn(grafanaData, 'transformDataFrame'); spy.mockClear(); ctx.runner.getData({ withTransforms: false, withFieldConfig: true }).subscribe({ next: (data: grafanaData.PanelData) => { return data; }, }); expect(spy).not.toBeCalled(); }); it('should not apply field config when applyFieldConfig option is false', async () => { ctx.runner.getData({ withFieldConfig: false, withTransforms: true }).subscribe({ next: (data: grafanaData.PanelData) => { return data; }, }); expect(applyFieldOverridesMock).not.toBeCalled(); }); }, { getFieldOverrideOptions: () => ({ fieldConfig: { defaults: { unit: 'm/s', }, // @ts-ignore overrides: [], }, replaceVariables: (v) => v, theme: grafanaData.createTheme(), }), // @ts-ignore getTransformations: () => [{}], getDataSupport: () => ({ annotations: false, alertStates: false }), } ); const snapshotData: grafanaData.DataFrameDTO[] = [ { fields: [ { name: 'time', type: grafanaData.FieldType.time, values: [1000] }, { name: 'value', type: grafanaData.FieldType.number, values: [1] }, ], }, ]; describeQueryRunnerScenario( 'getData with snapshot data', (ctx) => { it('should return snapshotted data', async () => { ctx.runner.getData({ withTransforms: false, withFieldConfig: true }).subscribe({ next: (data: grafanaData.PanelData) => { expect(data.state).toBe(grafanaData.LoadingState.Done); expect(data.series).toEqual(snapshotData); expect(data.timeRange).toEqual(grafanaData.getDefaultTimeRange()); return data; }, }); }); }, { ...defaultPanelConfig, snapshotData, } ); describeQueryRunnerScenario( 'shouldAddErrorwhenDatasourceVariableIsMultiple', (ctx) => { it('should add error when datasource variable is multiple and not repeated', async () => { // scopedVars is an object that represent the variables repeated in a panel const scopedVars = { server: { text: 'Server1', value: 'server-1' }, }; // We are spying on the replace method of the TemplateSrvMock to check if the custom format function is being called const spyReplace = jest.spyOn(TemplateSrvMock.prototype, 'replace'); const response = { data: [ { target: 'hello', datapoints: [ [1, 1000], [2, 2000], ], }, ], }; const datasource = { name: '${multi}', uid: '${multi}', interval: ctx.dsInterval, query: (options: grafanaData.DataQueryRequest) => { ctx.queryCalledWith = options; return Promise.resolve(response); }, getRef: () => ({ type: 'test', uid: 'TestDB-uid' }), testDatasource: jest.fn(), } as unknown as DataSourceApi; ctx.runner.shouldAddErrorWhenDatasourceVariableIsMultiple(datasource, scopedVars); // the test is checking implementation details :(, but it is the only way to check if the error will be added // if the getTemplateSrv.replace is called with the custom format function,it means we will check // if the error should be added expect(spyReplace.mock.calls[0][2]).toBeInstanceOf(Function); }); }, { ...defaultPanelConfig, } ); });
.
Edit
..
Edit
DashboardQueryRunner
Edit
PanelQueryRunner.test.ts
Edit
PanelQueryRunner.ts
Edit
QueryRunner.ts
Edit
mergePanelAndDashData.test.ts
Edit
mergePanelAndDashData.ts
Edit
mocks
Edit
processing
Edit
queryAnalytics.test.ts
Edit
queryAnalytics.ts
Edit
runRequest.test.ts
Edit
runRequest.ts
Edit
updateQueries.test.ts
Edit
updateQueries.ts
Edit