From 7f5dde6ed33c4f236381b25ec439eaa3e2552f78 Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Wed, 3 Jul 2024 16:21:29 +0200 Subject: [PATCH] Chore: Cleanup duplicated code in grafana-prometheus package (#89542) * remove redundant test matchers * use amendTable, trimTable functions from @grafana/data package * move getMockDataSource function into the mocks.ts * use LocalStorageValueProvider from @grafana/o11y-ds-frontend * move all mocks under __mocks__ directory * use store from @grafana/o11y-ds-frontend * move test related files under test directory * use getNextRefId from @grafana/data instead of deprecated getNextRefIdChar See: https://github.com/grafana/grafana/pull/87460 * betterer * remove unnecessary mockings * import from @grafana/data * import from @grafana/data --- .betterer.results | 18 -- .../src/components/PromQueryField.tsx | 10 +- .../components/VariableQueryEditor.test.tsx | 2 +- .../src/configuration/PromSettings.test.tsx | 3 +- .../src/configuration/mocks.ts | 15 -- .../grafana-prometheus/src/datasource.test.ts | 16 +- .../LocalStorageValueProvider.tsx | 51 ------ .../LocalStorageValueProvider/index.tsx | 2 - .../src/gcopypaste/app/core/store.ts | 66 -------- .../src/gcopypaste/app/core/utils/query.ts | 21 --- .../datasources/__mocks__/dataSourcesMocks.ts | 31 ---- .../app/features/live/data/amendTimeSeries.ts | 94 ----------- .../gcopypaste/public/test/matchers/index.ts | 11 -- .../public/test/matchers/toEmitValues.test.ts | 141 ---------------- .../public/test/matchers/toEmitValues.ts | 91 ----------- .../test/matchers/toEmitValuesWith.test.ts | 154 ------------------ .../public/test/matchers/toEmitValuesWith.ts | 63 ------- .../gcopypaste/public/test/matchers/types.ts | 14 -- .../gcopypaste/public/test/matchers/utils.ts | 64 -------- .../src/querybuilder/QueryPatternsModal.tsx | 5 +- .../components/LabelFilters.test.tsx | 2 +- .../PromQueryBuilderOptions.test.tsx | 2 +- ...omQueryCodeEditorAutocompleteInfo.test.tsx | 12 -- .../PromQueryEditorSelector.test.tsx | 12 -- .../components/promQail/PromQail.tsx | 3 +- .../src/querybuilder/hooks/useFlag.ts | 2 +- .../src/querybuilder/state.ts | 3 +- .../src/querycache/QueryCache.ts | 4 +- .../src/{ => test}/__mocks__/datasource.ts | 51 +++++- .../test/helpers/selectOptionInTest.ts | 0 30 files changed, 78 insertions(+), 885 deletions(-) delete mode 100644 packages/grafana-prometheus/src/configuration/mocks.ts delete mode 100644 packages/grafana-prometheus/src/gcopypaste/app/core/components/LocalStorageValueProvider/LocalStorageValueProvider.tsx delete mode 100644 packages/grafana-prometheus/src/gcopypaste/app/core/components/LocalStorageValueProvider/index.tsx delete mode 100644 packages/grafana-prometheus/src/gcopypaste/app/core/store.ts delete mode 100644 packages/grafana-prometheus/src/gcopypaste/app/core/utils/query.ts delete mode 100644 packages/grafana-prometheus/src/gcopypaste/app/features/datasources/__mocks__/dataSourcesMocks.ts delete mode 100644 packages/grafana-prometheus/src/gcopypaste/app/features/live/data/amendTimeSeries.ts delete mode 100644 packages/grafana-prometheus/src/gcopypaste/public/test/matchers/index.ts delete mode 100644 packages/grafana-prometheus/src/gcopypaste/public/test/matchers/toEmitValues.test.ts delete mode 100644 packages/grafana-prometheus/src/gcopypaste/public/test/matchers/toEmitValues.ts delete mode 100644 packages/grafana-prometheus/src/gcopypaste/public/test/matchers/toEmitValuesWith.test.ts delete mode 100644 packages/grafana-prometheus/src/gcopypaste/public/test/matchers/toEmitValuesWith.ts delete mode 100644 packages/grafana-prometheus/src/gcopypaste/public/test/matchers/types.ts delete mode 100644 packages/grafana-prometheus/src/gcopypaste/public/test/matchers/utils.ts rename packages/grafana-prometheus/src/{ => test}/__mocks__/datasource.ts (71%) rename packages/grafana-prometheus/src/{gcopypaste => }/test/helpers/selectOptionInTest.ts (100%) diff --git a/.betterer.results b/.betterer.results index a3f84ce1d29..089cb0a051a 100644 --- a/.betterer.results +++ b/.betterer.results @@ -455,24 +455,6 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "4"], [0, 0, 0, "Unexpected any. Specify a different type.", "5"] ], - "packages/grafana-prometheus/src/gcopypaste/app/features/live/data/amendTimeSeries.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"], - [0, 0, 0, "Do not use any type assertions.", "2"], - [0, 0, 0, "Do not use any type assertions.", "3"], - [0, 0, 0, "Do not use any type assertions.", "4"] - ], - "packages/grafana-prometheus/src/gcopypaste/public/test/matchers/index.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], - "packages/grafana-prometheus/src/gcopypaste/public/test/matchers/toEmitValuesWith.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Unexpected any. Specify a different type.", "1"], - [0, 0, 0, "Unexpected any. Specify a different type.", "2"] - ], - "packages/grafana-prometheus/src/gcopypaste/public/test/matchers/utils.ts:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"] - ], "packages/grafana-prometheus/src/language_provider.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "1"], diff --git a/packages/grafana-prometheus/src/components/PromQueryField.tsx b/packages/grafana-prometheus/src/components/PromQueryField.tsx index 35ff6174ad3..4c619f88b7a 100644 --- a/packages/grafana-prometheus/src/components/PromQueryField.tsx +++ b/packages/grafana-prometheus/src/components/PromQueryField.tsx @@ -2,13 +2,19 @@ import { css, cx } from '@emotion/css'; import { PureComponent, ReactNode } from 'react'; -import { isDataFrame, QueryEditorProps, QueryHint, TimeRange, toLegacyResponseData } from '@grafana/data'; +import { + isDataFrame, + LocalStorageValueProvider, + QueryEditorProps, + QueryHint, + TimeRange, + toLegacyResponseData, +} from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { reportInteraction } from '@grafana/runtime'; import { clearButtonStyles, Icon, Themeable2, withTheme2 } from '@grafana/ui'; import { PrometheusDatasource } from '../datasource'; -import { LocalStorageValueProvider } from '../gcopypaste/app/core/components/LocalStorageValueProvider'; import { roundMsToMin } from '../language_utils'; import { PromOptions, PromQuery } from '../types'; diff --git a/packages/grafana-prometheus/src/components/VariableQueryEditor.test.tsx b/packages/grafana-prometheus/src/components/VariableQueryEditor.test.tsx index 749416c5e14..dd9b17beb3c 100644 --- a/packages/grafana-prometheus/src/components/VariableQueryEditor.test.tsx +++ b/packages/grafana-prometheus/src/components/VariableQueryEditor.test.tsx @@ -5,9 +5,9 @@ import userEvent from '@testing-library/user-event'; import { dateTime, TimeRange } from '@grafana/data'; import { PrometheusDatasource } from '../datasource'; -import { selectOptionInTest } from '../gcopypaste/test/helpers/selectOptionInTest'; import PrometheusLanguageProvider from '../language_provider'; import { migrateVariableEditorBackToVariableSupport } from '../migrations/variableMigration'; +import { selectOptionInTest } from '../test/helpers/selectOptionInTest'; import { PromVariableQuery, PromVariableQueryType, StandardPromVariableQuery } from '../types'; import { PromVariableQueryEditor, Props, variableMigration } from './VariableQueryEditor'; diff --git a/packages/grafana-prometheus/src/configuration/PromSettings.test.tsx b/packages/grafana-prometheus/src/configuration/PromSettings.test.tsx index 8d90f39084f..15af11210cc 100644 --- a/packages/grafana-prometheus/src/configuration/PromSettings.test.tsx +++ b/packages/grafana-prometheus/src/configuration/PromSettings.test.tsx @@ -6,8 +6,9 @@ import { SelectableValue } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { config } from '@grafana/runtime'; +import { createDefaultConfigOptions } from '../test/__mocks__/datasource'; + import { countError, getValueFromEventItem, PromSettings } from './PromSettings'; -import { createDefaultConfigOptions } from './mocks'; beforeEach(() => { jest.replaceProperty(config, 'featureToggles', { diff --git a/packages/grafana-prometheus/src/configuration/mocks.ts b/packages/grafana-prometheus/src/configuration/mocks.ts deleted file mode 100644 index 3c1d7e29762..00000000000 --- a/packages/grafana-prometheus/src/configuration/mocks.ts +++ /dev/null @@ -1,15 +0,0 @@ -// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/configuration/mocks.ts -import { DataSourceSettings } from '@grafana/data'; - -import { getMockDataSource } from '../gcopypaste/app/features/datasources/__mocks__/dataSourcesMocks'; -import { PromOptions } from '../types'; - -export function createDefaultConfigOptions(): DataSourceSettings { - return getMockDataSource({ - jsonData: { - timeInterval: '1m', - queryTimeout: '1m', - httpMethod: 'GET', - }, - }); -} diff --git a/packages/grafana-prometheus/src/datasource.test.ts b/packages/grafana-prometheus/src/datasource.test.ts index 08104df79b3..8132d670aae 100644 --- a/packages/grafana-prometheus/src/datasource.test.ts +++ b/packages/grafana-prometheus/src/datasource.test.ts @@ -18,14 +18,6 @@ import { } from '@grafana/data'; import { config, getBackendSrv, setBackendSrv, TemplateSrv } from '@grafana/runtime'; -import { - createAnnotationResponse, - createDataRequest, - createDefaultPromResponse, - createEmptyAnnotationResponse, - fetchMockCalledWith, - getMockTimeRange, -} from './__mocks__/datasource'; import { alignRange, extractRuleMappingFromGroups, @@ -34,6 +26,14 @@ import { prometheusSpecialRegexEscape, } from './datasource'; import PromQlLanguageProvider from './language_provider'; +import { + createAnnotationResponse, + createDataRequest, + createDefaultPromResponse, + createEmptyAnnotationResponse, + fetchMockCalledWith, + getMockTimeRange, +} from './test/__mocks__/datasource'; import { PromApplication, PrometheusCacheLevel, PromOptions, PromQuery, PromQueryRequest } from './types'; const fetchMock = jest.fn().mockReturnValue(of(createDefaultPromResponse())); diff --git a/packages/grafana-prometheus/src/gcopypaste/app/core/components/LocalStorageValueProvider/LocalStorageValueProvider.tsx b/packages/grafana-prometheus/src/gcopypaste/app/core/components/LocalStorageValueProvider/LocalStorageValueProvider.tsx deleted file mode 100644 index 903a6b94b53..00000000000 --- a/packages/grafana-prometheus/src/gcopypaste/app/core/components/LocalStorageValueProvider/LocalStorageValueProvider.tsx +++ /dev/null @@ -1,51 +0,0 @@ -// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/core/components/LocalStorageValueProvider/LocalStorageValueProvider.tsx -import { useEffect, useState } from 'react'; -import * as React from 'react'; - -import store from '../../store'; - -export interface Props { - storageKey: string; - defaultValue: T; - children: (value: T, onSaveToStore: (value: T) => void, onDeleteFromStore: () => void) => React.ReactNode; -} - -export const LocalStorageValueProvider = (props: Props) => { - const { children, storageKey, defaultValue } = props; - - const [state, setState] = useState({ value: store.getObject(props.storageKey, props.defaultValue) }); - - useEffect(() => { - const onStorageUpdate = (v: StorageEvent) => { - if (v.key === storageKey) { - setState({ value: store.getObject(props.storageKey, props.defaultValue) }); - } - }; - - window.addEventListener('storage', onStorageUpdate); - - return () => { - window.removeEventListener('storage', onStorageUpdate); - }; - }); - - const onSaveToStore = (value: T) => { - try { - store.setObject(storageKey, value); - } catch (error) { - console.error(error); - } - setState({ value }); - }; - - const onDeleteFromStore = () => { - try { - store.delete(storageKey); - } catch (error) { - console.log(error); - } - setState({ value: defaultValue }); - }; - - return <>{children(state.value, onSaveToStore, onDeleteFromStore)}; -}; diff --git a/packages/grafana-prometheus/src/gcopypaste/app/core/components/LocalStorageValueProvider/index.tsx b/packages/grafana-prometheus/src/gcopypaste/app/core/components/LocalStorageValueProvider/index.tsx deleted file mode 100644 index e3df7a01194..00000000000 --- a/packages/grafana-prometheus/src/gcopypaste/app/core/components/LocalStorageValueProvider/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/core/components/LocalStorageValueProvider/index.tsx -export { LocalStorageValueProvider } from './LocalStorageValueProvider'; diff --git a/packages/grafana-prometheus/src/gcopypaste/app/core/store.ts b/packages/grafana-prometheus/src/gcopypaste/app/core/store.ts deleted file mode 100644 index a23d14c93cc..00000000000 --- a/packages/grafana-prometheus/src/gcopypaste/app/core/store.ts +++ /dev/null @@ -1,66 +0,0 @@ -// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/core/store.ts -type StoreValue = string | number | boolean | null; - -export class Store { - get(key: string) { - return window.localStorage[key]; - } - - set(key: string, value: StoreValue) { - window.localStorage[key] = value; - } - - getBool(key: string, def: boolean): boolean { - if (def !== void 0 && !this.exists(key)) { - return def; - } - return window.localStorage[key] === 'true'; - } - - getObject(key: string): T | undefined; - getObject(key: string, def: T): T; - getObject(key: string, def?: T) { - let ret = def; - if (this.exists(key)) { - const json = window.localStorage[key]; - try { - ret = JSON.parse(json); - } catch (error) { - console.error(`Error parsing store object: ${key}. Returning default: ${def}. [${error}]`); - } - } - return ret; - } - - /* Returns true when successfully stored, throws error if not successfully stored */ - setObject(key: string, value: unknown) { - let json; - try { - json = JSON.stringify(value); - } catch (error) { - throw new Error(`Could not stringify object: ${key}. [${error}]`); - } - try { - this.set(key, json); - } catch (error) { - // Likely hitting storage quota - const errorToThrow = new Error(`Could not save item in localStorage: ${key}. [${error}]`); - if (error instanceof Error) { - errorToThrow.name = error.name; - } - throw errorToThrow; - } - return true; - } - - exists(key: string) { - return window.localStorage[key] !== void 0; - } - - delete(key: string) { - window.localStorage.removeItem(key); - } -} - -const store = new Store(); -export default store; diff --git a/packages/grafana-prometheus/src/gcopypaste/app/core/utils/query.ts b/packages/grafana-prometheus/src/gcopypaste/app/core/utils/query.ts deleted file mode 100644 index 28b4af48c0b..00000000000 --- a/packages/grafana-prometheus/src/gcopypaste/app/core/utils/query.ts +++ /dev/null @@ -1,21 +0,0 @@ -// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/core/utils/query.ts -import { DataQuery } from '@grafana/data'; - -export const getNextRefIdChar = (queries: DataQuery[]): string => { - for (let num = 0; ; num++) { - const refId = getRefId(num); - if (!queries.some((query) => query.refId === refId)) { - return refId; - } - } -}; - -function getRefId(num: number): string { - const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; - - if (num < letters.length) { - return letters[num]; - } else { - return getRefId(Math.floor(num / letters.length) - 1) + letters[num % letters.length]; - } -} diff --git a/packages/grafana-prometheus/src/gcopypaste/app/features/datasources/__mocks__/dataSourcesMocks.ts b/packages/grafana-prometheus/src/gcopypaste/app/features/datasources/__mocks__/dataSourcesMocks.ts deleted file mode 100644 index 15ebe4229a9..00000000000 --- a/packages/grafana-prometheus/src/gcopypaste/app/features/datasources/__mocks__/dataSourcesMocks.ts +++ /dev/null @@ -1,31 +0,0 @@ -// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/features/datasources/__mocks__/dataSourcesMocks.ts -import { merge } from 'lodash'; - -import { DataSourceJsonData, DataSourceSettings } from '@grafana/data'; - -export const getMockDataSource = ( - overrides?: Partial> -): DataSourceSettings => - merge( - { - access: '', - basicAuth: false, - basicAuthUser: '', - withCredentials: false, - database: '', - id: 13, - uid: 'x', - isDefault: false, - jsonData: { authType: 'credentials', defaultRegion: 'eu-west-2' }, - name: 'gdev-prometheus', - typeName: 'Prometheus', - orgId: 1, - readOnly: false, - type: 'prometheus', - typeLogoUrl: 'packages/grafana-prometheus/src/img/prometheus_logo.svg', - url: '', - user: '', - secureJsonFields: {}, - }, - overrides - ); diff --git a/packages/grafana-prometheus/src/gcopypaste/app/features/live/data/amendTimeSeries.ts b/packages/grafana-prometheus/src/gcopypaste/app/features/live/data/amendTimeSeries.ts deleted file mode 100644 index a2d00ad34f4..00000000000 --- a/packages/grafana-prometheus/src/gcopypaste/app/features/live/data/amendTimeSeries.ts +++ /dev/null @@ -1,94 +0,0 @@ -// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/features/live/data/amendTimeSeries.ts -import { closestIdx } from '@grafana/data'; - -export type Table = [times: number[], ...values: any[][]]; - -// prevTable and nextTable are assumed sorted ASC on reference [0] arrays -// nextTable is assumed to be contiguous, only edges are checked for overlap -// ...so prev: [1,2,5] + next: [3,4,6] -> [1,2,3,4,6] -export function amendTable(prevTable: Table, nextTable: Table): Table { - let [prevTimes] = prevTable; - let [nextTimes] = nextTable; - - let pLen = prevTimes.length; - let pStart = prevTimes[0]; - let pEnd = prevTimes[pLen - 1]; - - let nLen = nextTimes.length; - let nStart = nextTimes[0]; - let nEnd = nextTimes[nLen - 1]; - - let outTable: Table; - - if (pLen) { - if (nLen) { - // append, no overlap - if (nStart > pEnd) { - outTable = prevTable.map((_, i) => prevTable[i].concat(nextTable[i])) as Table; - } - // prepend, no overlap - else if (nEnd < pStart) { - outTable = nextTable.map((_, i) => nextTable[i].concat(prevTable[i])) as Table; - } - // full replace - else if (nStart <= pStart && nEnd >= pEnd) { - outTable = nextTable; - } - // partial replace - else if (nStart > pStart && nEnd < pEnd) { - } - // append, with overlap - else if (nStart >= pStart) { - let idx = closestIdx(nStart, prevTimes); - idx = prevTimes[idx] < nStart ? idx - 1 : idx; - outTable = prevTable.map((_, i) => prevTable[i].slice(0, idx).concat(nextTable[i])) as Table; - } - // prepend, with overlap - else if (nEnd >= pStart) { - let idx = closestIdx(nEnd, prevTimes); - idx = prevTimes[idx] < nEnd ? idx : idx + 1; - outTable = nextTable.map((_, i) => nextTable[i].concat(prevTable[i].slice(idx))) as Table; - } - } else { - outTable = prevTable; - } - } else { - if (nLen) { - outTable = nextTable; - } else { - outTable = [[]]; - } - } - - return outTable!; -} - -export function trimTable(table: Table, fromTime: number, toTime: number): Table { - let [times, ...vals] = table; - let fromIdx: number | undefined; - let toIdx: number | undefined; - - // trim to bounds - if (times[0] < fromTime) { - fromIdx = closestIdx(fromTime, times); - - if (times[fromIdx] < fromTime) { - fromIdx++; - } - } - - if (times[times.length - 1] > toTime) { - toIdx = closestIdx(toTime, times); - - if (times[toIdx] > toTime) { - toIdx--; - } - } - - if (fromIdx != null || toIdx != null) { - times = times.slice(fromIdx ?? 0, toIdx); - vals = vals.map(vals2 => vals2.slice(fromIdx ?? 0, toIdx)); - } - - return [times, ...vals]; -} diff --git a/packages/grafana-prometheus/src/gcopypaste/public/test/matchers/index.ts b/packages/grafana-prometheus/src/gcopypaste/public/test/matchers/index.ts deleted file mode 100644 index ea1578a7482..00000000000 --- a/packages/grafana-prometheus/src/gcopypaste/public/test/matchers/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/test/matchers/index.ts -import { Observable } from 'rxjs'; - -import { toEmitValues } from './toEmitValues'; -import { toEmitValuesWith } from './toEmitValuesWith'; -import { ObservableMatchers } from './types'; - -export const matchers: ObservableMatchers> = { - toEmitValues, - toEmitValuesWith, -}; diff --git a/packages/grafana-prometheus/src/gcopypaste/public/test/matchers/toEmitValues.test.ts b/packages/grafana-prometheus/src/gcopypaste/public/test/matchers/toEmitValues.test.ts deleted file mode 100644 index bb0d9aa3c63..00000000000 --- a/packages/grafana-prometheus/src/gcopypaste/public/test/matchers/toEmitValues.test.ts +++ /dev/null @@ -1,141 +0,0 @@ -// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/test/matchers/toEmitValues.test.ts -import { interval, Observable, of, throwError } from 'rxjs'; -import { map, mergeMap, take } from 'rxjs/operators'; - -import { OBSERVABLE_TEST_TIMEOUT_IN_MS } from './types'; - -describe('toEmitValues matcher', () => { - describe('failing tests', () => { - describe('passing null in expect', () => { - it('should fail', async () => { - const observable = null as unknown as Observable; - - const rejects = expect(() => expect(observable).toEmitValues([1, 2, 3])).rejects; - await rejects.toThrow(); - }); - }); - - describe('passing undefined in expect', () => { - it('should fail', async () => { - const observable = undefined as unknown as Observable; - - const rejects = expect(() => expect(observable).toEmitValues([1, 2, 3])).rejects; - await rejects.toThrow(); - }); - }); - - describe('passing number instead of Observable in expect', () => { - it('should fail', async () => { - const observable = 1 as unknown as Observable; - - const rejects = expect(() => expect(observable).toEmitValues([1, 2, 3])).rejects; - await rejects.toThrow(); - }); - }); - - describe('wrong number of emitted values', () => { - it('should fail', async () => { - const observable = interval(10).pipe(take(3)); - - const rejects = expect(() => expect(observable).toEmitValues([0, 1])).rejects; - await rejects.toThrow(); - }); - }); - - describe('wrong emitted values', () => { - it('should fail', async () => { - const observable = interval(10).pipe(take(3)); - - const rejects = expect(() => expect(observable).toEmitValues([1, 2, 3])).rejects; - await rejects.toThrow(); - }); - }); - - describe('wrong emitted value types', () => { - it('should fail', async () => { - const observable = interval(10).pipe(take(3)) as unknown as Observable; - - const rejects = expect(() => expect(observable).toEmitValues(['0', '1', '2'])).rejects; - await rejects.toThrow(); - }); - }); - - describe(`observable that does not complete within ${OBSERVABLE_TEST_TIMEOUT_IN_MS}ms`, () => { - it('should fail', async () => { - const observable = interval(600); - - const rejects = expect(() => expect(observable).toEmitValues([0])).rejects; - await rejects.toThrow(); - }); - }); - }); - - describe('passing tests', () => { - describe('correct emitted values', () => { - it('should pass with correct message', async () => { - const observable = interval(10).pipe(take(3)); - await expect(observable).toEmitValues([0, 1, 2]); - }); - }); - - describe('using nested arrays', () => { - it('should pass with correct message', async () => { - const observable = interval(10).pipe( - map((interval) => [{ text: interval.toString(), value: interval }]), - take(3) - ); - await expect(observable).toEmitValues([ - [{ text: '0', value: 0 }], - [{ text: '1', value: 1 }], - [{ text: '2', value: 2 }], - ]); - }); - }); - - describe('using nested objects', () => { - it('should pass with correct message', async () => { - const observable = interval(10).pipe( - map((interval) => ({ inner: { text: interval.toString(), value: interval } })), - take(3) - ); - await expect(observable).toEmitValues([ - { inner: { text: '0', value: 0 } }, - { inner: { text: '1', value: 1 } }, - { inner: { text: '2', value: 2 } }, - ]); - }); - }); - - describe('correct emitted values with throw', () => { - it('should pass with correct message', async () => { - const observable = interval(10).pipe( - map((interval) => { - if (interval > 1) { - throw 'an error'; - } - - return interval; - }) - ) as unknown as Observable; - - await expect(observable).toEmitValues([0, 1, 'an error']); - }); - }); - - describe('correct emitted values with throwError', () => { - it('should pass with correct message', async () => { - const observable = interval(10).pipe( - mergeMap((interval) => { - if (interval === 1) { - return throwError('an error'); - } - - return of(interval); - }) - ) as unknown as Observable; - - await expect(observable).toEmitValues([0, 'an error']); - }); - }); - }); -}); diff --git a/packages/grafana-prometheus/src/gcopypaste/public/test/matchers/toEmitValues.ts b/packages/grafana-prometheus/src/gcopypaste/public/test/matchers/toEmitValues.ts deleted file mode 100644 index 5d87c1f342d..00000000000 --- a/packages/grafana-prometheus/src/gcopypaste/public/test/matchers/toEmitValues.ts +++ /dev/null @@ -1,91 +0,0 @@ -// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/test/matchers/toEmitValues.ts -import { matcherHint, printExpected, printReceived } from 'jest-matcher-utils'; -import { isEqual } from 'lodash'; -import { Observable, Subscription } from 'rxjs'; - -import { expectObservable, forceObservableCompletion } from './utils'; - -function passMessage(received: unknown[], expected: unknown[]) { - return `${matcherHint('.not.toEmitValues')} - - Expected observable to emit values: - ${printExpected(expected)} - Received: - ${printReceived(received)} - `; -} - -function failMessage(received: unknown[], expected: unknown[]) { - return `${matcherHint('.toEmitValues')} - - Expected observable to emit values: - ${printExpected(expected)} - Received: - ${printReceived(received)} - `; -} - -function tryExpectations(received: unknown[], expected: unknown[]): jest.CustomMatcherResult { - try { - if (received.length !== expected.length) { - return { - pass: false, - message: () => failMessage(received, expected), - }; - } - - for (let index = 0; index < received.length; index++) { - const left = received[index]; - const right = expected[index]; - - if (!isEqual(left, right)) { - return { - pass: false, - message: () => failMessage(received, expected), - }; - } - } - - return { - pass: true, - message: () => passMessage(received, expected), - }; - } catch (err) { - const message = err instanceof Error ? err.message : 'An unknown error occurred'; - return { - pass: false, - message: () => message, - }; - } -} - -export function toEmitValues(received: Observable, expected: unknown[]): Promise { - const failsChecks = expectObservable(received); - if (failsChecks) { - return Promise.resolve(failsChecks); - } - - return new Promise((resolve) => { - const receivedValues: unknown[] = []; - const subscription = new Subscription(); - - subscription.add( - received.subscribe({ - next: (value) => { - receivedValues.push(value); - }, - error: (err) => { - receivedValues.push(err); - subscription.unsubscribe(); - resolve(tryExpectations(receivedValues, expected)); - }, - complete: () => { - subscription.unsubscribe(); - resolve(tryExpectations(receivedValues, expected)); - }, - }) - ); - - forceObservableCompletion(subscription, resolve); - }); -} diff --git a/packages/grafana-prometheus/src/gcopypaste/public/test/matchers/toEmitValuesWith.test.ts b/packages/grafana-prometheus/src/gcopypaste/public/test/matchers/toEmitValuesWith.test.ts deleted file mode 100644 index 352c8237665..00000000000 --- a/packages/grafana-prometheus/src/gcopypaste/public/test/matchers/toEmitValuesWith.test.ts +++ /dev/null @@ -1,154 +0,0 @@ -// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/test/matchers/toEmitValuesWith.test.ts -import { interval, Observable, of, throwError } from 'rxjs'; -import { map, mergeMap, take } from 'rxjs/operators'; - -import { OBSERVABLE_TEST_TIMEOUT_IN_MS } from './types'; - -describe('toEmitValuesWith matcher', () => { - describe('failing tests', () => { - describe('passing null in expect', () => { - it('should fail with correct message', async () => { - const observable = null as unknown as Observable; - - const rejects = expect(() => - expect(observable).toEmitValuesWith((received) => { - expect(received).toEqual([1, 2, 3]); - }) - ).rejects; - - await rejects.toThrow(); - }); - }); - - describe('passing undefined in expect', () => { - it('should fail with correct message', async () => { - const observable = undefined as unknown as Observable; - - const rejects = expect(() => - expect(observable).toEmitValuesWith((received) => { - expect(received).toEqual([1, 2, 3]); - }) - ).rejects; - - await rejects.toThrow(); - }); - }); - - describe('passing number instead of Observable in expect', () => { - it('should fail with correct message', async () => { - const observable = 1 as unknown as Observable; - - const rejects = expect(() => - expect(observable).toEmitValuesWith((received) => { - expect(received).toEqual([1, 2, 3]); - }) - ).rejects; - - await rejects.toThrow(); - }); - }); - - describe('wrong number of emitted values', () => { - it('should fail with correct message', async () => { - const observable = interval(10).pipe(take(3)); - - const rejects = expect(() => - expect(observable).toEmitValuesWith((received) => { - expect(received).toEqual([0, 1]); - }) - ).rejects; - - await rejects.toThrow(); - }); - }); - - describe('wrong emitted values', () => { - it('should fail with correct message', async () => { - const observable = interval(10).pipe(take(3)); - - const rejects = expect(() => - expect(observable).toEmitValuesWith((received) => { - expect(received).toEqual([1, 2, 3]); - }) - ).rejects; - - await rejects.toThrow(); - }); - }); - - describe('wrong emitted value types', () => { - it('should fail with correct message', async () => { - const observable = interval(10).pipe(take(3)) as unknown as Observable; - - const rejects = expect(() => - expect(observable).toEmitValuesWith((received) => { - expect(received).toEqual(['0', '1', '2']); - }) - ).rejects; - - await rejects.toThrow(); - }); - }); - - describe(`observable that does not complete within ${OBSERVABLE_TEST_TIMEOUT_IN_MS}ms`, () => { - it('should fail with correct message', async () => { - const observable = interval(600); - - const rejects = expect(() => - expect(observable).toEmitValuesWith((received) => { - expect(received).toEqual([0]); - }) - ).rejects; - - await rejects.toThrow(); - }); - }); - }); - - describe('passing tests', () => { - describe('correct emitted values', () => { - it('should pass with correct message', async () => { - const observable = interval(10).pipe(take(3)); - await expect(observable).toEmitValuesWith((received) => { - expect(received).toEqual([0, 1, 2]); - }); - }); - }); - - describe('correct emitted values with throw', () => { - it('should pass with correct message', async () => { - const observable = interval(10).pipe( - map((interval) => { - if (interval > 1) { - throw 'an error'; - } - - return interval; - }) - ); - - await expect(observable).toEmitValuesWith((received) => { - expect(received).toEqual([0, 1, 'an error']); - }); - }); - }); - - describe('correct emitted values with throwError', () => { - it('should pass with correct message', async () => { - const observable = interval(10).pipe( - mergeMap((interval) => { - if (interval === 1) { - return throwError('an error'); - } - - return of(interval); - }) - ); - - await expect(observable).toEmitValuesWith((received) => { - expect(received).toEqual([0, 'an error']); - }); - }); - }); - }); -}); diff --git a/packages/grafana-prometheus/src/gcopypaste/public/test/matchers/toEmitValuesWith.ts b/packages/grafana-prometheus/src/gcopypaste/public/test/matchers/toEmitValuesWith.ts deleted file mode 100644 index 2c647baea5b..00000000000 --- a/packages/grafana-prometheus/src/gcopypaste/public/test/matchers/toEmitValuesWith.ts +++ /dev/null @@ -1,63 +0,0 @@ -// Core Grafana history -import { matcherHint, printReceived } from 'jest-matcher-utils'; -import { Observable, Subscription } from 'rxjs'; - -import { expectObservable, forceObservableCompletion } from './utils'; - -function tryExpectations(received: unknown[], expectations: (received: unknown[]) => void): jest.CustomMatcherResult { - try { - expectations(received); - return { - pass: true, - message: () => `${matcherHint('.not.toEmitValues')} - - Expected observable to complete with - ${printReceived(received)} - `, - }; - } catch (err) { - return { - pass: false, - message: () => 'failed ' + err, - }; - } -} - -/** - * Collect all the values emitted by the observables (also errors) and pass them to the expectations functions after - * the observable ended (or emitted error). If Observable does not complete within OBSERVABLE_TEST_TIMEOUT_IN_MS the - * test fails. - */ -export function toEmitValuesWith( - received: Observable, - expectations: (actual: any[]) => void -): Promise { - const failsChecks = expectObservable(received); - if (failsChecks) { - return Promise.resolve(failsChecks); - } - - return new Promise((resolve) => { - const receivedValues: any[] = []; - const subscription = new Subscription(); - - subscription.add( - received.subscribe({ - next: (value) => { - receivedValues.push(value); - }, - error: (err) => { - receivedValues.push(err); - subscription.unsubscribe(); - resolve(tryExpectations(receivedValues, expectations)); - }, - complete: () => { - subscription.unsubscribe(); - resolve(tryExpectations(receivedValues, expectations)); - }, - }) - ); - - forceObservableCompletion(subscription, resolve); - }); -} diff --git a/packages/grafana-prometheus/src/gcopypaste/public/test/matchers/types.ts b/packages/grafana-prometheus/src/gcopypaste/public/test/matchers/types.ts deleted file mode 100644 index 953b5ff86da..00000000000 --- a/packages/grafana-prometheus/src/gcopypaste/public/test/matchers/types.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/test/matchers/toEmitValuesWith.ts -import { Observable } from 'rxjs'; - -export const OBSERVABLE_TEST_TIMEOUT_IN_MS = 1000; - -export type ObservableType = T extends Observable ? V : never; - -export interface ObservableMatchers extends jest.ExpectExtendMap { - toEmitValues>(received: T, expected: E[]): Promise; - toEmitValuesWith>( - received: T, - expectations: (received: E[]) => void - ): Promise; -} diff --git a/packages/grafana-prometheus/src/gcopypaste/public/test/matchers/utils.ts b/packages/grafana-prometheus/src/gcopypaste/public/test/matchers/utils.ts deleted file mode 100644 index 3915cc13a4f..00000000000 --- a/packages/grafana-prometheus/src/gcopypaste/public/test/matchers/utils.ts +++ /dev/null @@ -1,64 +0,0 @@ -// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/test/matchers/utils.ts -import { matcherHint, printExpected, printReceived } from 'jest-matcher-utils'; -import { asapScheduler, Subscription, timer, isObservable } from 'rxjs'; - -import { OBSERVABLE_TEST_TIMEOUT_IN_MS } from './types'; - -export function forceObservableCompletion(subscription: Subscription, resolve: (args: any) => void) { - const timeoutObservable = timer(OBSERVABLE_TEST_TIMEOUT_IN_MS, asapScheduler); - - subscription.add( - timeoutObservable.subscribe(() => { - subscription.unsubscribe(); - resolve({ - pass: false, - message: () => - `${matcherHint('.toEmitValues')} - - Expected ${printReceived('Observable')} to be ${printExpected( - `completed within ${OBSERVABLE_TEST_TIMEOUT_IN_MS}ms` - )} but it did not.`, - }); - }) - ); -} - -export function expectObservableToBeDefined(received: unknown): jest.CustomMatcherResult | null { - if (received) { - return null; - } - - return { - pass: false, - message: () => `${matcherHint('.toEmitValues')} - -Expected ${printReceived(received)} to be ${printExpected('defined')}.`, - }; -} - -export function expectObservableToBeObservable(received: unknown): jest.CustomMatcherResult | null { - if (isObservable(received)) { - return null; - } - - return { - pass: false, - message: () => `${matcherHint('.toEmitValues')} - -Expected ${printReceived(received)} to be ${printExpected('an Observable')}.`, - }; -} - -export function expectObservable(received: unknown): jest.CustomMatcherResult | null { - const toBeDefined = expectObservableToBeDefined(received); - if (toBeDefined) { - return toBeDefined; - } - - const toBeObservable = expectObservableToBeObservable(received); - if (toBeObservable) { - return toBeObservable; - } - - return null; -} diff --git a/packages/grafana-prometheus/src/querybuilder/QueryPatternsModal.tsx b/packages/grafana-prometheus/src/querybuilder/QueryPatternsModal.tsx index 1fb721fdfad..a98cc3a44c1 100644 --- a/packages/grafana-prometheus/src/querybuilder/QueryPatternsModal.tsx +++ b/packages/grafana-prometheus/src/querybuilder/QueryPatternsModal.tsx @@ -3,11 +3,10 @@ import { css } from '@emotion/css'; import { capitalize } from 'lodash'; import { useMemo, useState } from 'react'; -import { CoreApp, DataQuery, GrafanaTheme2 } from '@grafana/data'; +import { CoreApp, DataQuery, getNextRefId, GrafanaTheme2 } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; import { Button, Collapse, Modal, useStyles2 } from '@grafana/ui'; -import { getNextRefIdChar } from '../gcopypaste/app/core/utils/query'; import { PromQuery } from '../types'; import { promQueryModeller } from './PromQueryModeller'; @@ -59,7 +58,7 @@ export const QueryPatternsModal = (props: Props) => { if (hasNewQueryOption && selectAsNewQuery) { onAddQuery({ ...query, - refId: getNextRefIdChar(queries ?? [query]), + refId: getNextRefId(queries ?? [query]), expr: promQueryModeller.renderQuery(visualQuery.query), }); } else { diff --git a/packages/grafana-prometheus/src/querybuilder/components/LabelFilters.test.tsx b/packages/grafana-prometheus/src/querybuilder/components/LabelFilters.test.tsx index 55151ae2800..8647feee695 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/LabelFilters.test.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/LabelFilters.test.tsx @@ -3,7 +3,7 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { ComponentProps } from 'react'; -import { selectOptionInTest } from '../../gcopypaste/test/helpers/selectOptionInTest'; +import { selectOptionInTest } from '../../test/helpers/selectOptionInTest'; import { getLabelSelects } from '../testUtils'; import { LabelFilters, MISSING_LABEL_FILTER_ERROR_MESSAGE, LabelFiltersProps } from './LabelFilters'; diff --git a/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilderOptions.test.tsx b/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilderOptions.test.tsx index b6654dadafb..61d6c4dfda2 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilderOptions.test.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilderOptions.test.tsx @@ -4,7 +4,7 @@ import userEvent from '@testing-library/user-event'; import { CoreApp } from '@grafana/data'; -import { selectOptionInTest } from '../../gcopypaste/test/helpers/selectOptionInTest'; +import { selectOptionInTest } from '../../test/helpers/selectOptionInTest'; import { PromQuery } from '../../types'; import { getQueryWithDefaults } from '../state'; diff --git a/packages/grafana-prometheus/src/querybuilder/components/PromQueryCodeEditorAutocompleteInfo.test.tsx b/packages/grafana-prometheus/src/querybuilder/components/PromQueryCodeEditorAutocompleteInfo.test.tsx index 42ae5cb2284..3a2d762db70 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/PromQueryCodeEditorAutocompleteInfo.test.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/PromQueryCodeEditorAutocompleteInfo.test.tsx @@ -30,18 +30,6 @@ jest.mock('../../components/monaco-query-field/MonacoQueryFieldWrapper', () => { }; }); -jest.mock('../../gcopypaste/app/core/store', () => { - return { - get() { - return undefined; - }, - set() {}, - getObject(key: string, defaultValue: unknown) { - return defaultValue; - }, - }; -}); - jest.mock('@grafana/runtime', () => { return { ...jest.requireActual('@grafana/runtime'), diff --git a/packages/grafana-prometheus/src/querybuilder/components/PromQueryEditorSelector.test.tsx b/packages/grafana-prometheus/src/querybuilder/components/PromQueryEditorSelector.test.tsx index dcba07eb367..6cfbf555999 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/PromQueryEditorSelector.test.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/PromQueryEditorSelector.test.tsx @@ -24,18 +24,6 @@ jest.mock('../../components/monaco-query-field/MonacoQueryFieldWrapper', () => { }; }); -jest.mock('app/core/store', () => { - return { - get() { - return undefined; - }, - set() {}, - getObject(key: string, defaultValue: unknown) { - return defaultValue; - }, - }; -}); - jest.mock('@grafana/runtime', () => { return { ...jest.requireActual('@grafana/runtime'), diff --git a/packages/grafana-prometheus/src/querybuilder/components/promQail/PromQail.tsx b/packages/grafana-prometheus/src/querybuilder/components/promQail/PromQail.tsx index 7c9304f8fc7..4993cb51127 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/promQail/PromQail.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/promQail/PromQail.tsx @@ -3,12 +3,11 @@ import { css, cx } from '@emotion/css'; import { PayloadAction, createSlice } from '@reduxjs/toolkit'; import { useEffect, useReducer, useRef, useState } from 'react'; -import { GrafanaTheme2 } from '@grafana/data'; +import { GrafanaTheme2, store } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; import { Alert, Button, Checkbox, Input, Spinner, useTheme2 } from '@grafana/ui'; import { PrometheusDatasource } from '../../../datasource'; -import store from '../../../gcopypaste/app/core/store'; import { PromVisualQuery } from '../../types'; import { QuerySuggestionContainer } from './QuerySuggestionContainer'; diff --git a/packages/grafana-prometheus/src/querybuilder/hooks/useFlag.ts b/packages/grafana-prometheus/src/querybuilder/hooks/useFlag.ts index 7dfc732b025..f0df6046d58 100644 --- a/packages/grafana-prometheus/src/querybuilder/hooks/useFlag.ts +++ b/packages/grafana-prometheus/src/querybuilder/hooks/useFlag.ts @@ -1,7 +1,7 @@ // Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/querybuilder/hooks/useFlag.ts import { useCallback, useState } from 'react'; -import store from '../../gcopypaste/app/core/store'; +import { store } from '@grafana/data'; export const promQueryEditorExplainKey = 'PrometheusQueryEditorExplainDefault'; diff --git a/packages/grafana-prometheus/src/querybuilder/state.ts b/packages/grafana-prometheus/src/querybuilder/state.ts index fddda52c406..915acb4aa60 100644 --- a/packages/grafana-prometheus/src/querybuilder/state.ts +++ b/packages/grafana-prometheus/src/querybuilder/state.ts @@ -1,7 +1,6 @@ // Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/querybuilder/state.ts -import { CoreApp } from '@grafana/data'; +import { CoreApp, store } from '@grafana/data'; -import store from '../gcopypaste/app/core/store'; import { LegendFormatMode, PromQuery } from '../types'; import { QueryEditorMode } from './shared/types'; diff --git a/packages/grafana-prometheus/src/querycache/QueryCache.ts b/packages/grafana-prometheus/src/querycache/QueryCache.ts index f4b955b82a6..4758e4cff58 100644 --- a/packages/grafana-prometheus/src/querycache/QueryCache.ts +++ b/packages/grafana-prometheus/src/querycache/QueryCache.ts @@ -1,5 +1,6 @@ // Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/querycache/QueryCache.ts import { + amendTable, DataFrame, DataQueryRequest, dateTime, @@ -8,9 +9,10 @@ import { incrRoundDn, isValidDuration, parseDuration, + Table, + trimTable, } from '@grafana/data'; -import { amendTable, Table, trimTable } from '../gcopypaste/app/features/live/data/amendTimeSeries'; import { PromQuery } from '../types'; // dashboardUID + panelId + refId diff --git a/packages/grafana-prometheus/src/__mocks__/datasource.ts b/packages/grafana-prometheus/src/test/__mocks__/datasource.ts similarity index 71% rename from packages/grafana-prometheus/src/__mocks__/datasource.ts rename to packages/grafana-prometheus/src/test/__mocks__/datasource.ts index 9237f1b65f0..e4e4fb21e3c 100644 --- a/packages/grafana-prometheus/src/__mocks__/datasource.ts +++ b/packages/grafana-prometheus/src/test/__mocks__/datasource.ts @@ -1,6 +1,53 @@ -import { CoreApp, DataQueryRequest, dateTime, rangeUtil, TimeRange } from '@grafana/data'; +import { merge } from 'lodash'; -import { PromQuery } from '../types'; +import { + CoreApp, + DataQueryRequest, + DataSourceJsonData, + DataSourceSettings, + dateTime, + rangeUtil, + TimeRange, +} from '@grafana/data'; + +import { PromOptions, PromQuery } from '../../types'; + +export const getMockDataSource = ( + overrides?: Partial> +): DataSourceSettings => + merge( + { + access: '', + basicAuth: false, + basicAuthUser: '', + withCredentials: false, + database: '', + id: 13, + uid: 'x', + isDefault: false, + jsonData: { authType: 'credentials', defaultRegion: 'eu-west-2' }, + name: 'gdev-prometheus', + typeName: 'Prometheus', + orgId: 1, + readOnly: false, + type: 'prometheus', + typeLogoUrl: 'packages/grafana-prometheus/src/img/prometheus_logo.svg', + url: '', + user: '', + secureJsonFields: {}, + }, + overrides + ); + +export function createDefaultConfigOptions(): DataSourceSettings { + return getMockDataSource({ + jsonData: { + timeInterval: '1m', + queryTimeout: '1m', + httpMethod: 'GET', + }, + }); +} export function createDataRequest( targets: PromQuery[], diff --git a/packages/grafana-prometheus/src/gcopypaste/test/helpers/selectOptionInTest.ts b/packages/grafana-prometheus/src/test/helpers/selectOptionInTest.ts similarity index 100% rename from packages/grafana-prometheus/src/gcopypaste/test/helpers/selectOptionInTest.ts rename to packages/grafana-prometheus/src/test/helpers/selectOptionInTest.ts