organizationMemberDetail.spec.jsx 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779
  1. import selectEvent from 'react-select-event';
  2. import {
  3. cleanup,
  4. render,
  5. renderGlobalModal,
  6. screen,
  7. userEvent,
  8. within,
  9. } from 'sentry-test/reactTestingLibrary';
  10. import {updateMember} from 'sentry/actionCreators/members';
  11. import TeamStore from 'sentry/stores/teamStore';
  12. import OrganizationMemberDetail from 'sentry/views/settings/organizationMembers/organizationMemberDetail';
  13. jest.mock('sentry/actionCreators/members', () => ({
  14. updateMember: jest.fn().mockReturnValue(new Promise(() => {})),
  15. }));
  16. describe('OrganizationMemberDetail', function () {
  17. let organization;
  18. let routerContext;
  19. const team = TestStubs.Team();
  20. const idpTeam = TestStubs.Team({
  21. id: '3',
  22. slug: 'idp-member-team',
  23. name: 'Idp Member Team',
  24. isMember: true,
  25. flags: {
  26. 'idp:provisioned': true,
  27. },
  28. });
  29. const managerTeam = TestStubs.Team({id: '5', orgRole: 'manager', slug: 'manager-team'});
  30. const otherManagerTeam = TestStubs.Team({
  31. id: '4',
  32. slug: 'org-role-team',
  33. name: 'Org Role Team',
  34. isMember: true,
  35. orgRole: 'manager',
  36. });
  37. const teams = [
  38. team,
  39. TestStubs.Team({
  40. id: '2',
  41. slug: 'new-team',
  42. name: 'New Team',
  43. isMember: false,
  44. }),
  45. idpTeam,
  46. managerTeam,
  47. otherManagerTeam,
  48. ];
  49. const teamAssignment = {
  50. teams: [team.slug],
  51. teamRoles: [
  52. {
  53. teamSlug: team.slug,
  54. role: null,
  55. },
  56. ],
  57. };
  58. const member = TestStubs.Member({
  59. roles: TestStubs.OrgRoleList(),
  60. dateCreated: new Date(),
  61. ...teamAssignment,
  62. });
  63. const pendingMember = TestStubs.Member({
  64. id: 2,
  65. roles: TestStubs.OrgRoleList(),
  66. dateCreated: new Date(),
  67. ...teamAssignment,
  68. invite_link: 'http://example.com/i/abc123',
  69. pending: true,
  70. });
  71. const expiredMember = TestStubs.Member({
  72. id: 3,
  73. roles: TestStubs.OrgRoleList(),
  74. dateCreated: new Date(),
  75. ...teamAssignment,
  76. invite_link: 'http://example.com/i/abc123',
  77. pending: true,
  78. expired: true,
  79. });
  80. const idpTeamMember = TestStubs.Member({
  81. id: 4,
  82. roles: TestStubs.OrgRoleList(),
  83. dateCreated: new Date(),
  84. teams: [idpTeam.slug],
  85. teamRoles: [
  86. {
  87. teamSlug: idpTeam.slug,
  88. role: null,
  89. },
  90. ],
  91. });
  92. const managerTeamMember = TestStubs.Member({
  93. id: 5,
  94. roles: TestStubs.OrgRoleList(),
  95. dateCreated: new Date(),
  96. teams: [otherManagerTeam.slug],
  97. teamRoles: [
  98. {
  99. teamSlug: otherManagerTeam.slug,
  100. role: null,
  101. },
  102. ],
  103. });
  104. const managerMember = TestStubs.Member({
  105. id: 6,
  106. roles: TestStubs.OrgRoleList(),
  107. role: 'manager',
  108. });
  109. beforeAll(() => {
  110. TeamStore.loadInitialData(teams);
  111. });
  112. describe('Can Edit', function () {
  113. beforeEach(function () {
  114. organization = TestStubs.Organization({teams, features: ['team-roles']});
  115. routerContext = TestStubs.routerContext([{organization}]);
  116. TeamStore.init();
  117. TeamStore.loadInitialData(teams);
  118. jest.resetAllMocks();
  119. MockApiClient.clearMockResponses();
  120. MockApiClient.addMockResponse({
  121. url: `/organizations/${organization.slug}/members/${member.id}/`,
  122. body: member,
  123. });
  124. MockApiClient.addMockResponse({
  125. url: `/organizations/${organization.slug}/members/${pendingMember.id}/`,
  126. body: pendingMember,
  127. });
  128. MockApiClient.addMockResponse({
  129. url: `/organizations/${organization.slug}/members/${expiredMember.id}/`,
  130. body: expiredMember,
  131. });
  132. MockApiClient.addMockResponse({
  133. url: `/organizations/${organization.slug}/members/${idpTeamMember.id}/`,
  134. body: idpTeamMember,
  135. });
  136. MockApiClient.addMockResponse({
  137. url: `/organizations/${organization.slug}/members/${managerTeamMember.id}/`,
  138. body: managerTeamMember,
  139. });
  140. MockApiClient.addMockResponse({
  141. url: `/organizations/${organization.slug}/members/${managerMember.id}/`,
  142. body: managerMember,
  143. });
  144. MockApiClient.addMockResponse({
  145. url: `/organizations/${organization.slug}/teams/`,
  146. body: teams,
  147. });
  148. });
  149. it('changes org role to owner', async function () {
  150. render(<OrganizationMemberDetail params={{memberId: member.id}} />, {
  151. context: routerContext,
  152. });
  153. // Should have 4 roles
  154. const radios = screen.getAllByRole('radio');
  155. expect(radios).toHaveLength(4);
  156. // Click last radio
  157. await userEvent.click(radios.at(-1));
  158. expect(radios.at(-1)).toBeChecked();
  159. // Save Member
  160. await userEvent.click(screen.getByRole('button', {name: 'Save Member'}));
  161. expect(updateMember).toHaveBeenCalledWith(
  162. expect.anything(),
  163. expect.objectContaining({
  164. data: expect.objectContaining({
  165. orgRole: 'owner',
  166. }),
  167. })
  168. );
  169. });
  170. it('leaves a team', async function () {
  171. render(<OrganizationMemberDetail params={{memberId: member.id}} />, {
  172. context: routerContext,
  173. });
  174. // Remove our one team
  175. await userEvent.click(screen.getByRole('button', {name: 'Remove'}));
  176. // Save Member
  177. await userEvent.click(screen.getByRole('button', {name: 'Save Member'}));
  178. expect(updateMember).toHaveBeenCalledWith(
  179. expect.anything(),
  180. expect.objectContaining({
  181. data: expect.objectContaining({
  182. teamRoles: [],
  183. }),
  184. })
  185. );
  186. });
  187. it('cannot leave idp-provisioned team', function () {
  188. render(<OrganizationMemberDetail params={{memberId: idpTeamMember.id}} />, {
  189. context: routerContext,
  190. });
  191. expect(screen.getByRole('button', {name: 'Remove'})).toBeDisabled();
  192. });
  193. it('cannot leave org role team if missing org:admin', function () {
  194. organization = TestStubs.Organization({
  195. teams,
  196. features: ['team-roles'],
  197. access: [],
  198. });
  199. routerContext = TestStubs.routerContext([{organization}]);
  200. render(<OrganizationMemberDetail params={{memberId: managerTeamMember.id}} />, {
  201. context: routerContext,
  202. });
  203. expect(screen.getByText('Manager Team')).toBeInTheDocument();
  204. expect(screen.getByRole('button', {name: 'Remove'})).toBeDisabled();
  205. });
  206. it('cannot join org role team if missing org:admin', async function () {
  207. organization = TestStubs.Organization({
  208. teams,
  209. features: ['team-roles'],
  210. access: ['org:write'],
  211. });
  212. routerContext = TestStubs.routerContext([{organization}]);
  213. render(<OrganizationMemberDetail params={{memberId: managerMember.id}} />, {
  214. context: routerContext,
  215. });
  216. await userEvent.click(screen.getByText('Add Team'));
  217. await userEvent.hover(screen.queryByText('#org-role-team'));
  218. expect(
  219. await screen.findByText(
  220. 'Membership to a team with an organization role is managed by org owners.'
  221. )
  222. ).toBeInTheDocument();
  223. });
  224. it('joins a team and assign a team-role', async function () {
  225. render(<OrganizationMemberDetail params={{memberId: member.id}} />, {
  226. context: routerContext,
  227. });
  228. // Should have one team enabled
  229. expect(screen.getByTestId('team-row-for-member')).toBeInTheDocument();
  230. // Select new team to join
  231. // Open the dropdown
  232. await userEvent.click(screen.getByText('Add Team'));
  233. // Click the first item
  234. await userEvent.click(screen.getByText('#new-team'));
  235. // Assign as admin to new team
  236. const teamRoleSelect = screen.getAllByText('Contributor')[0];
  237. await selectEvent.select(teamRoleSelect, ['Team Admin']);
  238. // Save Member
  239. await userEvent.click(screen.getByRole('button', {name: 'Save Member'}));
  240. expect(updateMember).toHaveBeenCalledWith(
  241. expect.anything(),
  242. expect.objectContaining({
  243. data: expect.objectContaining({
  244. teamRoles: [
  245. {teamSlug: 'team-slug', role: null},
  246. {teamSlug: 'new-team', role: 'admin'},
  247. ],
  248. }),
  249. })
  250. );
  251. });
  252. it('cannot join idp-provisioned team', async function () {
  253. render(<OrganizationMemberDetail params={{memberId: member.id}} />, {
  254. context: routerContext,
  255. });
  256. await userEvent.click(screen.getByText('Add Team'));
  257. await userEvent.hover(screen.queryByText('#idp-member-team'));
  258. expect(
  259. await screen.findByText(
  260. "Membership to this team is managed through your organization's identity provider."
  261. )
  262. ).toBeInTheDocument();
  263. });
  264. it('cannot change roles if member is idp-provisioned', function () {
  265. const roleRestrictedMember = TestStubs.Member({
  266. roles: TestStubs.OrgRoleList(),
  267. dateCreated: new Date(),
  268. teams: [team.slug],
  269. flags: {
  270. 'idp:role-restricted': true,
  271. },
  272. });
  273. MockApiClient.addMockResponse({
  274. url: `/organizations/${organization.slug}/members/${member.id}/`,
  275. body: roleRestrictedMember,
  276. });
  277. render(<OrganizationMemberDetail params={{memberId: roleRestrictedMember.id}} />, {
  278. context: routerContext,
  279. });
  280. const radios = screen.getAllByRole('radio');
  281. expect(radios.at(0)).toHaveAttribute('readonly');
  282. });
  283. });
  284. describe('Cannot Edit', function () {
  285. beforeEach(function () {
  286. organization = TestStubs.Organization({teams, access: ['org:read']});
  287. routerContext = TestStubs.routerContext([{organization}]);
  288. TeamStore.init();
  289. TeamStore.loadInitialData(teams);
  290. jest.resetAllMocks();
  291. MockApiClient.clearMockResponses();
  292. MockApiClient.addMockResponse({
  293. url: `/organizations/${organization.slug}/members/${member.id}/`,
  294. body: member,
  295. });
  296. MockApiClient.addMockResponse({
  297. url: `/organizations/${organization.slug}/members/${pendingMember.id}/`,
  298. body: pendingMember,
  299. });
  300. MockApiClient.addMockResponse({
  301. url: `/organizations/${organization.slug}/members/${expiredMember.id}/`,
  302. body: expiredMember,
  303. });
  304. MockApiClient.addMockResponse({
  305. url: `/organizations/${organization.slug}/teams/`,
  306. body: teams,
  307. });
  308. });
  309. it('can not change roles, teams, or save', function () {
  310. render(<OrganizationMemberDetail params={{memberId: member.id}} />, {
  311. context: routerContext,
  312. });
  313. // Should have 4 roles
  314. const radios = screen.getAllByRole('radio');
  315. expect(radios.at(0)).toHaveAttribute('readonly');
  316. // Save Member
  317. expect(screen.getByRole('button', {name: 'Save Member'})).toBeDisabled();
  318. });
  319. });
  320. describe('Display status', function () {
  321. beforeEach(function () {
  322. organization = TestStubs.Organization({teams, access: ['org:read']});
  323. routerContext = TestStubs.routerContext([{organization}]);
  324. TeamStore.init();
  325. TeamStore.loadInitialData(teams);
  326. jest.resetAllMocks();
  327. MockApiClient.clearMockResponses();
  328. MockApiClient.addMockResponse({
  329. url: `/organizations/${organization.slug}/members/${member.id}/`,
  330. body: member,
  331. });
  332. MockApiClient.addMockResponse({
  333. url: `/organizations/${organization.slug}/members/${pendingMember.id}/`,
  334. body: pendingMember,
  335. });
  336. MockApiClient.addMockResponse({
  337. url: `/organizations/${organization.slug}/members/${expiredMember.id}/`,
  338. body: expiredMember,
  339. });
  340. MockApiClient.addMockResponse({
  341. url: `/organizations/${organization.slug}/teams/`,
  342. body: teams,
  343. });
  344. });
  345. it('display pending status', function () {
  346. render(<OrganizationMemberDetail params={{memberId: pendingMember.id}} />, {
  347. context: routerContext,
  348. });
  349. expect(screen.getByTestId('member-status')).toHaveTextContent('Invitation Pending');
  350. });
  351. it('display expired status', function () {
  352. render(<OrganizationMemberDetail params={{memberId: expiredMember.id}} />, {
  353. context: routerContext,
  354. });
  355. expect(screen.getByTestId('member-status')).toHaveTextContent('Invitation Expired');
  356. });
  357. });
  358. describe('Show resend button', function () {
  359. beforeEach(function () {
  360. organization = TestStubs.Organization({teams, access: ['org:read']});
  361. routerContext = TestStubs.routerContext([{organization}]);
  362. TeamStore.init();
  363. TeamStore.loadInitialData(teams);
  364. jest.resetAllMocks();
  365. MockApiClient.clearMockResponses();
  366. MockApiClient.addMockResponse({
  367. url: `/organizations/${organization.slug}/members/${member.id}/`,
  368. body: member,
  369. });
  370. MockApiClient.addMockResponse({
  371. url: `/organizations/${organization.slug}/members/${pendingMember.id}/`,
  372. body: pendingMember,
  373. });
  374. MockApiClient.addMockResponse({
  375. url: `/organizations/${organization.slug}/members/${expiredMember.id}/`,
  376. body: expiredMember,
  377. });
  378. MockApiClient.addMockResponse({
  379. url: `/organizations/${organization.slug}/teams/`,
  380. body: teams,
  381. });
  382. });
  383. it('shows for pending', function () {
  384. render(<OrganizationMemberDetail params={{memberId: pendingMember.id}} />, {
  385. context: routerContext,
  386. });
  387. expect(screen.getByRole('button', {name: 'Resend Invite'})).toBeInTheDocument();
  388. });
  389. it('does not show for expired', function () {
  390. render(<OrganizationMemberDetail params={{memberId: expiredMember.id}} />, {
  391. context: routerContext,
  392. });
  393. expect(
  394. screen.queryByRole('button', {name: 'Resend Invite'})
  395. ).not.toBeInTheDocument();
  396. });
  397. });
  398. describe('Reset member 2FA', function () {
  399. const fields = {
  400. roles: TestStubs.OrgRoleList(),
  401. dateCreated: new Date(),
  402. ...teamAssignment,
  403. };
  404. const noAccess = TestStubs.Member({
  405. ...fields,
  406. id: '4',
  407. user: TestStubs.User({has2fa: false}),
  408. });
  409. const no2fa = TestStubs.Member({
  410. ...fields,
  411. id: '5',
  412. user: TestStubs.User({has2fa: false, authenticators: [], canReset2fa: true}),
  413. });
  414. const has2fa = TestStubs.Member({
  415. ...fields,
  416. id: '6',
  417. user: TestStubs.User({
  418. has2fa: true,
  419. authenticators: [
  420. TestStubs.Authenticators().Totp(),
  421. TestStubs.Authenticators().Sms(),
  422. TestStubs.Authenticators().U2f(),
  423. ],
  424. canReset2fa: true,
  425. }),
  426. });
  427. const multipleOrgs = TestStubs.Member({
  428. ...fields,
  429. id: '7',
  430. user: TestStubs.User({
  431. has2fa: true,
  432. authenticators: [TestStubs.Authenticators().Totp()],
  433. canReset2fa: false,
  434. }),
  435. });
  436. beforeEach(function () {
  437. organization = TestStubs.Organization({teams});
  438. routerContext = TestStubs.routerContext([{organization}]);
  439. MockApiClient.clearMockResponses();
  440. MockApiClient.addMockResponse({
  441. url: `/organizations/${organization.slug}/members/${pendingMember.id}/`,
  442. body: pendingMember,
  443. });
  444. MockApiClient.addMockResponse({
  445. url: `/organizations/${organization.slug}/members/${noAccess.id}/`,
  446. body: noAccess,
  447. });
  448. MockApiClient.addMockResponse({
  449. url: `/organizations/${organization.slug}/members/${no2fa.id}/`,
  450. body: no2fa,
  451. });
  452. MockApiClient.addMockResponse({
  453. url: `/organizations/${organization.slug}/members/${has2fa.id}/`,
  454. body: has2fa,
  455. });
  456. MockApiClient.addMockResponse({
  457. url: `/organizations/${organization.slug}/members/${multipleOrgs.id}/`,
  458. body: multipleOrgs,
  459. });
  460. MockApiClient.addMockResponse({
  461. url: `/organizations/${organization.slug}/teams/`,
  462. body: teams,
  463. });
  464. });
  465. const button = () =>
  466. screen.queryByRole('button', {name: 'Reset two-factor authentication'});
  467. const tooltip = () => screen.queryByTestId('reset-2fa-tooltip');
  468. const expectButtonEnabled = () => {
  469. expect(button()).toHaveTextContent('Reset two-factor authentication');
  470. expect(button()).toBeEnabled();
  471. expect(tooltip()).not.toBeInTheDocument();
  472. };
  473. const expectButtonDisabled = async title => {
  474. expect(button()).toHaveTextContent('Reset two-factor authentication');
  475. expect(button()).toBeDisabled();
  476. await userEvent.hover(button());
  477. expect(await screen.findByText(title)).toBeInTheDocument();
  478. };
  479. it('does not show for pending member', function () {
  480. render(<OrganizationMemberDetail params={{memberId: pendingMember.id}} />, {
  481. context: routerContext,
  482. });
  483. expect(button()).not.toBeInTheDocument();
  484. });
  485. it('shows tooltip for joined member without permission to edit', async function () {
  486. render(<OrganizationMemberDetail params={{memberId: noAccess.id}} />, {
  487. context: routerContext,
  488. });
  489. await expectButtonDisabled('You do not have permission to perform this action');
  490. });
  491. it('shows tooltip for member without 2fa', async function () {
  492. render(<OrganizationMemberDetail params={{memberId: no2fa.id}} />, {
  493. context: routerContext,
  494. });
  495. await expectButtonDisabled('Not enrolled in two-factor authentication');
  496. });
  497. it('can reset member 2FA', async function () {
  498. const deleteMocks = has2fa.user.authenticators.map(auth =>
  499. MockApiClient.addMockResponse({
  500. url: `/users/${has2fa.user.id}/authenticators/${auth.id}/`,
  501. method: 'DELETE',
  502. })
  503. );
  504. render(<OrganizationMemberDetail params={{memberId: has2fa.id}} />, {
  505. context: routerContext,
  506. });
  507. renderGlobalModal();
  508. expectButtonEnabled();
  509. await userEvent.click(button());
  510. await userEvent.click(screen.getByRole('button', {name: 'Confirm'}));
  511. deleteMocks.forEach(deleteMock => {
  512. expect(deleteMock).toHaveBeenCalled();
  513. });
  514. });
  515. it('shows tooltip for member in multiple orgs', async function () {
  516. render(<OrganizationMemberDetail params={{memberId: multipleOrgs.id}} />, {
  517. context: routerContext,
  518. });
  519. await expectButtonDisabled(
  520. 'Cannot be reset since user is in more than one organization'
  521. );
  522. });
  523. it('shows tooltip for member in 2FA required org', async function () {
  524. organization.require2FA = true;
  525. MockApiClient.addMockResponse({
  526. url: `/organizations/${organization.slug}/members/${has2fa.id}/`,
  527. body: has2fa,
  528. });
  529. render(<OrganizationMemberDetail params={{memberId: has2fa.id}} />, {
  530. context: routerContext,
  531. });
  532. await expectButtonDisabled(
  533. 'Cannot be reset since two-factor is required for this organization'
  534. );
  535. });
  536. });
  537. describe('Org Roles affect Team Roles', () => {
  538. // Org Admin will be deprecated
  539. const admin = TestStubs.Member({
  540. id: '4',
  541. role: 'admin',
  542. roleName: 'Admin',
  543. orgRole: 'admin',
  544. ...teamAssignment,
  545. });
  546. const manager = TestStubs.Member({
  547. id: '5',
  548. role: 'manager',
  549. roleName: 'Manager',
  550. orgRole: 'manager',
  551. ...teamAssignment,
  552. });
  553. const owner = TestStubs.Member({
  554. id: '6',
  555. role: 'owner',
  556. roleName: 'Owner',
  557. orgRole: 'owner',
  558. ...teamAssignment,
  559. });
  560. beforeAll(() => {
  561. organization = TestStubs.Organization({teams, features: ['team-roles']});
  562. routerContext = TestStubs.routerContext([{organization}]);
  563. });
  564. beforeEach(() => {
  565. MockApiClient.clearMockResponses();
  566. MockApiClient.addMockResponse({
  567. url: `/organizations/${organization.slug}/members/${member.id}/`,
  568. body: member,
  569. });
  570. MockApiClient.addMockResponse({
  571. url: `/organizations/${organization.slug}/members/${admin.id}/`,
  572. body: admin,
  573. });
  574. MockApiClient.addMockResponse({
  575. url: `/organizations/${organization.slug}/members/${manager.id}/`,
  576. body: manager,
  577. });
  578. MockApiClient.addMockResponse({
  579. url: `/organizations/${organization.slug}/members/${owner.id}/`,
  580. body: owner,
  581. });
  582. });
  583. it('does not overwrite team-roles for org members', async () => {
  584. render(<OrganizationMemberDetail params={{memberId: member.id}} />, {
  585. context: routerContext,
  586. });
  587. // Role info box is hidden
  588. expect(screen.queryByTestId('alert-role-overwrite')).not.toBeInTheDocument();
  589. // Dropdown has correct value set
  590. const teamRow = screen.getByTestId('team-row-for-member');
  591. const teamRoleSelect = within(teamRow).getByText('Contributor');
  592. // Dropdown options are not visible
  593. expect(screen.queryAllByText('...').length).toBe(0);
  594. // Dropdown can be opened
  595. selectEvent.openMenu(teamRoleSelect);
  596. expect(screen.queryAllByText('...').length).toBe(2);
  597. // Dropdown value can be changed
  598. await selectEvent.select(teamRoleSelect, ['Team Admin']);
  599. expect(teamRoleSelect).toHaveTextContent('Team Admin');
  600. });
  601. it('overwrite team-roles for org admin/manager/owner', () => {
  602. function testForOrgRole(testMember) {
  603. cleanup();
  604. render(<OrganizationMemberDetail params={{memberId: testMember.id}} />, {
  605. context: routerContext,
  606. });
  607. // Role info box is showed
  608. expect(screen.queryByTestId('alert-role-overwrite')).toBeInTheDocument();
  609. // Dropdown has correct value set
  610. const teamRow = screen.getByTestId('team-row-for-member');
  611. const teamRoleSelect = within(teamRow).getByText('Team Admin');
  612. // Dropdown options are not visible
  613. expect(screen.queryAllByText('...').length).toBe(0);
  614. // Dropdown cannot be opened
  615. selectEvent.openMenu(teamRoleSelect);
  616. expect(screen.queryAllByText('...').length).toBe(0);
  617. }
  618. for (const role of [admin, manager, owner]) {
  619. testForOrgRole(role);
  620. }
  621. });
  622. it('overwrites when changing from member to manager', async () => {
  623. render(<OrganizationMemberDetail params={{memberId: member.id}} />, {
  624. context: routerContext,
  625. });
  626. // Role info box is hidden
  627. expect(screen.queryByTestId('alert-role-overwrite')).not.toBeInTheDocument();
  628. // Dropdown has correct value set
  629. const teamRow = screen.getByTestId('team-row-for-member');
  630. const teamRoleSelect = within(teamRow).getByText('Contributor');
  631. // Change member to owner
  632. const orgRoleRadio = screen.getAllByRole('radio');
  633. expect(orgRoleRadio).toHaveLength(4);
  634. await userEvent.click(orgRoleRadio.at(-1));
  635. expect(orgRoleRadio.at(-1)).toBeChecked();
  636. // Role info box is shown
  637. expect(screen.queryByTestId('alert-role-overwrite')).toBeInTheDocument();
  638. // Dropdown has correct value set
  639. within(teamRow).getByText('Team Admin');
  640. // Dropdown options are not visible
  641. expect(screen.queryAllByText('...').length).toBe(0);
  642. // Dropdown cannot be opened
  643. selectEvent.openMenu(teamRoleSelect);
  644. expect(screen.queryAllByText('...').length).toBe(0);
  645. });
  646. });
  647. it('overwrites when member joins a manager team', async () => {
  648. render(<OrganizationMemberDetail params={{memberId: member.id}} />, {
  649. context: routerContext,
  650. });
  651. // Role info box is hidden
  652. expect(screen.queryByTestId('alert-role-overwrite')).not.toBeInTheDocument();
  653. // Dropdown has correct value set
  654. const teamRow = screen.getByTestId('team-row-for-member');
  655. const teamRoleSelect = within(teamRow).getByText('Contributor');
  656. // Join manager team
  657. await userEvent.click(screen.getByText('Add Team'));
  658. // Click the first item
  659. await userEvent.click(screen.getByText('#manager-team'));
  660. // Role info box is shown
  661. expect(screen.queryByTestId('alert-role-overwrite')).toBeInTheDocument();
  662. // Dropdowns have correct value set
  663. const teamRows = screen.getAllByTestId('team-row-for-member');
  664. within(teamRows[0]).getByText('Team Admin');
  665. within(teamRows[1]).getByText('Team Admin');
  666. // Dropdown options are not visible
  667. expect(screen.queryAllByText('...').length).toBe(0);
  668. // Dropdown cannot be opened
  669. selectEvent.openMenu(teamRoleSelect);
  670. expect(screen.queryAllByText('...').length).toBe(0);
  671. });
  672. });