organizationMemberDetail.spec.tsx 23 KB

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