pageFilters.spec.tsx 20 KB

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