@@ -13,19 +13,12 @@ test.describe.configure({ retries: 0, timeout: 45000 })
1313const openPolicyButtonName = / M a n a g e t h i s s e t t i n g | O p e n p o l i c y | O p e n s e t t i n g p o l i c y / i
1414const createRuleButtonName = / C r e a t e r u l e | C r e a t e p o l i c y r u l e / i
1515const saveRuleButtonName = / S a v e r u l e c h a n g e s | S a v e c h a n g e s | S a v e p o l i c y r u l e c h a n g e s / i
16- const createDefaultButtonName = / C r e a t e d e f a u l t r u l e | C r e a t e g l o b a l d e f a u l t r u l e / i
17- const editGlobalDefaultButtonName = / E d i t d e f a u l t | E d i t g l o b a l d e f a u l t / i
18- const resetGlobalDefaultButtonName = / R e s e t d e f a u l t | R e s e t g l o b a l d e f a u l t | R e s e t d e f a u l t r u l e / i
19- const newGroupRuleButtonName = / N e w g r o u p r u l e | N e w g r o u p o v e r r i d e / i
20- const newUserRuleButtonName = / N e w u s e r r u l e | N e w u s e r o v e r r i d e / i
21- const editGroupRuleButtonName = / E d i t g r o u p r u l e | E d i t g r o u p o v e r r i d e / i
22- const deleteGroupRuleButtonName = / D e l e t e g r o u p r u l e | D e l e t e g r o u p o v e r r i d e / i
23- const editUserRuleButtonName = / E d i t u s e r r u l e | E d i t u s e r o v e r r i d e / i
24- const deleteUserRuleButtonName = / D e l e t e u s e r r u l e | D e l e t e u s e r o v e r r i d e / i
25- const groupSectionName = / G r o u p r u l e s | G r o u p o v e r r i d e s / i
26- const userSectionName = / U s e r r u l e s | U s e r o v e r r i d e s / i
27- const globalEditorHeadingName = / D e f a u l t r u l e | G l o b a l d e f a u l t r u l e / i
28- const inheritValueMessage = / L o w e r l a y e r s m u s t i n h e r i t t h i s r u l e | L o w e r l a y e r s m u s t i n h e r i t t h i s v a l u e / i
16+ const changeDefaultButtonName = / ^ C h a n g e $ / i
17+ const removeExceptionButtonName = / R e m o v e e x c e p t i o n | R e m o v e r u l e / i
18+ const groupRuleTargetLabel = 'admin'
19+ const userRuleTargetLabel = 'policy-e2e-user'
20+ const instanceWideTargetLabel = 'Default (instance-wide)'
21+ const globalEditorHeadingName = / D e f a u l t r u l e | G l o b a l d e f a u l t r u l e | I n s t a n c e d e f a u l t r u l e / i
2922
3023async function openSigningOrderDialog ( page : Page ) {
3124 const manageButtons = page . getByRole ( 'button' , { name : openPolicyButtonName } )
@@ -45,29 +38,6 @@ async function waitForEditorIdle(dialog: Locator) {
4538 await savingOverlays . first ( ) . waitFor ( { state : 'hidden' , timeout : 10000 } ) . catch ( ( ) => { } )
4639}
4740
48- async function setAllowOverride ( dialog : Locator , enabled : boolean ) : Promise < boolean > {
49- const allowOverrideSwitch = dialog . getByLabel ( 'Allow lower layers to override this rule' ) . first ( )
50- const label = dialog . getByText ( 'Allow lower layers to override this rule' ) . first ( )
51-
52- if ( ! ( await allowOverrideSwitch . count ( ) ) ) {
53- return false
54- }
55-
56- if ( enabled ) {
57- if ( ! ( await allowOverrideSwitch . isChecked ( ) ) ) {
58- await label . click ( )
59- }
60- await expect ( allowOverrideSwitch ) . toBeChecked ( )
61- return true
62- }
63-
64- if ( await allowOverrideSwitch . isChecked ( ) ) {
65- await label . click ( )
66- }
67- await expect ( allowOverrideSwitch ) . not . toBeChecked ( )
68- return true
69- }
70-
7141async function setSigningFlow ( dialog : Locator , flow : 'parallel' | 'ordered_numeric' ) : Promise < boolean > {
7242 const label = flow === 'parallel'
7343 ? / S i m u l t a n e o u s \( P a r a l l e l \) / i
@@ -88,39 +58,59 @@ async function setSigningFlow(dialog: Locator, flow: 'parallel' | 'ordered_numer
8858async function submitRule ( dialog : Locator ) {
8959 await waitForEditorIdle ( dialog )
9060
91- const createButton = dialog . getByRole ( 'button' , { name : createRuleButtonName } ) . first ( )
61+ const editorPanel = dialog . locator ( '.policy-workbench__editor-panel' ) . last ( )
62+ const createButton = editorPanel . getByRole ( 'button' , { name : / C r e a t e p o l i c y r u l e / i } ) . first ( )
9263 if ( await createButton . isVisible ( ) . catch ( ( ) => false ) ) {
9364 await expect ( createButton ) . toBeEnabled ( { timeout : 8000 } )
9465 await createButton . click ( )
9566 await waitForEditorIdle ( dialog )
9667 return
9768 }
9869
99- const saveButton = dialog . getByRole ( 'button' , { name : saveRuleButtonName } ) . first ( )
70+ const saveButton = editorPanel . getByRole ( 'button' , { name : / S a v e p o l i c y r u l e c h a n g e s / i } ) . first ( )
10071 await expect ( saveButton ) . toBeVisible ( { timeout : 8000 } )
10172 await expect ( saveButton ) . toBeEnabled ( { timeout : 8000 } )
10273 await saveButton . click ( )
10374 await waitForEditorIdle ( dialog )
10475}
10576
106- async function openGlobalRuleEditor ( dialog : Locator , globalSection : Locator ) {
107- const createDefaultButton = globalSection . getByRole ( 'button' , { name : createDefaultButtonName } ) . first ( )
108- if ( await createDefaultButton . isVisible ( ) . catch ( ( ) => false ) ) {
109- await createDefaultButton . click ( )
110- return
111- }
77+ function getRuleRow ( dialog : Locator , _scope : 'Instance' | 'Group' | 'User' , targetLabel : string ) {
78+ return dialog . locator ( 'tbody tr' ) . filter ( {
79+ hasText : targetLabel ,
80+ } ) . first ( )
81+ }
11282
113- await globalSection . getByRole ( 'button' , { name : editGlobalDefaultButtonName } ) . first ( ) . click ( )
83+ async function openSystemDefaultEditor ( dialog : Locator ) {
84+ await dialog . getByRole ( 'button' , { name : changeDefaultButtonName } ) . first ( ) . click ( )
11485}
11586
116- async function expectGlobalBaselineState ( globalSection : Locator ) {
117- const createDefaultButton = globalSection . getByRole ( 'button' , { name : createDefaultButtonName } ) . first ( )
118- if ( await createDefaultButton . isVisible ( ) . catch ( ( ) => false ) ) {
119- await expect ( createDefaultButton ) . toBeVisible ( )
87+ async function openRuleActions ( dialog : Locator , scope : 'Instance' | 'Group' | 'User' , targetLabel : string ) {
88+ const row = getRuleRow ( dialog , scope , targetLabel )
89+ await expect ( row ) . toBeVisible ( { timeout : 8000 } )
90+ await row . getByRole ( 'button' , { name : 'Rule actions' } ) . first ( ) . click ( )
91+ return row
92+ }
93+
94+ async function clickRuleMenuAction ( dialog : Locator , actionName : 'Edit' | 'Remove' ) {
95+ const menuItem = dialog . getByRole ( 'menuitem' , { name : actionName } ) . first ( )
96+ if ( await menuItem . isVisible ( ) . catch ( ( ) => false ) ) {
97+ await menuItem . click ( )
12098 return
12199 }
122100
123- await expect ( globalSection . getByRole ( 'button' , { name : editGlobalDefaultButtonName } ) . first ( ) ) . toBeVisible ( )
101+ await dialog . getByText ( new RegExp ( `^${ actionName } $` , 'i' ) ) . last ( ) . click ( )
102+ }
103+
104+ async function editRule ( dialog : Locator , scope : 'Instance' | 'Group' | 'User' , targetLabel : string ) {
105+ await openRuleActions ( dialog , scope , targetLabel )
106+ await clickRuleMenuAction ( dialog , 'Edit' )
107+ }
108+
109+ async function removeRule ( dialog : Locator , scope : 'Instance' | 'Group' | 'User' , targetLabel : string ) {
110+ await openRuleActions ( dialog , scope , targetLabel )
111+ await clickRuleMenuAction ( dialog , 'Remove' )
112+ await dialog . getByRole ( 'button' , { name : removeExceptionButtonName } ) . first ( ) . click ( )
113+ await waitForEditorIdle ( dialog )
124114}
125115
126116async function chooseTarget ( dialog : Locator , ariaLabel : 'Target groups' | 'Target users' , optionText : string ) {
@@ -160,86 +150,23 @@ async function chooseTarget(dialog: Locator, ariaLabel: 'Target groups' | 'Targe
160150 }
161151}
162152
163- async function clickSectionAction ( section : Locator , actionLabel : string | RegExp ) {
164- let lastError : unknown
165-
166- for ( let attempt = 0 ; attempt < 5 ; attempt ++ ) {
167- const button = section . getByRole ( 'button' , { name : actionLabel } ) . first ( )
168-
169- try {
170- await button . waitFor ( { state : 'visible' , timeout : 2000 } )
171- await button . scrollIntoViewIfNeeded ( ) . catch ( ( ) => { } )
172- await button . click ( { timeout : 2000 } )
173- return
174- } catch ( error ) {
175- lastError = error
176- await new Promise ( ( resolve ) => setTimeout ( resolve , 250 ) )
177- }
153+ async function ensureRuleAbsent ( dialog : Locator , scope : 'Group' | 'User' , targetLabel : string ) {
154+ const row = getRuleRow ( dialog , scope , targetLabel )
155+ if ( await row . count ( ) ) {
156+ await removeRule ( dialog , scope , targetLabel )
157+ await expect ( getRuleRow ( dialog , scope , targetLabel ) ) . toHaveCount ( 0 )
178158 }
179-
180- throw lastError instanceof Error
181- ? lastError
182- : new Error ( `Failed to click section action matching "${ String ( actionLabel ) } "` )
183159}
184160
185- async function removeRuleWithConfirmation ( page : Page , dialog : Locator , section : Locator , actionLabel : string | RegExp ) {
186- const confirmButton = page . getByRole ( 'button' , { name : 'Remove rule' } ) . first ( )
187- if ( await confirmButton . isVisible ( ) . catch ( ( ) => false ) ) {
188- await confirmButton . click ( )
189- await waitForEditorIdle ( dialog )
190- }
191-
192- await clickSectionAction ( section , actionLabel )
193-
194- const confirmationShown = await confirmButton . waitFor ( { state : 'visible' , timeout : 4000 } ) . then ( ( ) => true ) . catch ( ( ) => false )
195- if ( confirmationShown ) {
196- await confirmButton . click ( )
197- await waitForEditorIdle ( dialog )
198- }
199- }
200-
201- async function removeAllRulesByAction (
202- page : Page ,
203- dialog : Locator ,
204- section : Locator ,
205- actionLabel : string | RegExp ,
206- ) {
207- for ( let attempt = 0 ; attempt < 8 ; attempt ++ ) {
208- const currentCount = await section . getByRole ( 'button' , { name : actionLabel } ) . count ( )
209- if ( ! currentCount ) {
210- return
211- }
212-
213- await removeRuleWithConfirmation ( page , dialog , section , actionLabel )
214- await waitForEditorIdle ( dialog )
215- await expect . poll ( async ( ) => {
216- return section . getByRole ( 'button' , { name : actionLabel } ) . count ( )
217- } , {
218- timeout : 5000 ,
219- } ) . toBeLessThan ( currentCount )
220- }
221-
222- throw new Error ( `Failed to remove all rules for action "${ actionLabel } " after multiple attempts` )
223- }
224-
225- async function ensureBaselineRulesForAdminTarget ( page : Page , dialog : Locator ) {
226- const globalSection = dialog . getByRole ( 'region' , { name : 'Global default rules' } )
227- const groupSection = dialog . getByRole ( 'region' , { name : groupSectionName } )
228- const userSection = dialog . getByRole ( 'region' , { name : userSectionName } )
229-
230- await removeAllRulesByAction ( page , dialog , userSection , deleteUserRuleButtonName )
231- await removeAllRulesByAction ( page , dialog , groupSection , deleteGroupRuleButtonName )
232-
233- // Normalize global default into a known baseline where lower layers may override.
234- if ( await globalSection . getByRole ( 'button' , { name : editGlobalDefaultButtonName } ) . count ( ) ) {
235- await globalSection . getByRole ( 'button' , { name : editGlobalDefaultButtonName } ) . click ( )
236- if ( await setAllowOverride ( dialog , true ) ) {
237- await submitRule ( dialog )
238- }
161+ async function ensureSystemDefaultBaseline ( dialog : Locator ) {
162+ const customBadge = dialog . getByText ( / \( c u s t o m \) / i) . first ( )
163+ if ( await customBadge . isVisible ( ) . catch ( ( ) => false ) ) {
164+ await removeRule ( dialog , 'Instance' , instanceWideTargetLabel )
165+ await expect ( dialog . getByText ( / \( s y s t e m \) / i) ) . toBeVisible ( )
239166 }
240167}
241168
242- test ( 'system default persists allow-override changes across edit cycles' , async ( { page } ) => {
169+ test ( 'system default persists across edit cycles and can be reset to the system baseline ' , async ( { page } ) => {
243170 await login (
244171 page . request ,
245172 process . env . NEXTCLOUD_ADMIN_USER ?? 'admin' ,
@@ -252,38 +179,34 @@ test('system default persists allow-override changes across edit cycles', async
252179 await openSigningOrderDialog ( page )
253180
254181 const signingOrderDialog = await getSigningOrderDialog ( page )
255- await ensureBaselineRulesForAdminTarget ( page , signingOrderDialog )
182+ await ensureSystemDefaultBaseline ( signingOrderDialog )
183+ await ensureRuleAbsent ( signingOrderDialog , 'Group' , groupRuleTargetLabel )
184+ await ensureRuleAbsent ( signingOrderDialog , 'User' , userRuleTargetLabel )
256185
257- const globalSection = signingOrderDialog . getByRole ( 'region' , { name : 'Global default rules' } )
258- await openGlobalRuleEditor ( signingOrderDialog , globalSection )
186+ await openSystemDefaultEditor ( signingOrderDialog )
259187 await expect ( signingOrderDialog . getByRole ( 'heading' , { name : globalEditorHeadingName } ) . last ( ) ) . toBeVisible ( )
260- await setAllowOverride ( signingOrderDialog , true )
188+ expect ( await setSigningFlow ( signingOrderDialog , 'ordered_numeric' ) , 'Expected signing-flow radios in system editor' ) . toBe ( true )
261189 await submitRule ( signingOrderDialog )
190+ await expect ( getRuleRow ( signingOrderDialog , 'Instance' , instanceWideTargetLabel ) ) . toContainText ( 'Sequential' )
262191
263- await signingOrderDialog . getByRole ( 'button' , { name : editGlobalDefaultButtonName } ) . click ( )
264- await setAllowOverride ( signingOrderDialog , true )
265-
266- await setAllowOverride ( signingOrderDialog , false )
192+ await openSystemDefaultEditor ( signingOrderDialog )
193+ expect ( await setSigningFlow ( signingOrderDialog , 'parallel' ) , 'Expected signing-flow radios in system editor' ) . toBe ( true )
267194 const saveChangesResponsePromise = page . waitForResponse ( ( response ) => {
268195 return response . request ( ) . method ( ) === 'POST'
269196 && response . url ( ) . includes ( '/apps/libresign/api/v1/policies/system/signature_flow' )
270197 } )
271198 await signingOrderDialog . getByRole ( 'button' , { name : saveRuleButtonName } ) . first ( ) . click ( )
272199 const saveChangesResponse = await saveChangesResponsePromise
273200 expect ( saveChangesResponse . status ( ) , 'Expected Save changes request to succeed' ) . toBe ( 200 )
201+ await expect ( getRuleRow ( signingOrderDialog , 'Instance' , instanceWideTargetLabel ) ) . toContainText ( 'Simultaneous (Parallel)' )
274202
275- await signingOrderDialog . getByRole ( 'button' , { name : editGlobalDefaultButtonName } ) . click ( )
276- await setAllowOverride ( signingOrderDialog , false )
277-
278- await expect ( signingOrderDialog . getByText ( inheritValueMessage ) ) . toBeVisible ( )
279-
280- // Reset should restore inherited baseline behavior.
281- await removeRuleWithConfirmation ( page , signingOrderDialog , globalSection , resetGlobalDefaultButtonName )
282- await expectGlobalBaselineState ( globalSection )
203+ await removeRule ( signingOrderDialog , 'Instance' , instanceWideTargetLabel )
204+ await expect ( signingOrderDialog . getByText ( / \( s y s t e m \) / i) ) . toBeVisible ( )
205+ await expect ( getRuleRow ( signingOrderDialog , 'Instance' , instanceWideTargetLabel ) ) . toContainText ( 'Let users choose' )
283206} )
284207
285208test ( 'admin can create, edit, and delete global, group, and user rules from the policy workbench' , async ( { page } ) => {
286- const userTarget = 'policy-e2e-user'
209+ const userTarget = userRuleTargetLabel
287210
288211 await ensureUserExists ( page . request , userTarget )
289212
@@ -297,66 +220,54 @@ test('admin can create, edit, and delete global, group, and user rules from the
297220 await openSigningOrderDialog ( page )
298221
299222 const dialog = await getSigningOrderDialog ( page )
300- const globalSection = dialog . getByRole ( 'region' , { name : 'Global default rules' } )
301- const groupSection = dialog . getByRole ( 'region' , { name : groupSectionName } )
302- const userSection = dialog . getByRole ( 'region' , { name : userSectionName } )
303-
304- await ensureBaselineRulesForAdminTarget ( page , dialog )
223+ await ensureSystemDefaultBaseline ( dialog )
224+ await ensureRuleAbsent ( dialog , 'Group' , groupRuleTargetLabel )
225+ await ensureRuleAbsent ( dialog , 'User' , userTarget )
305226
306227 // Global rule: edit
307- await openGlobalRuleEditor ( dialog , globalSection )
228+ await openSystemDefaultEditor ( dialog )
308229 expect ( await setSigningFlow ( dialog , 'ordered_numeric' ) , 'Expected signing-flow radios in global editor' ) . toBe ( true )
309- await setAllowOverride ( dialog , true )
310- await submitRule ( dialog )
311- await expect ( globalSection . getByRole ( 'button' , { name : editGlobalDefaultButtonName } ) ) . toBeVisible ( )
312-
313- // Global rule: enforce inheritance
314- await globalSection . getByRole ( 'button' , { name : editGlobalDefaultButtonName } ) . click ( )
315- expect ( await setAllowOverride ( dialog , false ) , 'Expected global allow-override switch in editor' ) . toBe ( true )
316- await submitRule ( dialog )
317- await expect ( globalSection . getByRole ( 'button' , { name : editGlobalDefaultButtonName } ) ) . toBeVisible ( )
318-
319- await globalSection . getByRole ( 'button' , { name : editGlobalDefaultButtonName } ) . click ( )
320- expect ( await setAllowOverride ( dialog , true ) , 'Expected global allow-override switch in editor' ) . toBe ( true )
321230 await submitRule ( dialog )
322- await expect ( globalSection . getByRole ( 'button ', { name : editGlobalDefaultButtonName } ) ) . toBeVisible ( )
231+ await expect ( getRuleRow ( dialog , 'Instance ', instanceWideTargetLabel ) ) . toContainText ( 'Sequential' )
323232
324233 // Group rule: create
325- await dialog . getByRole ( 'button' , { name : newGroupRuleButtonName } ) . first ( ) . click ( )
234+ await dialog . getByRole ( 'button' , { name : 'Create rule' } ) . first ( ) . click ( )
235+ await dialog . getByRole ( 'option' , { name : / G r o u p / i } ) . click ( )
326236 await chooseTarget ( dialog , 'Target groups' , 'admin' )
327237 expect ( await setSigningFlow ( dialog , 'ordered_numeric' ) , 'Expected signing-flow radios in group editor' ) . toBe ( true )
328- await setAllowOverride ( dialog , true )
329238 await submitRule ( dialog )
330- await expect ( groupSection . getByRole ( 'button ', { name : editGroupRuleButtonName } ) . first ( ) ) . toBeVisible ( )
239+ await expect ( getRuleRow ( dialog , 'Group ', groupRuleTargetLabel ) ) . toContainText ( 'Sequential' )
331240
332241 // Group rule: edit
333- await groupSection . getByRole ( 'button ', { name : editGroupRuleButtonName } ) . first ( ) . click ( )
242+ await editRule ( dialog , 'Group ', groupRuleTargetLabel )
334243 expect ( await setSigningFlow ( dialog , 'parallel' ) , 'Expected signing-flow radios in group editor' ) . toBe ( true )
335244 await submitRule ( dialog )
336- await expect ( groupSection . getByRole ( 'button ', { name : editGroupRuleButtonName } ) . first ( ) ) . toBeVisible ( )
245+ await expect ( getRuleRow ( dialog , 'Group ', groupRuleTargetLabel ) ) . toContainText ( 'Simultaneous (Parallel)' )
337246
338247 // User rule: create
339- await dialog . getByRole ( 'button' , { name : newUserRuleButtonName } ) . first ( ) . click ( )
248+ await dialog . getByRole ( 'button' , { name : 'Create rule' } ) . first ( ) . click ( )
249+ await dialog . getByRole ( 'option' , { name : / U s e r / i } ) . click ( )
340250 await chooseTarget ( dialog , 'Target users' , userTarget )
341251 expect ( await setSigningFlow ( dialog , 'ordered_numeric' ) , 'Expected signing-flow radios in user editor' ) . toBe ( true )
342252 await submitRule ( dialog )
343- await expect ( userSection . getByRole ( 'button ', { name : editUserRuleButtonName } ) . first ( ) ) . toBeVisible ( )
253+ await expect ( getRuleRow ( dialog , 'User ', userTarget ) ) . toContainText ( 'Sequential' )
344254
345255 // User rule: edit
346- await clickSectionAction ( userSection , editUserRuleButtonName )
256+ await editRule ( dialog , 'User' , userTarget )
347257 expect ( await setSigningFlow ( dialog , 'parallel' ) , 'Expected signing-flow radios in user editor' ) . toBe ( true )
348258 await submitRule ( dialog )
349- await expect ( userSection . getByRole ( 'button ', { name : editUserRuleButtonName } ) . first ( ) ) . toBeVisible ( )
259+ await expect ( getRuleRow ( dialog , 'User ', userTarget ) ) . toContainText ( 'Simultaneous (Parallel)' )
350260
351261 // User rule: delete
352- await removeAllRulesByAction ( page , dialog , userSection , deleteUserRuleButtonName )
353- await expect ( userSection . getByRole ( 'button ', { name : deleteUserRuleButtonName } ) ) . toHaveCount ( 0 )
262+ await removeRule ( dialog , 'User' , userTarget )
263+ await expect ( getRuleRow ( dialog , 'User ', userTarget ) ) . toHaveCount ( 0 )
354264
355265 // Group rule: delete
356- await removeAllRulesByAction ( page , dialog , groupSection , deleteGroupRuleButtonName )
357- await expect ( groupSection . getByRole ( 'button ', { name : deleteGroupRuleButtonName } ) ) . toHaveCount ( 0 )
266+ await removeRule ( dialog , 'Group' , groupRuleTargetLabel )
267+ await expect ( getRuleRow ( dialog , 'Group ', groupRuleTargetLabel ) ) . toHaveCount ( 0 )
358268
359269 // Global rule: reset to inherited baseline
360- await removeRuleWithConfirmation ( page , dialog , globalSection , resetGlobalDefaultButtonName )
361- await expectGlobalBaselineState ( globalSection )
270+ await removeRule ( dialog , 'Instance' , instanceWideTargetLabel )
271+ await expect ( dialog . getByText ( / \( s y s t e m \) / i) ) . toBeVisible ( )
272+ await expect ( getRuleRow ( dialog , 'Instance' , instanceWideTargetLabel ) ) . toContainText ( 'Let users choose' )
362273} )
0 commit comments