projectTeams.spec.jsx 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. import {mountWithTheme} from 'sentry-test/enzyme';
  2. import {mountGlobalModal} from 'sentry-test/modal';
  3. import {act} from 'sentry-test/reactTestingLibrary';
  4. import * as modals from 'sentry/actionCreators/modal';
  5. import TeamStore from 'sentry/stores/teamStore';
  6. import App from 'sentry/views/app';
  7. import ProjectTeams from 'sentry/views/settings/project/projectTeams';
  8. jest.unmock('sentry/actionCreators/modal');
  9. describe('ProjectTeams', function () {
  10. let org;
  11. let project;
  12. const team1 = TestStubs.Team();
  13. const team2 = TestStubs.Team({
  14. id: '2',
  15. slug: 'team-slug-2',
  16. name: 'Team Name 2',
  17. hasAccess: true,
  18. });
  19. beforeEach(function () {
  20. jest.spyOn(modals, 'openCreateTeamModal');
  21. org = TestStubs.Organization();
  22. project = TestStubs.ProjectDetails();
  23. act(() => void TeamStore.loadInitialData([team1, team2]));
  24. MockApiClient.addMockResponse({
  25. url: `/projects/${org.slug}/${project.slug}/`,
  26. method: 'GET',
  27. body: project,
  28. });
  29. MockApiClient.addMockResponse({
  30. url: `/projects/${org.slug}/${project.slug}/teams/`,
  31. method: 'GET',
  32. body: [team1],
  33. });
  34. MockApiClient.addMockResponse({
  35. url: `/organizations/${org.slug}/teams/`,
  36. method: 'GET',
  37. body: [team1, team2],
  38. });
  39. });
  40. afterEach(function () {
  41. MockApiClient.clearMockResponses();
  42. modals.openCreateTeamModal.mockRestore();
  43. });
  44. it('renders', function () {
  45. const wrapper = mountWithTheme(
  46. <ProjectTeams
  47. params={{orgId: org.slug, projectId: project.slug}}
  48. organization={org}
  49. />
  50. );
  51. // Wait for team list to fetch.
  52. wrapper.update();
  53. expect(wrapper).toSnapshot();
  54. });
  55. it('can remove a team from project', async function () {
  56. MockApiClient.addMockResponse({
  57. url: `/projects/${org.slug}/${project.slug}/teams/`,
  58. method: 'GET',
  59. body: [team1, team2],
  60. });
  61. const endpoint = `/projects/${org.slug}/${project.slug}/teams/${team1.slug}/`;
  62. const mock = MockApiClient.addMockResponse({
  63. url: endpoint,
  64. method: 'DELETE',
  65. statusCode: 200,
  66. });
  67. const endpoint2 = `/projects/${org.slug}/${project.slug}/teams/${team2.slug}/`;
  68. const mock2 = MockApiClient.addMockResponse({
  69. url: endpoint2,
  70. method: 'DELETE',
  71. statusCode: 200,
  72. });
  73. const wrapper = mountWithTheme(
  74. <ProjectTeams
  75. params={{orgId: org.slug, projectId: project.slug}}
  76. organization={org}
  77. />
  78. );
  79. // Wait for team list to fetch.
  80. wrapper.update();
  81. expect(mock).not.toHaveBeenCalled();
  82. // Click "Remove"
  83. wrapper.find('PanelBody Button').first().simulate('click');
  84. expect(mock).toHaveBeenCalledWith(
  85. endpoint,
  86. expect.objectContaining({
  87. method: 'DELETE',
  88. })
  89. );
  90. await tick();
  91. // Remove second team
  92. wrapper.update().find('PanelBody Button').first().simulate('click');
  93. // Modal opens because this is the last team in project
  94. // Click confirm
  95. const modal = await mountGlobalModal();
  96. modal.find('Button[priority="primary"]').simulate('click');
  97. expect(mock2).toHaveBeenCalledWith(
  98. endpoint2,
  99. expect.objectContaining({
  100. method: 'DELETE',
  101. })
  102. );
  103. });
  104. it('removes team from project when project team is not in org list', async function () {
  105. MockApiClient.clearMockResponses();
  106. MockApiClient.addMockResponse({
  107. url: `/projects/${org.slug}/${project.slug}/teams/`,
  108. method: 'GET',
  109. body: [team1, team2],
  110. });
  111. const endpoint = `/projects/${org.slug}/${project.slug}/teams/${team1.slug}/`;
  112. const mock = MockApiClient.addMockResponse({
  113. url: endpoint,
  114. method: 'DELETE',
  115. });
  116. const endpoint2 = `/projects/${org.slug}/${project.slug}/teams/${team2.slug}/`;
  117. const mock2 = MockApiClient.addMockResponse({
  118. url: endpoint2,
  119. method: 'DELETE',
  120. });
  121. MockApiClient.addMockResponse({
  122. url: `/organizations/${org.slug}/teams/`,
  123. method: 'GET',
  124. body: [
  125. TestStubs.Team({
  126. id: '3',
  127. slug: 'team-slug-3',
  128. name: 'Team Name 3',
  129. hasAccess: true,
  130. }),
  131. ],
  132. });
  133. const wrapper = mountWithTheme(
  134. <ProjectTeams
  135. params={{orgId: org.slug, projectId: project.slug}}
  136. organization={org}
  137. />
  138. );
  139. // Wait for team list to fetch.
  140. wrapper.update();
  141. expect(mock).not.toHaveBeenCalled();
  142. // Click "Remove"
  143. wrapper.find('PanelBody Button').first().simulate('click');
  144. expect(mock).toHaveBeenCalledWith(
  145. endpoint,
  146. expect.objectContaining({
  147. method: 'DELETE',
  148. })
  149. );
  150. await tick();
  151. // Remove second team
  152. wrapper.update().find('PanelBody Button').first().simulate('click');
  153. // Modal opens because this is the last team in project
  154. // Click confirm
  155. const modal = await mountGlobalModal();
  156. modal.find('Button[priority="primary"]').simulate('click');
  157. expect(mock2).toHaveBeenCalledWith(
  158. endpoint2,
  159. expect.objectContaining({
  160. method: 'DELETE',
  161. })
  162. );
  163. });
  164. it('can associate a team with project', function () {
  165. const endpoint = `/projects/${org.slug}/${project.slug}/teams/${team2.slug}/`;
  166. const mock = MockApiClient.addMockResponse({
  167. url: endpoint,
  168. method: 'POST',
  169. statusCode: 200,
  170. });
  171. const wrapper = mountWithTheme(
  172. <ProjectTeams
  173. params={{orgId: org.slug, projectId: project.slug}}
  174. organization={org}
  175. />
  176. );
  177. // Wait for team list to fetch.
  178. wrapper.update();
  179. expect(mock).not.toHaveBeenCalled();
  180. // open dropdown
  181. wrapper.find('DropdownButton').simulate('click');
  182. // click a team
  183. const el = wrapper.find('AutoCompleteItem').first();
  184. el.simulate('click');
  185. expect(mock).toHaveBeenCalledWith(
  186. endpoint,
  187. expect.objectContaining({
  188. method: 'POST',
  189. })
  190. );
  191. });
  192. it('creates a new team adds it to current project using the "create team modal" in dropdown', async function () {
  193. MockApiClient.addMockResponse({
  194. url: '/internal/health/',
  195. body: {},
  196. });
  197. MockApiClient.addMockResponse({
  198. url: '/assistant/',
  199. body: {},
  200. });
  201. MockApiClient.addMockResponse({
  202. url: '/organizations/',
  203. body: [org],
  204. });
  205. const addTeamToProject = MockApiClient.addMockResponse({
  206. url: `/projects/${org.slug}/${project.slug}/teams/new-team/`,
  207. method: 'POST',
  208. });
  209. const createTeam = MockApiClient.addMockResponse({
  210. url: `/organizations/${org.slug}/teams/`,
  211. method: 'POST',
  212. body: {slug: 'new-team'},
  213. });
  214. const wrapper = mountWithTheme(
  215. <App params={{orgId: org.slug}}>
  216. <ProjectTeams
  217. params={{orgId: org.slug, projectId: project.slug}}
  218. project={project}
  219. organization={org}
  220. />
  221. </App>
  222. );
  223. // Wait for team list to fetch.
  224. wrapper.update();
  225. // Open the dropdown
  226. wrapper.find('TeamSelect DropdownButton').simulate('click');
  227. // Click "Create Team" inside of dropdown
  228. wrapper.find('TeamSelect StyledCreateTeamLink').simulate('click');
  229. // action creator to open "create team modal" is called
  230. expect(modals.openCreateTeamModal).toHaveBeenCalledWith(
  231. expect.objectContaining({
  232. project: expect.objectContaining({
  233. slug: project.slug,
  234. }),
  235. organization: expect.objectContaining({
  236. slug: org.slug,
  237. }),
  238. })
  239. );
  240. // Two ticks are required
  241. await act(tick);
  242. await act(tick);
  243. wrapper.update();
  244. wrapper.find('input[name="slug"]').simulate('change', {target: {value: 'new-team'}});
  245. wrapper.find('[data-test-id="create-team-form"] form').simulate('submit');
  246. expect(createTeam).toHaveBeenCalledTimes(1);
  247. expect(createTeam).toHaveBeenCalledWith(
  248. '/organizations/org-slug/teams/',
  249. expect.objectContaining({
  250. data: {slug: 'new-team'},
  251. })
  252. );
  253. await tick();
  254. expect(addTeamToProject).toHaveBeenCalledTimes(1);
  255. expect(addTeamToProject).toHaveBeenCalledWith(
  256. '/projects/org-slug/project-slug/teams/new-team/',
  257. expect.anything()
  258. );
  259. });
  260. });