projectSelector.spec.jsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  1. import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
  2. import ProjectSelector from 'sentry/components/organizations/projectSelector';
  3. describe('ProjectSelector', function () {
  4. const testProject = TestStubs.Project({
  5. id: '1',
  6. slug: 'test-project',
  7. isBookmarked: true,
  8. isMember: true,
  9. });
  10. const anotherProject = TestStubs.Project({
  11. id: '2',
  12. slug: 'another-project',
  13. isMember: true,
  14. });
  15. const mockOrg = TestStubs.Organization({
  16. id: '1',
  17. slug: 'org',
  18. features: ['new-teams', 'global-views'],
  19. access: [],
  20. });
  21. const routerContext = TestStubs.routerContext([{organization: mockOrg}]);
  22. function openMenu() {
  23. userEvent.click(screen.getByRole('button'));
  24. }
  25. function applyMenu() {
  26. userEvent.click(screen.getByRole('button', {name: 'Apply Filter'}));
  27. }
  28. const props = {
  29. isGlobalSelectionReady: true,
  30. organization: mockOrg,
  31. memberProjects: [testProject, anotherProject],
  32. nonMemberProjects: [],
  33. value: [],
  34. onApplyChange: () => {},
  35. onChange: () => {},
  36. menuFooter: () => {},
  37. };
  38. it('should show empty message with no projects button, when no projects, and has no "project:write" access', function () {
  39. render(
  40. <ProjectSelector
  41. {...props}
  42. memberProjects={[]}
  43. organization={{
  44. id: 'org',
  45. slug: 'org-slug',
  46. access: [],
  47. features: [],
  48. }}
  49. />,
  50. {context: routerContext}
  51. );
  52. openMenu();
  53. expect(screen.getByText('You have no projects')).toBeInTheDocument();
  54. // Should not have "Create Project" button
  55. const createProject = screen.getByLabelText('Add Project');
  56. expect(createProject).toBeInTheDocument();
  57. expect(createProject).toBeDisabled();
  58. });
  59. it('should show empty message and create project button, when no projects and has "project:write" access', function () {
  60. render(
  61. <ProjectSelector
  62. {...props}
  63. memberProjects={[]}
  64. organization={{
  65. id: 'org',
  66. slug: 'org-slug',
  67. access: ['project:write'],
  68. features: [],
  69. }}
  70. />,
  71. {context: routerContext}
  72. );
  73. openMenu();
  74. expect(screen.getByText('You have no projects')).toBeInTheDocument();
  75. // Should not have "Create Project" button
  76. const createProject = screen.getByLabelText('Add Project');
  77. expect(createProject).toBeInTheDocument();
  78. expect(createProject).toBeEnabled();
  79. });
  80. it('does not open selector menu when disabled', function () {
  81. render(<ProjectSelector {...props} disabled />, {context: routerContext});
  82. openMenu();
  83. expect(screen.queryByRole('checkbox')).not.toBeInTheDocument();
  84. });
  85. it('lists projects and has filter', function () {
  86. render(<ProjectSelector {...props} />, {context: routerContext});
  87. openMenu();
  88. expect(screen.getByText(testProject.slug)).toBeInTheDocument();
  89. expect(screen.getByText(anotherProject.slug)).toBeInTheDocument();
  90. });
  91. it('can filter projects by project name', function () {
  92. render(<ProjectSelector {...props} />, {context: routerContext});
  93. openMenu();
  94. screen.getByRole('textbox').focus();
  95. userEvent.keyboard('TEST');
  96. const item = screen.getByTestId('badge-display-name');
  97. expect(item).toBeInTheDocument();
  98. expect(item).toHaveTextContent(testProject.slug);
  99. });
  100. it('shows empty filter message when filtering has no results', function () {
  101. render(<ProjectSelector {...props} />, {context: routerContext});
  102. openMenu();
  103. screen.getByRole('textbox').focus();
  104. userEvent.keyboard('Foo');
  105. expect(screen.queryByTestId('badge-display-name')).not.toBeInTheDocument();
  106. expect(screen.getByText('No projects found')).toBeInTheDocument();
  107. });
  108. it('does not close dropdown when input is clicked', function () {
  109. render(<ProjectSelector {...props} />, {context: routerContext});
  110. openMenu();
  111. userEvent.click(screen.getByRole('textbox'));
  112. // Dropdown is still open
  113. expect(screen.getByText(testProject.slug)).toBeInTheDocument();
  114. });
  115. it('closes dropdown when project is selected', function () {
  116. render(<ProjectSelector {...props} />, {context: routerContext});
  117. openMenu();
  118. userEvent.click(screen.getByText(testProject.slug));
  119. // Dropdown is closed
  120. expect(screen.queryByText(testProject.slug)).not.toBeInTheDocument();
  121. });
  122. it('calls callback when project is selected', function () {
  123. const onApplyChangeMock = jest.fn();
  124. render(<ProjectSelector {...props} onApplyChange={onApplyChangeMock} />, {
  125. context: routerContext,
  126. });
  127. openMenu();
  128. // Select first project
  129. userEvent.click(screen.getByText(testProject.slug));
  130. expect(onApplyChangeMock).toHaveBeenCalledWith([parseInt(testProject.id, 10)]);
  131. });
  132. it('does not call `onUpdate` when using multi select', function () {
  133. const onChangeMock = jest.fn();
  134. const onApplyChangeMock = jest.fn();
  135. render(
  136. <ProjectSelector
  137. {...props}
  138. onChange={onChangeMock}
  139. onApplyChange={onApplyChangeMock}
  140. />,
  141. {context: routerContext}
  142. );
  143. openMenu();
  144. // Check the first project
  145. userEvent.click(screen.getByRole('checkbox', {name: testProject.slug}));
  146. expect(onChangeMock).toHaveBeenCalled();
  147. expect(onApplyChangeMock).not.toHaveBeenCalled();
  148. });
  149. it('displays multi projects with non member projects', function () {
  150. const nonMemberProject = TestStubs.Project({id: '2'});
  151. render(<ProjectSelector {...props} nonMemberProjects={[nonMemberProject]} />, {
  152. context: routerContext,
  153. });
  154. openMenu();
  155. expect(screen.getByText("Projects I don't belong to")).toBeInTheDocument();
  156. expect(screen.getAllByTestId('badge-display-name')).toHaveLength(3);
  157. });
  158. it('displays projects in alphabetical order partitioned by project membership', function () {
  159. const projectA = TestStubs.Project({id: '1', slug: 'a-project'});
  160. const projectB = TestStubs.Project({id: '2', slug: 'b-project'});
  161. const projectANonM = TestStubs.Project({id: '3', slug: 'a-non-m-project'});
  162. const projectBNonM = TestStubs.Project({id: '4', slug: 'b-non-m-project'});
  163. const multiProjectProps = {
  164. ...props,
  165. memberProjects: [projectB, projectA],
  166. nonMemberProjects: [projectBNonM, projectANonM],
  167. value: [],
  168. };
  169. render(<ProjectSelector {...multiProjectProps} />, {context: routerContext});
  170. openMenu();
  171. expect(screen.getByText("Projects I don't belong to")).toBeInTheDocument();
  172. const projectLabels = screen.getAllByTestId('badge-display-name');
  173. expect(projectLabels).toHaveLength(4);
  174. expect(projectLabels[0]).toHaveTextContent(projectA.slug);
  175. expect(projectLabels[1]).toHaveTextContent(projectB.slug);
  176. expect(projectLabels[2]).toHaveTextContent(projectANonM.slug);
  177. expect(projectLabels[3]).toHaveTextContent(projectBNonM.slug);
  178. });
  179. it('displays multi projects in sort order rules: selected, bookmarked, alphabetical', function () {
  180. const projectA = TestStubs.Project({id: '1', slug: 'a-project'});
  181. const projectBBookmarked = TestStubs.Project({
  182. id: '2',
  183. slug: 'b-project',
  184. isBookmarked: true,
  185. });
  186. const projectCBookmarked = TestStubs.Project({
  187. id: '3',
  188. slug: 'c-project',
  189. isBookmarked: true,
  190. });
  191. const projectDSelected = TestStubs.Project({id: '4', slug: 'd-project'});
  192. const projectESelected = TestStubs.Project({id: '5', slug: 'e-project'});
  193. const projectFSelectedBookmarked = TestStubs.Project({
  194. id: '6',
  195. slug: 'f-project',
  196. isBookmarked: true,
  197. });
  198. const projectGSelectedBookmarked = TestStubs.Project({
  199. id: '7',
  200. slug: 'g-project',
  201. isBookmarked: true,
  202. });
  203. const projectH = TestStubs.Project({id: '8', slug: 'h-project'});
  204. const projectJ = TestStubs.Project({id: '9', slug: 'j-project'});
  205. const projectKSelectedBookmarked = TestStubs.Project({
  206. id: '10',
  207. slug: 'k-project',
  208. isBookmarked: true,
  209. });
  210. const projectL = TestStubs.Project({id: '11', slug: 'l-project'});
  211. const multiProjectProps = {
  212. ...props,
  213. // XXX: Intentionally sorted arbitrarily
  214. memberProjects: [
  215. projectBBookmarked,
  216. projectFSelectedBookmarked,
  217. projectDSelected,
  218. projectA,
  219. projectESelected,
  220. projectGSelectedBookmarked,
  221. projectCBookmarked,
  222. projectH,
  223. ],
  224. nonMemberProjects: [projectL, projectJ, projectKSelectedBookmarked],
  225. value: [
  226. projectESelected.id,
  227. projectDSelected.id,
  228. projectGSelectedBookmarked.id,
  229. projectFSelectedBookmarked.id,
  230. projectKSelectedBookmarked.id,
  231. ].map(p => parseInt(p, 10)),
  232. };
  233. render(<ProjectSelector {...multiProjectProps} />, {context: routerContext});
  234. openMenu();
  235. const projectLabels = screen.getAllByTestId('badge-display-name');
  236. expect(projectLabels).toHaveLength(11);
  237. // member projects
  238. expect(projectLabels[0]).toHaveTextContent(projectFSelectedBookmarked.slug);
  239. expect(projectLabels[1]).toHaveTextContent(projectGSelectedBookmarked.slug);
  240. expect(projectLabels[2]).toHaveTextContent(projectDSelected.slug);
  241. expect(projectLabels[3]).toHaveTextContent(projectESelected.slug);
  242. expect(projectLabels[4]).toHaveTextContent(projectBBookmarked.slug);
  243. expect(projectLabels[5]).toHaveTextContent(projectCBookmarked.slug);
  244. expect(projectLabels[6]).toHaveTextContent(projectA.slug);
  245. expect(projectLabels[7]).toHaveTextContent(projectH.slug);
  246. expect(projectLabels[6]).toHaveTextContent(projectA.slug);
  247. expect(projectLabels[7]).toHaveTextContent(projectH.slug);
  248. // non member projects
  249. expect(projectLabels[8]).toHaveTextContent(projectKSelectedBookmarked.slug);
  250. expect(projectLabels[9]).toHaveTextContent(projectJ.slug);
  251. expect(projectLabels[10]).toHaveTextContent(projectL.slug);
  252. });
  253. it('does not change sort order while selecting projects with the dropdown open', function () {
  254. const projectA = TestStubs.Project({id: '1', slug: 'a-project'});
  255. const projectBBookmarked = TestStubs.Project({
  256. id: '2',
  257. slug: 'b-project',
  258. isBookmarked: true,
  259. });
  260. const projectDSelected = TestStubs.Project({id: '4', slug: 'd-project'});
  261. const multiProjectProps = {
  262. ...props,
  263. // XXX: Intentionally sorted arbitrarily
  264. memberProjects: [projectBBookmarked, projectDSelected, projectA],
  265. nonMemberProjects: [],
  266. value: [projectDSelected.id].map(p => parseInt(p, 10)),
  267. };
  268. const {rerender} = render(<ProjectSelector {...multiProjectProps} />, {
  269. context: routerContext,
  270. });
  271. openMenu();
  272. const projectLabels = screen.getAllByTestId('badge-display-name');
  273. expect(projectLabels).toHaveLength(3);
  274. // member projects
  275. expect(projectLabels[0]).toHaveTextContent(projectDSelected.slug);
  276. expect(projectLabels[1]).toHaveTextContent(projectBBookmarked.slug);
  277. expect(projectLabels[2]).toHaveTextContent(projectA.slug);
  278. // Unselect project D (re-render with the updated selection value)
  279. userEvent.click(screen.getByRole('checkbox', {name: projectDSelected.slug}));
  280. rerender(<ProjectSelector {...multiProjectProps} value={[]} />, {
  281. context: routerContext,
  282. });
  283. // Project D is no longer checked
  284. expect(screen.getByRole('checkbox', {name: projectDSelected.slug})).not.toBeChecked();
  285. // Project D is still the first selected item
  286. expect(screen.getAllByTestId('badge-display-name')[0]).toHaveTextContent(
  287. projectDSelected.slug
  288. );
  289. // Open and close the menu
  290. applyMenu();
  291. openMenu();
  292. const resortedProjectLabels = screen.getAllByTestId('badge-display-name');
  293. // Project D has been moved to the bottom since it was unselected
  294. expect(resortedProjectLabels[0]).toHaveTextContent(projectBBookmarked.slug);
  295. expect(resortedProjectLabels[1]).toHaveTextContent(projectA.slug);
  296. expect(resortedProjectLabels[2]).toHaveTextContent(projectDSelected.slug);
  297. });
  298. });