contextPickerModal.spec.tsx 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. import selectEvent from 'react-select-event';
  2. import {Organization} from 'sentry-fixture/organization';
  3. import {render, screen, waitFor} from 'sentry-test/reactTestingLibrary';
  4. import ContextPickerModal from 'sentry/components/contextPickerModal';
  5. import {
  6. makeCloseButton,
  7. ModalBody,
  8. ModalFooter,
  9. } from 'sentry/components/globalModal/components';
  10. import ConfigStore from 'sentry/stores/configStore';
  11. import OrganizationsStore from 'sentry/stores/organizationsStore';
  12. import OrganizationStore from 'sentry/stores/organizationStore';
  13. import ProjectsStore from 'sentry/stores/projectsStore';
  14. describe('ContextPickerModal', function () {
  15. let project, project2, project4, org, org2;
  16. const onFinish = jest.fn();
  17. beforeEach(function () {
  18. ProjectsStore.reset();
  19. MockApiClient.clearMockResponses();
  20. project = TestStubs.Project();
  21. org = Organization({projects: [project]});
  22. project2 = TestStubs.Project({slug: 'project2'});
  23. org2 = Organization({
  24. slug: 'org2',
  25. id: '21',
  26. });
  27. project4 = TestStubs.Project({slug: 'project4', isMember: false});
  28. OrganizationsStore.load([]);
  29. OrganizationStore.reset();
  30. jest.clearAllMocks();
  31. });
  32. const getComponent = (props = {}) => (
  33. <ContextPickerModal
  34. Header={() => <div />}
  35. Body={ModalBody}
  36. nextPath="/test/:orgId/path/"
  37. needOrg
  38. onFinish={onFinish}
  39. needProject={false}
  40. CloseButton={makeCloseButton(() => {})}
  41. Footer={ModalFooter}
  42. closeModal={jest.fn()}
  43. {...props}
  44. />
  45. );
  46. it('renders with only org selector when no org is selected', function () {
  47. render(getComponent());
  48. expect(screen.getByText('Select an Organization')).toBeInTheDocument();
  49. expect(screen.queryByText('Select a Project to continue')).not.toBeInTheDocument();
  50. });
  51. it('calls onFinish, if project id is not needed, and only 1 org', async function () {
  52. OrganizationsStore.load([org2]);
  53. OrganizationStore.onUpdate(org2);
  54. MockApiClient.addMockResponse({
  55. url: `/organizations/${org2.slug}/projects/`,
  56. body: [],
  57. });
  58. render(getComponent());
  59. await waitFor(() => {
  60. expect(onFinish).toHaveBeenCalledWith('/test/org2/path/');
  61. });
  62. });
  63. it('calls onFinish if there is only 1 org and 1 project', async function () {
  64. OrganizationsStore.load([org2]);
  65. OrganizationStore.onUpdate(org2);
  66. const fetchProjectsForOrg = MockApiClient.addMockResponse({
  67. url: `/organizations/${org2.slug}/projects/`,
  68. body: [project2],
  69. });
  70. render(
  71. getComponent({
  72. needOrg: true,
  73. needProject: true,
  74. nextPath: '/test/:orgId/path/:projectId/',
  75. })
  76. );
  77. await waitFor(() => {
  78. expect(fetchProjectsForOrg).toHaveBeenCalled();
  79. expect(onFinish).toHaveBeenLastCalledWith('/test/org2/path/project2/');
  80. });
  81. });
  82. it('selects an org and calls `onFinish` with URL with organization slug', async function () {
  83. OrganizationsStore.load([org]);
  84. render(getComponent());
  85. MockApiClient.addMockResponse({
  86. url: `/organizations/${org.slug}/projects/`,
  87. body: [],
  88. });
  89. await selectEvent.select(screen.getByText('Select an Organization'), 'org-slug');
  90. await waitFor(() => {
  91. expect(onFinish).toHaveBeenCalledWith('/test/org-slug/path/');
  92. });
  93. });
  94. it('renders with project selector and org selector selected when org is already selected', async function () {
  95. OrganizationsStore.load([org]);
  96. OrganizationStore.onUpdate(org);
  97. const fetchProjectsForOrg = MockApiClient.addMockResponse({
  98. url: `/organizations/${org.slug}/projects/`,
  99. body: [project, project2, project4],
  100. });
  101. render(
  102. getComponent({
  103. needOrg: true,
  104. needProject: true,
  105. })
  106. );
  107. await waitFor(() => {
  108. expect(fetchProjectsForOrg).toHaveBeenCalled();
  109. });
  110. // Default to org in latest context
  111. // Should see 1 selected, and 1 as an option
  112. expect(screen.getAllByText('org-slug')).toHaveLength(2);
  113. expect(screen.getByText('My Projects')).toBeInTheDocument();
  114. expect(screen.getByText(project.slug)).toBeInTheDocument();
  115. expect(screen.getByText(project2.slug)).toBeInTheDocument();
  116. expect(screen.getByText('All Projects')).toBeInTheDocument();
  117. expect(screen.getByText(project4.slug)).toBeInTheDocument();
  118. });
  119. it('can select org and project', async function () {
  120. const organizations = [
  121. {
  122. ...org,
  123. projects: [project],
  124. },
  125. {
  126. ...org2,
  127. projects: [project2, TestStubs.Project({slug: 'project3'})],
  128. },
  129. ];
  130. const fetchProjectsForOrg = MockApiClient.addMockResponse({
  131. url: `/organizations/${org2.slug}/projects/`,
  132. body: organizations[1].projects,
  133. });
  134. OrganizationsStore.load(organizations);
  135. render(
  136. getComponent({
  137. needOrg: true,
  138. needProject: true,
  139. nextPath: '/test/:orgId/path/:projectId/',
  140. organizations,
  141. })
  142. );
  143. // Should not have anything selected
  144. expect(screen.getByText('Select an Organization')).toBeInTheDocument();
  145. // Select org2
  146. await selectEvent.select(screen.getByText('Select an Organization'), org2.slug);
  147. // <Projects> will fetch projects for org2
  148. expect(fetchProjectsForOrg).toHaveBeenCalled();
  149. expect(screen.getByText('My Projects')).toBeInTheDocument();
  150. expect(screen.getByText(project2.slug)).toBeInTheDocument();
  151. expect(screen.getByText('project3')).toBeInTheDocument();
  152. expect(screen.queryByText('All Projects')).not.toBeInTheDocument();
  153. // Select project3
  154. await selectEvent.select(screen.getByText(/Select a Project/), 'project3');
  155. expect(onFinish).toHaveBeenCalledWith('/test/org2/path/project3/');
  156. });
  157. it('isSuperUser and selects an integrationConfig and calls `onFinish` with URL to that configuration', async function () {
  158. OrganizationsStore.load([org]);
  159. OrganizationStore.onUpdate(org);
  160. ConfigStore.config.user = TestStubs.User({isSuperuser: true});
  161. const provider = {slug: 'github'};
  162. const configUrl = `/api/0/organizations/${org.slug}/integrations/?provider_key=${provider.slug}&includeConfig=0`;
  163. const integration = TestStubs.GitHubIntegration();
  164. const fetchGithubConfigs = MockApiClient.addMockResponse({
  165. url: configUrl,
  166. body: [integration],
  167. });
  168. MockApiClient.addMockResponse({
  169. url: `/organizations/${org.slug}/projects/`,
  170. body: [],
  171. });
  172. render(
  173. getComponent({
  174. needOrg: false,
  175. needProject: false,
  176. nextPath: `/settings/${org.slug}/integrations/${provider.slug}/`,
  177. configUrl,
  178. })
  179. );
  180. await waitFor(() => {
  181. expect(fetchGithubConfigs).toHaveBeenCalled();
  182. });
  183. await selectEvent.select(
  184. screen.getByText(/Select a configuration/i),
  185. integration.domainName
  186. );
  187. expect(onFinish).toHaveBeenCalledWith(
  188. `/settings/${org.slug}/integrations/github/${integration.id}/`
  189. );
  190. });
  191. it('not superUser and cannot select an integrationConfig and calls `onFinish` with URL to integration overview page', async function () {
  192. OrganizationsStore.load([org]);
  193. OrganizationStore.onUpdate(org);
  194. ConfigStore.config.user = TestStubs.User({isSuperuser: false});
  195. const provider = {slug: 'github'};
  196. const configUrl = `/api/0/organizations/${org.slug}/integrations/?provider_key=${provider.slug}&includeConfig=0`;
  197. const fetchGithubConfigs = MockApiClient.addMockResponse({
  198. url: configUrl,
  199. body: [TestStubs.GitHubIntegration()],
  200. });
  201. MockApiClient.addMockResponse({
  202. url: `/organizations/${org.slug}/projects/`,
  203. body: [],
  204. });
  205. render(
  206. getComponent({
  207. needOrg: false,
  208. needProject: false,
  209. nextPath: `/settings/${org.slug}/integrations/${provider.slug}/`,
  210. configUrl,
  211. })
  212. );
  213. await waitFor(() => {
  214. expect(fetchGithubConfigs).toHaveBeenCalled();
  215. });
  216. });
  217. it('is superUser and no integration configurations and calls `onFinish` with URL to integration overview page', async function () {
  218. OrganizationsStore.load([org]);
  219. OrganizationStore.onUpdate(org);
  220. ConfigStore.config = TestStubs.User({isSuperuser: false});
  221. const provider = {slug: 'github'};
  222. const configUrl = `/api/0/organizations/${org.slug}/integrations/?provider_key=${provider.slug}&includeConfig=0`;
  223. const fetchGithubConfigs = MockApiClient.addMockResponse({
  224. url: configUrl,
  225. body: [],
  226. });
  227. MockApiClient.addMockResponse({
  228. url: `/organizations/${org.slug}/projects/`,
  229. body: [],
  230. });
  231. render(
  232. getComponent({
  233. needOrg: false,
  234. needProject: false,
  235. nextPath: `/settings/${org.slug}/integrations/${provider.slug}/`,
  236. configUrl,
  237. })
  238. );
  239. await waitFor(() => {
  240. expect(fetchGithubConfigs).toHaveBeenCalled();
  241. });
  242. expect(onFinish).toHaveBeenCalledWith(`/settings/${org.slug}/integrations/github/`);
  243. });
  244. });