projectSelector.spec.jsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381
  1. import {mountWithTheme} from 'sentry-test/enzyme';
  2. import ProjectSelector from 'app/components/organizations/projectSelector';
  3. describe('ProjectSelector', function () {
  4. const testTeam = TestStubs.Team({
  5. id: 'test-team',
  6. slug: 'test-team',
  7. isMember: true,
  8. });
  9. const testProject = TestStubs.Project({
  10. id: 'test-project',
  11. slug: 'test-project',
  12. isBookmarked: true,
  13. isMember: true,
  14. teams: [testTeam],
  15. });
  16. const anotherProject = TestStubs.Project({
  17. id: 'another-project',
  18. slug: 'another-project',
  19. isMember: true,
  20. teams: [testTeam],
  21. });
  22. const mockOrg = TestStubs.Organization({
  23. id: 'org',
  24. slug: 'org',
  25. teams: [testTeam],
  26. projects: [testProject, anotherProject],
  27. features: ['new-teams'],
  28. access: [],
  29. });
  30. const routerContext = TestStubs.routerContext([{organization: mockOrg}]);
  31. const openMenu = wrapper =>
  32. wrapper.find('[data-test-id="test-actor"]').simulate('click');
  33. const actorRenderer = jest.fn(() => <div data-test-id="test-actor" />);
  34. const props = {
  35. organization: mockOrg,
  36. projectId: '',
  37. children: actorRenderer,
  38. multiProjects: mockOrg.projects,
  39. selectedProjects: [],
  40. onSelect: () => {},
  41. menuFooter: () => {},
  42. };
  43. it('should show empty message with no projects button, when no projects, and has no "project:write" access', function () {
  44. const wrapper = mountWithTheme(
  45. <ProjectSelector
  46. {...props}
  47. multiProjects={[]}
  48. organization={{
  49. id: 'org',
  50. slug: 'org-slug',
  51. teams: [],
  52. projects: [],
  53. access: [],
  54. }}
  55. />,
  56. routerContext
  57. );
  58. openMenu(wrapper);
  59. expect(wrapper.find('EmptyMessage').prop('children')).toBe('You have no projects');
  60. // Should not have "Create Project" button
  61. expect(wrapper.find('CreateProjectButton')).toHaveLength(0);
  62. });
  63. it('should show empty message and create project button, when no projects and has "project:write" access', function () {
  64. const wrapper = mountWithTheme(
  65. <ProjectSelector
  66. {...props}
  67. multiProjects={[]}
  68. organization={{
  69. id: 'org',
  70. slug: 'org-slug',
  71. teams: [],
  72. projects: [],
  73. access: ['project:write'],
  74. }}
  75. />,
  76. routerContext
  77. );
  78. openMenu(wrapper);
  79. expect(wrapper.find('EmptyMessage').prop('children')).toBe('You have no projects');
  80. // Should not have "Create Project" button
  81. expect(wrapper.find('CreateProjectButton')).toHaveLength(1);
  82. });
  83. it('lists projects and has filter', function () {
  84. const wrapper = mountWithTheme(<ProjectSelector {...props} />, routerContext);
  85. openMenu(wrapper);
  86. expect(wrapper.find('AutoCompleteItem')).toHaveLength(2);
  87. });
  88. it('can filter projects by project name', function () {
  89. const wrapper = mountWithTheme(<ProjectSelector {...props} />, routerContext);
  90. openMenu(wrapper);
  91. wrapper.find('StyledInput').simulate('change', {target: {value: 'TEST'}});
  92. const result = wrapper.find('AutoCompleteItem ProjectBadge');
  93. expect(result).toHaveLength(1);
  94. expect(result.prop('project').slug).toBe('test-project');
  95. });
  96. it('does not close dropdown when input is clicked', async function () {
  97. const wrapper = mountWithTheme(<ProjectSelector {...props} />, routerContext);
  98. openMenu(wrapper);
  99. wrapper.find('StyledInput').simulate('click');
  100. await tick();
  101. wrapper.update();
  102. expect(wrapper.find('DropdownMenu').prop('isOpen')).toBe(true);
  103. });
  104. it('closes dropdown when project is selected', function () {
  105. const wrapper = mountWithTheme(<ProjectSelector {...props} />, routerContext);
  106. openMenu(wrapper);
  107. // Select first project
  108. wrapper.find('AutoCompleteItem').first().simulate('click');
  109. expect(wrapper.find('DropdownMenu').prop('isOpen')).toBe(false);
  110. });
  111. it('calls callback when project is selected', function () {
  112. const mock = jest.fn();
  113. const wrapper = mountWithTheme(
  114. <ProjectSelector {...props} onSelect={mock} />,
  115. routerContext
  116. );
  117. openMenu(wrapper);
  118. // Select first project
  119. wrapper.find('AutoCompleteItem').first().simulate('click');
  120. expect(mock).toHaveBeenCalledWith(
  121. expect.objectContaining({
  122. slug: 'test-project',
  123. })
  124. );
  125. });
  126. it('shows empty filter message when filtering has no results', function () {
  127. const wrapper = mountWithTheme(<ProjectSelector {...props} />, routerContext);
  128. openMenu(wrapper);
  129. wrapper.find('StyledInput').simulate('change', {target: {value: 'Foo'}});
  130. expect(wrapper.find('EmptyMessage').prop('children')).toBe('No projects found');
  131. });
  132. it('does not call `onSelect` when using multi select', function () {
  133. const mock = jest.fn();
  134. const onMultiSelectMock = jest.fn();
  135. const wrapper = mountWithTheme(
  136. <ProjectSelector
  137. {...props}
  138. multi
  139. onSelect={mock}
  140. onMultiSelect={onMultiSelectMock}
  141. />,
  142. routerContext
  143. );
  144. openMenu(wrapper);
  145. // Select first project
  146. wrapper.find('CheckboxHitbox').first().simulate('click');
  147. // onSelect callback should NOT be called
  148. expect(mock).not.toHaveBeenCalled();
  149. expect(onMultiSelectMock).toHaveBeenCalled();
  150. });
  151. it('displays multi projects', function () {
  152. const project = TestStubs.Project();
  153. const multiProjectProps = {...props, multiProjects: [project]};
  154. const wrapper = mountWithTheme(
  155. <ProjectSelector {...multiProjectProps} />,
  156. routerContext
  157. );
  158. openMenu(wrapper);
  159. expect(wrapper.find('AutoCompleteItem')).toHaveLength(1);
  160. expect(wrapper.text()).not.toContain("Projects I don't belong to");
  161. });
  162. it('displays multi projects with non member projects', function () {
  163. const project = TestStubs.Project({id: '1'});
  164. const nonMemberProject = TestStubs.Project({id: '2'});
  165. const multiProjectProps = {
  166. ...props,
  167. multiProjects: [project],
  168. nonMemberProjects: [nonMemberProject],
  169. };
  170. const wrapper = mountWithTheme(
  171. <ProjectSelector {...multiProjectProps} />,
  172. routerContext
  173. );
  174. openMenu(wrapper);
  175. expect(wrapper.text()).toContain("Projects I don't belong to");
  176. expect(wrapper.find('AutoCompleteItem')).toHaveLength(2);
  177. });
  178. it('displays projects in alphabetical order partitioned by project membership', function () {
  179. const projectA = TestStubs.Project({id: '1', slug: 'a-project'});
  180. const projectB = TestStubs.Project({id: '2', slug: 'b-project'});
  181. const projectANonM = TestStubs.Project({id: '3', slug: 'a-non-m-project'});
  182. const projectBNonM = TestStubs.Project({id: '4', slug: 'b-non-m-project'});
  183. const multiProjectProps = {
  184. ...props,
  185. multiProjects: [projectB, projectA],
  186. nonMemberProjects: [projectBNonM, projectANonM],
  187. selectedProjects: [],
  188. };
  189. const wrapper = mountWithTheme(
  190. <ProjectSelector {...multiProjectProps} />,
  191. routerContext
  192. );
  193. openMenu(wrapper);
  194. const positionA = wrapper.text().indexOf(projectA.slug);
  195. const positionB = wrapper.text().indexOf(projectB.slug);
  196. const positionANonM = wrapper.text().indexOf(projectANonM.slug);
  197. const positionBNonM = wrapper.text().indexOf(projectBNonM.slug);
  198. expect(wrapper.text()).toContain("Projects I don't belong to");
  199. expect(wrapper.find('AutoCompleteItem')).toHaveLength(4);
  200. [positionA, positionB, positionANonM, positionBNonM].forEach(position =>
  201. expect(position).toBeGreaterThan(-1)
  202. );
  203. expect(positionA).toBeLessThan(positionB);
  204. expect(positionB).toBeLessThan(positionANonM);
  205. expect(positionANonM).toBeLessThan(positionBNonM);
  206. });
  207. it('displays multi projects in sort order rules: selected, bookmarked, alphabetical', function () {
  208. const projectA = TestStubs.Project({id: '1', slug: 'a-project'});
  209. const projectBBookmarked = TestStubs.Project({
  210. id: '2',
  211. slug: 'b-project',
  212. isBookmarked: true,
  213. });
  214. const projectCBookmarked = TestStubs.Project({
  215. id: '3',
  216. slug: 'c-project',
  217. isBookmarked: true,
  218. });
  219. const projectDSelected = TestStubs.Project({id: '4', slug: 'd-project'});
  220. const projectESelected = TestStubs.Project({id: '5', slug: 'e-project'});
  221. const projectFSelectedBookmarked = TestStubs.Project({
  222. id: '6',
  223. slug: 'f-project',
  224. isBookmarked: true,
  225. });
  226. const projectGSelectedBookmarked = TestStubs.Project({
  227. id: '7',
  228. slug: 'g-project',
  229. isBookmarked: true,
  230. });
  231. const projectH = TestStubs.Project({id: '8', slug: 'h-project'});
  232. const multiProjectProps = {
  233. ...props,
  234. multiProjects: [
  235. projectA,
  236. projectBBookmarked,
  237. projectCBookmarked,
  238. projectDSelected,
  239. projectESelected,
  240. projectFSelectedBookmarked,
  241. projectGSelectedBookmarked,
  242. projectH,
  243. ],
  244. nonMemberProjects: [],
  245. selectedProjects: [
  246. projectESelected,
  247. projectDSelected,
  248. projectGSelectedBookmarked,
  249. projectFSelectedBookmarked,
  250. ],
  251. };
  252. const wrapper = mountWithTheme(
  253. <ProjectSelector {...multiProjectProps} />,
  254. routerContext
  255. );
  256. openMenu(wrapper);
  257. const positionA = wrapper.text().indexOf(projectA.slug);
  258. const positionB = wrapper.text().indexOf(projectBBookmarked.slug);
  259. const positionC = wrapper.text().indexOf(projectCBookmarked.slug);
  260. const positionD = wrapper.text().indexOf(projectDSelected.slug);
  261. const positionE = wrapper.text().indexOf(projectESelected.slug);
  262. const positionF = wrapper.text().indexOf(projectFSelectedBookmarked.slug);
  263. const positionG = wrapper.text().indexOf(projectGSelectedBookmarked.slug);
  264. const positionH = wrapper.text().indexOf(projectH.slug);
  265. expect(wrapper.text()).not.toContain("Projects I don't belong to");
  266. expect(wrapper.find('AutoCompleteItem')).toHaveLength(8);
  267. [
  268. positionA,
  269. positionB,
  270. positionC,
  271. positionD,
  272. positionE,
  273. positionF,
  274. positionG,
  275. positionH,
  276. ].forEach(position => expect(position).toBeGreaterThan(-1));
  277. expect(positionF).toBeLessThan(positionG);
  278. expect(positionG).toBeLessThan(positionD);
  279. expect(positionD).toBeLessThan(positionE);
  280. expect(positionE).toBeLessThan(positionB);
  281. expect(positionB).toBeLessThan(positionC);
  282. expect(positionC).toBeLessThan(positionA);
  283. expect(positionA).toBeLessThan(positionH);
  284. });
  285. it('displays non member projects in alphabetical sort order', function () {
  286. const projectA = TestStubs.Project({id: '1', slug: 'a-project'});
  287. const projectBBookmarked = TestStubs.Project({
  288. id: '2',
  289. slug: 'b-project',
  290. isBookmarked: true,
  291. });
  292. const projectCSelected = TestStubs.Project({id: '3', slug: 'c-project'});
  293. const projectDSelectedBookmarked = TestStubs.Project({
  294. id: '4',
  295. slug: 'd-project',
  296. isBookmarked: true,
  297. });
  298. const multiProjectProps = {
  299. ...props,
  300. multiProjects: [],
  301. nonMemberProjects: [
  302. projectCSelected,
  303. projectA,
  304. projectDSelectedBookmarked,
  305. projectBBookmarked,
  306. ],
  307. selectedProjects: [projectCSelected, projectDSelectedBookmarked],
  308. };
  309. const wrapper = mountWithTheme(
  310. <ProjectSelector {...multiProjectProps} />,
  311. routerContext
  312. );
  313. openMenu(wrapper);
  314. const positionA = wrapper.text().indexOf(projectA.slug);
  315. const positionB = wrapper.text().indexOf(projectBBookmarked.slug);
  316. const positionC = wrapper.text().indexOf(projectCSelected.slug);
  317. const positionD = wrapper.text().indexOf(projectDSelectedBookmarked.slug);
  318. expect(wrapper.text()).toContain("Projects I don't belong to");
  319. expect(wrapper.find('AutoCompleteItem')).toHaveLength(4);
  320. [positionA, positionB, positionC, positionD].forEach(position =>
  321. expect(position).toBeGreaterThan(-1)
  322. );
  323. expect(positionA).toBeLessThan(positionB);
  324. expect(positionB).toBeLessThan(positionC);
  325. expect(positionC).toBeLessThan(positionD);
  326. });
  327. });