diff --git a/public/app/features/explore/Wrapper.test.tsx b/public/app/features/explore/Wrapper.test.tsx new file mode 100644 index 00000000000..34acb92352e --- /dev/null +++ b/public/app/features/explore/Wrapper.test.tsx @@ -0,0 +1,291 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import Wrapper from './Wrapper'; +import { configureStore } from '../../store/configureStore'; +import { Provider } from 'react-redux'; +import { store } from '../../store/store'; +import { setDataSourceSrv } from '@grafana/runtime'; +import { + ArrayDataFrame, + DataQueryResponse, + DataSourceApi, + DataSourceInstanceSettings, + FieldType, + QueryEditorProps, + ScopedVars, +} from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; + +import { setTimeSrv } from '../dashboard/services/TimeSrv'; +import { from, Observable } from 'rxjs'; +import { updateLocation } from '../../core/reducers/location'; +import { LokiDatasource } from '../../plugins/datasource/loki/datasource'; +import { LokiQuery } from '../../plugins/datasource/loki/types'; +import { fromPairs } from 'lodash'; + +type Mock = jest.Mock; + +jest.mock('react-virtualized-auto-sizer', () => { + return { + __esModule: true, + default: (props: any) =>
{props.children({ width: 1000 })}
, + }; +}); + +describe('Wrapper', () => { + it('shows warning if there are no data sources', async () => { + setup({ datasources: [] }); + // Will throw if isn't found + screen.getByText(/Explore requires at least one data source/i); + }); + + it('inits url and renders editor but does not call query on empty url', async () => { + const { datasources } = setup(); + + // Wait for rendering the editor + await screen.findByText(/Editor/i); + + // At this point url should be initialised to some defaults + expect(store.getState().location.query).toEqual({ + orgId: '1', + left: JSON.stringify(['now-1h', 'now', 'loki', {}]), + }); + expect(datasources.loki.query).not.toBeCalled(); + }); + + it('runs query when url contains query and renders results', async () => { + const query = { left: JSON.stringify(['now-1h', 'now', 'loki', { expr: '{ label="value"}' }]) }; + const { datasources } = setup({ query }); + (datasources.loki.query as Mock).mockReturnValueOnce(makeLogsQueryResponse()); + + // Make sure we render the logs panel + await screen.findByText(/^Logs$/i); + + // Make sure we render the log line + await screen.findByText(/custom log line/i); + + // And that the editor gets the expr from the url + await screen.findByText(`loki Editor input: { label="value"}`); + + // We did not change the url + expect(store.getState().location.query).toEqual({ + orgId: '1', + ...query, + }); + + // We called the data source query method once + expect(datasources.loki.query).toBeCalledTimes(1); + expect((datasources.loki.query as Mock).mock.calls[0][0]).toMatchObject({ + targets: [{ expr: '{ label="value"}' }], + }); + }); + + it('handles url change and runs the new query', async () => { + const query = { left: JSON.stringify(['now-1h', 'now', 'loki', { expr: '{ label="value"}' }]) }; + const { datasources } = setup({ query }); + (datasources.loki.query as Mock).mockReturnValueOnce(makeLogsQueryResponse()); + // Wait for rendering the logs + await screen.findByText(/custom log line/i); + + (datasources.loki.query as Mock).mockReturnValueOnce(makeLogsQueryResponse('different log')); + store.dispatch( + updateLocation({ + path: '/explore', + query: { left: JSON.stringify(['now-1h', 'now', 'loki', { expr: '{ label="different"}' }]) }, + }) + ); + + // Editor renders the new query + await screen.findByText(`loki Editor input: { label="different"}`); + // Renders new response + await screen.findByText(/different log/i); + }); + + it('handles url change and runs the new query with different datasource', async () => { + const query = { left: JSON.stringify(['now-1h', 'now', 'loki', { expr: '{ label="value"}' }]) }; + const { datasources } = setup({ query }); + (datasources.loki.query as Mock).mockReturnValueOnce(makeLogsQueryResponse()); + // Wait for rendering the logs + await screen.findByText(/custom log line/i); + + (datasources.elastic.query as Mock).mockReturnValueOnce(makeMetricsQueryResponse()); + store.dispatch( + updateLocation({ + path: '/explore', + query: { left: JSON.stringify(['now-1h', 'now', 'elastic', { expr: 'other query' }]) }, + }) + ); + + // Editor renders the new query + await screen.findByText(`loki Editor input: other query`); + // Renders graph + await screen.findByText(/Graph/i); + }); + + it('handles changing the datasource manually', async () => { + const { datasources } = setup(); + // Wait for rendering the editor + await screen.findByText(/Editor/i); + await changeDatasource('elastic'); + + await screen.findByText('elastic Editor input:'); + expect(datasources.elastic.query).not.toBeCalled(); + }); + + it('opens the split pane', async () => { + const { datasources } = setup(); + // Wait for rendering the editor + const splitButton = await screen.findByText(/split/i); + fireEvent.click(splitButton); + const editors = await screen.findAllByText('loki Editor input:'); + + expect(editors.length).toBe(2); + expect(datasources.loki.query).not.toBeCalled(); + }); + + it('inits with two panes if specified in url', async () => { + const query = { + left: JSON.stringify(['now-1h', 'now', 'loki', { expr: '{ label="value"}' }]), + right: JSON.stringify(['now-1h', 'now', 'elastic', { expr: 'error' }]), + }; + + const { datasources } = setup({ query }); + (datasources.loki.query as Mock).mockReturnValueOnce(makeLogsQueryResponse()); + (datasources.elastic.query as Mock).mockReturnValueOnce(makeLogsQueryResponse()); + + // Make sure we render the logs panel + const logsPanels = await screen.findAllByText(/^Logs$/i); + expect(logsPanels.length).toBe(2); + + // Make sure we render the log line + const logsLines = await screen.findAllByText(/custom log line/i); + expect(logsLines.length).toBe(2); + + // And that the editor gets the expr from the url + await screen.findByText(`loki Editor input: { label="value"}`); + await screen.findByText(`elastic Editor input: error`); + + // We did not change the url + expect(store.getState().location.query).toEqual({ + orgId: '1', + ...query, + }); + + // We called the data source query method once + expect(datasources.loki.query).toBeCalledTimes(1); + expect((datasources.loki.query as Mock).mock.calls[0][0]).toMatchObject({ + targets: [{ expr: '{ label="value"}' }], + }); + + expect(datasources.elastic.query).toBeCalledTimes(1); + expect((datasources.elastic.query as Mock).mock.calls[0][0]).toMatchObject({ + targets: [{ expr: 'error' }], + }); + }); +}); + +type DatasourceSetup = { settings: DataSourceInstanceSettings; api: DataSourceApi }; +type SetupOptions = { + datasources?: DatasourceSetup[]; + query?: any; +}; +function setup(options?: SetupOptions): { datasources: { [name: string]: DataSourceApi } } { + // Clear this up otherwise it persists data source selection + // TODO: probably add test for that too + window.localStorage.clear(); + + // Create this here so any mocks are recreated on setup and don't retain state + const defaultDatasources: DatasourceSetup[] = [makeDatasourceSetup(), makeDatasourceSetup({ name: 'elastic' })]; + + const dsSettings = options?.datasources || defaultDatasources; + + setDataSourceSrv({ + getExternal(): DataSourceInstanceSettings[] { + return dsSettings.map(d => d.settings); + }, + + get(name?: string | null, scopedVars?: ScopedVars): Promise { + return Promise.resolve((name ? dsSettings.find(d => d.api.name === name) : dsSettings[0])!.api); + }, + } as any); + + setTimeSrv({ + init() {}, + getValidIntervals(intervals: string[]): string[] { + return intervals; + }, + } as any); + + configureStore(); + store.getState().user = { + orgId: 1, + timeZone: 'utc', + }; + + if (options?.query) { + // We have to dispatch cause right now we take the url state from the action not from the store + store.dispatch(updateLocation({ query: options.query, path: '/explore' })); + } + + render( + + + + ); + return { datasources: fromPairs(dsSettings.map(d => [d.api.name, d.api])) }; +} + +function makeDatasourceSetup({ name = 'loki' }: { name?: string } = {}): DatasourceSetup { + const meta: any = { + info: { + logos: { + small: '', + }, + }, + id: '1', + }; + return { + settings: { + id: 1, + uid: name, + type: 'logs', + name, + meta, + jsonData: {}, + }, + api: { + components: { + QueryEditor: (props: QueryEditorProps) => ( +
+ {name} Editor input: {props.query.expr} +
+ ), + }, + name: name, + query: jest.fn(), + meta, + } as any, + }; +} + +function makeLogsQueryResponse(marker = ''): Observable { + const df = new ArrayDataFrame([{ ts: Date.now(), line: `custom log line ${marker}` }]); + df.meta = { + preferredVisualisationType: 'logs', + }; + df.fields[0].type = FieldType.time; + return from([{ data: [df] }]); +} + +function makeMetricsQueryResponse(): Observable { + const df = new ArrayDataFrame([{ ts: Date.now(), val: 1 }]); + df.fields[0].type = FieldType.time; + return from([{ data: [df] }]); +} + +async function changeDatasource(name: string) { + const datasourcePicker = (await screen.findByLabelText(selectors.components.DataSourcePicker.container)).children[0]; + fireEvent.keyDown(datasourcePicker, { keyCode: 40 }); + const option = screen.getByText(name); + fireEvent.click(option); +}