Beobachtbarkeit von Ladezuständen
Readiness-Signale definieren und zuverlässig prüfen – statt fixer Sleeps.
Ziel: Klare, messbare Readiness-Signale definieren (und prüfen), damit Keywords ereignis-/zustandsbasiert warten – ohne Sleeps.
Bezug zur Heiligen Dreifaltigkeit (Was–Wie–Tut):
Was: Seite/Widget (abstrakter Lokator, z. B.
TravelExpenses
/Destination
)Tut: Keyword (
Click
,Set Value
,Verify Value
)Wie: Adapter prüft Readiness (keine Spinner, network idle, sichtbar/enabled …) und führt aus.
1. Begriffe & Signale
Spinner / Busy-Icon / indeterminate progress Sichtbares Lade-Signal ohne Restdauer. Ready ⇒ nicht sichtbar / entfernt.
Determinate Progressbar (
role="progressbar"
mit Wert) Ready ⇒ 100 % oder Progressbar nicht sichtbar.Overlay/Backdrop (halbtransparente Sperre) Ready ⇒ Overlay verschwunden / nicht sichtbar.
Skeletons (graue Platzhalter) Ready ⇒ Skeleton weg und echter Content sichtbar.
ARIA busy (
aria-busy="true"
) Ready ⇒aria-busy="false"
am relevanten Container.Network idle Keine offenen Requests seit X ms (Quiet-Zeit).
Praxis: Ein Readiness-Check kombiniert mehrere Signale (AND/OR) – z. B. „kein Spinner und network idle“.
2. Designrichtlinien (Selektoren & Scopes)
Stabile TestIDs vergeben:
data-testid="loading-spinner"
,data-testid="app-root"
.Scope setzen: innerhalb des Seiten-Roots suchen (z. B.
[data-testid="expenses-page"]
), damit fremde Spinner nicht stören.Entfernt vs. versteckt: Entscheidet, ob Spinner aus dem DOM entfernt oder nur versteckt wird → entsprechend auf „nicht vorhanden“ oder „nicht sichtbar“ prüfen.
Zugänglichkeit nutzen:
role="progressbar"
,aria-busy
sind robuste, technikunabhängige Signale.
3. Defaults & Overrides (YAML)
_defaults:
timeouts:
page_ready: 15s
widget_ready: 5s
post_click_settle: 2s
poll_interval_ms: 200
spinner_selector:
- '[data-testid="loading-spinner"]'
- '.ant-spin'
- '.MuiCircularProgress-root'
- '[role="progressbar"]'
aria_busy_container: '[data-testid="app-root"]'
network_idle_quiet_ms: 500
TravelExpenses:
__self__:
class: okw4robot_web.gui.web.widgets.PageRoot
locator: { css: '[data-testid="expenses-page"]' }
timeouts:
page_ready: 20s # Seite darf länger brauchen
readyWhen:
- no_spinner: true
- aria_busy: false
- network_idle: true
Destination:
class: okw4robot_web.gui.web.widgets.TextField
locator: { css: '#destination' }
timeouts:
widget_ready: 3s # Widget ist schnell erreichbar
readyWhen:
- visible: true
- enabled: true
Save:
class: okw4robot_web.gui.web.widgets.Button
locator: { css: 'button.save' }
readyWhen:
- visible: true
- enabled: true
4. Adapter-Keywords (Robot, gekürzt)
*** Settings ***
Library SeleniumLibrary
Library DateTime
Library Collections
*** Variables ***
${PAGE_READY} 15s
${WIDGET_READY} 5s
${POST_CLICK_SETTLE} 2s
${POLL_INTERVAL} 200ms
${NETWORK_IDLE_QUIET} 500ms
${SPINNER} css:[data-testid="loading-spinner"]
${PAGE_ROOT} css:[data-testid="expenses-page"]
# Beispielhafte abstrakte Lokatoren → konkrete Selektoren
&{LOCATORS} Destination=css:#destination
... Save=css:button.save
*** Keywords ***
# ------------------------------ Page/Widget Readiness ------------------------------
Wait Until Page Ready
[Arguments] ${page}=TravelExpenses
${cfg}= Get Config For Page ${page}
${deadline}= Compute Deadline ${cfg.timeouts.page_ready}
WHILE ${True}
${ok1}= No Spinner Visible ${cfg.spinner_selector} ${cfg.page_root}
${ok2}= Aria Busy Is False ${cfg.page_root}
${ok3}= Network Is Idle ${NETWORK_IDLE_QUIET}
Run Keyword If ${ok1} and ${ok2} and ${ok3} Exit For Loop
Run Keyword If Is After Deadline ${deadline} Fail Page not ready in ${cfg.timeouts.page_ready}
Sleep ${POLL_INTERVAL}
END
Wait For Widget Ready
[Arguments] ${name}
${cfg}= Get Config For Widget ${name}
# Wartet innerhalb des Widget-Ready-Budgets mit Poll-Intervallen
Wait Until Keyword Succeeds ${cfg.timeouts.widget_ready} ${POLL_INTERVAL}
... Element Should Be Visible ${cfg.locator}
Element Should Be Enabled ${cfg.locator}
# ------------------------------ Click mit Pre-/Post-Handling ------------------------------
Click
[Arguments] ${name}
${cfg}= Get Config For Widget ${name}
# PRE: Widget-Readiness (sichtbar, enabled, im Viewport etc.)
Wait For Widget Ready ${name}
# ACTION: Klick auf den konkreten Locator
Click Element ${cfg.locator}
# POST: Settle/Network-Idle innerhalb des Post-Click-Budgets
# (mit Bordmitteln simuliert – echtes Network-Idle hängt vom Stack ab)
Wait Until Keyword Succeeds ${POST_CLICK_SETTLE} ${POLL_INTERVAL}
... Network Is Idle ${NETWORK_IDLE_QUIET}
# ------------------------------ Helfer (Minimal-Stubs) ------------------------------
Get Config For Page
[Arguments] ${page}
&{timeouts}= Create Dictionary page_ready=${PAGE_READY}
&{cfg}= Create Dictionary timeouts=&{timeouts} poll_interval=${POLL_INTERVAL}
... page_root=${PAGE_ROOT} spinner_selector=${SPINNER}
[Return] &{cfg}
Get Config For Widget
[Arguments] ${name}
${locator}= Get From Dictionary ${LOCATORS} ${name}
&{timeouts}= Create Dictionary widget_ready=${WIDGET_READY} post_click_settle=${POST_CLICK_SETTLE}
&{cfg}= Create Dictionary locator=${locator} timeouts=&{timeouts} poll_interval=${POLL_INTERVAL}
[Return] &{cfg}
Compute Deadline
[Arguments] ${timeout}
${now}= Get Time epoch
${secs}= Convert Time ${timeout} result_format=number
${deadline}= Evaluate ${now} + ${secs}
[Return] ${deadline}
Is After Deadline
[Arguments] ${deadline}
${now}= Get Time epoch
${after}= Evaluate ${now} > ${deadline}
[Return] ${after}
No Spinner Visible
[Arguments] ${selector} ${scope}=${EMPTY}
# Einfacher Check: Spinner darf nicht vorhanden/ sichtbar sein
${in_scope}= Set Variable If '${scope}'!='' ${scope} >> ${selector} ${selector}
${ok}= Run Keyword And Return Status Page Should Not Contain Element ${in_scope}
[Return] ${ok}
Aria Busy Is False
[Arguments] ${container}
${val}= Get Element Attribute ${container} aria-busy
${val}= Set Variable If '${val}'=='' false ${val}
${ok}= Evaluate str(${val}).lower() != 'true'
[Return] ${ok}
Network Is Idle
[Arguments] ${quiet_ms}=500ms
# Platzhalter für Bordmittel: kurze Quiet-Zeit abwarten und „True“ liefern.
# In OKW4Robot würde hier echter Netz-/Event-Idle geprüft.
Sleep ${quiet_ms}
[Return] ${True}
Hinweise:
Budgetierung:
Click
verbraucht sein Budget in zwei Phasen:widget_ready
(PRE) undpost_click_settle
(POST). In einer echten Lib könntest du das als Gesamtbudget messen; hier halten wir’s Bordmittel-einfach.Network Is Idle: ist hier absichtlich ein Stub. Im Framework ersetzt du das durch einen echten Idle-Check (DevTools, Playwright
networkidle
, App-Hook).No Spinner Visible: für Demo simpel gehalten; wenn ihr mehrere Spinner-Selektoren habt, kannst du das leicht erweitern (Liste iterieren).
5. Network-Idle messen (Hook-Beispiel, Browser/JS)
Falls euer Stack es nicht schon anbietet, könnt ihr Fetch/XHR zählen:
// in app test-mode oder via devtools injection
(function () {
const w = window;
w.__activeRequests = 0;
const origFetch = w.fetch;
w.fetch = function() {
w.__activeRequests++;
return origFetch.apply(this, arguments).finally(() => w.__activeRequests--);
};
const origOpen = XMLHttpRequest.prototype.open;
const origSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function() { this.__tracked = true; return origOpen.apply(this, arguments); };
XMLHttpRequest.prototype.send = function() {
if (this.__tracked) {
w.__activeRequests++;
this.addEventListener('loadend', () => w.__activeRequests--, { once: true });
}
return origSend.apply(this, arguments);
};
})();
Euer Adapter prüft dann „idle“ so:
// Pseudocode aus Adapter
function isNetworkIdle(quietMs = 500) {
if (window.__activeRequests > 0) return false;
// zusätzlich: in den letzten quietMs keine Änderung
// z. B. Zeitstempel der letzten Aktivität tracken
return true;
}
Alternativen: Playwrights
wait_for_load_state('networkidle')
, Browser-DevTools-Protokoll, App-seitige Events.
6. Telemetry & KPIs
Pro Keyword loggen:
keyword
, locator_name
, wait_reason
(spinner/network/visibility), wait_ms
, retries
, timeout_budget_ms
, result
.
Ziele (Startwerte):
p95
wait_ms
pro Keyword < 2 s0 Sleeps/explicit Waits in Testdateien
Flake-Rate < 1 % pro Woche
7. Anti-Pattern vs. Clean
Schlecht
Sleep 5s
Click Element xpath=//div[3]/span[2]
Wait Until Element Is Visible css=.toast-success 10s
Gut (Readiness hinter Keywords)
Click Save
Verify Value Status Message Expense report saved
8. Do/Don’t-Checkliste
Do
Readiness-Signale zentral definieren (YAML)
Keywords warten implizit (Budget, Poll-Intervalle)
Scope beim Suchen beachten (Page-Root)
Telemetry & KPIs aktiv auswerten
Don’t
Keine
Sleep
/Wait Until ...
im TestfallKeine Spinner-Heuristiken im Testfall
Keine konkreten Selektoren in Tests (nur abstrakte Lokatoren)
Zuletzt aktualisiert