globalSelectionHeader.spec.jsx 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327
  1. import {enforceActOnUseLegacyStoreHook, mountWithTheme} from 'sentry-test/enzyme';
  2. import {initializeOrg} from 'sentry-test/initializeOrg';
  3. import {mockRouterPush} from 'sentry-test/mockRouterPush';
  4. import {act} from 'sentry-test/reactTestingLibrary';
  5. import * as globalActions from 'sentry/actionCreators/pageFilters';
  6. import OrganizationActions from 'sentry/actions/organizationActions';
  7. import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container';
  8. import ConfigStore from 'sentry/stores/configStore';
  9. import OrganizationsStore from 'sentry/stores/organizationsStore';
  10. import PageFiltersStore from 'sentry/stores/pageFiltersStore';
  11. import ProjectsStore from 'sentry/stores/projectsStore';
  12. import {getItem} from 'sentry/utils/localStorage';
  13. import {OrganizationContext} from 'sentry/views/organizationContext';
  14. const changeQuery = (routerContext, query) => ({
  15. ...routerContext,
  16. context: {
  17. ...routerContext.context,
  18. router: {
  19. ...routerContext.context.router,
  20. location: {
  21. query,
  22. },
  23. },
  24. },
  25. });
  26. jest.mock('sentry/utils/localStorage', () => ({
  27. getItem: jest.fn(),
  28. setItem: jest.fn(),
  29. }));
  30. describe('GlobalSelectionHeader', function () {
  31. enforceActOnUseLegacyStoreHook();
  32. let wrapper;
  33. const {organization, router, routerContext} = initializeOrg({
  34. organization: {features: ['global-views']},
  35. projects: [
  36. {
  37. id: 2,
  38. slug: 'project-2',
  39. },
  40. {
  41. id: 3,
  42. slug: 'project-3',
  43. environments: ['prod', 'staging'],
  44. },
  45. ],
  46. router: {
  47. location: {query: {}},
  48. params: {orgId: 'org-slug'},
  49. },
  50. });
  51. beforeAll(function () {
  52. jest.spyOn(globalActions, 'updateDateTime');
  53. jest.spyOn(globalActions, 'updateEnvironments');
  54. jest.spyOn(globalActions, 'updateProjects');
  55. });
  56. beforeEach(function () {
  57. MockApiClient.clearMockResponses();
  58. ProjectsStore.loadInitialData(organization.projects);
  59. OrganizationActions.update(organization);
  60. OrganizationsStore.add(organization);
  61. getItem.mockImplementation(() => null);
  62. MockApiClient.addMockResponse({
  63. url: '/organizations/org-slug/projects/',
  64. body: [],
  65. });
  66. });
  67. afterEach(function () {
  68. wrapper.unmount();
  69. [
  70. globalActions.updateDateTime,
  71. globalActions.updateProjects,
  72. globalActions.updateEnvironments,
  73. router.push,
  74. router.replace,
  75. getItem,
  76. ].forEach(mock => mock.mockClear());
  77. PageFiltersStore.reset();
  78. });
  79. it('does not update router if there is custom routing', function () {
  80. wrapper = mountWithTheme(
  81. <PageFiltersContainer organization={organization} hasCustomRouting />,
  82. routerContext
  83. );
  84. expect(router.push).not.toHaveBeenCalled();
  85. });
  86. it('does not update router if org in URL params is different than org in context/props', function () {
  87. wrapper = mountWithTheme(
  88. <PageFiltersContainer organization={organization} hasCustomRouting />,
  89. {
  90. ...routerContext,
  91. context: {
  92. ...routerContext.context,
  93. router: {...routerContext.context.router, params: {orgId: 'diff-org'}},
  94. },
  95. }
  96. );
  97. expect(router.push).not.toHaveBeenCalled();
  98. });
  99. it('does not replace URL with values from store when mounted with no query params', function () {
  100. wrapper = mountWithTheme(
  101. <PageFiltersContainer organization={organization} />,
  102. routerContext
  103. );
  104. expect(router.replace).not.toHaveBeenCalled();
  105. });
  106. it('only updates GlobalSelection store when mounted with query params', async function () {
  107. wrapper = mountWithTheme(
  108. <PageFiltersContainer
  109. organization={organization}
  110. params={{orgId: organization.slug}}
  111. />,
  112. changeQuery(routerContext, {
  113. statsPeriod: '7d',
  114. })
  115. );
  116. expect(router.push).not.toHaveBeenCalled();
  117. await tick();
  118. expect(PageFiltersStore.getState().selection).toEqual({
  119. datetime: {
  120. period: '7d',
  121. utc: null,
  122. start: null,
  123. end: null,
  124. },
  125. environments: [],
  126. projects: [],
  127. });
  128. });
  129. it('can change environments with a project selected', async function () {
  130. wrapper = mountWithTheme(
  131. <PageFiltersContainer
  132. organization={organization}
  133. projects={organization.projects}
  134. />,
  135. routerContext
  136. );
  137. await tick();
  138. wrapper.update();
  139. mockRouterPush(wrapper, router);
  140. // Open dropdown and select one project
  141. wrapper.find('MultipleProjectSelector HeaderItem').simulate('click');
  142. wrapper.find('MultipleProjectSelector CheckboxFancy').at(1).simulate('click');
  143. wrapper.find('MultipleProjectSelector HeaderItem').simulate('click');
  144. await tick();
  145. wrapper.update();
  146. expect(wrapper.find('MultipleProjectSelector Content').text()).toBe('project-3');
  147. // Select environment
  148. wrapper.find('MultipleEnvironmentSelector HeaderItem').simulate('click');
  149. wrapper.find('MultipleEnvironmentSelector CheckboxFancy').at(0).simulate('click');
  150. wrapper.find('MultipleEnvironmentSelector HeaderItem').simulate('click');
  151. await tick();
  152. expect(wrapper.find('MultipleEnvironmentSelector Content').text()).toBe('prod');
  153. expect(PageFiltersStore.getState().selection).toEqual({
  154. datetime: {
  155. period: '14d',
  156. utc: null,
  157. start: null,
  158. end: null,
  159. },
  160. environments: ['prod'],
  161. projects: [3],
  162. });
  163. const query = wrapper.prop('location').query;
  164. expect(query).toEqual({
  165. environment: 'prod',
  166. project: '3',
  167. });
  168. });
  169. it('updates environments when switching projects', async function () {
  170. wrapper = mountWithTheme(
  171. <PageFiltersContainer
  172. organization={organization}
  173. projects={organization.projects}
  174. />,
  175. routerContext
  176. );
  177. await tick();
  178. wrapper.update();
  179. mockRouterPush(wrapper, router);
  180. // Open dropdown and select both projects
  181. wrapper.find('MultipleProjectSelector HeaderItem').simulate('click');
  182. wrapper.find('MultipleProjectSelector CheckboxFancy').at(0).simulate('click');
  183. wrapper.find('MultipleProjectSelector CheckboxFancy').at(1).simulate('click');
  184. wrapper.find('MultipleProjectSelector HeaderItem').simulate('click');
  185. await tick();
  186. wrapper.update();
  187. expect(wrapper.find('MultipleProjectSelector Content').text()).toBe(
  188. 'project-2, project-3'
  189. );
  190. // Select environment
  191. wrapper.find('MultipleEnvironmentSelector HeaderItem').simulate('click');
  192. wrapper.find('MultipleEnvironmentSelector CheckboxFancy').at(1).simulate('click');
  193. wrapper.find('MultipleEnvironmentSelector HeaderItem').simulate('click');
  194. await tick();
  195. expect(wrapper.find('MultipleEnvironmentSelector Content').text()).toBe('staging');
  196. expect(PageFiltersStore.getState().selection).toEqual({
  197. datetime: {
  198. period: '14d',
  199. utc: null,
  200. start: null,
  201. end: null,
  202. },
  203. environments: ['staging'],
  204. projects: [2, 3],
  205. });
  206. const query = wrapper.prop('location').query;
  207. expect(query).toEqual({
  208. environment: 'staging',
  209. project: ['2', '3'],
  210. });
  211. // Now change projects, first project has no environments
  212. wrapper.find('MultipleProjectSelector HeaderItem').simulate('click');
  213. wrapper.find('MultipleProjectSelector CheckboxFancy').at(1).simulate('click');
  214. wrapper.find('MultipleProjectSelector HeaderItem').simulate('click');
  215. await tick();
  216. wrapper.update();
  217. // Store should not have any environments selected
  218. expect(PageFiltersStore.getState().selection).toEqual({
  219. datetime: {
  220. period: '14d',
  221. utc: null,
  222. start: null,
  223. end: null,
  224. },
  225. environments: [],
  226. projects: [2],
  227. });
  228. expect(wrapper.find('MultipleEnvironmentSelector Content').text()).toBe(
  229. 'All Environments'
  230. );
  231. });
  232. it('shows environments for non-member projects', async function () {
  233. const initialData = initializeOrg({
  234. organization: {features: ['global-views']},
  235. projects: [
  236. {id: 1, slug: 'staging-project', environments: ['staging'], isMember: false},
  237. {id: 2, slug: 'prod-project', environments: ['prod']},
  238. ],
  239. router: {
  240. location: {query: {project: ['1']}},
  241. params: {orgId: 'org-slug'},
  242. },
  243. });
  244. ProjectsStore.loadInitialData(initialData.projects);
  245. wrapper = mountWithTheme(
  246. <PageFiltersContainer
  247. router={initialData.router}
  248. organization={initialData.organization}
  249. projects={initialData.projects}
  250. />,
  251. changeQuery(initialData.routerContext, {project: 1})
  252. );
  253. await tick();
  254. wrapper.update();
  255. // Open environment picker
  256. wrapper.find('MultipleEnvironmentSelector HeaderItem').simulate('click');
  257. const checkboxes = wrapper.find('MultipleEnvironmentSelector AutoCompleteItem');
  258. expect(checkboxes).toHaveLength(1);
  259. expect(checkboxes.text()).toBe('staging');
  260. });
  261. it('updates GlobalSelection store with default period', async function () {
  262. wrapper = mountWithTheme(
  263. <PageFiltersContainer organization={organization} />,
  264. changeQuery(routerContext, {
  265. environment: 'prod',
  266. })
  267. );
  268. await tick();
  269. expect(PageFiltersStore.getState()).toEqual({
  270. isReady: true,
  271. desyncedFilters: new Set(),
  272. pinnedFilters: new Set(),
  273. selection: {
  274. datetime: {
  275. period: '14d',
  276. utc: null,
  277. start: null,
  278. end: null,
  279. },
  280. environments: ['prod'],
  281. projects: [],
  282. },
  283. });
  284. // Not called because of the default date
  285. expect(router.replace).not.toHaveBeenCalled();
  286. });
  287. it('updates GlobalSelection store with empty dates in URL', async function () {
  288. wrapper = mountWithTheme(
  289. <PageFiltersContainer organization={organization} />,
  290. changeQuery(routerContext, {
  291. statsPeriod: null,
  292. })
  293. );
  294. await tick();
  295. expect(PageFiltersStore.getState()).toEqual({
  296. isReady: true,
  297. desyncedFilters: new Set(),
  298. pinnedFilters: new Set(),
  299. selection: {
  300. datetime: {
  301. period: '14d',
  302. utc: null,
  303. start: null,
  304. end: null,
  305. },
  306. environments: [],
  307. projects: [],
  308. },
  309. });
  310. });
  311. it('resets start&end if showAbsolute prop is false', async function () {
  312. wrapper = mountWithTheme(
  313. <PageFiltersContainer organization={organization} showAbsolute={false} />,
  314. changeQuery(routerContext, {
  315. start: '2020-05-05T07:26:53.000',
  316. end: '2020-05-05T09:19:12.000',
  317. })
  318. );
  319. await tick();
  320. expect(PageFiltersStore.getState()).toEqual({
  321. isReady: true,
  322. desyncedFilters: new Set(),
  323. pinnedFilters: new Set(),
  324. selection: {
  325. datetime: {
  326. period: '14d',
  327. utc: null,
  328. start: null,
  329. end: null,
  330. },
  331. environments: [],
  332. projects: [],
  333. },
  334. });
  335. });
  336. /**
  337. * I don't think this test is really applicable anymore
  338. */
  339. it('does not update store if url params have not changed', async function () {
  340. wrapper = mountWithTheme(
  341. <PageFiltersContainer organization={organization} />,
  342. changeQuery(routerContext, {
  343. statsPeriod: '7d',
  344. })
  345. );
  346. [
  347. globalActions.updateDateTime,
  348. globalActions.updateProjects,
  349. globalActions.updateEnvironments,
  350. ].forEach(mock => mock.mockClear());
  351. wrapper.setContext(
  352. changeQuery(routerContext, {
  353. statsPeriod: '7d',
  354. }).context
  355. );
  356. await tick();
  357. wrapper.update();
  358. expect(globalActions.updateDateTime).not.toHaveBeenCalled();
  359. expect(globalActions.updateProjects).not.toHaveBeenCalled();
  360. expect(globalActions.updateEnvironments).not.toHaveBeenCalled();
  361. expect(PageFiltersStore.getState()).toEqual({
  362. isReady: true,
  363. desyncedFilters: new Set(),
  364. pinnedFilters: new Set(),
  365. selection: {
  366. datetime: {
  367. period: '7d',
  368. utc: null,
  369. start: null,
  370. end: null,
  371. },
  372. environments: [],
  373. projects: [],
  374. },
  375. });
  376. });
  377. it('loads from local storage when no URL parameters', async function () {
  378. getItem.mockImplementation(() =>
  379. JSON.stringify({projects: [3], environments: ['staging']})
  380. );
  381. const initializationObj = initializeOrg({
  382. organization: {
  383. features: ['global-views'],
  384. },
  385. router: {
  386. // we need this to be set to make sure org in context is same as
  387. // current org in URL
  388. params: {orgId: 'org-slug'},
  389. },
  390. });
  391. wrapper = mountWithTheme(
  392. <PageFiltersContainer organization={initializationObj.organization} />,
  393. initializationObj.routerContext
  394. );
  395. await tick(); // reflux tick
  396. expect(PageFiltersStore.getState().selection.projects).toEqual([3]);
  397. // Since these are coming from URL, there should be no changes and
  398. // router does not need to be called
  399. expect(initializationObj.router.replace).toHaveBeenLastCalledWith(
  400. expect.objectContaining({
  401. query: {
  402. environment: ['staging'],
  403. project: ['3'],
  404. },
  405. })
  406. );
  407. });
  408. it('does not load from local storage when there are URL params', async function () {
  409. getItem.mockImplementation(() =>
  410. JSON.stringify({projects: [3], environments: ['staging']})
  411. );
  412. const initializationObj = initializeOrg({
  413. organization: {
  414. features: ['global-views'],
  415. },
  416. router: {
  417. // we need this to be set to make sure org in context is same as
  418. // current org in URL
  419. params: {orgId: 'org-slug'},
  420. location: {query: {project: ['1', '2']}},
  421. },
  422. });
  423. wrapper = mountWithTheme(
  424. <PageFiltersContainer organization={initializationObj.organization} />,
  425. initializationObj.routerContext
  426. );
  427. await tick(); // reflux tick
  428. expect(PageFiltersStore.getState().selection.projects).toEqual([1, 2]);
  429. // Since these are coming from URL, there should be no changes and
  430. // router does not need to be called
  431. expect(initializationObj.router.replace).not.toHaveBeenCalled();
  432. });
  433. it('updates store when there are query params in URL', async function () {
  434. const initializationObj = initializeOrg({
  435. organization: {
  436. features: ['global-views'],
  437. },
  438. router: {
  439. // we need this to be set to make sure org in context is same as
  440. // current org in URL
  441. params: {orgId: 'org-slug'},
  442. location: {query: {project: ['1', '2']}},
  443. },
  444. });
  445. wrapper = mountWithTheme(
  446. <PageFiltersContainer organization={initializationObj.organization} />,
  447. initializationObj.routerContext
  448. );
  449. await tick(); // reflux tick
  450. expect(PageFiltersStore.getState().selection.projects).toEqual([1, 2]);
  451. // Since these are coming from URL, there should be no changes and
  452. // router does not need to be called
  453. expect(initializationObj.router.replace).not.toHaveBeenCalled();
  454. });
  455. it('updates store with default values when there are no query params in URL', async function () {
  456. const initializationObj = initializeOrg({
  457. organization: {
  458. features: ['global-views'],
  459. },
  460. router: {
  461. // we need this to be set to make sure org in context is same as
  462. // current org in URL
  463. params: {orgId: 'org-slug'},
  464. location: {query: {}},
  465. },
  466. });
  467. wrapper = mountWithTheme(
  468. <PageFiltersContainer organization={initializationObj.organization} />,
  469. initializationObj.routerContext
  470. );
  471. // Router does not update because params have not changed
  472. expect(initializationObj.router.replace).not.toHaveBeenCalled();
  473. });
  474. it('updates store with desynced values when url params do not match local storage', async function () {
  475. getItem.mockImplementation(() =>
  476. JSON.stringify({
  477. projects: [1],
  478. pinnedFilters: ['projects'],
  479. })
  480. );
  481. const initializationObj = initializeOrg({
  482. organization: {
  483. features: ['global-views', 'selection-filters-v2'],
  484. },
  485. router: {
  486. // we need this to be set to make sure org in context is same as
  487. // current org in URL
  488. params: {orgId: 'org-slug'},
  489. location: {
  490. query: {project: ['2']},
  491. // TODO: This is only temporary while selection-filters-v2 is limited
  492. // to certan pages
  493. pathname: '/organizations/org-slug/issues/',
  494. },
  495. },
  496. });
  497. OrganizationActions.update(initializationObj.organization);
  498. wrapper = mountWithTheme(
  499. <OrganizationContext.Provider value={initializationObj.organization}>
  500. <PageFiltersContainer
  501. organization={initializationObj.organization}
  502. hideGlobalHeader
  503. />
  504. </OrganizationContext.Provider>,
  505. initializationObj.routerContext
  506. );
  507. // reflux tick
  508. await tick();
  509. expect(PageFiltersStore.getState().selection.projects).toEqual([2]);
  510. // Wait for desynced filters to update
  511. await tick();
  512. expect(PageFiltersStore.getState().desyncedFilters).toEqual(new Set(['projects']));
  513. wrapper.update();
  514. expect(wrapper.find('DesyncedFilterAlert')).toHaveLength(1);
  515. });
  516. /**
  517. * GSH: (no global-views)
  518. * - mounts with no state from router
  519. * - params org id === org.slug
  520. *
  521. * - updateProjects should not be called (enforceSingleProject should not be
  522. * called)
  523. *
  524. * - componentDidUpdate with loadingProjects === true, and pass in list of
  525. * projects (via projects store)
  526. *
  527. * - enforceProject should be called and updateProjects() called with the new
  528. * project
  529. * - variation:
  530. * - params.orgId !== org.slug (e.g. just switched orgs)
  531. *
  532. * When switching orgs when not in Issues view, the issues view gets rendered
  533. * with params.orgId !== org.slug
  534. *
  535. * Global selection header gets unmounted and mounted, and in this case
  536. * nothing should be done until it gets updated and params.orgId === org.slug
  537. *
  538. * Separate issue:
  539. *
  540. * IssuesList ("child view") renders before a single project is enforced,
  541. * will require refactoring views so that they depend on GSH enforcing a
  542. * single project first IF they don't have required feature (and no project id
  543. * in URL).
  544. */
  545. describe('Single project selection mode', function () {
  546. it('does not do anything while organization is switching in single project', async function () {
  547. const initialData = initializeOrg({
  548. organization: {slug: 'old-org-slug'},
  549. router: {
  550. // we need this to be set to make sure org in context is same as
  551. // current org in URL
  552. params: {orgId: 'org-slug'},
  553. location: {query: {project: ['1']}},
  554. },
  555. });
  556. MockApiClient.addMockResponse({
  557. url: '/organizations/old-org-slug/projects/',
  558. body: [],
  559. });
  560. ProjectsStore.reset();
  561. // This can happen when you switch organization so params.orgId !== the
  562. // current org in context In this case params.orgId = 'org-slug'
  563. wrapper = mountWithTheme(
  564. <PageFiltersContainer organization={initialData.organization} />,
  565. initialData.routerContext
  566. );
  567. expect(globalActions.updateProjects).not.toHaveBeenCalled();
  568. const updatedOrganization = {
  569. ...organization,
  570. slug: 'org-slug',
  571. features: [],
  572. projects: [TestStubs.Project({id: '123', slug: 'org-slug-project1'})],
  573. };
  574. MockApiClient.addMockResponse({
  575. url: '/organizations/org-slug/',
  576. body: updatedOrganization,
  577. });
  578. // Eventually OrganizationContext will fetch org details for `org-slug`
  579. // and update `organization` prop emulate fetchOrganizationDetails
  580. OrganizationActions.update(updatedOrganization);
  581. wrapper.setContext({
  582. organization: updatedOrganization,
  583. location: {query: {}},
  584. router: {
  585. ...initialData.router,
  586. location: {query: {}},
  587. },
  588. });
  589. wrapper.setProps({organization: updatedOrganization});
  590. act(() => ProjectsStore.loadInitialData(updatedOrganization.projects));
  591. expect(initialData.router.replace).toHaveBeenLastCalledWith(
  592. expect.objectContaining({
  593. query: {environment: [], project: ['123']},
  594. })
  595. );
  596. });
  597. it('selects first project if more than one is requested', function () {
  598. const initializationObj = initializeOrg({
  599. router: {
  600. // we need this to be set to make sure org in context is same as
  601. // current org in URL
  602. params: {orgId: 'org-slug'},
  603. location: {query: {project: ['1', '2']}},
  604. },
  605. });
  606. wrapper = mountWithTheme(
  607. <PageFiltersContainer organization={initializationObj.organization} />,
  608. initializationObj.routerContext
  609. );
  610. expect(initializationObj.router.replace).toHaveBeenCalledWith(
  611. expect.objectContaining({
  612. query: {environment: [], project: ['1']},
  613. })
  614. );
  615. });
  616. it('selects first project if none (i.e. all) is requested', async function () {
  617. const project = TestStubs.Project({id: '3'});
  618. const org = TestStubs.Organization({projects: [project]});
  619. ProjectsStore.loadInitialData(org.projects);
  620. const initializationObj = initializeOrg({
  621. organization: org,
  622. router: {
  623. params: {orgId: 'org-slug'},
  624. location: {query: {}},
  625. },
  626. });
  627. wrapper = mountWithTheme(
  628. <PageFiltersContainer organization={initializationObj.organization} />,
  629. initializationObj.routerContext
  630. );
  631. expect(initializationObj.router.replace).toHaveBeenCalledWith(
  632. expect.objectContaining({
  633. query: {environment: [], project: ['3']},
  634. })
  635. );
  636. });
  637. });
  638. describe('forceProject selection mode', function () {
  639. beforeEach(async function () {
  640. MockApiClient.addMockResponse({
  641. url: '/organizations/org-slug/projects/',
  642. body: [],
  643. });
  644. const initialData = initializeOrg({
  645. organization: {features: ['global-views']},
  646. projects: [
  647. {id: 1, slug: 'staging-project', environments: ['staging']},
  648. {id: 2, slug: 'prod-project', environments: ['prod']},
  649. ],
  650. router: {
  651. location: {query: {}},
  652. },
  653. });
  654. ProjectsStore.loadInitialData(initialData.projects);
  655. wrapper = mountWithTheme(
  656. <PageFiltersContainer
  657. organization={initialData.organization}
  658. shouldForceProject
  659. forceProject={initialData.projects[0]}
  660. showIssueStreamLink
  661. />,
  662. initialData.routerContext
  663. );
  664. await tick();
  665. wrapper.update();
  666. });
  667. it('renders a back button to the forced project', function () {
  668. const back = wrapper.find('BackButtonWrapper');
  669. expect(back).toHaveLength(1);
  670. });
  671. it('renders only environments from the forced project', function () {
  672. wrapper.find('MultipleEnvironmentSelector HeaderItem').simulate('click');
  673. wrapper.update();
  674. const items = wrapper.find('MultipleEnvironmentSelector EnvironmentSelectorItem');
  675. expect(items.length).toEqual(1);
  676. expect(items.at(0).text()).toBe('staging');
  677. });
  678. });
  679. describe('forceProject + forceEnvironment selection mode', function () {
  680. beforeEach(async function () {
  681. MockApiClient.addMockResponse({
  682. url: '/organizations/org-slug/projects/',
  683. body: [],
  684. });
  685. const initialData = initializeOrg({
  686. organization: {features: ['global-views']},
  687. projects: [
  688. {id: 1, slug: 'staging-project', environments: ['staging']},
  689. {id: 2, slug: 'prod-project', environments: ['prod']},
  690. ],
  691. });
  692. ProjectsStore.loadInitialData(initialData.projects);
  693. wrapper = mountWithTheme(
  694. <PageFiltersContainer
  695. organization={initialData.organization}
  696. shouldForceProject
  697. forceProject={initialData.projects[0]}
  698. forceEnvironment="test-env"
  699. />,
  700. initialData.routerContext
  701. );
  702. await tick();
  703. wrapper.update();
  704. });
  705. it('renders the forced environment', function () {
  706. expect(wrapper.find('MultipleEnvironmentSelector HeaderItem').text()).toBe(
  707. 'test-env'
  708. );
  709. });
  710. });
  711. describe('without global-views (multi-project feature)', function () {
  712. describe('without existing URL params', function () {
  713. const initialData = initializeOrg({
  714. projects: [
  715. {id: 0, slug: 'random project', isMember: true},
  716. {id: 1, slug: 'staging-project', environments: ['staging']},
  717. {id: 2, slug: 'prod-project', environments: ['prod']},
  718. ],
  719. router: {
  720. location: {query: {}},
  721. params: {orgId: 'org-slug'},
  722. },
  723. });
  724. const createWrapper = props => {
  725. wrapper = mountWithTheme(
  726. <PageFiltersContainer
  727. params={{orgId: initialData.organization.slug}}
  728. organization={initialData.organization}
  729. {...props}
  730. />,
  731. initialData.routerContext
  732. );
  733. return wrapper;
  734. };
  735. beforeEach(function () {
  736. ProjectsStore.loadInitialData(initialData.projects);
  737. initialData.router.push.mockClear();
  738. initialData.router.replace.mockClear();
  739. });
  740. it('uses first project in org projects when mounting', async function () {
  741. createWrapper();
  742. // Projects are returned in sorted slug order, so `prod-project` would
  743. // be the first project
  744. expect(initialData.router.replace).toHaveBeenLastCalledWith({
  745. pathname: undefined,
  746. query: {cursor: undefined, environment: [], project: ['2']},
  747. });
  748. });
  749. it('appends projectId to URL when `forceProject` becomes available (async)', async function () {
  750. ProjectsStore.reset();
  751. // forceProject generally starts undefined
  752. createWrapper({shouldForceProject: true});
  753. wrapper.setProps({
  754. forceProject: initialData.projects[1],
  755. });
  756. // load the projects
  757. act(() => ProjectsStore.loadInitialData(initialData.projects));
  758. wrapper.update();
  759. expect(initialData.router.replace).toHaveBeenLastCalledWith({
  760. pathname: undefined,
  761. query: {environment: [], project: ['1']},
  762. });
  763. expect(initialData.router.replace).toHaveBeenCalledTimes(1);
  764. });
  765. it('does not append projectId to URL when `forceProject` becomes available but project id already exists in URL', async function () {
  766. // forceProject generally starts undefined
  767. createWrapper({shouldForceProject: true});
  768. wrapper.setContext({
  769. router: {
  770. ...initialData.router,
  771. location: {
  772. ...initialData.router.location,
  773. query: {
  774. project: '321',
  775. },
  776. },
  777. },
  778. });
  779. wrapper.setProps({
  780. forceProject: initialData.projects[1],
  781. });
  782. wrapper.update();
  783. expect(initialData.router.replace).not.toHaveBeenCalled();
  784. });
  785. it('appends projectId to URL when mounted with `forceProject`', async function () {
  786. // forceProject generally starts undefined
  787. createWrapper({
  788. shouldForceProject: true,
  789. forceProject: initialData.projects[1],
  790. });
  791. wrapper.update();
  792. expect(initialData.router.replace).toHaveBeenLastCalledWith({
  793. pathname: undefined,
  794. query: {environment: [], project: ['1']},
  795. });
  796. });
  797. });
  798. describe('with existing URL params', function () {
  799. const initialData = initializeOrg({
  800. projects: [
  801. {id: 0, slug: 'random project', isMember: true},
  802. {id: 1, slug: 'staging-project', environments: ['staging']},
  803. {id: 2, slug: 'prod-project', environments: ['prod']},
  804. ],
  805. router: {
  806. location: {query: {statsPeriod: '90d'}},
  807. params: {orgId: 'org-slug'},
  808. },
  809. });
  810. ProjectsStore.loadInitialData(initialData.projects);
  811. const createWrapper = props => {
  812. wrapper = mountWithTheme(
  813. <PageFiltersContainer
  814. params={{orgId: initialData.organization.slug}}
  815. organization={initialData.organization}
  816. {...props}
  817. />,
  818. initialData.routerContext
  819. );
  820. return wrapper;
  821. };
  822. beforeEach(function () {
  823. initialData.router.push.mockClear();
  824. initialData.router.replace.mockClear();
  825. });
  826. it('appends projectId to URL when mounted with `forceProject`', async function () {
  827. // forceProject generally starts undefined
  828. createWrapper({
  829. shouldForceProject: true,
  830. forceProject: initialData.projects[1],
  831. });
  832. wrapper.update();
  833. expect(initialData.router.replace).toHaveBeenLastCalledWith({
  834. pathname: undefined,
  835. query: {environment: [], project: ['1'], statsPeriod: '90d'},
  836. });
  837. });
  838. });
  839. });
  840. describe('with global-views (multi-project feature)', function () {
  841. describe('without existing URL params', function () {
  842. const initialData = initializeOrg({
  843. organization: {features: ['global-views']},
  844. projects: [
  845. {id: 0, slug: 'random project', isMember: true},
  846. {id: 1, slug: 'staging-project', environments: ['staging']},
  847. {id: 2, slug: 'prod-project', environments: ['prod']},
  848. ],
  849. router: {
  850. location: {query: {}},
  851. params: {orgId: 'org-slug'},
  852. },
  853. });
  854. const createWrapper = (props, ctx) => {
  855. wrapper = mountWithTheme(
  856. <PageFiltersContainer
  857. params={{orgId: initialData.organization.slug}}
  858. organization={initialData.organization}
  859. {...props}
  860. />,
  861. {
  862. ...initialData.routerContext,
  863. ...ctx,
  864. }
  865. );
  866. return wrapper;
  867. };
  868. beforeEach(function () {
  869. ProjectsStore.loadInitialData(initialData.projects);
  870. initialData.router.push.mockClear();
  871. initialData.router.replace.mockClear();
  872. });
  873. it('does not use first project in org projects when mounting (and without localStorage data)', async function () {
  874. createWrapper();
  875. await tick();
  876. wrapper.update();
  877. expect(initialData.router.replace).not.toHaveBeenCalled();
  878. });
  879. it('does not append projectId to URL when `loadingProjects` changes and finishes loading', async function () {
  880. ProjectsStore.reset();
  881. createWrapper();
  882. // load the projects
  883. act(() => ProjectsStore.loadInitialData(initialData.projects));
  884. wrapper.setProps({
  885. forceProject: initialData.projects[1],
  886. });
  887. expect(initialData.router.replace).not.toHaveBeenCalled();
  888. });
  889. it('appends projectId to URL when `forceProject` becomes available (async)', async function () {
  890. ProjectsStore.reset();
  891. // forceProject generally starts undefined
  892. createWrapper({shouldForceProject: true});
  893. wrapper.setProps({
  894. forceProject: initialData.projects[1],
  895. });
  896. // load the projects
  897. act(() => ProjectsStore.loadInitialData(initialData.projects));
  898. expect(initialData.router.replace).toHaveBeenLastCalledWith({
  899. pathname: undefined,
  900. query: {environment: [], project: ['1']},
  901. });
  902. expect(initialData.router.replace).toHaveBeenCalledTimes(1);
  903. });
  904. it('does not append projectId to URL when `forceProject` becomes available but project id already exists in URL', async function () {
  905. // forceProject generally starts undefined
  906. createWrapper(
  907. {shouldForceProject: true},
  908. changeQuery(initialData.routerContext, {project: 321})
  909. );
  910. await tick();
  911. wrapper.setProps({
  912. forceProject: initialData.projects[1],
  913. });
  914. wrapper.update();
  915. expect(initialData.router.replace).not.toHaveBeenCalled();
  916. });
  917. });
  918. });
  919. describe('projects list', function () {
  920. let memberProject, nonMemberProject, initialData;
  921. beforeEach(async function () {
  922. memberProject = TestStubs.Project({id: '3', isMember: true});
  923. nonMemberProject = TestStubs.Project({id: '4', isMember: false});
  924. initialData = initializeOrg({
  925. projects: [memberProject, nonMemberProject],
  926. router: {
  927. location: {query: {}},
  928. params: {
  929. orgId: 'org-slug',
  930. },
  931. },
  932. });
  933. ProjectsStore.loadInitialData(initialData.projects);
  934. wrapper = mountWithTheme(
  935. <PageFiltersContainer organization={initialData.organization} />,
  936. initialData.routerContext
  937. );
  938. await tick();
  939. wrapper.update();
  940. });
  941. it('gets member projects', function () {
  942. expect(wrapper.find('MultipleProjectSelector').prop('projects')).toEqual([
  943. memberProject,
  944. ]);
  945. });
  946. it('gets all projects if superuser', async function () {
  947. ConfigStore.config = {
  948. user: {
  949. isSuperuser: true,
  950. },
  951. };
  952. wrapper = mountWithTheme(
  953. <PageFiltersContainer organization={initialData.organization} />,
  954. initialData.routerContext
  955. );
  956. await tick();
  957. wrapper.update();
  958. expect(wrapper.find('MultipleProjectSelector').prop('projects')).toEqual([
  959. memberProject,
  960. ]);
  961. expect(wrapper.find('MultipleProjectSelector').prop('nonMemberProjects')).toEqual([
  962. nonMemberProject,
  963. ]);
  964. });
  965. it('shows "My Projects" button', async function () {
  966. initialData.organization.features.push('global-views');
  967. wrapper = mountWithTheme(
  968. <PageFiltersContainer
  969. organization={initialData.organization}
  970. projects={initialData.projects}
  971. />,
  972. initialData.routerContext
  973. );
  974. await tick();
  975. wrapper.update();
  976. // open the project menu.
  977. wrapper.find('MultipleProjectSelector HeaderItem').simulate('click');
  978. const projectSelector = wrapper.find('MultipleProjectSelector');
  979. // Two projects
  980. expect(projectSelector.find('AutoCompleteItem')).toHaveLength(2);
  981. // My projects in the footer
  982. expect(
  983. projectSelector.find('SelectorFooterControls Button').first().text()
  984. ).toEqual('Select My Projects');
  985. });
  986. it('shows "All Projects" button based on features', async function () {
  987. initialData.organization.features.push('global-views');
  988. initialData.organization.features.push('open-membership');
  989. wrapper = mountWithTheme(
  990. <PageFiltersContainer
  991. organization={initialData.organization}
  992. projects={initialData.projects}
  993. />,
  994. initialData.routerContext
  995. );
  996. await tick();
  997. wrapper.update();
  998. // open the project menu.
  999. wrapper.find('MultipleProjectSelector HeaderItem').simulate('click');
  1000. const projectSelector = wrapper.find('MultipleProjectSelector');
  1001. // Two projects
  1002. expect(projectSelector.find('AutoCompleteItem')).toHaveLength(2);
  1003. // All projects in the footer
  1004. expect(
  1005. projectSelector.find('SelectorFooterControls Button').first().text()
  1006. ).toEqual('Select All Projects');
  1007. });
  1008. it('shows "All Projects" button based on role', async function () {
  1009. initialData.organization.features.push('global-views');
  1010. initialData.organization.role = 'owner';
  1011. wrapper = mountWithTheme(
  1012. <PageFiltersContainer
  1013. organization={initialData.organization}
  1014. projects={initialData.projects}
  1015. />,
  1016. initialData.routerContext
  1017. );
  1018. await tick();
  1019. wrapper.update();
  1020. // open the project menu.
  1021. wrapper.find('MultipleProjectSelector HeaderItem').simulate('click');
  1022. const projectSelector = wrapper.find('MultipleProjectSelector');
  1023. // Two projects
  1024. expect(projectSelector.find('AutoCompleteItem')).toHaveLength(2);
  1025. // All projects in the footer
  1026. expect(
  1027. projectSelector.find('SelectorFooterControls Button').first().text()
  1028. ).toEqual('Select All Projects');
  1029. });
  1030. it('shows "My Projects" when "all projects" is selected', async function () {
  1031. initialData.organization.features.push('global-views');
  1032. initialData.organization.role = 'owner';
  1033. wrapper = mountWithTheme(
  1034. <PageFiltersContainer
  1035. organization={initialData.organization}
  1036. projects={initialData.projects}
  1037. />,
  1038. changeQuery(initialData.routerContext, {project: -1})
  1039. );
  1040. await tick();
  1041. wrapper.update();
  1042. // open the project menu.
  1043. wrapper.find('MultipleProjectSelector HeaderItem').simulate('click');
  1044. const projectSelector = wrapper.find('MultipleProjectSelector');
  1045. // My projects in the footer
  1046. expect(
  1047. projectSelector.find('SelectorFooterControls Button').first().text()
  1048. ).toEqual('Select My Projects');
  1049. });
  1050. });
  1051. describe('project icons', function () {
  1052. const initialData = initializeOrg({
  1053. organization: {features: ['global-views']},
  1054. projects: [
  1055. {id: 0, slug: 'go', platform: 'go'},
  1056. {id: 1, slug: 'javascript', platform: 'javascript'},
  1057. {id: 2, slug: 'other', platform: 'other'},
  1058. {id: 3, slug: 'php', platform: 'php'},
  1059. {id: 4, slug: 'python', platform: 'python'},
  1060. {id: 5, slug: 'rust', platform: 'rust'},
  1061. {id: 6, slug: 'swift', platform: 'swift'},
  1062. ],
  1063. });
  1064. beforeEach(function () {
  1065. ProjectsStore.loadInitialData(initialData.projects);
  1066. });
  1067. it('shows IconProject when no projects are selected', async function () {
  1068. wrapper = mountWithTheme(
  1069. <PageFiltersContainer
  1070. organization={initialData.organization}
  1071. projects={initialData.projects}
  1072. />,
  1073. changeQuery(initialData.routerContext, {project: -1})
  1074. );
  1075. await tick();
  1076. wrapper.update();
  1077. const projectSelector = wrapper.find('MultipleProjectSelector');
  1078. expect(projectSelector.find('IconContainer svg').exists()).toBeTruthy();
  1079. expect(projectSelector.find('PlatformIcon').exists()).toBeFalsy();
  1080. expect(projectSelector.find('Content').text()).toEqual('All Projects');
  1081. });
  1082. it('shows PlatformIcon when one project is selected', async function () {
  1083. wrapper = mountWithTheme(
  1084. <PageFiltersContainer
  1085. organization={initialData.organization}
  1086. projects={initialData.projects}
  1087. />,
  1088. changeQuery(initialData.routerContext, {project: 1})
  1089. );
  1090. await tick();
  1091. wrapper.update();
  1092. const projectSelector = wrapper.find('MultipleProjectSelector');
  1093. expect(projectSelector.find('StyledPlatformIcon').props().platform).toEqual(
  1094. 'javascript'
  1095. );
  1096. expect(projectSelector.find('Content').text()).toEqual('javascript');
  1097. });
  1098. it('shows multiple PlatformIcons when multiple projects are selected, no more than 5', async function () {
  1099. wrapper = mountWithTheme(
  1100. <PageFiltersContainer
  1101. organization={initialData.organization}
  1102. projects={initialData.projects}
  1103. />,
  1104. initialData.routerContext
  1105. );
  1106. await tick();
  1107. wrapper.update();
  1108. // select 6 projects
  1109. const headerItem = wrapper.find('MultipleProjectSelector HeaderItem');
  1110. headerItem.simulate('click');
  1111. wrapper
  1112. .find('MultipleProjectSelector CheckboxFancy')
  1113. .forEach(project => project.simulate('click'));
  1114. headerItem.simulate('click');
  1115. await tick();
  1116. wrapper.update();
  1117. // assert title and icons
  1118. const title = wrapper.find('MultipleProjectSelector Content');
  1119. const icons = wrapper.find('MultipleProjectSelector StyledPlatformIcon');
  1120. expect(title.text()).toBe('javascript, other, php, python, rust, swift');
  1121. expect(icons.length).toBe(5);
  1122. expect(icons.at(3).props().platform).toBe('rust');
  1123. expect(icons.at(4).props().platform).toBe('swift');
  1124. });
  1125. });
  1126. });