*** Settings *** Resource ${CURDIR}/../resources/common.resource Resource ${CURDIR}/../resources/auth.resource Suite Setup Login As Admin *** Test Cases *** Config Field Edit Persists After Reload [Documentation] Verifies auto-save round-trip: UI edit → debounced PATCH → reload rehydration. ... ... - Restores original value in teardown so subsequent tests are not affected. ... - Runs last in suite ordering to avoid destabilising fail2ban health for other tests. [Teardown] Run Keyword And Ignore Error Restore Original Ban Time # Step 1 — navigate to config page and click Jails tab. Go To ${FRONTEND_URL}/config Wait For Load State domcontentloaded Sleep 5s # Use JS click for the Jails tab — avoids Playwright role=tab selector timing issues Evaluate JavaScript ${None} ... () => { ... const tabs = document.querySelectorAll('[role="tab"]'); ... for (const tab of tabs) { ... if (tab.getAttribute('data-testid') === 'jails-tab' || ... (tab.textContent?.trim() === 'Jails' && tab.getAttribute('aria-label') === 'Jails')) { ... tab.click(); return; ... } ... } ... // Fallback: click any tab whose visible text includes 'Jails' ... for (const tab of tabs) { ... if (tab.textContent?.trim() === 'Jails') { tab.click(); return; } ... } ... } Sleep 5s # Step 2 — wait for jail list to load (retry until options appear). FOR ${i} IN RANGE 1 21 ${opts}= Get Elements css=[role="option"] ${count}= Get Length ${opts} IF ${count} > 0 BREAK END Sleep 2s END Log Jail options loaded: ${count} found. # Scroll the list pane to top to ensure options are visible Evaluate JavaScript ${None} ... () => { ... const listPane = document.querySelector('[role="listbox"]'); ... if (listPane) listPane.scrollTop = 0; ... } Sleep 1s # Step 3 — find active jail name via JS (avoids strict-mode selector issues with virtual lists) ${active_jail_name}= Evaluate JavaScript ${None} ... () => { ... const items = document.querySelectorAll('[role="option"]'); ... let activeCount = 0; ... let firstActiveName = null; ... items.forEach(el => { ... const badge = el.querySelector('[class*="Badge"]'); ... if (badge && badge.textContent?.trim() === 'Active') { ... activeCount++; ... if (firstActiveName === null) firstActiveName = el.getAttribute('data-name'); ... } ... }); ... return { active: activeCount, firstActiveName }; ... } Log Active jail info: ${active_jail_name} IF ${active_jail_name['active']} == 0 Log No active jails found. Test requires at least one active jail to verify auto-save. Skip Test requires at least one active jail. Activate a jail via the UI or API first. END Set Suite Variable ${ACTIVE_JAIL_NAME} ${active_jail_name['firstActiveName']} Log Active jail name: ${ACTIVE_JAIL_NAME} # Click the active jail directly by name using JS (bypasses Playwright strict-mode selector conflicts) ${click_result}= Evaluate JavaScript ${None} ... () => { ... const opts = document.querySelectorAll('[role="option"]'); ... for (const opt of opts) { ... if (opt.getAttribute('data-name') === '${ACTIVE_JAIL_NAME}') { ... opt.click(); ... return 'CLICKED:' + opt.getAttribute('data-name'); ... } ... } ... return 'NOT_FOUND'; ... } Log JS click result: ${click_result} Sleep 5s # Verify ban_time input appeared ${has_bantime}= Evaluate JavaScript ${None} ... () => { ... return document.querySelector('input[data-field="ban_time"]') !== null; ... } Log Has editable ban_time input: ${has_bantime} IF '${has_bantime}' != 'True' Fatal Error TEST_BODY: ban_time input not found for jail ${ACTIVE_JAIL_NAME}. END # Get original ban_time value ${ban_time_value}= Get Attribute css=input[data-field="ban_time"] value Set Suite Variable ${ORIGINAL_BANTIME} ${ban_time_value} Log Original bantime: ${ORIGINAL_BANTIME} # Step 4 — edit ban_time to 7200 using keyboard press (bypasses React synthetic event issues with Fill Text) # Clear the field first by selecting all text Click css=input[data-field="ban_time"] Sleep 500ms Evaluate JavaScript ${None} ... () => { ... const input = document.querySelector('input[data-field="ban_time"]'); ... if (input) { ... input.select(); ... input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Delete', bubbles: true })); ... } ... } Sleep 500ms Fill Text css=input[data-field="ban_time"] 7200 Sleep 1s # Verify the fill worked by checking value via JS ${fill_value}= Evaluate JavaScript ${None} ... () => { ... const el = document.querySelector('input[data-field="ban_time"]'); ... return el ? el.value : 'NOT_FOUND'; ... } Log Value after fill attempt: ${fill_value} # Step 5 — wait for auto-save indicator. # Also check that the fill actually changed the value (auto-save needs the value to differ from server state) ${fill_check}= Get Attribute css=input[data-field="ban_time"] value Log Value after fill: ${fill_check} ${saved}= Set Variable ${FALSE} FOR ${i} IN RANGE 1 31 ${status_visible}= Run Keyword And Return Status Wait For Elements State css=[role="status"] visible timeout=2s IF ${status_visible} ${status_text}= Evaluate JavaScript ${None} ... () => { ... const el = document.querySelector('[role="status"]'); ... return el ? el.textContent : ''; ... } Log Status element text: ${status_text} IF 'saved' in '${status_text}'.toLowerCase() ${saved}= Set Variable ${TRUE} BREAK END END Sleep 1s END Log Auto-save confirmed: ${saved} # Step 6 — verify via API. ${resp}= Run Keyword And Ignore Error GET ${BACKEND_URL}/api/jails ${verify_ok}= Run Keyword And Return Status Should Contain ${resp.text} 7200 IF ${verify_ok} Log API verification passed: 7200 found in jail configs. ELSE Log API verification skipped (rate-limited or error): ${resp} END # Step 7 — reload and verify persistence. Reload Wait For Load State domcontentloaded Sleep 5s # Re-authenticate if session was lost after reload ${needs_auth}= Evaluate JavaScript ${None} ... async () => { ... // Check current URL - if on login page, need to re-auth ... if (window.location.pathname.includes('/login')) return true; ... // Check if we can fetch authenticated API ... try { ... const res = await fetch('/api/v1/auth/login', { ... method: 'POST', ... headers: { 'Content-Type': 'application/json' }, ... body: JSON.stringify({ password: 'Hallo123!' }), ... credentials: 'include' ... }); ... return !res.ok; ... } catch(e) { return true; } ... } Log Needs re-authentication: ${needs_auth} IF ${needs_auth} Evaluate JavaScript ${None} ... async () => { ... await new Promise(r => setTimeout(r, 2000)); ... const res = await fetch('/api/v1/auth/login', { ... method: 'POST', ... headers: { 'Content-Type': 'application/json' }, ... body: JSON.stringify({ password: 'Hallo123!' }), ... credentials: 'include' ... }); ... return res.ok; ... } Evaluate JavaScript ${None} () => sessionStorage.setItem('bangui_authenticated', 'true') END # Check if Jails tab is already visible, otherwise go to config page ${tab_visible}= Run Keyword And Return Status Wait For Elements State role=tab[name=Jails] visible timeout=3s IF not ${tab_visible} Go To ${FRONTEND_URL}/config Wait For Load State domcontentloaded Sleep 5s END # Use JS click for the Jails tab — avoids Playwright role=tab selector timing issues Evaluate JavaScript ${None} ... () => { ... const tabs = document.querySelectorAll('[role="tab"]'); ... for (const tab of tabs) { ... if (tab.textContent?.trim() === 'Jails') { tab.click(); return; } ... } ... } Sleep 5s FOR ${i} IN RANGE 1 21 ${opts}= Get Elements css=[role="option"] ${count}= Get Length ${opts} IF ${count} > 0 BREAK END Sleep 2s END # Re-click the active jail by name to verify reloaded value. Evaluate JavaScript ${None} ... () => { ... const opts = document.querySelectorAll('[role="option"]'); ... for (const opt of opts) { ... if (opt.getAttribute('data-name') === '${ACTIVE_JAIL_NAME}') { ... opt.scrollIntoView({ behavior: 'instant', block: 'center' }); ... opt.click(); ... return 'CLICKED:' + opt.getAttribute('data-name'); ... } ... } ... // Fallback: click first option ... if (opts.length > 0) { opts[0].click(); return 'FALLBACK:' + opts[0].getAttribute('data-name'); } ... return 'NO_OPTS'; ... } Log Jail re-click result: ${click_result} Sleep 8s # Debug: check if detail panel has rendered ${detail_html}= Evaluate JavaScript ${None} ... () => { ... const panel = document.querySelector('[data-testid="jail-detail-panel"]') || ... document.querySelector('.f22iagw') || ... document.querySelector('[class*="fieldRow"]'); ... const bantimeInput = document.querySelector('input[data-field="ban_time"]'); ... const allInputs = document.querySelectorAll('input'); ... return { ... hasDetailPanel: !!panel, ... hasBantimeInput: !!bantimeInput, ... inputCount: allInputs.length, ... bantimeValue: bantimeInput ? bantimeInput.value : 'NOT_FOUND', ... firstInputDataField: allInputs[0]?.getAttribute('data-field') || 'none' ... }; ... } Log Detail panel state after re-click: ${detail_html} ${reloaded}= Set Variable ${EMPTY} FOR ${i} IN RANGE 1 31 ${input_visible}= Run Keyword And Return Status Wait For Elements State css=input[data-field="ban_time"] visible timeout=5s IF ${input_visible} ${reloaded}= Evaluate JavaScript ${None} ... () => { ... const el = document.querySelector('input[data-field="ban_time"]'); ... return el ? el.value : 'NOT_FOUND'; ... } Log Reloaded bantime at attempt ${i}: ${reloaded} IF '${reloaded}' != '${EMPTY}' and '${reloaded}' != 'None' and '${reloaded}' != 'FAIL' and '${reloaded}' != 'NOT_FOUND' BREAK END END Sleep 1s END Log Reloaded bantime: ${reloaded} IF '${reloaded}' == '${EMPTY}' or '${reloaded}' == 'None' Fatal Error TEST_BODY: Ban Time input not found after reload. END Should Be Equal As Strings ${reloaded} 7200 Log Reload verification passed — value persisted. *** Keywords *** Restore Original Ban Time [Documentation] Restore jail's original ban_time so subsequent tests are unaffected. ${has_original}= Run Keyword And Return Status Should Not Be Empty ${ORIGINAL_BANTIME} IF not ${has_original} Log No original ban_time to restore. RETURN END Go To ${FRONTEND_URL}/config Wait For Load State domcontentloaded Sleep 5s Run Keyword And Ignore Error Click role=tab[name=Jails] Sleep 3s Run Keyword And Ignore Error Evaluate JavaScript ${None} ... () => { ... const listPane = document.querySelector('[role="listbox"]'); ... if (listPane) listPane.scrollTop = 0; ... const opts = document.querySelectorAll('[role="option"]'); ... if (opts.length > 0) opts[0].click(); ... } Sleep 3s Run Keyword And Ignore Error Fill Text css=input[aria-label="Ban Time (s)"] ${ORIGINAL_BANTIME} Sleep 3s Run Keyword And Ignore Error Wait For Elements State css=[role="status"]:has-text("Saved") visible timeout=15s Log Teardown restore ${ORIGINAL_BANTIME}