reducers.test.ts 18 KB

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