processVariable.test.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  1. import { UrlQueryMap } from '@grafana/data';
  2. import { setDataSourceSrv } from '@grafana/runtime';
  3. import { reduxTester } from '../../../../test/core/redux/reduxTester';
  4. import { variableAdapters } from '../adapters';
  5. import { createCustomVariableAdapter } from '../custom/adapter';
  6. import { setVariableQueryRunner, VariableQueryRunner } from '../query/VariableQueryRunner';
  7. import { createQueryVariableAdapter } from '../query/adapter';
  8. import { updateVariableOptions } from '../query/reducer';
  9. import { customBuilder, queryBuilder } from '../shared/testing/builders';
  10. import { VariableRefresh } from '../types';
  11. import { toKeyedVariableIdentifier, toVariablePayload } from '../utils';
  12. import { initDashboardTemplating, processVariable } from './actions';
  13. import { getTemplatingRootReducer, TemplatingReducerType } from './helpers';
  14. import { toKeyedAction } from './keyedVariablesReducer';
  15. import { setCurrentVariableValue, variableStateCompleted, variableStateFetching } from './sharedReducer';
  16. import { variablesInitTransaction } from './transactionReducer';
  17. jest.mock('app/features/dashboard/services/TimeSrv', () => ({
  18. getTimeSrv: jest.fn().mockReturnValue({
  19. timeRange: jest.fn().mockReturnValue({
  20. from: '2001-01-01T01:00:00.000Z',
  21. to: '2001-01-01T02:00:00.000Z',
  22. raw: {
  23. from: 'now-1h',
  24. to: 'now',
  25. },
  26. }),
  27. }),
  28. }));
  29. setDataSourceSrv({
  30. get: jest.fn().mockResolvedValue({
  31. metricFindQuery: jest.fn().mockImplementation((query, options) => {
  32. if (query === '$custom.*') {
  33. return Promise.resolve([
  34. { value: 'AA', text: 'AA' },
  35. { value: 'AB', text: 'AB' },
  36. { value: 'AC', text: 'AC' },
  37. ]);
  38. }
  39. if (query === '$custom.$queryDependsOnCustom.*') {
  40. return Promise.resolve([
  41. { value: 'AAA', text: 'AAA' },
  42. { value: 'AAB', text: 'AAB' },
  43. { value: 'AAC', text: 'AAC' },
  44. ]);
  45. }
  46. if (query === '*') {
  47. return Promise.resolve([
  48. { value: 'A', text: 'A' },
  49. { value: 'B', text: 'B' },
  50. { value: 'C', text: 'C' },
  51. ]);
  52. }
  53. return Promise.resolve([]);
  54. }),
  55. }),
  56. } as any);
  57. variableAdapters.setInit(() => [createCustomVariableAdapter(), createQueryVariableAdapter()]);
  58. describe('processVariable', () => {
  59. // these following processVariable tests will test the following base setup
  60. // custom doesn't depend on any other variable
  61. // queryDependsOnCustom depends on custom
  62. // queryNoDepends doesn't depend on any other variable
  63. const key = 'key';
  64. const getTestContext = () => {
  65. const custom = customBuilder()
  66. .withId('custom')
  67. .withRootStateKey(key)
  68. .withName('custom')
  69. .withQuery('A,B,C')
  70. .withOptions('A', 'B', 'C')
  71. .withCurrent('A')
  72. .build();
  73. const queryDependsOnCustom = queryBuilder()
  74. .withId('queryDependsOnCustom')
  75. .withRootStateKey(key)
  76. .withName('queryDependsOnCustom')
  77. .withQuery('$custom.*')
  78. .withOptions('AA', 'AB', 'AC')
  79. .withCurrent('AA')
  80. .build();
  81. const queryNoDepends = queryBuilder()
  82. .withId('queryNoDepends')
  83. .withRootStateKey(key)
  84. .withName('queryNoDepends')
  85. .withQuery('*')
  86. .withOptions('A', 'B', 'C')
  87. .withCurrent('A')
  88. .build();
  89. const list = [custom, queryDependsOnCustom, queryNoDepends];
  90. const dashboard: any = { templating: { list } };
  91. setVariableQueryRunner(new VariableQueryRunner());
  92. return {
  93. key,
  94. custom,
  95. queryDependsOnCustom,
  96. queryNoDepends,
  97. dashboard,
  98. };
  99. };
  100. // testing processVariable for the custom variable from case described above
  101. describe('when processVariable is dispatched for a custom variable without dependencies', () => {
  102. describe('and queryParams does not match variable', () => {
  103. it('then correct actions are dispatched', async () => {
  104. const { key, dashboard, custom } = getTestContext();
  105. const queryParams: UrlQueryMap = {};
  106. const tester = await reduxTester<TemplatingReducerType>()
  107. .givenRootReducer(getTemplatingRootReducer())
  108. .whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
  109. .whenActionIsDispatched(initDashboardTemplating(key, dashboard))
  110. .whenAsyncActionIsDispatched(processVariable(toKeyedVariableIdentifier(custom), queryParams), true);
  111. await tester.thenDispatchedActionsShouldEqual(
  112. toKeyedAction(key, variableStateCompleted(toVariablePayload(custom)))
  113. );
  114. });
  115. });
  116. describe('and queryParams does match variable', () => {
  117. it('then correct actions are dispatched', async () => {
  118. const { key, dashboard, custom } = getTestContext();
  119. const queryParams: UrlQueryMap = { 'var-custom': 'B' };
  120. const tester = await reduxTester<TemplatingReducerType>()
  121. .givenRootReducer(getTemplatingRootReducer())
  122. .whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
  123. .whenActionIsDispatched(initDashboardTemplating(key, dashboard))
  124. .whenAsyncActionIsDispatched(processVariable(toKeyedVariableIdentifier(custom), queryParams), true);
  125. await tester.thenDispatchedActionsShouldEqual(
  126. toKeyedAction(
  127. key,
  128. setCurrentVariableValue(
  129. toVariablePayload(
  130. { type: 'custom', id: 'custom' },
  131. { option: { text: 'B', value: 'B', selected: false } }
  132. )
  133. )
  134. ),
  135. toKeyedAction(key, variableStateCompleted(toVariablePayload(custom)))
  136. );
  137. });
  138. });
  139. });
  140. // testing processVariable for the queryNoDepends variable from case described above
  141. describe('when processVariable is dispatched for a query variable without dependencies', () => {
  142. describe('and queryParams does not match variable', () => {
  143. const queryParams: UrlQueryMap = {};
  144. describe('and refresh is VariableRefresh.never', () => {
  145. const refresh = VariableRefresh.never;
  146. it('then correct actions are dispatched', async () => {
  147. const { dashboard, key, queryNoDepends } = getTestContext();
  148. queryNoDepends.refresh = refresh;
  149. const tester = await reduxTester<TemplatingReducerType>()
  150. .givenRootReducer(getTemplatingRootReducer())
  151. .whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
  152. .whenActionIsDispatched(initDashboardTemplating(key, dashboard))
  153. .whenAsyncActionIsDispatched(processVariable(toKeyedVariableIdentifier(queryNoDepends), queryParams), true);
  154. await tester.thenDispatchedActionsShouldEqual(
  155. toKeyedAction(key, variableStateCompleted(toVariablePayload(queryNoDepends)))
  156. );
  157. });
  158. });
  159. it.each`
  160. refresh
  161. ${VariableRefresh.onDashboardLoad}
  162. ${VariableRefresh.onTimeRangeChanged}
  163. `('and refresh is $refresh then correct actions are dispatched', async ({ refresh }) => {
  164. const { dashboard, key, queryNoDepends } = getTestContext();
  165. queryNoDepends.refresh = refresh;
  166. const tester = await reduxTester<TemplatingReducerType>()
  167. .givenRootReducer(getTemplatingRootReducer())
  168. .whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
  169. .whenActionIsDispatched(initDashboardTemplating(key, dashboard))
  170. .whenAsyncActionIsDispatched(processVariable(toKeyedVariableIdentifier(queryNoDepends), queryParams), true);
  171. await tester.thenDispatchedActionsShouldEqual(
  172. toKeyedAction(key, variableStateFetching(toVariablePayload({ type: 'query', id: 'queryNoDepends' }))),
  173. toKeyedAction(
  174. key,
  175. updateVariableOptions(
  176. toVariablePayload(
  177. { type: 'query', id: 'queryNoDepends' },
  178. {
  179. results: [
  180. { value: 'A', text: 'A' },
  181. { value: 'B', text: 'B' },
  182. { value: 'C', text: 'C' },
  183. ],
  184. templatedRegex: '',
  185. }
  186. )
  187. )
  188. ),
  189. toKeyedAction(
  190. key,
  191. setCurrentVariableValue(
  192. toVariablePayload(
  193. { type: 'query', id: 'queryNoDepends' },
  194. { option: { text: 'A', value: 'A', selected: false } }
  195. )
  196. )
  197. ),
  198. toKeyedAction(key, variableStateCompleted(toVariablePayload({ type: 'query', id: 'queryNoDepends' })))
  199. );
  200. });
  201. });
  202. describe('and queryParams does match variable', () => {
  203. const queryParams: UrlQueryMap = { 'var-queryNoDepends': 'B' };
  204. describe('and refresh is VariableRefresh.never', () => {
  205. const refresh = VariableRefresh.never;
  206. it('then correct actions are dispatched', async () => {
  207. const { dashboard, key, queryNoDepends } = getTestContext();
  208. queryNoDepends.refresh = refresh;
  209. const tester = await reduxTester<TemplatingReducerType>()
  210. .givenRootReducer(getTemplatingRootReducer())
  211. .whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
  212. .whenActionIsDispatched(initDashboardTemplating(key, dashboard))
  213. .whenAsyncActionIsDispatched(processVariable(toKeyedVariableIdentifier(queryNoDepends), queryParams), true);
  214. await tester.thenDispatchedActionsShouldEqual(
  215. toKeyedAction(
  216. key,
  217. setCurrentVariableValue(
  218. toVariablePayload(
  219. { type: 'query', id: 'queryNoDepends' },
  220. { option: { text: 'B', value: 'B', selected: false } }
  221. )
  222. )
  223. ),
  224. toKeyedAction(key, variableStateCompleted(toVariablePayload({ type: 'query', id: 'queryNoDepends' })))
  225. );
  226. });
  227. });
  228. it.each`
  229. refresh
  230. ${VariableRefresh.onDashboardLoad}
  231. ${VariableRefresh.onTimeRangeChanged}
  232. `('and refresh is $refresh then correct actions are dispatched', async ({ refresh }) => {
  233. const { dashboard, key, queryNoDepends } = getTestContext();
  234. queryNoDepends.refresh = refresh;
  235. const tester = await reduxTester<TemplatingReducerType>()
  236. .givenRootReducer(getTemplatingRootReducer())
  237. .whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
  238. .whenActionIsDispatched(initDashboardTemplating(key, dashboard))
  239. .whenAsyncActionIsDispatched(processVariable(toKeyedVariableIdentifier(queryNoDepends), queryParams), true);
  240. await tester.thenDispatchedActionsShouldEqual(
  241. toKeyedAction(key, variableStateFetching(toVariablePayload({ type: 'query', id: 'queryNoDepends' }))),
  242. toKeyedAction(
  243. key,
  244. updateVariableOptions(
  245. toVariablePayload(
  246. { type: 'query', id: 'queryNoDepends' },
  247. {
  248. results: [
  249. { value: 'A', text: 'A' },
  250. { value: 'B', text: 'B' },
  251. { value: 'C', text: 'C' },
  252. ],
  253. templatedRegex: '',
  254. }
  255. )
  256. )
  257. ),
  258. toKeyedAction(
  259. key,
  260. setCurrentVariableValue(
  261. toVariablePayload(
  262. { type: 'query', id: 'queryNoDepends' },
  263. { option: { text: 'A', value: 'A', selected: false } }
  264. )
  265. )
  266. ),
  267. toKeyedAction(key, variableStateCompleted(toVariablePayload({ type: 'query', id: 'queryNoDepends' }))),
  268. toKeyedAction(
  269. key,
  270. setCurrentVariableValue(
  271. toVariablePayload(
  272. { type: 'query', id: 'queryNoDepends' },
  273. { option: { text: 'B', value: 'B', selected: false } }
  274. )
  275. )
  276. )
  277. );
  278. });
  279. });
  280. });
  281. // testing processVariable for the queryDependsOnCustom variable from case described above
  282. describe('when processVariable is dispatched for a query variable with one dependency', () => {
  283. describe('and queryParams does not match variable', () => {
  284. const queryParams: UrlQueryMap = {};
  285. describe('and refresh is VariableRefresh.never', () => {
  286. const refresh = VariableRefresh.never;
  287. it('then correct actions are dispatched', async () => {
  288. const { key, dashboard, custom, queryDependsOnCustom } = getTestContext();
  289. queryDependsOnCustom.refresh = refresh;
  290. const customProcessed = await reduxTester<TemplatingReducerType>()
  291. .givenRootReducer(getTemplatingRootReducer())
  292. .whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
  293. .whenActionIsDispatched(initDashboardTemplating(key, dashboard))
  294. .whenAsyncActionIsDispatched(processVariable(toKeyedVariableIdentifier(custom), queryParams)); // Need to process this dependency otherwise we never complete the promise chain
  295. const tester = await customProcessed.whenAsyncActionIsDispatched(
  296. processVariable(toKeyedVariableIdentifier(queryDependsOnCustom), queryParams),
  297. true
  298. );
  299. await tester.thenDispatchedActionsShouldEqual(
  300. toKeyedAction(key, variableStateCompleted(toVariablePayload({ type: 'query', id: 'queryDependsOnCustom' })))
  301. );
  302. });
  303. });
  304. it.each`
  305. refresh
  306. ${VariableRefresh.onDashboardLoad}
  307. ${VariableRefresh.onTimeRangeChanged}
  308. `('and refresh is $refresh then correct actions are dispatched', async ({ refresh }) => {
  309. const { key, dashboard, custom, queryDependsOnCustom } = getTestContext();
  310. queryDependsOnCustom.refresh = refresh;
  311. const customProcessed = await reduxTester<TemplatingReducerType>()
  312. .givenRootReducer(getTemplatingRootReducer())
  313. .whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
  314. .whenActionIsDispatched(initDashboardTemplating(key, dashboard))
  315. .whenAsyncActionIsDispatched(processVariable(toKeyedVariableIdentifier(custom), queryParams)); // Need to process this dependency otherwise we never complete the promise chain
  316. const tester = await customProcessed.whenAsyncActionIsDispatched(
  317. processVariable(toKeyedVariableIdentifier(queryDependsOnCustom), queryParams),
  318. true
  319. );
  320. await tester.thenDispatchedActionsShouldEqual(
  321. toKeyedAction(key, variableStateFetching(toVariablePayload({ type: 'query', id: 'queryDependsOnCustom' }))),
  322. toKeyedAction(
  323. key,
  324. updateVariableOptions(
  325. toVariablePayload(
  326. { type: 'query', id: 'queryDependsOnCustom' },
  327. {
  328. results: [
  329. { value: 'AA', text: 'AA' },
  330. { value: 'AB', text: 'AB' },
  331. { value: 'AC', text: 'AC' },
  332. ],
  333. templatedRegex: '',
  334. }
  335. )
  336. )
  337. ),
  338. toKeyedAction(
  339. key,
  340. setCurrentVariableValue(
  341. toVariablePayload(
  342. { type: 'query', id: 'queryDependsOnCustom' },
  343. { option: { text: 'AA', value: 'AA', selected: false } }
  344. )
  345. )
  346. ),
  347. toKeyedAction(key, variableStateCompleted(toVariablePayload({ type: 'query', id: 'queryDependsOnCustom' })))
  348. );
  349. });
  350. });
  351. describe('and queryParams does match variable', () => {
  352. const queryParams: UrlQueryMap = { 'var-queryDependsOnCustom': 'AB' };
  353. describe('and refresh is VariableRefresh.never', () => {
  354. const refresh = VariableRefresh.never;
  355. it('then correct actions are dispatched', async () => {
  356. const { key, dashboard, custom, queryDependsOnCustom } = getTestContext();
  357. queryDependsOnCustom.refresh = refresh;
  358. const customProcessed = await reduxTester<TemplatingReducerType>()
  359. .givenRootReducer(getTemplatingRootReducer())
  360. .whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
  361. .whenActionIsDispatched(initDashboardTemplating(key, dashboard))
  362. .whenAsyncActionIsDispatched(processVariable(toKeyedVariableIdentifier(custom), queryParams)); // Need to process this dependency otherwise we never complete the promise chain
  363. const tester = await customProcessed.whenAsyncActionIsDispatched(
  364. processVariable(toKeyedVariableIdentifier(queryDependsOnCustom), queryParams),
  365. true
  366. );
  367. await tester.thenDispatchedActionsShouldEqual(
  368. toKeyedAction(
  369. key,
  370. setCurrentVariableValue(
  371. toVariablePayload(
  372. { type: 'query', id: 'queryDependsOnCustom' },
  373. { option: { text: 'AB', value: 'AB', selected: false } }
  374. )
  375. )
  376. ),
  377. toKeyedAction(key, variableStateCompleted(toVariablePayload({ type: 'query', id: 'queryDependsOnCustom' })))
  378. );
  379. });
  380. });
  381. it.each`
  382. refresh
  383. ${VariableRefresh.onDashboardLoad}
  384. ${VariableRefresh.onTimeRangeChanged}
  385. `('and refresh is $refresh then correct actions are dispatched', async ({ refresh }) => {
  386. const { key, dashboard, custom, queryDependsOnCustom } = getTestContext();
  387. queryDependsOnCustom.refresh = refresh;
  388. const customProcessed = await reduxTester<TemplatingReducerType>()
  389. .givenRootReducer(getTemplatingRootReducer())
  390. .whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
  391. .whenActionIsDispatched(initDashboardTemplating(key, dashboard))
  392. .whenAsyncActionIsDispatched(processVariable(toKeyedVariableIdentifier(custom), queryParams)); // Need to process this dependency otherwise we never complete the promise chain
  393. const tester = await customProcessed.whenAsyncActionIsDispatched(
  394. processVariable(toKeyedVariableIdentifier(queryDependsOnCustom), queryParams),
  395. true
  396. );
  397. await tester.thenDispatchedActionsShouldEqual(
  398. toKeyedAction(key, variableStateFetching(toVariablePayload({ type: 'query', id: 'queryDependsOnCustom' }))),
  399. toKeyedAction(
  400. key,
  401. updateVariableOptions(
  402. toVariablePayload(
  403. { type: 'query', id: 'queryDependsOnCustom' },
  404. {
  405. results: [
  406. { value: 'AA', text: 'AA' },
  407. { value: 'AB', text: 'AB' },
  408. { value: 'AC', text: 'AC' },
  409. ],
  410. templatedRegex: '',
  411. }
  412. )
  413. )
  414. ),
  415. toKeyedAction(
  416. key,
  417. setCurrentVariableValue(
  418. toVariablePayload(
  419. { type: 'query', id: 'queryDependsOnCustom' },
  420. { option: { text: 'AA', value: 'AA', selected: false } }
  421. )
  422. )
  423. ),
  424. toKeyedAction(key, variableStateCompleted(toVariablePayload({ type: 'query', id: 'queryDependsOnCustom' }))),
  425. toKeyedAction(
  426. key,
  427. setCurrentVariableValue(
  428. toVariablePayload(
  429. { type: 'query', id: 'queryDependsOnCustom' },
  430. { option: { text: 'AB', value: 'AB', selected: false } }
  431. )
  432. )
  433. )
  434. );
  435. });
  436. });
  437. });
  438. });