@@ -2,37 +2,51 @@ import { screen, waitFor } from '@testing-library/react';
22import userEvent from '@testing-library/user-event' ;
33import { renderWrapper } from '@src/setupTest' ;
44import { getAuthenticatedUser } from '@edx/frontend-platform/auth' ;
5- import { useValidateUsers } from '../data/hooks' ;
5+ import { useValidateUsers , useAssignTeamMembersRole } from '../data/hooks' ;
66import { ToastManagerProvider } from '../libraries-manager/ToastManagerContext' ;
77import AssignRoleWizard from './AssignRoleWizard' ;
88
99jest . mock ( '@edx/frontend-platform/auth' , ( ) => ( {
1010 getAuthenticatedUser : jest . fn ( ) ,
1111} ) ) ;
1212
13+ jest . mock ( '../data/hooks' , ( ) => ( {
14+ useValidateUsers : jest . fn ( ) ,
15+ useAssignTeamMembersRole : jest . fn ( ) ,
16+ } ) ) ;
17+
18+ jest . mock ( './DefineApplicationScopeStep' , ( ) => ( {
19+ __esModule : true ,
20+ default : ( { onScopeToggle } : { onScopeToggle : ( id : string ) => void } ) => (
21+ < div >
22+ < button type = "button" data-testid = "toggle-scope" onClick = { ( ) => onScopeToggle ( 'lib:test:123' ) } >
23+ Toggle scope
24+ </ button >
25+ </ div >
26+ ) ,
27+ } ) ) ;
28+
1329const mockUseValidateUsers = useValidateUsers as jest . Mock ;
30+ const mockUseAssignTeamMembersRole = useAssignTeamMembersRole as jest . Mock ;
1431const mockValidateMutateAsync = jest . fn ( ) ;
32+ const mockAssignMutateAsync = jest . fn ( ) ;
1533
1634const renderWizard = ( props = { } ) => renderWrapper (
1735 < ToastManagerProvider >
1836 < AssignRoleWizard onClose = { jest . fn ( ) } { ...props } />
1937 </ ToastManagerProvider > ,
2038) ;
2139
22- // selectors
2340const getUsersInput = ( ) => screen . getByLabelText ( / A d d u s e r s b y u s e r n a m e o r e m a i l / i) ;
2441const getRoleRadio = ( name : RegExp ) => screen . getByRole ( 'radio' , { name } ) ;
2542const getNextButton = ( ) => screen . getByRole ( 'button' , { name : / ^ N e x t $ / i } ) ;
2643const getCancelButton = ( ) => screen . getByRole ( 'button' , { name : / ^ C a n c e l $ / i } ) ;
27- const getStep2Heading = ( ) => screen . queryByRole ( 'heading' , { name : 'Step 2' } ) ;
2844
2945describe ( 'AssignRoleWizard — Step 1' , ( ) => {
3046 beforeEach ( ( ) => {
3147 jest . clearAllMocks ( ) ;
32- mockUseValidateUsers . mockReturnValue ( {
33- mutateAsync : mockValidateMutateAsync ,
34- isPending : false ,
35- } ) ;
48+ mockUseValidateUsers . mockReturnValue ( { mutateAsync : mockValidateMutateAsync , isPending : false } ) ;
49+ mockUseAssignTeamMembersRole . mockReturnValue ( { mutateAsync : mockAssignMutateAsync , isPending : false } ) ;
3650 ( getAuthenticatedUser as jest . Mock ) . mockReturnValue ( { administrator : true } ) ;
3751 } ) ;
3852
@@ -49,6 +63,11 @@ describe('AssignRoleWizard — Step 1', () => {
4963 expect ( getUsersInput ( ) ) . toHaveValue ( 'alice' ) ;
5064 } ) ;
5165
66+ it ( 'Next button is disabled without both users and a role' , ( ) => {
67+ renderWizard ( ) ;
68+ expect ( getNextButton ( ) ) . toBeDisabled ( ) ;
69+ } ) ;
70+
5271 it ( 'selecting a different role replaces the previous selection' , async ( ) => {
5372 const user = userEvent . setup ( ) ;
5473 renderWizard ( ) ;
@@ -67,7 +86,7 @@ describe('AssignRoleWizard — Step 1', () => {
6786 await user . click ( getNextButton ( ) ) ;
6887 await waitFor ( ( ) => {
6988 expect ( screen . getByText ( / n o t a s s o c i a t e d w i t h a n a c c o u n t / i) ) . toBeInTheDocument ( ) ;
70- expect ( getStep2Heading ( ) ) . not . toBeInTheDocument ( ) ;
89+ expect ( screen . queryByTestId ( 'toggle-scope' ) ) . not . toBeInTheDocument ( ) ;
7190 } ) ;
7291 } ) ;
7392
@@ -79,7 +98,7 @@ describe('AssignRoleWizard — Step 1', () => {
7998 await user . click ( getRoleRadio ( / L i b r a r y A d m i n / i) ) ;
8099 await user . click ( getNextButton ( ) ) ;
81100 await waitFor ( ( ) => {
82- expect ( screen . getByRole ( 'heading' , { name : 'Step 2' } ) ) . toBeInTheDocument ( ) ;
101+ expect ( screen . getByTestId ( 'toggle-scope' ) ) . toBeInTheDocument ( ) ;
83102 } ) ;
84103 } ) ;
85104
@@ -91,9 +110,8 @@ describe('AssignRoleWizard — Step 1', () => {
91110 await user . click ( getRoleRadio ( / L i b r a r y A d m i n / i) ) ;
92111 await user . click ( getNextButton ( ) ) ;
93112 await waitFor ( ( ) => {
94- // Toast should appear with retry option
95113 expect ( screen . getByRole ( 'alert' ) ) . toBeInTheDocument ( ) ;
96- expect ( getStep2Heading ( ) ) . not . toBeInTheDocument ( ) ;
114+ expect ( screen . queryByTestId ( 'toggle-scope' ) ) . not . toBeInTheDocument ( ) ;
97115 } ) ;
98116 } ) ;
99117
@@ -117,18 +135,11 @@ describe('AssignRoleWizard — Step 1', () => {
117135 renderWizard ( ) ;
118136 await user . type ( getUsersInput ( ) , 'alice' ) ;
119137 await user . click ( getRoleRadio ( / L i b r a r y A d m i n / i) ) ;
120-
121- // First attempt fails - toast appears with retry
122138 await user . click ( getNextButton ( ) ) ;
123139 await waitFor ( ( ) => expect ( screen . getByRole ( 'alert' ) ) . toBeInTheDocument ( ) ) ;
124-
125- // Click retry button
126- const retryButton = screen . getByRole ( 'button' , { name : / r e t r y / i } ) ;
127- await user . click ( retryButton ) ;
128-
129- // Should advance to step 2 on retry
140+ await user . click ( screen . getByRole ( 'button' , { name : / r e t r y / i } ) ) ;
130141 await waitFor ( ( ) => {
131- expect ( getStep2Heading ( ) ) . toBeInTheDocument ( ) ;
142+ expect ( screen . getByTestId ( 'toggle-scope' ) ) . toBeInTheDocument ( ) ;
132143 } ) ;
133144 } ) ;
134145} ) ;
@@ -140,16 +151,15 @@ describe('AssignRoleWizard — Step 2', () => {
140151 await user . click ( getRoleRadio ( / L i b r a r y A d m i n / i) ) ;
141152 await user . click ( getNextButton ( ) ) ;
142153 await waitFor ( ( ) => {
143- expect ( screen . getByRole ( 'heading' , { name : 'Step 2' } ) ) . toBeInTheDocument ( ) ;
154+ expect ( screen . getByTestId ( 'toggle-scope' ) ) . toBeInTheDocument ( ) ;
144155 } ) ;
145156 } ;
146157
147158 beforeEach ( ( ) => {
148159 jest . clearAllMocks ( ) ;
149- mockUseValidateUsers . mockReturnValue ( {
150- mutateAsync : mockValidateMutateAsync ,
151- isPending : false ,
152- } ) ;
160+ mockUseValidateUsers . mockReturnValue ( { mutateAsync : mockValidateMutateAsync , isPending : false } ) ;
161+ mockUseAssignTeamMembersRole . mockReturnValue ( { mutateAsync : mockAssignMutateAsync , isPending : false } ) ;
162+ ( getAuthenticatedUser as jest . Mock ) . mockReturnValue ( { administrator : true } ) ;
153163 } ) ;
154164
155165 it ( 'Back button returns to Step 1' , async ( ) => {
@@ -161,119 +171,57 @@ describe('AssignRoleWizard — Step 2', () => {
161171 expect ( getUsersInput ( ) ) . toBeInTheDocument ( ) ;
162172 } ) ;
163173 } ) ;
164- } ) ;
165-
166- describe ( 'AssignRoleWizard — Step 2' , ( ) => {
167- const mockOnScopeToggle = jest . fn ( ) ;
168174
169- beforeEach ( ( ) => {
170- jest . clearAllMocks ( ) ;
171- mockOnScopeToggle . mockClear ( ) ;
172- mockUseValidateUsers . mockReturnValue ( {
173- mutateAsync : mockValidateMutateAsync ,
174- isPending : false ,
175- } ) ;
176- ( getAuthenticatedUser as jest . Mock ) . mockReturnValue ( { administrator : true } ) ;
177- } ) ;
178-
179- it ( 'advances to Step 2 with valid users' , async ( ) => {
180- const user = userEvent . setup ( ) ;
181- mockValidateMutateAsync . mockResolvedValue ( { invalidUsers : [ ] , validUsers : [ 'alice' ] } ) ;
182- renderWizard ( ) ;
183- await user . type ( screen . getByTestId ( 'users-input' ) , 'alice' ) ;
184- await user . click ( screen . getByTestId ( 'role-option-library_admin' ) ) ;
185- await user . click ( screen . getByRole ( 'button' , { name : / N e x t / i } ) ) ;
186- await waitFor ( ( ) => {
187- expect ( screen . getByTestId ( 'stepper' ) ) . toHaveAttribute ( 'data-active-step' , 'define-application-scope' ) ;
188- } ) ;
189- } ) ;
190-
191- it ( 'calls onScopeToggle when scope is selected' , async ( ) => {
175+ it ( 'Save button is disabled when no scopes are selected' , async ( ) => {
192176 const user = userEvent . setup ( ) ;
193- mockValidateMutateAsync . mockResolvedValue ( { invalidUsers : [ ] , validUsers : [ 'alice' ] } ) ;
194177 renderWizard ( ) ;
195- await user . type ( screen . getByTestId ( 'users-input' ) , 'alice' ) ;
196- await user . click ( screen . getByTestId ( 'role-option-library_admin' ) ) ;
197- await user . click ( screen . getByRole ( 'button' , { name : / N e x t / i } ) ) ;
198- await waitFor ( ( ) => {
199- expect ( screen . getByTestId ( 'stepper' ) ) . toHaveAttribute ( 'data-active-step' , 'define-application-scope' ) ;
200- } ) ;
201- await user . click ( screen . getByTestId ( 'toggle-scope' ) ) ;
202- // Scope toggle is called from the mocked component
178+ await advanceToStep2 ( user ) ;
179+ expect ( screen . getByRole ( 'button' , { name : / ^ S a v e $ / i } ) ) . toBeDisabled ( ) ;
203180 } ) ;
204181
205- it ( 'saves role assignment successfully' , async ( ) => {
182+ it ( 'saves role assignment successfully and closes the wizard ' , async ( ) => {
206183 const user = userEvent . setup ( ) ;
207- mockValidateMutateAsync . mockResolvedValue ( { invalidUsers : [ ] , validUsers : [ 'alice' ] } ) ;
208184 const onClose = jest . fn ( ) ;
185+ mockAssignMutateAsync . mockResolvedValue ( {
186+ completed : [ { userIdentifier : 'alice' , scope : 'lib:test:123' , status : 'role_added' } ] ,
187+ errors : [ ] ,
188+ } ) ;
209189 renderWizard ( { onClose } ) ;
210- await user . type ( screen . getByTestId ( 'users-input' ) , 'alice' ) ;
211- await user . click ( screen . getByTestId ( 'role-option-library_admin ' ) ) ;
212- await user . click ( screen . getByRole ( 'button' , { name : / N e x t / i } ) ) ;
190+ await advanceToStep2 ( user ) ;
191+ await user . click ( screen . getByTestId ( 'toggle-scope ' ) ) ;
192+ await user . click ( screen . getByRole ( 'button' , { name : / ^ S a v e $ / i } ) ) ;
213193 await waitFor ( ( ) => {
214- expect ( screen . getByTestId ( 'stepper' ) ) . toHaveAttribute ( 'data-active-step' , 'define-application-scope' ) ;
194+ expect ( mockAssignMutateAsync ) . toHaveBeenCalledWith ( {
195+ data : { users : [ 'alice' ] , role : 'library_admin' , scopes : [ 'lib:test:123' ] } ,
196+ } ) ;
197+ expect ( onClose ) . toHaveBeenCalled ( ) ;
215198 } ) ;
216- await user . click ( screen . getByTestId ( 'toggle-scope' ) ) ;
217- await user . click ( screen . getByRole ( 'button' , { name : / S a v e / i } ) ) ;
218- // The save button is in Step 2
219199 } ) ;
220200
221- // Skipped - complex integration test that requires proper scope selection propagation
222- // The error handling logic is tested in the code and works correctly in production
223- it . skip ( 'shows error when save fails' , async ( ) => {
201+ it ( 'shows error toast when the API returns assignment errors' , async ( ) => {
224202 const user = userEvent . setup ( ) ;
225- mockValidateMutateAsync . mockResolvedValue ( { invalidUsers : [ ] , validUsers : [ 'alice' ] } ) ;
203+ mockAssignMutateAsync . mockResolvedValue ( {
204+ completed : [ ] ,
205+ errors : [ { userIdentifier : 'alice' , scope : 'lib:test:123' , error : 'already has role' } ] ,
206+ } ) ;
226207 renderWizard ( ) ;
227- await user . type ( screen . getByTestId ( 'users-input' ) , 'alice' ) ;
228- await user . click ( screen . getByTestId ( 'role-option-library_admin ' ) ) ;
229- await user . click ( screen . getByRole ( 'button' , { name : / N e x t / i } ) ) ;
208+ await advanceToStep2 ( user ) ;
209+ await user . click ( screen . getByTestId ( 'toggle-scope ' ) ) ;
210+ await user . click ( screen . getByRole ( 'button' , { name : / ^ S a v e $ / i } ) ) ;
230211 await waitFor ( ( ) => {
231- expect ( screen . getByTestId ( 'stepper ') ) . toHaveAttribute ( 'data-active-step' , 'define-application-scope' ) ;
212+ expect ( screen . getByRole ( 'alert ') ) . toBeInTheDocument ( ) ;
232213 } ) ;
233214 } ) ;
234215
235- it ( 'goes back to Step 1 ' , async ( ) => {
216+ it ( 'shows error toast when save throws a network error ' , async ( ) => {
236217 const user = userEvent . setup ( ) ;
237- mockValidateMutateAsync . mockResolvedValue ( { invalidUsers : [ ] , validUsers : [ 'alice' ] } ) ;
218+ mockAssignMutateAsync . mockRejectedValue ( new Error ( 'Network error' ) ) ;
238219 renderWizard ( ) ;
239- await user . type ( screen . getByTestId ( 'users-input' ) , 'alice' ) ;
240- await user . click ( screen . getByTestId ( 'role-option-library_admin' ) ) ;
241- await user . click ( screen . getByRole ( 'button' , { name : / N e x t / i } ) ) ;
242- await waitFor ( ( ) => {
243- expect ( screen . getByTestId ( 'stepper' ) ) . toHaveAttribute ( 'data-active-step' , 'define-application-scope' ) ;
244- } ) ;
245- await user . click ( screen . getByRole ( 'button' , { name : / B a c k / i } ) ) ;
220+ await advanceToStep2 ( user ) ;
221+ await user . click ( screen . getByTestId ( 'toggle-scope' ) ) ;
222+ await user . click ( screen . getByRole ( 'button' , { name : / ^ S a v e $ / i } ) ) ;
246223 await waitFor ( ( ) => {
247- expect ( screen . getByTestId ( 'stepper' ) ) . toHaveAttribute ( 'data-active-step' , 'select-users-and-role' ) ;
248- } ) ;
249- } ) ;
250- } ) ;
251-
252- describe ( 'AssignRoleWizard — error handling' , ( ) => {
253- beforeEach ( ( ) => {
254- jest . clearAllMocks ( ) ;
255- mockUseValidateUsers . mockReturnValue ( {
256- mutateAsync : mockValidateMutateAsync ,
257- isPending : false ,
224+ expect ( screen . getByRole ( 'alert' ) ) . toBeInTheDocument ( ) ;
258225 } ) ;
259- ( getAuthenticatedUser as jest . Mock ) . mockReturnValue ( { administrator : true } ) ;
260- } ) ;
261-
262- it ( 'returns early when no role selected' , async ( ) => {
263- mockValidateMutateAsync . mockResolvedValue ( { invalidUsers : [ ] , validUsers : [ 'alice' ] } ) ;
264- renderWizard ( ) ;
265- await userEvent . type ( screen . getByTestId ( 'users-input' ) , 'alice' ) ;
266- await userEvent . click ( screen . getByRole ( 'button' , { name : / N e x t / i } ) ) ;
267- // Without a role, should not proceed
268- } ) ;
269-
270- it ( 'returns early when no scopes selected' , async ( ) => {
271- const user = userEvent . setup ( ) ;
272- mockValidateMutateAsync . mockResolvedValue ( { invalidUsers : [ ] , validUsers : [ 'alice' ] } ) ;
273- renderWizard ( ) ;
274- await user . type ( screen . getByTestId ( 'users-input' ) , 'alice' ) ;
275- await user . click ( screen . getByTestId ( 'role-option-library_admin' ) ) ;
276- await user . click ( screen . getByRole ( 'button' , { name : / N e x t / i } ) ) ;
277- // Without scopes in step 2, save returns early
278226 } ) ;
279227} ) ;
0 commit comments