reducers.test.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576
  1. import {
  2. itemReducer,
  3. makeExploreItemState,
  4. exploreReducer,
  5. makeInitialUpdateState,
  6. initialExploreState,
  7. DEFAULT_RANGE,
  8. } from './reducers';
  9. import {
  10. ExploreId,
  11. ExploreItemState,
  12. ExploreUrlState,
  13. ExploreState,
  14. RangeScanner,
  15. ExploreMode,
  16. } from 'app/types/explore';
  17. import { reducerTester } from 'test/core/redux/reducerTester';
  18. import {
  19. scanStartAction,
  20. testDataSourcePendingAction,
  21. testDataSourceSuccessAction,
  22. testDataSourceFailureAction,
  23. updateDatasourceInstanceAction,
  24. splitOpenAction,
  25. splitCloseAction,
  26. changeModeAction,
  27. scanStopAction,
  28. runQueriesAction,
  29. } from './actionTypes';
  30. import { Reducer } from 'redux';
  31. import { ActionOf } from 'app/core/redux/actionCreatorFactory';
  32. import { updateLocation } from 'app/core/actions/location';
  33. import { serializeStateToUrlParam } from 'app/core/utils/explore';
  34. import TableModel from 'app/core/table_model';
  35. import { DataSourceApi, DataQuery, LogsModel, LogsDedupStrategy, LoadingState, dateTime } from '@grafana/ui';
  36. describe('Explore item reducer', () => {
  37. describe('scanning', () => {
  38. it('should start scanning', () => {
  39. const scanner = jest.fn();
  40. const initalState = {
  41. ...makeExploreItemState(),
  42. scanning: false,
  43. scanner: undefined as RangeScanner,
  44. };
  45. reducerTester()
  46. .givenReducer(itemReducer as Reducer<ExploreItemState, ActionOf<any>>, initalState)
  47. .whenActionIsDispatched(scanStartAction({ exploreId: ExploreId.left, scanner }))
  48. .thenStateShouldEqual({
  49. ...makeExploreItemState(),
  50. scanning: true,
  51. scanner,
  52. });
  53. });
  54. it('should stop scanning', () => {
  55. const scanner = jest.fn();
  56. const initalState = {
  57. ...makeExploreItemState(),
  58. scanning: true,
  59. scanner,
  60. scanRange: {},
  61. };
  62. reducerTester()
  63. .givenReducer(itemReducer as Reducer<ExploreItemState, ActionOf<any>>, initalState)
  64. .whenActionIsDispatched(scanStopAction({ exploreId: ExploreId.left }))
  65. .thenStateShouldEqual({
  66. ...makeExploreItemState(),
  67. scanning: false,
  68. scanner: undefined,
  69. scanRange: undefined,
  70. });
  71. });
  72. });
  73. describe('testing datasource', () => {
  74. describe('when testDataSourcePendingAction is dispatched', () => {
  75. it('then it should set datasourceError', () => {
  76. reducerTester()
  77. .givenReducer(itemReducer, { datasourceError: {} })
  78. .whenActionIsDispatched(testDataSourcePendingAction({ exploreId: ExploreId.left }))
  79. .thenStateShouldEqual({ datasourceError: null });
  80. });
  81. });
  82. describe('when testDataSourceSuccessAction is dispatched', () => {
  83. it('then it should set datasourceError', () => {
  84. reducerTester()
  85. .givenReducer(itemReducer, { datasourceError: {} })
  86. .whenActionIsDispatched(testDataSourceSuccessAction({ exploreId: ExploreId.left }))
  87. .thenStateShouldEqual({ datasourceError: null });
  88. });
  89. });
  90. describe('when testDataSourceFailureAction is dispatched', () => {
  91. it('then it should set correct state', () => {
  92. const error = 'some error';
  93. const initalState: Partial<ExploreItemState> = {
  94. datasourceError: null,
  95. graphResult: [],
  96. tableResult: {} as TableModel,
  97. logsResult: {} as LogsModel,
  98. update: {
  99. datasource: true,
  100. queries: true,
  101. range: true,
  102. ui: true,
  103. },
  104. };
  105. const expectedState = {
  106. datasourceError: error,
  107. graphResult: undefined as any[],
  108. tableResult: undefined as TableModel,
  109. logsResult: undefined as LogsModel,
  110. update: makeInitialUpdateState(),
  111. };
  112. reducerTester()
  113. .givenReducer(itemReducer, initalState)
  114. .whenActionIsDispatched(testDataSourceFailureAction({ exploreId: ExploreId.left, error }))
  115. .thenStateShouldEqual(expectedState);
  116. });
  117. });
  118. describe('when changeDataType is dispatched', () => {
  119. it('then it should set correct state', () => {
  120. reducerTester()
  121. .givenReducer(itemReducer, {})
  122. .whenActionIsDispatched(changeModeAction({ exploreId: ExploreId.left, mode: ExploreMode.Logs }))
  123. .thenStateShouldEqual({
  124. mode: ExploreMode.Logs,
  125. });
  126. });
  127. });
  128. });
  129. describe('changing datasource', () => {
  130. describe('when updateDatasourceInstanceAction is dispatched', () => {
  131. describe('and datasourceInstance supports graph, logs, table and has a startpage', () => {
  132. it('then it should set correct state', () => {
  133. const StartPage = {};
  134. const datasourceInstance = {
  135. meta: {
  136. metrics: true,
  137. logs: true,
  138. },
  139. components: {
  140. ExploreStartPage: StartPage,
  141. },
  142. } as DataSourceApi;
  143. const queries: DataQuery[] = [];
  144. const queryKeys: string[] = [];
  145. const initalState: Partial<ExploreItemState> = {
  146. datasourceInstance: null,
  147. StartPage: null,
  148. showingStartPage: false,
  149. queries,
  150. queryKeys,
  151. };
  152. const expectedState = {
  153. datasourceInstance,
  154. StartPage,
  155. showingStartPage: true,
  156. queries,
  157. queryKeys,
  158. supportedModes: [ExploreMode.Metrics, ExploreMode.Logs],
  159. mode: ExploreMode.Metrics,
  160. loadingState: LoadingState.NotStarted,
  161. latency: 0,
  162. queryErrors: [],
  163. };
  164. reducerTester()
  165. .givenReducer(itemReducer, initalState)
  166. .whenActionIsDispatched(updateDatasourceInstanceAction({ exploreId: ExploreId.left, datasourceInstance }))
  167. .thenStateShouldEqual(expectedState);
  168. });
  169. });
  170. });
  171. });
  172. describe('run queries', () => {
  173. describe('when runQueriesAction is dispatched', () => {
  174. it('then it should set correct state', () => {
  175. const initalState: Partial<ExploreItemState> = {
  176. showingStartPage: true,
  177. range: null,
  178. };
  179. const expectedState = {
  180. queryIntervals: {
  181. interval: '1s',
  182. intervalMs: 1000,
  183. },
  184. showingStartPage: false,
  185. range: {
  186. from: dateTime(),
  187. to: dateTime(),
  188. raw: DEFAULT_RANGE,
  189. },
  190. };
  191. reducerTester()
  192. .givenReducer(itemReducer, initalState)
  193. .whenActionIsDispatched(runQueriesAction({ exploreId: ExploreId.left, range: expectedState.range }))
  194. .thenStateShouldEqual(expectedState);
  195. });
  196. });
  197. });
  198. });
  199. export const setup = (urlStateOverrides?: any) => {
  200. const update = makeInitialUpdateState();
  201. const urlStateDefaults: ExploreUrlState = {
  202. datasource: 'some-datasource',
  203. queries: [],
  204. range: {
  205. from: '',
  206. to: '',
  207. },
  208. ui: {
  209. dedupStrategy: LogsDedupStrategy.none,
  210. showingGraph: false,
  211. showingTable: false,
  212. showingLogs: false,
  213. },
  214. };
  215. const urlState: ExploreUrlState = { ...urlStateDefaults, ...urlStateOverrides };
  216. const serializedUrlState = serializeStateToUrlParam(urlState);
  217. const initalState = { split: false, left: { urlState, update }, right: { urlState, update } };
  218. return {
  219. initalState,
  220. serializedUrlState,
  221. };
  222. };
  223. describe('Explore reducer', () => {
  224. describe('split view', () => {
  225. it("should make right pane a duplicate of the given item's state on split open", () => {
  226. const leftItemMock = {
  227. containerWidth: 100,
  228. } as ExploreItemState;
  229. const initalState = {
  230. split: null,
  231. left: leftItemMock as ExploreItemState,
  232. right: makeExploreItemState(),
  233. } as ExploreState;
  234. reducerTester()
  235. .givenReducer(exploreReducer as Reducer<ExploreState, ActionOf<any>>, initalState)
  236. .whenActionIsDispatched(splitOpenAction({ itemState: leftItemMock }))
  237. .thenStateShouldEqual({
  238. split: true,
  239. left: leftItemMock,
  240. right: leftItemMock,
  241. });
  242. });
  243. describe('split close', () => {
  244. it('should keep right pane as left when left is closed', () => {
  245. const leftItemMock = {
  246. containerWidth: 100,
  247. } as ExploreItemState;
  248. const rightItemMock = {
  249. containerWidth: 200,
  250. } as ExploreItemState;
  251. const initalState = {
  252. split: null,
  253. left: leftItemMock,
  254. right: rightItemMock,
  255. } as ExploreState;
  256. // closing left item
  257. reducerTester()
  258. .givenReducer(exploreReducer as Reducer<ExploreState, ActionOf<any>>, initalState)
  259. .whenActionIsDispatched(splitCloseAction({ itemId: ExploreId.left }))
  260. .thenStateShouldEqual({
  261. split: false,
  262. left: rightItemMock,
  263. right: initialExploreState.right,
  264. });
  265. });
  266. it('should reset right pane when it is closed ', () => {
  267. const leftItemMock = {
  268. containerWidth: 100,
  269. } as ExploreItemState;
  270. const rightItemMock = {
  271. containerWidth: 200,
  272. } as ExploreItemState;
  273. const initalState = {
  274. split: null,
  275. left: leftItemMock,
  276. right: rightItemMock,
  277. } as ExploreState;
  278. // closing left item
  279. reducerTester()
  280. .givenReducer(exploreReducer as Reducer<ExploreState, ActionOf<any>>, initalState)
  281. .whenActionIsDispatched(splitCloseAction({ itemId: ExploreId.right }))
  282. .thenStateShouldEqual({
  283. split: false,
  284. left: leftItemMock,
  285. right: initialExploreState.right,
  286. });
  287. });
  288. });
  289. });
  290. describe('when updateLocation is dispatched', () => {
  291. describe('and payload does not contain a query', () => {
  292. it('then it should just return state', () => {
  293. reducerTester()
  294. .givenReducer(exploreReducer, {})
  295. .whenActionIsDispatched(updateLocation({ query: null }))
  296. .thenStateShouldEqual({});
  297. });
  298. });
  299. describe('and payload contains a query', () => {
  300. describe("but does not contain 'left'", () => {
  301. it('then it should just return state', () => {
  302. reducerTester()
  303. .givenReducer(exploreReducer, {})
  304. .whenActionIsDispatched(updateLocation({ query: {} }))
  305. .thenStateShouldEqual({});
  306. });
  307. });
  308. describe("and query contains a 'right'", () => {
  309. it('then it should add split in state', () => {
  310. const { initalState, serializedUrlState } = setup();
  311. const expectedState = { ...initalState, split: true };
  312. reducerTester()
  313. .givenReducer(exploreReducer, initalState)
  314. .whenActionIsDispatched(
  315. updateLocation({
  316. query: {
  317. left: serializedUrlState,
  318. right: serializedUrlState,
  319. },
  320. })
  321. )
  322. .thenStateShouldEqual(expectedState);
  323. });
  324. });
  325. describe("and query contains a 'left'", () => {
  326. describe('but urlState is not set in state', () => {
  327. it('then it should just add urlState and update in state', () => {
  328. const { initalState, serializedUrlState } = setup();
  329. const urlState: ExploreUrlState = null;
  330. const stateWithoutUrlState = { ...initalState, left: { urlState } };
  331. const expectedState = { ...initalState };
  332. reducerTester()
  333. .givenReducer(exploreReducer, stateWithoutUrlState)
  334. .whenActionIsDispatched(
  335. updateLocation({
  336. query: {
  337. left: serializedUrlState,
  338. },
  339. path: '/explore',
  340. })
  341. )
  342. .thenStateShouldEqual(expectedState);
  343. });
  344. });
  345. describe("but '/explore' is missing in path", () => {
  346. it('then it should just add urlState and update in state', () => {
  347. const { initalState, serializedUrlState } = setup();
  348. const expectedState = { ...initalState };
  349. reducerTester()
  350. .givenReducer(exploreReducer, initalState)
  351. .whenActionIsDispatched(
  352. updateLocation({
  353. query: {
  354. left: serializedUrlState,
  355. },
  356. path: '/dashboard',
  357. })
  358. )
  359. .thenStateShouldEqual(expectedState);
  360. });
  361. });
  362. describe("and '/explore' is in path", () => {
  363. describe('and datasource differs', () => {
  364. it('then it should return update datasource', () => {
  365. const { initalState, serializedUrlState } = setup();
  366. const expectedState = {
  367. ...initalState,
  368. left: {
  369. ...initalState.left,
  370. update: {
  371. ...initalState.left.update,
  372. datasource: true,
  373. },
  374. },
  375. };
  376. const stateWithDifferentDataSource = {
  377. ...initalState,
  378. left: {
  379. ...initalState.left,
  380. urlState: {
  381. ...initalState.left.urlState,
  382. datasource: 'different datasource',
  383. },
  384. },
  385. };
  386. reducerTester()
  387. .givenReducer(exploreReducer, stateWithDifferentDataSource)
  388. .whenActionIsDispatched(
  389. updateLocation({
  390. query: {
  391. left: serializedUrlState,
  392. },
  393. path: '/explore',
  394. })
  395. )
  396. .thenStateShouldEqual(expectedState);
  397. });
  398. });
  399. describe('and range differs', () => {
  400. it('then it should return update range', () => {
  401. const { initalState, serializedUrlState } = setup();
  402. const expectedState = {
  403. ...initalState,
  404. left: {
  405. ...initalState.left,
  406. update: {
  407. ...initalState.left.update,
  408. range: true,
  409. },
  410. },
  411. };
  412. const stateWithDifferentDataSource = {
  413. ...initalState,
  414. left: {
  415. ...initalState.left,
  416. urlState: {
  417. ...initalState.left.urlState,
  418. range: {
  419. from: 'now',
  420. to: 'now-6h',
  421. },
  422. },
  423. },
  424. };
  425. reducerTester()
  426. .givenReducer(exploreReducer, stateWithDifferentDataSource)
  427. .whenActionIsDispatched(
  428. updateLocation({
  429. query: {
  430. left: serializedUrlState,
  431. },
  432. path: '/explore',
  433. })
  434. )
  435. .thenStateShouldEqual(expectedState);
  436. });
  437. });
  438. describe('and queries differs', () => {
  439. it('then it should return update queries', () => {
  440. const { initalState, serializedUrlState } = setup();
  441. const expectedState = {
  442. ...initalState,
  443. left: {
  444. ...initalState.left,
  445. update: {
  446. ...initalState.left.update,
  447. queries: true,
  448. },
  449. },
  450. };
  451. const stateWithDifferentDataSource = {
  452. ...initalState,
  453. left: {
  454. ...initalState.left,
  455. urlState: {
  456. ...initalState.left.urlState,
  457. queries: [{ expr: '{__filename__="some.log"}' }],
  458. },
  459. },
  460. };
  461. reducerTester()
  462. .givenReducer(exploreReducer, stateWithDifferentDataSource)
  463. .whenActionIsDispatched(
  464. updateLocation({
  465. query: {
  466. left: serializedUrlState,
  467. },
  468. path: '/explore',
  469. })
  470. )
  471. .thenStateShouldEqual(expectedState);
  472. });
  473. });
  474. describe('and ui differs', () => {
  475. it('then it should return update ui', () => {
  476. const { initalState, serializedUrlState } = setup();
  477. const expectedState = {
  478. ...initalState,
  479. left: {
  480. ...initalState.left,
  481. update: {
  482. ...initalState.left.update,
  483. ui: true,
  484. },
  485. },
  486. };
  487. const stateWithDifferentDataSource = {
  488. ...initalState,
  489. left: {
  490. ...initalState.left,
  491. urlState: {
  492. ...initalState.left.urlState,
  493. ui: {
  494. ...initalState.left.urlState.ui,
  495. showingGraph: true,
  496. },
  497. },
  498. },
  499. };
  500. reducerTester()
  501. .givenReducer(exploreReducer, stateWithDifferentDataSource)
  502. .whenActionIsDispatched(
  503. updateLocation({
  504. query: {
  505. left: serializedUrlState,
  506. },
  507. path: '/explore',
  508. })
  509. )
  510. .thenStateShouldEqual(expectedState);
  511. });
  512. });
  513. describe('and nothing differs', () => {
  514. it('then it should return update ui', () => {
  515. const { initalState, serializedUrlState } = setup();
  516. const expectedState = { ...initalState };
  517. reducerTester()
  518. .givenReducer(exploreReducer, initalState)
  519. .whenActionIsDispatched(
  520. updateLocation({
  521. query: {
  522. left: serializedUrlState,
  523. },
  524. path: '/explore',
  525. })
  526. )
  527. .thenStateShouldEqual(expectedState);
  528. });
  529. });
  530. });
  531. });
  532. });
  533. });
  534. });