organizationMemberDetail.spec.jsx 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758
  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. });
  265. describe('Cannot Edit', function () {
  266. beforeEach(function () {
  267. organization = TestStubs.Organization({teams, access: ['org:read']});
  268. routerContext = TestStubs.routerContext([{organization}]);
  269. TeamStore.init();
  270. TeamStore.loadInitialData(teams);
  271. jest.resetAllMocks();
  272. MockApiClient.clearMockResponses();
  273. MockApiClient.addMockResponse({
  274. url: `/organizations/${organization.slug}/members/${member.id}/`,
  275. body: member,
  276. });
  277. MockApiClient.addMockResponse({
  278. url: `/organizations/${organization.slug}/members/${pendingMember.id}/`,
  279. body: pendingMember,
  280. });
  281. MockApiClient.addMockResponse({
  282. url: `/organizations/${organization.slug}/members/${expiredMember.id}/`,
  283. body: expiredMember,
  284. });
  285. MockApiClient.addMockResponse({
  286. url: `/organizations/${organization.slug}/teams/`,
  287. body: teams,
  288. });
  289. });
  290. it('can not change roles, teams, or save', function () {
  291. render(<OrganizationMemberDetail params={{memberId: member.id}} />, {
  292. context: routerContext,
  293. });
  294. // Should have 4 roles
  295. const radios = screen.getAllByRole('radio');
  296. expect(radios.at(0)).toHaveAttribute('readonly');
  297. // Save Member
  298. expect(screen.getByRole('button', {name: 'Save Member'})).toBeDisabled();
  299. });
  300. });
  301. describe('Display status', function () {
  302. beforeEach(function () {
  303. organization = TestStubs.Organization({teams, access: ['org:read']});
  304. routerContext = TestStubs.routerContext([{organization}]);
  305. TeamStore.init();
  306. TeamStore.loadInitialData(teams);
  307. jest.resetAllMocks();
  308. MockApiClient.clearMockResponses();
  309. MockApiClient.addMockResponse({
  310. url: `/organizations/${organization.slug}/members/${member.id}/`,
  311. body: member,
  312. });
  313. MockApiClient.addMockResponse({
  314. url: `/organizations/${organization.slug}/members/${pendingMember.id}/`,
  315. body: pendingMember,
  316. });
  317. MockApiClient.addMockResponse({
  318. url: `/organizations/${organization.slug}/members/${expiredMember.id}/`,
  319. body: expiredMember,
  320. });
  321. MockApiClient.addMockResponse({
  322. url: `/organizations/${organization.slug}/teams/`,
  323. body: teams,
  324. });
  325. });
  326. it('display pending status', function () {
  327. render(<OrganizationMemberDetail params={{memberId: pendingMember.id}} />, {
  328. context: routerContext,
  329. });
  330. expect(screen.getByTestId('member-status')).toHaveTextContent('Invitation Pending');
  331. });
  332. it('display expired status', function () {
  333. render(<OrganizationMemberDetail params={{memberId: expiredMember.id}} />, {
  334. context: routerContext,
  335. });
  336. expect(screen.getByTestId('member-status')).toHaveTextContent('Invitation Expired');
  337. });
  338. });
  339. describe('Show resend button', function () {
  340. beforeEach(function () {
  341. organization = TestStubs.Organization({teams, access: ['org:read']});
  342. routerContext = TestStubs.routerContext([{organization}]);
  343. TeamStore.init();
  344. TeamStore.loadInitialData(teams);
  345. jest.resetAllMocks();
  346. MockApiClient.clearMockResponses();
  347. MockApiClient.addMockResponse({
  348. url: `/organizations/${organization.slug}/members/${member.id}/`,
  349. body: member,
  350. });
  351. MockApiClient.addMockResponse({
  352. url: `/organizations/${organization.slug}/members/${pendingMember.id}/`,
  353. body: pendingMember,
  354. });
  355. MockApiClient.addMockResponse({
  356. url: `/organizations/${organization.slug}/members/${expiredMember.id}/`,
  357. body: expiredMember,
  358. });
  359. MockApiClient.addMockResponse({
  360. url: `/organizations/${organization.slug}/teams/`,
  361. body: teams,
  362. });
  363. });
  364. it('shows for pending', function () {
  365. render(<OrganizationMemberDetail params={{memberId: pendingMember.id}} />, {
  366. context: routerContext,
  367. });
  368. expect(screen.getByRole('button', {name: 'Resend Invite'})).toBeInTheDocument();
  369. });
  370. it('does not show for expired', function () {
  371. render(<OrganizationMemberDetail params={{memberId: expiredMember.id}} />, {
  372. context: routerContext,
  373. });
  374. expect(
  375. screen.queryByRole('button', {name: 'Resend Invite'})
  376. ).not.toBeInTheDocument();
  377. });
  378. });
  379. describe('Reset member 2FA', function () {
  380. const fields = {
  381. roles: TestStubs.OrgRoleList(),
  382. dateCreated: new Date(),
  383. ...teamAssignment,
  384. };
  385. const noAccess = TestStubs.Member({
  386. ...fields,
  387. id: '4',
  388. user: TestStubs.User({has2fa: false, authenticators: undefined}),
  389. });
  390. const no2fa = TestStubs.Member({
  391. ...fields,
  392. id: '5',
  393. user: TestStubs.User({has2fa: false, authenticators: [], canReset2fa: true}),
  394. });
  395. const has2fa = TestStubs.Member({
  396. ...fields,
  397. id: '6',
  398. user: TestStubs.User({
  399. has2fa: true,
  400. authenticators: [
  401. TestStubs.Authenticators().Totp(),
  402. TestStubs.Authenticators().Sms(),
  403. TestStubs.Authenticators().U2f(),
  404. ],
  405. canReset2fa: true,
  406. }),
  407. });
  408. const multipleOrgs = TestStubs.Member({
  409. ...fields,
  410. id: '7',
  411. user: TestStubs.User({
  412. has2fa: true,
  413. authenticators: [TestStubs.Authenticators().Totp()],
  414. canReset2fa: false,
  415. }),
  416. });
  417. beforeEach(function () {
  418. organization = TestStubs.Organization({teams});
  419. routerContext = TestStubs.routerContext([{organization}]);
  420. MockApiClient.clearMockResponses();
  421. MockApiClient.addMockResponse({
  422. url: `/organizations/${organization.slug}/members/${pendingMember.id}/`,
  423. body: pendingMember,
  424. });
  425. MockApiClient.addMockResponse({
  426. url: `/organizations/${organization.slug}/members/${noAccess.id}/`,
  427. body: noAccess,
  428. });
  429. MockApiClient.addMockResponse({
  430. url: `/organizations/${organization.slug}/members/${no2fa.id}/`,
  431. body: no2fa,
  432. });
  433. MockApiClient.addMockResponse({
  434. url: `/organizations/${organization.slug}/members/${has2fa.id}/`,
  435. body: has2fa,
  436. });
  437. MockApiClient.addMockResponse({
  438. url: `/organizations/${organization.slug}/members/${multipleOrgs.id}/`,
  439. body: multipleOrgs,
  440. });
  441. MockApiClient.addMockResponse({
  442. url: `/organizations/${organization.slug}/teams/`,
  443. body: teams,
  444. });
  445. });
  446. const button = () =>
  447. screen.queryByRole('button', {name: 'Reset two-factor authentication'});
  448. const tooltip = () => screen.queryByTestId('reset-2fa-tooltip');
  449. const expectButtonEnabled = () => {
  450. expect(button()).toHaveTextContent('Reset two-factor authentication');
  451. expect(button()).toBeEnabled();
  452. expect(tooltip()).not.toBeInTheDocument();
  453. };
  454. const expectButtonDisabled = async title => {
  455. expect(button()).toHaveTextContent('Reset two-factor authentication');
  456. expect(button()).toBeDisabled();
  457. await userEvent.hover(button());
  458. expect(await screen.findByText(title)).toBeInTheDocument();
  459. };
  460. it('does not show for pending member', function () {
  461. render(<OrganizationMemberDetail params={{memberId: pendingMember.id}} />, {
  462. context: routerContext,
  463. });
  464. expect(button()).not.toBeInTheDocument();
  465. });
  466. it('shows tooltip for joined member without permission to edit', async function () {
  467. render(<OrganizationMemberDetail params={{memberId: noAccess.id}} />, {
  468. context: routerContext,
  469. });
  470. await expectButtonDisabled('You do not have permission to perform this action');
  471. });
  472. it('shows tooltip for member without 2fa', async function () {
  473. render(<OrganizationMemberDetail params={{memberId: no2fa.id}} />, {
  474. context: routerContext,
  475. });
  476. await expectButtonDisabled('Not enrolled in two-factor authentication');
  477. });
  478. it('can reset member 2FA', async function () {
  479. const deleteMocks = has2fa.user.authenticators.map(auth =>
  480. MockApiClient.addMockResponse({
  481. url: `/users/${has2fa.user.id}/authenticators/${auth.id}/`,
  482. method: 'DELETE',
  483. })
  484. );
  485. render(<OrganizationMemberDetail params={{memberId: has2fa.id}} />, {
  486. context: routerContext,
  487. });
  488. renderGlobalModal();
  489. expectButtonEnabled();
  490. await userEvent.click(button());
  491. await userEvent.click(screen.getByRole('button', {name: 'Confirm'}));
  492. deleteMocks.forEach(deleteMock => {
  493. expect(deleteMock).toHaveBeenCalled();
  494. });
  495. });
  496. it('shows tooltip for member in multiple orgs', async function () {
  497. render(<OrganizationMemberDetail params={{memberId: multipleOrgs.id}} />, {
  498. context: routerContext,
  499. });
  500. await expectButtonDisabled(
  501. 'Cannot be reset since user is in more than one organization'
  502. );
  503. });
  504. it('shows tooltip for member in 2FA required org', async function () {
  505. organization.require2FA = true;
  506. MockApiClient.addMockResponse({
  507. url: `/organizations/${organization.slug}/members/${has2fa.id}/`,
  508. body: has2fa,
  509. });
  510. render(<OrganizationMemberDetail params={{memberId: has2fa.id}} />, {
  511. context: routerContext,
  512. });
  513. await expectButtonDisabled(
  514. 'Cannot be reset since two-factor is required for this organization'
  515. );
  516. });
  517. });
  518. describe('Org Roles affect Team Roles', () => {
  519. // Org Admin will be deprecated
  520. const admin = TestStubs.Member({
  521. id: '4',
  522. role: 'admin',
  523. roleName: 'Admin',
  524. orgRole: 'admin',
  525. ...teamAssignment,
  526. });
  527. const manager = TestStubs.Member({
  528. id: '5',
  529. role: 'manager',
  530. roleName: 'Manager',
  531. orgRole: 'manager',
  532. ...teamAssignment,
  533. });
  534. const owner = TestStubs.Member({
  535. id: '6',
  536. role: 'owner',
  537. roleName: 'Owner',
  538. orgRole: 'owner',
  539. ...teamAssignment,
  540. });
  541. beforeAll(() => {
  542. organization = TestStubs.Organization({teams, features: ['team-roles']});
  543. routerContext = TestStubs.routerContext([{organization}]);
  544. });
  545. beforeEach(() => {
  546. MockApiClient.clearMockResponses();
  547. MockApiClient.addMockResponse({
  548. url: `/organizations/${organization.slug}/members/${member.id}/`,
  549. body: member,
  550. });
  551. MockApiClient.addMockResponse({
  552. url: `/organizations/${organization.slug}/members/${admin.id}/`,
  553. body: admin,
  554. });
  555. MockApiClient.addMockResponse({
  556. url: `/organizations/${organization.slug}/members/${manager.id}/`,
  557. body: manager,
  558. });
  559. MockApiClient.addMockResponse({
  560. url: `/organizations/${organization.slug}/members/${owner.id}/`,
  561. body: owner,
  562. });
  563. });
  564. it('does not overwrite team-roles for org members', async () => {
  565. render(<OrganizationMemberDetail params={{memberId: member.id}} />, {
  566. context: routerContext,
  567. });
  568. // Role info box is hidden
  569. expect(screen.queryByTestId('alert-role-overwrite')).not.toBeInTheDocument();
  570. // Dropdown has correct value set
  571. const teamRow = screen.getByTestId('team-row-for-member');
  572. const teamRoleSelect = within(teamRow).getByText('Contributor');
  573. // Dropdown options are not visible
  574. expect(screen.queryAllByText('...').length).toBe(0);
  575. // Dropdown can be opened
  576. selectEvent.openMenu(teamRoleSelect);
  577. expect(screen.queryAllByText('...').length).toBe(2);
  578. // Dropdown value can be changed
  579. await selectEvent.select(teamRoleSelect, ['Team Admin']);
  580. expect(teamRoleSelect).toHaveTextContent('Team Admin');
  581. });
  582. it('overwrite team-roles for org admin/manager/owner', () => {
  583. function testForOrgRole(testMember) {
  584. cleanup();
  585. render(<OrganizationMemberDetail params={{memberId: testMember.id}} />, {
  586. context: routerContext,
  587. });
  588. // Role info box is showed
  589. expect(screen.queryByTestId('alert-role-overwrite')).toBeInTheDocument();
  590. // Dropdown has correct value set
  591. const teamRow = screen.getByTestId('team-row-for-member');
  592. const teamRoleSelect = within(teamRow).getByText('Team Admin');
  593. // Dropdown options are not visible
  594. expect(screen.queryAllByText('...').length).toBe(0);
  595. // Dropdown cannot be opened
  596. selectEvent.openMenu(teamRoleSelect);
  597. expect(screen.queryAllByText('...').length).toBe(0);
  598. }
  599. for (const role of [admin, manager, owner]) {
  600. testForOrgRole(role);
  601. }
  602. });
  603. it('overwrites when changing from member to manager', async () => {
  604. render(<OrganizationMemberDetail params={{memberId: member.id}} />, {
  605. context: routerContext,
  606. });
  607. // Role info box is hidden
  608. expect(screen.queryByTestId('alert-role-overwrite')).not.toBeInTheDocument();
  609. // Dropdown has correct value set
  610. const teamRow = screen.getByTestId('team-row-for-member');
  611. const teamRoleSelect = within(teamRow).getByText('Contributor');
  612. // Change member to owner
  613. const orgRoleRadio = screen.getAllByRole('radio');
  614. expect(orgRoleRadio).toHaveLength(4);
  615. await userEvent.click(orgRoleRadio.at(-1));
  616. expect(orgRoleRadio.at(-1)).toBeChecked();
  617. // Role info box is shown
  618. expect(screen.queryByTestId('alert-role-overwrite')).toBeInTheDocument();
  619. // Dropdown has correct value set
  620. within(teamRow).getByText('Team Admin');
  621. // Dropdown options are not visible
  622. expect(screen.queryAllByText('...').length).toBe(0);
  623. // Dropdown cannot be opened
  624. selectEvent.openMenu(teamRoleSelect);
  625. expect(screen.queryAllByText('...').length).toBe(0);
  626. });
  627. });
  628. it('overwrites when member joins a manager team', async () => {
  629. render(<OrganizationMemberDetail params={{memberId: member.id}} />, {
  630. context: routerContext,
  631. });
  632. // Role info box is hidden
  633. expect(screen.queryByTestId('alert-role-overwrite')).not.toBeInTheDocument();
  634. // Dropdown has correct value set
  635. const teamRow = screen.getByTestId('team-row-for-member');
  636. const teamRoleSelect = within(teamRow).getByText('Contributor');
  637. // Join manager team
  638. await userEvent.click(screen.getByText('Add Team'));
  639. // Click the first item
  640. await userEvent.click(screen.getByText('#manager-team'));
  641. // Role info box is shown
  642. expect(screen.queryByTestId('alert-role-overwrite')).toBeInTheDocument();
  643. // Dropdowns have correct value set
  644. const teamRows = screen.getAllByTestId('team-row-for-member');
  645. within(teamRows[0]).getByText('Team Admin');
  646. within(teamRows[1]).getByText('Team Admin');
  647. // Dropdown options are not visible
  648. expect(screen.queryAllByText('...').length).toBe(0);
  649. // Dropdown cannot be opened
  650. selectEvent.openMenu(teamRoleSelect);
  651. expect(screen.queryAllByText('...').length).toBe(0);
  652. });
  653. });