globalSelectionHeader.spec.jsx 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240
  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. const changeQuery = (routerContext, query) => ({
  14. ...routerContext,
  15. context: {
  16. ...routerContext.context,
  17. router: {
  18. ...routerContext.context.router,
  19. location: {
  20. query,
  21. },
  22. },
  23. },
  24. });
  25. jest.mock('sentry/utils/localStorage', () => ({
  26. getItem: jest.fn(),
  27. setItem: jest.fn(),
  28. }));
  29. describe('GlobalSelectionHeader', function () {
  30. enforceActOnUseLegacyStoreHook();
  31. let wrapper;
  32. const {organization, router, routerContext} = initializeOrg({
  33. organization: {features: ['global-views']},
  34. projects: [
  35. {
  36. id: 2,
  37. slug: 'project-2',
  38. },
  39. {
  40. id: 3,
  41. slug: 'project-3',
  42. environments: ['prod', 'staging'],
  43. },
  44. ],
  45. router: {
  46. location: {query: {}},
  47. params: {orgId: 'org-slug'},
  48. },
  49. });
  50. beforeAll(function () {
  51. jest.spyOn(globalActions, 'updateDateTime');
  52. jest.spyOn(globalActions, 'updateEnvironments');
  53. jest.spyOn(globalActions, 'updateProjects');
  54. jest.spyOn(globalActions, 'updateParams');
  55. });
  56. beforeEach(function () {
  57. MockApiClient.clearMockResponses();
  58. ProjectsStore.loadInitialData(organization.projects);
  59. OrganizationsStore.add(organization);
  60. getItem.mockImplementation(() => null);
  61. MockApiClient.addMockResponse({
  62. url: '/organizations/org-slug/projects/',
  63. body: [],
  64. });
  65. });
  66. afterEach(function () {
  67. wrapper.unmount();
  68. [
  69. globalActions.updateDateTime,
  70. globalActions.updateProjects,
  71. globalActions.updateEnvironments,
  72. globalActions.updateParams,
  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. organization,
  271. isReady: true,
  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. organization,
  297. isReady: true,
  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. organization,
  322. isReady: true,
  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. organization,
  363. isReady: true,
  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. /**
  475. * GSH: (no global-views)
  476. * - mounts with no state from router
  477. * - params org id === org.slug
  478. *
  479. * - updateProjects should not be called (enforceSingleProject should not be
  480. * called)
  481. *
  482. * - componentDidUpdate with loadingProjects === true, and pass in list of
  483. * projects (via projects store)
  484. *
  485. * - enforceProject should be called and updateProjects() called with the new
  486. * project
  487. * - variation:
  488. * - params.orgId !== org.slug (e.g. just switched orgs)
  489. *
  490. * When switching orgs when not in Issues view, the issues view gets rendered
  491. * with params.orgId !== org.slug
  492. *
  493. * Global selection header gets unmounted and mounted, and in this case
  494. * nothing should be done until it gets updated and params.orgId === org.slug
  495. *
  496. * Separate issue:
  497. *
  498. * IssuesList ("child view") renders before a single project is enforced,
  499. * will require refactoring views so that they depend on GSH enforcing a
  500. * single project first IF they don't have required feature (and no project id
  501. * in URL).
  502. */
  503. describe('Single project selection mode', function () {
  504. it('does not do anything while organization is switching in single project', async function () {
  505. const initialData = initializeOrg({
  506. organization: {slug: 'old-org-slug'},
  507. router: {
  508. // we need this to be set to make sure org in context is same as
  509. // current org in URL
  510. params: {orgId: 'org-slug'},
  511. location: {query: {project: ['1']}},
  512. },
  513. });
  514. MockApiClient.addMockResponse({
  515. url: '/organizations/old-org-slug/projects/',
  516. body: [],
  517. });
  518. ProjectsStore.reset();
  519. // This can happen when you switch organization so params.orgId !== the
  520. // current org in context In this case params.orgId = 'org-slug'
  521. wrapper = mountWithTheme(
  522. <PageFiltersContainer organization={initialData.organization} />,
  523. initialData.routerContext
  524. );
  525. expect(globalActions.updateProjects).not.toHaveBeenCalled();
  526. const updatedOrganization = {
  527. ...organization,
  528. slug: 'org-slug',
  529. features: [],
  530. projects: [TestStubs.Project({id: '123', slug: 'org-slug-project1'})],
  531. };
  532. MockApiClient.addMockResponse({
  533. url: '/organizations/org-slug/',
  534. body: updatedOrganization,
  535. });
  536. // Eventually OrganizationContext will fetch org details for `org-slug`
  537. // and update `organization` prop emulate fetchOrganizationDetails
  538. OrganizationActions.update(updatedOrganization);
  539. wrapper.setContext({
  540. organization: updatedOrganization,
  541. location: {query: {}},
  542. router: {
  543. ...initialData.router,
  544. location: {query: {}},
  545. },
  546. });
  547. wrapper.setProps({organization: updatedOrganization});
  548. act(() => ProjectsStore.loadInitialData(updatedOrganization.projects));
  549. expect(initialData.router.replace).toHaveBeenLastCalledWith(
  550. expect.objectContaining({
  551. query: {environment: [], project: ['123']},
  552. })
  553. );
  554. });
  555. it('selects first project if more than one is requested', function () {
  556. const initializationObj = initializeOrg({
  557. router: {
  558. // we need this to be set to make sure org in context is same as
  559. // current org in URL
  560. params: {orgId: 'org-slug'},
  561. location: {query: {project: ['1', '2']}},
  562. },
  563. });
  564. wrapper = mountWithTheme(
  565. <PageFiltersContainer organization={initializationObj.organization} />,
  566. initializationObj.routerContext
  567. );
  568. expect(initializationObj.router.replace).toHaveBeenCalledWith(
  569. expect.objectContaining({
  570. query: {environment: [], project: ['1']},
  571. })
  572. );
  573. });
  574. it('selects first project if none (i.e. all) is requested', async function () {
  575. const project = TestStubs.Project({id: '3'});
  576. const org = TestStubs.Organization({projects: [project]});
  577. ProjectsStore.loadInitialData(org.projects);
  578. const initializationObj = initializeOrg({
  579. organization: org,
  580. router: {
  581. params: {orgId: 'org-slug'},
  582. location: {query: {}},
  583. },
  584. });
  585. wrapper = mountWithTheme(
  586. <PageFiltersContainer organization={initializationObj.organization} />,
  587. initializationObj.routerContext
  588. );
  589. expect(initializationObj.router.replace).toHaveBeenCalledWith(
  590. expect.objectContaining({
  591. query: {environment: [], project: ['3']},
  592. })
  593. );
  594. });
  595. });
  596. describe('forceProject selection mode', function () {
  597. beforeEach(async function () {
  598. MockApiClient.addMockResponse({
  599. url: '/organizations/org-slug/projects/',
  600. body: [],
  601. });
  602. const initialData = initializeOrg({
  603. organization: {features: ['global-views']},
  604. projects: [
  605. {id: 1, slug: 'staging-project', environments: ['staging']},
  606. {id: 2, slug: 'prod-project', environments: ['prod']},
  607. ],
  608. router: {
  609. location: {query: {}},
  610. },
  611. });
  612. ProjectsStore.loadInitialData(initialData.projects);
  613. wrapper = mountWithTheme(
  614. <PageFiltersContainer
  615. organization={initialData.organization}
  616. shouldForceProject
  617. forceProject={initialData.projects[0]}
  618. showIssueStreamLink
  619. />,
  620. initialData.routerContext
  621. );
  622. await tick();
  623. wrapper.update();
  624. });
  625. it('renders a back button to the forced project', function () {
  626. const back = wrapper.find('BackButtonWrapper');
  627. expect(back).toHaveLength(1);
  628. });
  629. it('renders only environments from the forced project', function () {
  630. wrapper.find('MultipleEnvironmentSelector HeaderItem').simulate('click');
  631. wrapper.update();
  632. const items = wrapper.find('MultipleEnvironmentSelector EnvironmentSelectorItem');
  633. expect(items.length).toEqual(1);
  634. expect(items.at(0).text()).toBe('staging');
  635. });
  636. });
  637. describe('without global-views (multi-project feature)', function () {
  638. describe('without existing URL params', function () {
  639. const initialData = initializeOrg({
  640. projects: [
  641. {id: 0, slug: 'random project', isMember: true},
  642. {id: 1, slug: 'staging-project', environments: ['staging']},
  643. {id: 2, slug: 'prod-project', environments: ['prod']},
  644. ],
  645. router: {
  646. location: {query: {}},
  647. params: {orgId: 'org-slug'},
  648. },
  649. });
  650. const createWrapper = props => {
  651. wrapper = mountWithTheme(
  652. <PageFiltersContainer
  653. params={{orgId: initialData.organization.slug}}
  654. organization={initialData.organization}
  655. {...props}
  656. />,
  657. initialData.routerContext
  658. );
  659. return wrapper;
  660. };
  661. beforeEach(function () {
  662. ProjectsStore.loadInitialData(initialData.projects);
  663. initialData.router.push.mockClear();
  664. initialData.router.replace.mockClear();
  665. });
  666. it('uses first project in org projects when mounting', async function () {
  667. createWrapper();
  668. // Projects are returned in sorted slug order, so `prod-project` would
  669. // be the first project
  670. expect(initialData.router.replace).toHaveBeenLastCalledWith({
  671. pathname: undefined,
  672. query: {cursor: undefined, environment: [], project: ['2']},
  673. });
  674. });
  675. it('appends projectId to URL when `forceProject` becomes available (async)', async function () {
  676. ProjectsStore.reset();
  677. // forceProject generally starts undefined
  678. createWrapper({shouldForceProject: true});
  679. wrapper.setProps({
  680. forceProject: initialData.projects[1],
  681. });
  682. // load the projects
  683. act(() => ProjectsStore.loadInitialData(initialData.projects));
  684. wrapper.update();
  685. expect(initialData.router.replace).toHaveBeenLastCalledWith({
  686. pathname: undefined,
  687. query: {environment: [], project: ['1']},
  688. });
  689. expect(initialData.router.replace).toHaveBeenCalledTimes(1);
  690. });
  691. it('does not append projectId to URL when `forceProject` becomes available but project id already exists in URL', async function () {
  692. // forceProject generally starts undefined
  693. createWrapper({shouldForceProject: true});
  694. wrapper.setContext({
  695. router: {
  696. ...initialData.router,
  697. location: {
  698. ...initialData.router.location,
  699. query: {
  700. project: '321',
  701. },
  702. },
  703. },
  704. });
  705. wrapper.setProps({
  706. forceProject: initialData.projects[1],
  707. });
  708. wrapper.update();
  709. expect(initialData.router.replace).not.toHaveBeenCalled();
  710. });
  711. it('appends projectId to URL when mounted with `forceProject`', async function () {
  712. // forceProject generally starts undefined
  713. createWrapper({
  714. shouldForceProject: true,
  715. forceProject: initialData.projects[1],
  716. });
  717. wrapper.update();
  718. expect(initialData.router.replace).toHaveBeenLastCalledWith({
  719. pathname: undefined,
  720. query: {environment: [], project: ['1']},
  721. });
  722. });
  723. });
  724. describe('with existing URL params', function () {
  725. const initialData = initializeOrg({
  726. projects: [
  727. {id: 0, slug: 'random project', isMember: true},
  728. {id: 1, slug: 'staging-project', environments: ['staging']},
  729. {id: 2, slug: 'prod-project', environments: ['prod']},
  730. ],
  731. router: {
  732. location: {query: {statsPeriod: '90d'}},
  733. params: {orgId: 'org-slug'},
  734. },
  735. });
  736. ProjectsStore.loadInitialData(initialData.projects);
  737. const createWrapper = props => {
  738. wrapper = mountWithTheme(
  739. <PageFiltersContainer
  740. params={{orgId: initialData.organization.slug}}
  741. organization={initialData.organization}
  742. {...props}
  743. />,
  744. initialData.routerContext
  745. );
  746. return wrapper;
  747. };
  748. beforeEach(function () {
  749. initialData.router.push.mockClear();
  750. initialData.router.replace.mockClear();
  751. });
  752. it('appends projectId to URL when mounted with `forceProject`', async function () {
  753. // forceProject generally starts undefined
  754. createWrapper({
  755. shouldForceProject: true,
  756. forceProject: initialData.projects[1],
  757. });
  758. wrapper.update();
  759. expect(initialData.router.replace).toHaveBeenLastCalledWith({
  760. pathname: undefined,
  761. query: {environment: [], project: ['1'], statsPeriod: '90d'},
  762. });
  763. });
  764. });
  765. });
  766. describe('with global-views (multi-project feature)', function () {
  767. describe('without existing URL params', function () {
  768. const initialData = initializeOrg({
  769. organization: {features: ['global-views']},
  770. projects: [
  771. {id: 0, slug: 'random project', isMember: true},
  772. {id: 1, slug: 'staging-project', environments: ['staging']},
  773. {id: 2, slug: 'prod-project', environments: ['prod']},
  774. ],
  775. router: {
  776. location: {query: {}},
  777. params: {orgId: 'org-slug'},
  778. },
  779. });
  780. const createWrapper = (props, ctx) => {
  781. wrapper = mountWithTheme(
  782. <PageFiltersContainer
  783. params={{orgId: initialData.organization.slug}}
  784. organization={initialData.organization}
  785. {...props}
  786. />,
  787. {
  788. ...initialData.routerContext,
  789. ...ctx,
  790. }
  791. );
  792. return wrapper;
  793. };
  794. beforeEach(function () {
  795. ProjectsStore.loadInitialData(initialData.projects);
  796. initialData.router.push.mockClear();
  797. initialData.router.replace.mockClear();
  798. });
  799. it('does not use first project in org projects when mounting (and without localStorage data)', async function () {
  800. createWrapper();
  801. await tick();
  802. wrapper.update();
  803. expect(initialData.router.replace).not.toHaveBeenCalled();
  804. });
  805. it('does not append projectId to URL when `loadingProjects` changes and finishes loading', async function () {
  806. ProjectsStore.reset();
  807. createWrapper();
  808. // load the projects
  809. act(() => ProjectsStore.loadInitialData(initialData.projects));
  810. wrapper.setProps({
  811. forceProject: initialData.projects[1],
  812. });
  813. expect(initialData.router.replace).not.toHaveBeenCalled();
  814. });
  815. it('appends projectId to URL when `forceProject` becomes available (async)', async function () {
  816. ProjectsStore.reset();
  817. // forceProject generally starts undefined
  818. createWrapper({shouldForceProject: true});
  819. wrapper.setProps({
  820. forceProject: initialData.projects[1],
  821. });
  822. // load the projects
  823. act(() => ProjectsStore.loadInitialData(initialData.projects));
  824. expect(initialData.router.replace).toHaveBeenLastCalledWith({
  825. pathname: undefined,
  826. query: {environment: [], project: ['1']},
  827. });
  828. expect(initialData.router.replace).toHaveBeenCalledTimes(1);
  829. });
  830. it('does not append projectId to URL when `forceProject` becomes available but project id already exists in URL', async function () {
  831. // forceProject generally starts undefined
  832. createWrapper(
  833. {shouldForceProject: true},
  834. changeQuery(initialData.routerContext, {project: 321})
  835. );
  836. await tick();
  837. wrapper.setProps({
  838. forceProject: initialData.projects[1],
  839. });
  840. wrapper.update();
  841. expect(initialData.router.replace).not.toHaveBeenCalled();
  842. });
  843. });
  844. });
  845. describe('projects list', function () {
  846. let memberProject, nonMemberProject, initialData;
  847. beforeEach(async function () {
  848. memberProject = TestStubs.Project({id: '3', isMember: true});
  849. nonMemberProject = TestStubs.Project({id: '4', isMember: false});
  850. initialData = initializeOrg({
  851. projects: [memberProject, nonMemberProject],
  852. router: {
  853. location: {query: {}},
  854. params: {
  855. orgId: 'org-slug',
  856. },
  857. },
  858. });
  859. ProjectsStore.loadInitialData(initialData.projects);
  860. wrapper = mountWithTheme(
  861. <PageFiltersContainer organization={initialData.organization} />,
  862. initialData.routerContext
  863. );
  864. await tick();
  865. wrapper.update();
  866. });
  867. it('gets member projects', function () {
  868. expect(wrapper.find('MultipleProjectSelector').prop('projects')).toEqual([
  869. memberProject,
  870. ]);
  871. });
  872. it('gets all projects if superuser', async function () {
  873. ConfigStore.config = {
  874. user: {
  875. isSuperuser: true,
  876. },
  877. };
  878. wrapper = mountWithTheme(
  879. <PageFiltersContainer organization={initialData.organization} />,
  880. initialData.routerContext
  881. );
  882. await tick();
  883. wrapper.update();
  884. expect(wrapper.find('MultipleProjectSelector').prop('projects')).toEqual([
  885. memberProject,
  886. ]);
  887. expect(wrapper.find('MultipleProjectSelector').prop('nonMemberProjects')).toEqual([
  888. nonMemberProject,
  889. ]);
  890. });
  891. it('shows "My Projects" button', async function () {
  892. initialData.organization.features.push('global-views');
  893. wrapper = mountWithTheme(
  894. <PageFiltersContainer
  895. organization={initialData.organization}
  896. projects={initialData.projects}
  897. />,
  898. initialData.routerContext
  899. );
  900. await tick();
  901. wrapper.update();
  902. // open the project menu.
  903. wrapper.find('MultipleProjectSelector HeaderItem').simulate('click');
  904. const projectSelector = wrapper.find('MultipleProjectSelector');
  905. // Two projects
  906. expect(projectSelector.find('AutoCompleteItem')).toHaveLength(2);
  907. // My projects in the footer
  908. expect(
  909. projectSelector.find('SelectorFooterControls Button').first().text()
  910. ).toEqual('Select My Projects');
  911. });
  912. it('shows "All Projects" button based on features', async function () {
  913. initialData.organization.features.push('global-views');
  914. initialData.organization.features.push('open-membership');
  915. wrapper = mountWithTheme(
  916. <PageFiltersContainer
  917. organization={initialData.organization}
  918. projects={initialData.projects}
  919. />,
  920. initialData.routerContext
  921. );
  922. await tick();
  923. wrapper.update();
  924. // open the project menu.
  925. wrapper.find('MultipleProjectSelector HeaderItem').simulate('click');
  926. const projectSelector = wrapper.find('MultipleProjectSelector');
  927. // Two projects
  928. expect(projectSelector.find('AutoCompleteItem')).toHaveLength(2);
  929. // All projects in the footer
  930. expect(
  931. projectSelector.find('SelectorFooterControls Button').first().text()
  932. ).toEqual('Select All Projects');
  933. });
  934. it('shows "All Projects" button based on role', async function () {
  935. initialData.organization.features.push('global-views');
  936. initialData.organization.role = 'owner';
  937. wrapper = mountWithTheme(
  938. <PageFiltersContainer
  939. organization={initialData.organization}
  940. projects={initialData.projects}
  941. />,
  942. initialData.routerContext
  943. );
  944. await tick();
  945. wrapper.update();
  946. // open the project menu.
  947. wrapper.find('MultipleProjectSelector HeaderItem').simulate('click');
  948. const projectSelector = wrapper.find('MultipleProjectSelector');
  949. // Two projects
  950. expect(projectSelector.find('AutoCompleteItem')).toHaveLength(2);
  951. // All projects in the footer
  952. expect(
  953. projectSelector.find('SelectorFooterControls Button').first().text()
  954. ).toEqual('Select All Projects');
  955. });
  956. it('shows "My Projects" when "all projects" is selected', async function () {
  957. initialData.organization.features.push('global-views');
  958. initialData.organization.role = 'owner';
  959. wrapper = mountWithTheme(
  960. <PageFiltersContainer
  961. organization={initialData.organization}
  962. projects={initialData.projects}
  963. />,
  964. changeQuery(initialData.routerContext, {project: -1})
  965. );
  966. await tick();
  967. wrapper.update();
  968. // open the project menu.
  969. wrapper.find('MultipleProjectSelector HeaderItem').simulate('click');
  970. const projectSelector = wrapper.find('MultipleProjectSelector');
  971. // My projects in the footer
  972. expect(
  973. projectSelector.find('SelectorFooterControls Button').first().text()
  974. ).toEqual('Select My Projects');
  975. });
  976. });
  977. describe('project icons', function () {
  978. const initialData = initializeOrg({
  979. organization: {features: ['global-views']},
  980. projects: [
  981. {id: 0, slug: 'go', platform: 'go'},
  982. {id: 1, slug: 'javascript', platform: 'javascript'},
  983. {id: 2, slug: 'other', platform: 'other'},
  984. {id: 3, slug: 'php', platform: 'php'},
  985. {id: 4, slug: 'python', platform: 'python'},
  986. {id: 5, slug: 'rust', platform: 'rust'},
  987. {id: 6, slug: 'swift', platform: 'swift'},
  988. ],
  989. });
  990. beforeEach(function () {
  991. ProjectsStore.loadInitialData(initialData.projects);
  992. });
  993. it('shows IconProject when no projects are selected', async function () {
  994. wrapper = mountWithTheme(
  995. <PageFiltersContainer
  996. organization={initialData.organization}
  997. projects={initialData.projects}
  998. />,
  999. changeQuery(initialData.routerContext, {project: -1})
  1000. );
  1001. await tick();
  1002. wrapper.update();
  1003. const projectSelector = wrapper.find('MultipleProjectSelector');
  1004. expect(projectSelector.find('IconContainer svg').exists()).toBeTruthy();
  1005. expect(projectSelector.find('PlatformIcon').exists()).toBeFalsy();
  1006. expect(projectSelector.find('Content').text()).toEqual('All Projects');
  1007. });
  1008. it('shows PlatformIcon when one project is selected', async function () {
  1009. wrapper = mountWithTheme(
  1010. <PageFiltersContainer
  1011. organization={initialData.organization}
  1012. projects={initialData.projects}
  1013. />,
  1014. changeQuery(initialData.routerContext, {project: 1})
  1015. );
  1016. await tick();
  1017. wrapper.update();
  1018. const projectSelector = wrapper.find('MultipleProjectSelector');
  1019. expect(projectSelector.find('StyledPlatformIcon').props().platform).toEqual(
  1020. 'javascript'
  1021. );
  1022. expect(projectSelector.find('Content').text()).toEqual('javascript');
  1023. });
  1024. it('shows multiple PlatformIcons when multiple projects are selected, no more than 5', async function () {
  1025. wrapper = mountWithTheme(
  1026. <PageFiltersContainer
  1027. organization={initialData.organization}
  1028. projects={initialData.projects}
  1029. />,
  1030. initialData.routerContext
  1031. );
  1032. await tick();
  1033. wrapper.update();
  1034. // select 6 projects
  1035. const headerItem = wrapper.find('MultipleProjectSelector HeaderItem');
  1036. headerItem.simulate('click');
  1037. wrapper
  1038. .find('MultipleProjectSelector CheckboxFancy')
  1039. .forEach(project => project.simulate('click'));
  1040. headerItem.simulate('click');
  1041. await tick();
  1042. wrapper.update();
  1043. // assert title and icons
  1044. const title = wrapper.find('MultipleProjectSelector Content');
  1045. const icons = wrapper.find('MultipleProjectSelector StyledPlatformIcon');
  1046. expect(title.text()).toBe('javascript, other, php, python, rust, swift');
  1047. expect(icons.length).toBe(5);
  1048. expect(icons.at(3).props().platform).toBe('rust');
  1049. expect(icons.at(4).props().platform).toBe('swift');
  1050. });
  1051. });
  1052. });