VariableQueryRunner.test.ts 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. import { of, throwError } from 'rxjs';
  2. import { delay } from 'rxjs/operators';
  3. import { getDefaultTimeRange, LoadingState, VariableSupportType } from '@grafana/data';
  4. import { queryBuilder } from '../shared/testing/builders';
  5. import { getPreloadedState } from '../state/helpers';
  6. import { toKeyedAction } from '../state/keyedVariablesReducer';
  7. import { initialTransactionState } from '../state/transactionReducer';
  8. import { KeyedVariableIdentifier } from '../state/types';
  9. import { QueryVariableModel } from '../types';
  10. import { toKeyedVariableIdentifier } from '../utils';
  11. import { UpdateOptionsResults, VariableQueryRunner } from './VariableQueryRunner';
  12. import { QueryRunner, QueryRunners } from './queryRunners';
  13. import { updateVariableOptions } from './reducer';
  14. type DoneCallback = {
  15. (...args: any[]): any;
  16. fail(error?: string | { message: string }): any;
  17. };
  18. function expectOnResults(args: {
  19. runner: VariableQueryRunner;
  20. identifier: KeyedVariableIdentifier;
  21. done: DoneCallback;
  22. expect: (results: UpdateOptionsResults[]) => void;
  23. }) {
  24. const { runner, identifier, done, expect: expectCallback } = args;
  25. const results: UpdateOptionsResults[] = [];
  26. const subscription = runner.getResponse(identifier).subscribe({
  27. next: (value) => {
  28. results.push(value);
  29. if (value.state === LoadingState.Done || value.state === LoadingState.Error) {
  30. try {
  31. expectCallback(results);
  32. subscription.unsubscribe();
  33. done();
  34. } catch (err) {
  35. subscription.unsubscribe();
  36. done(err);
  37. }
  38. }
  39. },
  40. });
  41. }
  42. function getTestContext(variable?: QueryVariableModel) {
  43. const getTimeSrv = jest.fn().mockReturnValue({
  44. timeRange: jest.fn().mockReturnValue(getDefaultTimeRange()),
  45. });
  46. const key = '0123456789';
  47. variable = variable ?? queryBuilder().withId('query').withRootStateKey(key).withName('query').build();
  48. const datasource: any = { metricFindQuery: jest.fn().mockResolvedValue([]) };
  49. const identifier = toKeyedVariableIdentifier(variable);
  50. const searchFilter = undefined;
  51. const getTemplatedRegex = jest.fn().mockReturnValue('getTemplatedRegex result');
  52. const dispatch = jest.fn().mockResolvedValue({});
  53. const templatingState = {
  54. transaction: { ...initialTransactionState, uid: key },
  55. variables: {
  56. [variable.id]: variable,
  57. },
  58. };
  59. const getState = jest.fn().mockReturnValue(getPreloadedState(key, templatingState));
  60. const queryRunner: QueryRunner = {
  61. type: VariableSupportType.Standard,
  62. canRun: jest.fn().mockReturnValue(true),
  63. getTarget: jest.fn().mockReturnValue({ refId: 'A', query: 'A query' }),
  64. runRequest: jest.fn().mockReturnValue(of({ series: [], state: LoadingState.Done })),
  65. };
  66. const queryRunners = {
  67. getRunnerForDatasource: jest.fn().mockReturnValue(queryRunner),
  68. } as unknown as QueryRunners;
  69. const getVariable = jest.fn().mockReturnValue(variable);
  70. const runRequest = jest.fn().mockReturnValue(of({}));
  71. const runner = new VariableQueryRunner({
  72. getTimeSrv,
  73. getTemplatedRegex,
  74. dispatch,
  75. getState,
  76. getVariable,
  77. queryRunners,
  78. runRequest,
  79. });
  80. return {
  81. key,
  82. identifier,
  83. datasource,
  84. runner,
  85. searchFilter,
  86. getTemplatedRegex,
  87. dispatch,
  88. getState,
  89. queryRunner,
  90. queryRunners,
  91. getVariable,
  92. runRequest,
  93. variable,
  94. getTimeSrv,
  95. };
  96. }
  97. describe('VariableQueryRunner', () => {
  98. describe('happy case', () => {
  99. it('then it should work as expected', (done) => {
  100. const { key, identifier, runner, datasource, getState, getVariable, queryRunners, queryRunner, dispatch } =
  101. getTestContext();
  102. expectOnResults({
  103. identifier,
  104. runner,
  105. expect: (results) => {
  106. // verify that the observable works as expected
  107. expect(results).toEqual([
  108. { state: LoadingState.Loading, identifier },
  109. { state: LoadingState.Done, identifier },
  110. ]);
  111. // verify that mocks have been called as expected
  112. expect(getState).toHaveBeenCalledTimes(3);
  113. expect(getVariable).toHaveBeenCalledTimes(1);
  114. expect(queryRunners.getRunnerForDatasource).toHaveBeenCalledTimes(1);
  115. expect(queryRunner.getTarget).toHaveBeenCalledTimes(1);
  116. expect(queryRunner.runRequest).toHaveBeenCalledTimes(1);
  117. expect(datasource.metricFindQuery).not.toHaveBeenCalled();
  118. // updateVariableOptions and validateVariableSelectionState
  119. expect(dispatch).toHaveBeenCalledTimes(2);
  120. expect(dispatch.mock.calls[0][0]).toEqual(
  121. toKeyedAction(
  122. key,
  123. updateVariableOptions({
  124. id: 'query',
  125. type: 'query',
  126. data: { results: [], templatedRegex: 'getTemplatedRegex result' },
  127. })
  128. )
  129. );
  130. },
  131. done,
  132. });
  133. runner.queueRequest({ identifier, datasource });
  134. });
  135. });
  136. describe('error cases', () => {
  137. describe('queryRunners.getRunnerForDatasource throws', () => {
  138. it('then it should work as expected', (done) => {
  139. const { identifier, runner, datasource, getState, getVariable, queryRunners, queryRunner, dispatch } =
  140. getTestContext();
  141. queryRunners.getRunnerForDatasource = jest.fn().mockImplementation(() => {
  142. throw new Error('getRunnerForDatasource error');
  143. });
  144. expectOnResults({
  145. identifier,
  146. runner,
  147. expect: (results) => {
  148. // verify that the observable works as expected
  149. expect(results).toEqual([
  150. { state: LoadingState.Loading, identifier },
  151. { state: LoadingState.Error, identifier, error: new Error('getRunnerForDatasource error') },
  152. ]);
  153. // verify that mocks have been called as expected
  154. expect(getState).toHaveBeenCalledTimes(2);
  155. expect(getVariable).toHaveBeenCalledTimes(1);
  156. expect(queryRunners.getRunnerForDatasource).toHaveBeenCalledTimes(1);
  157. expect(queryRunner.getTarget).not.toHaveBeenCalled();
  158. expect(queryRunner.runRequest).not.toHaveBeenCalled();
  159. expect(datasource.metricFindQuery).not.toHaveBeenCalled();
  160. expect(dispatch).not.toHaveBeenCalled();
  161. },
  162. done,
  163. });
  164. runner.queueRequest({ identifier, datasource });
  165. });
  166. });
  167. describe('runRequest throws', () => {
  168. it('then it should work as expected', (done) => {
  169. const { identifier, runner, datasource, getState, getVariable, queryRunners, queryRunner, dispatch } =
  170. getTestContext();
  171. queryRunner.runRequest = jest.fn().mockReturnValue(throwError(new Error('runRequest error')));
  172. expectOnResults({
  173. identifier,
  174. runner,
  175. expect: (results) => {
  176. // verify that the observable works as expected
  177. expect(results).toEqual([
  178. { state: LoadingState.Loading, identifier },
  179. { state: LoadingState.Error, identifier, error: new Error('runRequest error') },
  180. ]);
  181. // verify that mocks have been called as expected
  182. expect(getState).toHaveBeenCalledTimes(2);
  183. expect(getVariable).toHaveBeenCalledTimes(1);
  184. expect(queryRunners.getRunnerForDatasource).toHaveBeenCalledTimes(1);
  185. expect(queryRunner.getTarget).toHaveBeenCalledTimes(1);
  186. expect(queryRunner.runRequest).toHaveBeenCalledTimes(1);
  187. expect(datasource.metricFindQuery).not.toHaveBeenCalled();
  188. expect(dispatch).not.toHaveBeenCalled();
  189. },
  190. done,
  191. });
  192. runner.queueRequest({ identifier, datasource });
  193. });
  194. });
  195. });
  196. describe('cancellation cases', () => {
  197. describe('long running request is cancelled', () => {
  198. it('then it should work as expected', (done) => {
  199. const { identifier, datasource, runner, queryRunner } = getTestContext();
  200. queryRunner.runRequest = jest
  201. .fn()
  202. .mockReturnValue(of({ series: [], state: LoadingState.Done }).pipe(delay(10000)));
  203. expectOnResults({
  204. identifier,
  205. runner,
  206. expect: (results) => {
  207. // verify that the observable works as expected
  208. expect(results).toEqual([
  209. { state: LoadingState.Loading, identifier },
  210. { state: LoadingState.Loading, identifier, cancelled: true },
  211. { state: LoadingState.Done, identifier },
  212. ]);
  213. },
  214. done,
  215. });
  216. runner.queueRequest({ identifier, datasource });
  217. runner.cancelRequest(identifier);
  218. });
  219. });
  220. describe('an identical request is triggered before first request is finished', () => {
  221. it('then it should work as expected', (done) => {
  222. const { identifier, datasource, runner, queryRunner } = getTestContext();
  223. queryRunner.runRequest = jest
  224. .fn()
  225. .mockReturnValueOnce(of({ series: [], state: LoadingState.Done }).pipe(delay(10000)))
  226. .mockReturnValue(of({ series: [], state: LoadingState.Done }));
  227. expectOnResults({
  228. identifier,
  229. runner,
  230. expect: (results) => {
  231. // verify that the observable works as expected
  232. expect(results).toEqual([
  233. { state: LoadingState.Loading, identifier },
  234. { state: LoadingState.Loading, identifier },
  235. { state: LoadingState.Loading, identifier, cancelled: true },
  236. { state: LoadingState.Done, identifier },
  237. ]);
  238. },
  239. done,
  240. });
  241. runner.queueRequest({ identifier, datasource });
  242. runner.queueRequest({ identifier, datasource });
  243. });
  244. });
  245. });
  246. });