mirror of https://github.com/grafana/grafana
Explore: Integration test for running a query and saving it in query history (#45728)
* Create basic query history test * Clean up * Clean up * Use run button selector * Mock AutoSizer instead of monkey-patching * Reset local storage after each test * Add accessible name to Run Query button and use it in the test * Update public/app/features/explore/spec/helper/interactions.ts Co-authored-by: Giordano Ricci <me@giordanoricci.com> * Rename query to urlParams * Fix linting errors * Remove unused import Co-authored-by: Giordano Ricci <me@giordanoricci.com>pull/45765/head^2
parent
f75bea481d
commit
5715be4afa
@ -0,0 +1,7 @@ |
||||
import { screen } from '@testing-library/react'; |
||||
|
||||
export const assertQueryHistoryExists = (query: string) => { |
||||
expect(screen.getByText('1 queries')).toBeInTheDocument(); |
||||
const queryItem = screen.getByLabelText('Query text'); |
||||
expect(queryItem).toHaveTextContent(query); |
||||
}; |
||||
@ -0,0 +1,28 @@ |
||||
import { selectors } from '@grafana/e2e-selectors'; |
||||
import userEvent from '@testing-library/user-event'; |
||||
import { fireEvent, screen } from '@testing-library/react'; |
||||
|
||||
export const changeDatasource = async (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); |
||||
}; |
||||
|
||||
export const inputQuery = (query: string) => { |
||||
const input = screen.getByRole('textbox', { name: 'query' }); |
||||
userEvent.type(input, query); |
||||
}; |
||||
|
||||
export const runQuery = () => { |
||||
const button = screen.getByRole('button', { name: /run query/i }); |
||||
userEvent.click(button); |
||||
}; |
||||
|
||||
export const openQueryHistory = async () => { |
||||
const button = screen.getByRole('button', { name: 'Rich history button' }); |
||||
userEvent.click(button); |
||||
expect( |
||||
await screen.findByText('The history is local to your browser and is not shared with others.') |
||||
).toBeInTheDocument(); |
||||
}; |
||||
@ -0,0 +1,17 @@ |
||||
import { from, Observable } from 'rxjs'; |
||||
import { ArrayDataFrame, DataQueryResponse, FieldType } from '@grafana/data'; |
||||
|
||||
export function makeLogsQueryResponse(marker = ''): Observable<DataQueryResponse> { |
||||
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] }]); |
||||
} |
||||
|
||||
export function makeMetricsQueryResponse(): Observable<DataQueryResponse> { |
||||
const df = new ArrayDataFrame([{ ts: Date.now(), val: 1 }]); |
||||
df.fields[0].type = FieldType.time; |
||||
return from([{ data: [df] }]); |
||||
} |
||||
@ -0,0 +1,152 @@ |
||||
import React from 'react'; |
||||
import { render, screen } from '@testing-library/react'; |
||||
import { EnhancedStore } from '@reduxjs/toolkit'; |
||||
import { Provider } from 'react-redux'; |
||||
import { Route, Router } from 'react-router-dom'; |
||||
import { fromPairs } from 'lodash'; |
||||
|
||||
import { DataSourceApi, DataSourceInstanceSettings, QueryEditorProps, ScopedVars } from '@grafana/data'; |
||||
import { locationService, setDataSourceSrv, setEchoSrv } from '@grafana/runtime'; |
||||
import { GrafanaRoute } from 'app/core/navigation/GrafanaRoute'; |
||||
import { Echo } from 'app/core/services/echo/Echo'; |
||||
import { configureStore } from 'app/store/configureStore'; |
||||
|
||||
import Wrapper from '../../Wrapper'; |
||||
import { initialUserState } from '../../../profile/state/reducers'; |
||||
|
||||
import { LokiDatasource } from '../../../../plugins/datasource/loki/datasource'; |
||||
import { LokiQuery } from '../../../../plugins/datasource/loki/types'; |
||||
|
||||
type DatasourceSetup = { settings: DataSourceInstanceSettings; api: DataSourceApi }; |
||||
|
||||
type SetupOptions = { |
||||
// default true
|
||||
clearLocalStorage?: boolean; |
||||
datasources?: DatasourceSetup[]; |
||||
urlParams?: { left: string; right?: string }; |
||||
searchParams?: string; |
||||
}; |
||||
|
||||
export function setupExplore(options?: SetupOptions): { |
||||
datasources: { [name: string]: DataSourceApi }; |
||||
store: EnhancedStore; |
||||
unmount: () => void; |
||||
} { |
||||
// Clear this up otherwise it persists data source selection
|
||||
// TODO: probably add test for that too
|
||||
if (options?.clearLocalStorage !== false) { |
||||
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', id: 2 }), |
||||
]; |
||||
|
||||
const dsSettings = options?.datasources || defaultDatasources; |
||||
|
||||
setDataSourceSrv({ |
||||
getList(): DataSourceInstanceSettings[] { |
||||
return dsSettings.map((d) => d.settings); |
||||
}, |
||||
getInstanceSettings(name: string) { |
||||
return dsSettings.map((d) => d.settings).find((x) => x.name === name || x.uid === name); |
||||
}, |
||||
get(name?: string | null, scopedVars?: ScopedVars): Promise<DataSourceApi> { |
||||
return Promise.resolve( |
||||
(name ? dsSettings.find((d) => d.api.name === name || d.api.uid === name) : dsSettings[0])!.api |
||||
); |
||||
}, |
||||
} as any); |
||||
|
||||
setEchoSrv(new Echo()); |
||||
|
||||
const store = configureStore(); |
||||
store.getState().user = { |
||||
...initialUserState, |
||||
orgId: 1, |
||||
timeZone: 'utc', |
||||
}; |
||||
|
||||
store.getState().navIndex = { |
||||
explore: { |
||||
id: 'explore', |
||||
text: 'Explore', |
||||
subTitle: 'Explore your data', |
||||
icon: 'compass', |
||||
url: '/explore', |
||||
}, |
||||
}; |
||||
|
||||
locationService.push({ pathname: '/explore', search: options?.searchParams }); |
||||
|
||||
if (options?.urlParams) { |
||||
locationService.partial(options.urlParams); |
||||
} |
||||
|
||||
const route = { component: Wrapper }; |
||||
|
||||
const { unmount } = render( |
||||
<Provider store={store}> |
||||
<Router history={locationService.getHistory()}> |
||||
<Route path="/explore" exact render={(props) => <GrafanaRoute {...props} route={route as any} />} /> |
||||
</Router> |
||||
</Provider> |
||||
); |
||||
|
||||
return { datasources: fromPairs(dsSettings.map((d) => [d.api.name, d.api])), store, unmount }; |
||||
} |
||||
|
||||
function makeDatasourceSetup({ name = 'loki', id = 1 }: { name?: string; id?: number } = {}): DatasourceSetup { |
||||
const meta: any = { |
||||
info: { |
||||
logos: { |
||||
small: '', |
||||
}, |
||||
}, |
||||
id: id.toString(), |
||||
}; |
||||
return { |
||||
settings: { |
||||
id, |
||||
uid: name, |
||||
type: 'logs', |
||||
name, |
||||
meta, |
||||
access: 'proxy', |
||||
jsonData: {}, |
||||
}, |
||||
api: { |
||||
components: { |
||||
QueryEditor(props: QueryEditorProps<LokiDatasource, LokiQuery>) { |
||||
return ( |
||||
<div> |
||||
<input |
||||
aria-label="query" |
||||
defaultValue={props.query.expr} |
||||
onChange={(event) => { |
||||
props.onChange({ ...props.query, expr: event.target.value }); |
||||
}} |
||||
/> |
||||
{name} Editor input: {props.query.expr} |
||||
</div> |
||||
); |
||||
}, |
||||
}, |
||||
name: name, |
||||
uid: name, |
||||
query: jest.fn(), |
||||
getRef: jest.fn(), |
||||
meta, |
||||
} as any, |
||||
}; |
||||
} |
||||
|
||||
export const waitForExplore = async () => { |
||||
await screen.findByText(/Editor/i); |
||||
}; |
||||
|
||||
export const tearDown = () => { |
||||
window.localStorage.clear(); |
||||
}; |
||||
@ -0,0 +1,47 @@ |
||||
import React from 'react'; |
||||
import { setupExplore, tearDown, waitForExplore } from './helper/setup'; |
||||
import { inputQuery, openQueryHistory, runQuery } from './helper/interactions'; |
||||
import { assertQueryHistoryExists } from './helper/assert'; |
||||
import { makeLogsQueryResponse } from './helper/query'; |
||||
|
||||
jest.mock('react-virtualized-auto-sizer', () => { |
||||
return { |
||||
__esModule: true, |
||||
default(props: any) { |
||||
return <div>{props.children({ width: 1000 })}</div>; |
||||
}, |
||||
}; |
||||
}); |
||||
|
||||
describe('Explore: Query History', () => { |
||||
const USER_INPUT = 'my query'; |
||||
const RAW_QUERY = `{"expr":"${USER_INPUT}"}`; |
||||
|
||||
afterEach(() => { |
||||
tearDown(); |
||||
}); |
||||
|
||||
it('adds new query history items after the query is run.', async () => { |
||||
// when Explore is opened
|
||||
const { datasources, unmount } = setupExplore(); |
||||
(datasources.loki.query as jest.Mock).mockReturnValueOnce(makeLogsQueryResponse()); |
||||
await waitForExplore(); |
||||
|
||||
// and a user runs a query and opens query history
|
||||
inputQuery(USER_INPUT); |
||||
runQuery(); |
||||
await openQueryHistory(); |
||||
|
||||
// the query that was run is in query history
|
||||
assertQueryHistoryExists(RAW_QUERY); |
||||
|
||||
// when Explore is opened again
|
||||
unmount(); |
||||
setupExplore({ clearLocalStorage: false }); |
||||
await waitForExplore(); |
||||
|
||||
// previously added query is in query history
|
||||
await openQueryHistory(); |
||||
assertQueryHistoryExists(RAW_QUERY); |
||||
}); |
||||
}); |
||||
Loading…
Reference in new issue