pageFilters.spec.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660
  1. import {act} from 'sentry-test/reactTestingLibrary';
  2. import {
  3. initializeUrlState,
  4. revertToPinnedFilters,
  5. updateDateTime,
  6. updateEnvironments,
  7. updatePersistence,
  8. updateProjects,
  9. } from 'sentry/actionCreators/pageFilters';
  10. import * as PageFilterPersistence from 'sentry/components/organizations/pageFilters/persistence';
  11. import PageFiltersStore from 'sentry/stores/pageFiltersStore';
  12. import localStorage from 'sentry/utils/localStorage';
  13. jest.mock('sentry/utils/localStorage');
  14. describe('PageFilters ActionCreators', function () {
  15. const organization = TestStubs.Organization();
  16. beforeEach(function () {
  17. jest.spyOn(PageFiltersStore, 'updateProjects');
  18. jest.spyOn(PageFiltersStore, 'onInitializeUrlState').mockImplementation();
  19. jest.clearAllMocks();
  20. });
  21. describe('initializeUrlState', function () {
  22. let router;
  23. const key = `global-selection:${organization.slug}`;
  24. beforeEach(() => {
  25. router = TestStubs.router();
  26. localStorage.setItem(
  27. key,
  28. JSON.stringify({
  29. environments: [],
  30. projects: [1],
  31. })
  32. );
  33. });
  34. it('loads from local storage when no query params and filters are pinned', function () {
  35. localStorage.setItem(
  36. key,
  37. JSON.stringify({
  38. environments: [],
  39. projects: [1],
  40. pinnedFilters: ['projects', 'environments'],
  41. })
  42. );
  43. initializeUrlState({
  44. organization,
  45. queryParams: {},
  46. router,
  47. memberProjects: [],
  48. shouldEnforceSingleProject: false,
  49. });
  50. expect(localStorage.getItem).toHaveBeenCalledWith(
  51. `global-selection:${organization.slug}`
  52. );
  53. expect(PageFiltersStore.onInitializeUrlState).toHaveBeenCalledWith(
  54. expect.objectContaining({
  55. environments: [],
  56. projects: [1],
  57. }),
  58. new Set(['projects', 'environments']),
  59. true
  60. );
  61. expect(router.replace).toHaveBeenCalledWith(
  62. expect.objectContaining({
  63. query: {
  64. environment: [],
  65. project: ['1'],
  66. },
  67. })
  68. );
  69. });
  70. it('does not load from local storage when no query params and `skipLoadLastUsed` is true', function () {
  71. jest.spyOn(localStorage, 'getItem');
  72. initializeUrlState({
  73. organization,
  74. queryParams: {},
  75. skipLoadLastUsed: true,
  76. memberProjects: [],
  77. shouldEnforceSingleProject: false,
  78. router,
  79. });
  80. expect(localStorage.getItem).not.toHaveBeenCalled();
  81. });
  82. it('does not update local storage (persist) when `shouldPersist` is false', async function () {
  83. jest.clearAllMocks();
  84. jest.spyOn(localStorage, 'getItem').mockReturnValueOnce(
  85. JSON.stringify({
  86. environments: [],
  87. projects: [],
  88. pinnedFilters: ['projects'],
  89. })
  90. );
  91. initializeUrlState({
  92. organization,
  93. queryParams: {},
  94. shouldPersist: false,
  95. router,
  96. memberProjects: [],
  97. shouldEnforceSingleProject: false,
  98. });
  99. expect(PageFiltersStore.onInitializeUrlState).toHaveBeenCalledWith(
  100. expect.objectContaining({
  101. environments: [],
  102. projects: [],
  103. }),
  104. new Set(['projects']),
  105. false
  106. );
  107. // `onInitializeUrlState` is being spied on, so PageFiltersStore wasn't actually
  108. // updated. We need to call `updatePersistence` manually.
  109. updatePersistence(false);
  110. await act(async () => {
  111. // Filters shouldn't persist even when `save` is true
  112. updateProjects([1], router, {save: true});
  113. // Page filter values are asynchronously persisted to local storage after a tick,
  114. // so we need to wait before checking for commits to local storage
  115. await tick();
  116. });
  117. // New value wasn't committed to local storage
  118. expect(localStorage.setItem).not.toHaveBeenCalled();
  119. });
  120. it('does not change dates with no query params or defaultSelection', function () {
  121. initializeUrlState({
  122. organization,
  123. queryParams: {
  124. project: '1',
  125. },
  126. memberProjects: [],
  127. shouldEnforceSingleProject: false,
  128. router,
  129. });
  130. expect(PageFiltersStore.onInitializeUrlState).toHaveBeenCalledWith(
  131. expect.objectContaining({
  132. datetime: {
  133. start: null,
  134. end: null,
  135. period: '14d',
  136. utc: null,
  137. },
  138. }),
  139. new Set(),
  140. true
  141. );
  142. });
  143. it('does changes to default dates with defaultSelection and no query params', function () {
  144. initializeUrlState({
  145. organization,
  146. queryParams: {
  147. project: '1',
  148. },
  149. memberProjects: [],
  150. shouldEnforceSingleProject: false,
  151. defaultSelection: {
  152. datetime: {
  153. period: '3h',
  154. utc: null,
  155. start: null,
  156. end: null,
  157. },
  158. },
  159. router,
  160. });
  161. expect(PageFiltersStore.onInitializeUrlState).toHaveBeenCalledWith(
  162. expect.objectContaining({
  163. datetime: {
  164. start: null,
  165. end: null,
  166. period: '3h',
  167. utc: null,
  168. },
  169. }),
  170. new Set(),
  171. true
  172. );
  173. });
  174. it('uses query params statsPeriod over defaults', function () {
  175. initializeUrlState({
  176. organization,
  177. queryParams: {
  178. statsPeriod: '1h',
  179. project: '1',
  180. },
  181. memberProjects: [],
  182. shouldEnforceSingleProject: false,
  183. defaultSelection: {
  184. datetime: {
  185. period: '24h',
  186. utc: null,
  187. start: null,
  188. end: null,
  189. },
  190. },
  191. router,
  192. });
  193. expect(router.replace).toHaveBeenCalledWith(
  194. expect.objectContaining({
  195. query: {
  196. cursor: undefined,
  197. project: ['1'],
  198. environment: [],
  199. statsPeriod: '1h',
  200. },
  201. })
  202. );
  203. });
  204. it('uses absolute dates over defaults', function () {
  205. initializeUrlState({
  206. organization,
  207. queryParams: {
  208. start: '2020-03-22T00:53:38',
  209. end: '2020-04-21T00:53:38',
  210. project: '1',
  211. },
  212. memberProjects: [],
  213. shouldEnforceSingleProject: false,
  214. defaultSelection: {
  215. datetime: {
  216. period: '24h',
  217. utc: null,
  218. start: null,
  219. end: null,
  220. },
  221. },
  222. router,
  223. });
  224. expect(router.replace).toHaveBeenCalledWith(
  225. expect.objectContaining({
  226. query: {
  227. cursor: undefined,
  228. project: ['1'],
  229. environment: [],
  230. start: '2020-03-22T00:53:38',
  231. end: '2020-04-21T00:53:38',
  232. },
  233. })
  234. );
  235. });
  236. it('does not load from local storage when there are query params', function () {
  237. initializeUrlState({
  238. organization,
  239. queryParams: {
  240. project: '1',
  241. },
  242. memberProjects: [],
  243. shouldEnforceSingleProject: false,
  244. router,
  245. });
  246. expect(PageFiltersStore.onInitializeUrlState).toHaveBeenCalledWith(
  247. {
  248. datetime: {
  249. start: null,
  250. end: null,
  251. period: '14d',
  252. utc: null,
  253. },
  254. projects: [1],
  255. environments: [],
  256. },
  257. new Set(),
  258. true
  259. );
  260. expect(router.replace).toHaveBeenCalledWith(
  261. expect.objectContaining({
  262. query: {
  263. environment: [],
  264. project: ['1'],
  265. },
  266. })
  267. );
  268. });
  269. it('does not add non-pinned filters to query for pages with new page filters', function () {
  270. // Mock storage to have a saved value
  271. const pageFilterStorageMock = jest
  272. .spyOn(PageFilterPersistence, 'getPageFilterStorage')
  273. .mockReturnValueOnce({
  274. state: {
  275. project: [1],
  276. environment: [],
  277. start: null,
  278. end: null,
  279. period: '14d',
  280. utc: null,
  281. },
  282. pinnedFilters: new Set(),
  283. });
  284. // Initialize state with a page that shouldn't restore from local storage
  285. initializeUrlState({
  286. organization,
  287. queryParams: {},
  288. router,
  289. memberProjects: [],
  290. shouldEnforceSingleProject: false,
  291. });
  292. // Confirm that query params are not restored from local storage
  293. expect(router.replace).not.toHaveBeenCalled();
  294. pageFilterStorageMock.mockRestore();
  295. });
  296. it('uses pinned filters for pages with new page filters', function () {
  297. // Mock storage to have a saved/pinned value
  298. const pageFilterStorageMock = jest
  299. .spyOn(PageFilterPersistence, 'getPageFilterStorage')
  300. .mockReturnValueOnce({
  301. state: {
  302. project: [1],
  303. environment: ['prod'],
  304. start: null,
  305. end: null,
  306. period: '7d',
  307. utc: null,
  308. },
  309. pinnedFilters: new Set(['environments', 'datetime', 'projects']),
  310. });
  311. // Initialize state with a page that uses pinned filters
  312. initializeUrlState({
  313. organization,
  314. queryParams: {},
  315. router,
  316. memberProjects: [],
  317. shouldEnforceSingleProject: false,
  318. });
  319. // Confirm that only environment is restored from local storage
  320. expect(router.replace).toHaveBeenCalledWith(
  321. expect.objectContaining({
  322. query: {
  323. environment: ['prod'],
  324. project: ['1'],
  325. statsPeriod: '7d',
  326. },
  327. })
  328. );
  329. pageFilterStorageMock.mockRestore();
  330. });
  331. it('retrieves filters from a separate key when storageNamespace is provided', function () {
  332. const starfishKey = `global-selection:starfish:${organization.slug}`;
  333. localStorage.setItem(
  334. starfishKey,
  335. JSON.stringify({
  336. environments: [],
  337. projects: [1],
  338. pinnedFilters: ['projects', 'environments'],
  339. })
  340. );
  341. initializeUrlState({
  342. organization,
  343. queryParams: {},
  344. router,
  345. memberProjects: [],
  346. shouldEnforceSingleProject: false,
  347. storageNamespace: 'starfish',
  348. });
  349. expect(localStorage.getItem).toHaveBeenCalledWith(starfishKey);
  350. expect(PageFiltersStore.onInitializeUrlState).toHaveBeenCalledWith(
  351. expect.objectContaining({
  352. environments: [],
  353. projects: [1],
  354. }),
  355. new Set(['projects', 'environments']),
  356. true
  357. );
  358. expect(router.replace).toHaveBeenCalledWith(
  359. expect.objectContaining({
  360. query: {
  361. environment: [],
  362. project: ['1'],
  363. },
  364. })
  365. );
  366. });
  367. });
  368. describe('updateProjects()', function () {
  369. it('updates', function () {
  370. updateProjects([1, 2]);
  371. expect(PageFiltersStore.updateProjects).toHaveBeenCalledWith([1, 2], null);
  372. });
  373. it('updates history when queries are different', function () {
  374. const router = TestStubs.router({
  375. location: {
  376. pathname: '/test/',
  377. query: {project: '2'},
  378. },
  379. });
  380. // this can be passed w/ `project` as an array (e.g. multiple projects being selected)
  381. // however react-router will treat it as a string if there is only one param
  382. updateProjects([1], router);
  383. expect(router.push).toHaveBeenCalledWith({
  384. pathname: '/test/',
  385. query: {project: ['1']},
  386. });
  387. });
  388. it('does not update history when queries are the same', function () {
  389. const router = TestStubs.router({
  390. location: {
  391. pathname: '/test/',
  392. query: {project: '1'},
  393. },
  394. });
  395. // this can be passed w/ `project` as an array (e.g. multiple projects
  396. // being selected) however react-router will treat it as a string if
  397. // there is only one param
  398. updateProjects([1], router);
  399. expect(router.push).not.toHaveBeenCalled();
  400. });
  401. it('updates history when queries are different with replace', function () {
  402. const router = TestStubs.router({
  403. location: {
  404. pathname: '/test/',
  405. query: {project: '2'},
  406. },
  407. });
  408. updateProjects([1], router, {replace: true});
  409. expect(router.replace).toHaveBeenCalledWith({
  410. pathname: '/test/',
  411. query: {project: ['1']},
  412. });
  413. });
  414. it('does not update history when queries are the same with replace', function () {
  415. const router = TestStubs.router({
  416. location: {
  417. pathname: '/test/',
  418. query: {project: '1'},
  419. },
  420. });
  421. updateProjects([1], router, {replace: true});
  422. expect(router.replace).not.toHaveBeenCalled();
  423. });
  424. it('does not override an absolute date selection', function () {
  425. const router = TestStubs.router({
  426. location: {
  427. pathname: '/test/',
  428. query: {project: '1', start: '2020-03-22T00:53:38', end: '2020-04-21T00:53:38'},
  429. },
  430. });
  431. updateProjects([2], router, {replace: true});
  432. expect(router.replace).toHaveBeenCalledWith({
  433. pathname: '/test/',
  434. query: {project: ['2'], start: '2020-03-22T00:53:38', end: '2020-04-21T00:53:38'},
  435. });
  436. });
  437. });
  438. describe('updateEnvironments()', function () {
  439. it('updates single', function () {
  440. const router = TestStubs.router({
  441. location: {
  442. pathname: '/test/',
  443. query: {environment: 'test'},
  444. },
  445. });
  446. updateEnvironments(['new-env'], router);
  447. expect(router.push).toHaveBeenCalledWith({
  448. pathname: '/test/',
  449. query: {environment: ['new-env']},
  450. });
  451. });
  452. it('updates multiple', function () {
  453. const router = TestStubs.router({
  454. location: {
  455. pathname: '/test/',
  456. query: {environment: 'test'},
  457. },
  458. });
  459. updateEnvironments(['new-env', 'another-env'], router);
  460. expect(router.push).toHaveBeenCalledWith({
  461. pathname: '/test/',
  462. query: {environment: ['new-env', 'another-env']},
  463. });
  464. });
  465. it('removes environment', function () {
  466. const router = TestStubs.router({
  467. location: {
  468. pathname: '/test/',
  469. query: {environment: 'test'},
  470. },
  471. });
  472. updateEnvironments(null, router);
  473. expect(router.push).toHaveBeenCalledWith({
  474. pathname: '/test/',
  475. query: {},
  476. });
  477. });
  478. it('does not override an absolute date selection', function () {
  479. const router = TestStubs.router({
  480. location: {
  481. pathname: '/test/',
  482. query: {
  483. environment: 'test',
  484. start: '2020-03-22T00:53:38',
  485. end: '2020-04-21T00:53:38',
  486. },
  487. },
  488. });
  489. updateEnvironments(['new-env'], router, {replace: true});
  490. expect(router.replace).toHaveBeenCalledWith({
  491. pathname: '/test/',
  492. query: {
  493. environment: ['new-env'],
  494. start: '2020-03-22T00:53:38',
  495. end: '2020-04-21T00:53:38',
  496. },
  497. });
  498. });
  499. });
  500. describe('updateDateTime()', function () {
  501. it('updates statsPeriod when there is no existing stats period', function () {
  502. const router = TestStubs.router({
  503. location: {
  504. pathname: '/test/',
  505. query: {},
  506. },
  507. });
  508. updateDateTime({period: '24h'}, router);
  509. expect(router.push).toHaveBeenCalledWith({
  510. pathname: '/test/',
  511. query: {
  512. statsPeriod: '24h',
  513. },
  514. });
  515. });
  516. it('updates statsPeriod when there is an existing stats period', function () {
  517. const router = TestStubs.router({
  518. location: {
  519. pathname: '/test/',
  520. query: {statsPeriod: '14d'},
  521. },
  522. });
  523. updateDateTime({period: '24h'}, router);
  524. expect(router.push).toHaveBeenCalledWith({
  525. pathname: '/test/',
  526. query: {
  527. statsPeriod: '24h',
  528. },
  529. });
  530. });
  531. it('changes to absolute date', function () {
  532. const router = TestStubs.router({
  533. location: {
  534. pathname: '/test/',
  535. query: {statsPeriod: '24h'},
  536. },
  537. });
  538. updateDateTime({start: '2020-03-22T00:53:38', end: '2020-04-21T00:53:38'}, router);
  539. expect(router.push).toHaveBeenCalledWith({
  540. pathname: '/test/',
  541. query: {
  542. start: '2020-03-22T00:53:38',
  543. end: '2020-04-21T00:53:38',
  544. },
  545. });
  546. });
  547. });
  548. describe('revertToPinnedFilters()', function () {
  549. it('reverts all filters that are desynced from localStorage', function () {
  550. const router = TestStubs.router({
  551. location: {
  552. pathname: '/test/',
  553. query: {},
  554. },
  555. });
  556. // Mock storage to have a saved value
  557. const pageFilterStorageMock = jest
  558. .spyOn(PageFilterPersistence, 'getPageFilterStorage')
  559. .mockReturnValueOnce({
  560. state: {
  561. project: [1],
  562. environment: [],
  563. start: null,
  564. end: null,
  565. period: '14d',
  566. utc: null,
  567. },
  568. pinnedFilters: new Set(['projects', 'environments', 'datetime']),
  569. });
  570. PageFiltersStore.onInitializeUrlState(
  571. {
  572. projects: [2],
  573. environments: ['prod'],
  574. datetime: {
  575. start: null,
  576. end: null,
  577. period: '1d',
  578. utc: null,
  579. },
  580. },
  581. new Set()
  582. );
  583. PageFiltersStore.updateDesyncedFilters(
  584. new Set(['projects', 'environments', 'datetime'])
  585. );
  586. revertToPinnedFilters('org-slug', router);
  587. expect(router.push).toHaveBeenCalledWith({
  588. pathname: '/test/',
  589. query: {
  590. environment: [],
  591. project: ['1'],
  592. statsPeriod: '14d',
  593. },
  594. });
  595. pageFilterStorageMock.mockRestore();
  596. });
  597. });
  598. });