robotframework-sapfx 0.6.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- SapApiLibrary/SapApiLibrary.py +380 -0
- SapApiLibrary/__init__.py +4 -0
- SapApiLibrary/py.typed +0 -0
- SapEccLibrary/SapEccLibrary.py +310 -0
- SapEccLibrary/__init__.py +9 -0
- SapEccLibrary/_vendor/__init__.py +4 -0
- SapEccLibrary/_vendor/sapgui_base.py +791 -0
- SapEccLibrary/keywords/__init__.py +16 -0
- SapEccLibrary/keywords/_connection.py +214 -0
- SapEccLibrary/keywords/_diagnostics.py +347 -0
- SapEccLibrary/keywords/_embedded_browser.py +188 -0
- SapEccLibrary/keywords/_grid.py +204 -0
- SapEccLibrary/keywords/_healing.py +106 -0
- SapEccLibrary/keywords/_perception.py +940 -0
- SapEccLibrary/keywords/_pointer.py +117 -0
- SapEccLibrary/keywords/_semantic.py +206 -0
- SapEccLibrary/keywords/_sessions.py +321 -0
- SapEccLibrary/keywords/_waits.py +189 -0
- SapEccLibrary/py.typed +0 -0
- SapFioriLibrary/SapFioriLibrary.py +1308 -0
- SapFioriLibrary/__init__.py +5 -0
- SapFioriLibrary/_ui5_js.py +2340 -0
- SapFioriLibrary/_ui5_runtime.py +295 -0
- SapFioriLibrary/py.typed +0 -0
- SapFioriLibrary/regen_recorder.py +33 -0
- robotframework_sapfx-0.6.4.dist-info/METADATA +263 -0
- robotframework_sapfx-0.6.4.dist-info/RECORD +50 -0
- robotframework_sapfx-0.6.4.dist-info/WHEEL +5 -0
- robotframework_sapfx-0.6.4.dist-info/licenses/LICENSE +201 -0
- robotframework_sapfx-0.6.4.dist-info/licenses/NOTICE +92 -0
- robotframework_sapfx-0.6.4.dist-info/top_level.txt +4 -0
- sapfx_common/__init__.py +7 -0
- sapfx_common/abap_list.py +48 -0
- sapfx_common/auth_flows.py +77 -0
- sapfx_common/client_security.py +111 -0
- sapfx_common/com_safety.py +35 -0
- sapfx_common/fiori_diagnostics.py +215 -0
- sapfx_common/healing.py +119 -0
- sapfx_common/healing_telemetry.py +68 -0
- sapfx_common/object_tree.py +159 -0
- sapfx_common/perception_diff.py +144 -0
- sapfx_common/polling.py +77 -0
- sapfx_common/py.typed +0 -0
- sapfx_common/screen_watch.py +160 -0
- sapfx_common/secrets.py +10 -0
- sapfx_common/semantic.py +600 -0
- sapfx_common/session_context.py +22 -0
- sapfx_common/visual_baseline.py +124 -0
- sapfx_common/visual_hash.py +214 -0
- sapfx_common/vocabulary.py +227 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""SapEccLibrary — un fork robuste de robotframework-sapguilibrary pour SAP ECC.
|
|
2
|
+
|
|
3
|
+
Hérite de tous les keywords de l'upstream ``SapGuiBase`` (vendored, Apache 2.0)
|
|
4
|
+
et ajoute :
|
|
5
|
+
|
|
6
|
+
* amorçage autonome de la connexion -> ``keywords/_connection.py``
|
|
7
|
+
* synchronisation réelle / attentes intelligentes -> ``keywords/_waits.py``
|
|
8
|
+
* ergonomie de grille ALV (lecture par titre) -> ``keywords/_grid.py``
|
|
9
|
+
* un override de `Run Transaction` indépendant de la locale (ci-dessous)
|
|
10
|
+
|
|
11
|
+
Il s'agit de la bibliothèque SAP GUI de *bas niveau*. Les keywords lisibles
|
|
12
|
+
métier pour les tests se trouvent dans ``resources/ecc_keywords.resource``
|
|
13
|
+
au-dessus de celle-ci.
|
|
14
|
+
"""
|
|
15
|
+
import os
|
|
16
|
+
import threading
|
|
17
|
+
|
|
18
|
+
from robot.api import logger
|
|
19
|
+
from robot.api.types import Secret
|
|
20
|
+
from robot.utils import timestr_to_secs
|
|
21
|
+
|
|
22
|
+
from sapfx_common.com_safety import ensure_com_initialized
|
|
23
|
+
from sapfx_common.secrets import reveal_secret
|
|
24
|
+
from sapfx_common.session_context import current_execution_namespace
|
|
25
|
+
|
|
26
|
+
from ._vendor.sapgui_base import SapGuiBase
|
|
27
|
+
from .keywords import (
|
|
28
|
+
ConnectionKeywords,
|
|
29
|
+
DiagnosticsKeywords,
|
|
30
|
+
EmbeddedBrowserKeywords,
|
|
31
|
+
GridKeywords,
|
|
32
|
+
HealingKeywords,
|
|
33
|
+
PerceptionKeywords,
|
|
34
|
+
PointerKeywords,
|
|
35
|
+
SemanticKeywords,
|
|
36
|
+
SessionKeywords,
|
|
37
|
+
WaitKeywords,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class SapEccLibrary(ConnectionKeywords, WaitKeywords, GridKeywords,
|
|
42
|
+
PerceptionKeywords, DiagnosticsKeywords, HealingKeywords,
|
|
43
|
+
SemanticKeywords, EmbeddedBrowserKeywords, PointerKeywords,
|
|
44
|
+
SessionKeywords, SapGuiBase):
|
|
45
|
+
"""Bibliothèque Robot Framework pour automatiser le client bureau SAP GUI (ECC,
|
|
46
|
+
backend S/4HANA GUI). Superset compatible de SapGuiLibrary.
|
|
47
|
+
|
|
48
|
+
== Avant d'exécuter les tests ==
|
|
49
|
+
Le scripting doit être activé côté serveur (transaction ``RZ11`` ->
|
|
50
|
+
``sapgui/user_scripting = TRUE``) et côté client (Options SAP GUI ->
|
|
51
|
+
Accessibilité & Scripting). Démarrez ensuite le Logon Pad manuellement et
|
|
52
|
+
appelez `Connect To Session`, ou laissez la bibliothèque le démarrer avec
|
|
53
|
+
`Open Sap Logon`.
|
|
54
|
+
|
|
55
|
+
== Localisateurs ==
|
|
56
|
+
Les éléments sont adressés par leur identifiant SAP depuis la fenêtre, ex.
|
|
57
|
+
``wnd[0]/usr/txtRSYST-BNAME``. Capturez les identifiants avec l'outil Spy
|
|
58
|
+
du projet (``tools/recorder``) ou l'enregistreur de scripts intégré au SAP GUI.
|
|
59
|
+
|
|
60
|
+
En complément, les keywords `Find Element By Label`, `Fill Field By Label`,
|
|
61
|
+
`Read Field By Label` et `Click Button By Label` acceptent des localisateurs
|
|
62
|
+
**humains** (libellé visible, ``Gauche @ Haut``, ``N @ Libellé``/``Libellé
|
|
63
|
+
@ N`` pour une grille, ``Ancre >> Reste`` pour une portée, ``= contenu`` —
|
|
64
|
+
voir `Find Element By Label`), résolus géométriquement sur l'écran réel ; et
|
|
65
|
+
`Resolve Element With Healing` accepte une ancre ``label=`` de secours.
|
|
66
|
+
|
|
67
|
+
== Contrôles navigateur embarqués ==
|
|
68
|
+
Certains écrans embarquent un contrôle WebView2 (aide moderne, Business
|
|
69
|
+
Client) : `Enable Embedded Browser Debugging` (à appeler avant `Open Sap
|
|
70
|
+
Logon`) puis `Switch To Embedded Browser Page` le rendent pilotable par la
|
|
71
|
+
bibliothèque Browser (``Library Browser`` requise dans la suite).
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
__version__ = "0.6.4"
|
|
75
|
+
ROBOT_LIBRARY_SCOPE = "SUITE"
|
|
76
|
+
ROBOT_LIBRARY_DOC_FORMAT = "ROBOT"
|
|
77
|
+
|
|
78
|
+
# Vrais préfixes de navigation de l'OK-code (nouvelle session/fenêtre/onglet).
|
|
79
|
+
# Un tcode de namespace client/partenaire (ex. ``/BEV1/RCA01``, ``/SAPAPO/...``)
|
|
80
|
+
# commence lui aussi par ``/`` mais n'EST PAS un préfixe de navigation.
|
|
81
|
+
_NAV_PREFIXES = ("/n", "/o", "/i")
|
|
82
|
+
|
|
83
|
+
# Alias de la session historique (hors registre) — voir keywords/_sessions.py.
|
|
84
|
+
_DEFAULT_ALIAS = "default"
|
|
85
|
+
|
|
86
|
+
def _context_state(self):
|
|
87
|
+
namespace = current_execution_namespace()
|
|
88
|
+
return self._state_by_namespace.setdefault(namespace, {})
|
|
89
|
+
|
|
90
|
+
# -- registre de sessions par alias (multi-session : keywords/_sessions.py) --
|
|
91
|
+
# ``sapapp`` (le moteur de scripting, un par process saplogon) reste PARTAGÉ
|
|
92
|
+
# au niveau du namespace ; ``session``/``connection`` sont routés vers le
|
|
93
|
+
# slot de l'alias ACTIF — la compatibilité est totale : sans keyword de
|
|
94
|
+
# registre, tout vit dans l'alias ``default`` comme avant.
|
|
95
|
+
|
|
96
|
+
def _session_registry(self):
|
|
97
|
+
return self._context_state().setdefault("sessions", {})
|
|
98
|
+
|
|
99
|
+
def _active_alias(self):
|
|
100
|
+
return self._context_state().get("active_alias", self._DEFAULT_ALIAS)
|
|
101
|
+
|
|
102
|
+
def _set_active_alias(self, alias):
|
|
103
|
+
self._context_state()["active_alias"] = alias
|
|
104
|
+
|
|
105
|
+
def _active_slot(self):
|
|
106
|
+
return self._session_registry().setdefault(self._active_alias(), {})
|
|
107
|
+
|
|
108
|
+
def _touch_com_thread(self, slot):
|
|
109
|
+
"""Rail de sûreté **STA** : la session appartient au thread COM qui l'a
|
|
110
|
+
bindée. Un accès depuis un AUTRE thread initialise COM défensivement
|
|
111
|
+
(marshaling — le mode dont dépendent les state providers rf-mcp,
|
|
112
|
+
validé live) et se journalise une fois ; ``SAPFX_STRICT_COM_THREAD=1``
|
|
113
|
+
en fait une erreur actionnable — au lieu du ``RPC_E_WRONG_THREAD``
|
|
114
|
+
cryptique de COM, ou pire d'un plantage différé."""
|
|
115
|
+
ident = threading.get_ident()
|
|
116
|
+
owner = slot.get("com_thread")
|
|
117
|
+
if owner is None or owner == ident:
|
|
118
|
+
return
|
|
119
|
+
if os.environ.get("SAPFX_STRICT_COM_THREAD") == "1":
|
|
120
|
+
raise RuntimeError(
|
|
121
|
+
"SAP session '%s' is COM-bound to thread %s but accessed from "
|
|
122
|
+
"thread %s, and SAPFX_STRICT_COM_THREAD=1 forbids cross-thread "
|
|
123
|
+
"access. SAP GUI Scripting is STA COM: drive every keyword of "
|
|
124
|
+
"a session from the thread that bound it — multiplex sessions "
|
|
125
|
+
"with `Switch Sap Session`, never with threads."
|
|
126
|
+
% (self._active_alias(), owner, ident))
|
|
127
|
+
seen = slot.setdefault("threads_seen", set())
|
|
128
|
+
if ident not in seen:
|
|
129
|
+
seen.add(ident)
|
|
130
|
+
ensure_com_initialized()
|
|
131
|
+
logger.debug(
|
|
132
|
+
"SAP session '%s': first access from thread %s (COM-bound on "
|
|
133
|
+
"thread %s); COM initialised defensively for marshalled "
|
|
134
|
+
"cross-thread access." % (self._active_alias(), ident, owner))
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def sapapp(self):
|
|
138
|
+
return self._context_state().get("sapapp", -1)
|
|
139
|
+
|
|
140
|
+
@sapapp.setter
|
|
141
|
+
def sapapp(self, value):
|
|
142
|
+
self._context_state()["sapapp"] = value
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def session(self):
|
|
146
|
+
slot = self._active_slot()
|
|
147
|
+
value = slot.get("session", -1)
|
|
148
|
+
if value is not None and not isinstance(value, int):
|
|
149
|
+
self._touch_com_thread(slot)
|
|
150
|
+
return value
|
|
151
|
+
|
|
152
|
+
@session.setter
|
|
153
|
+
def session(self, value):
|
|
154
|
+
slot = self._active_slot()
|
|
155
|
+
slot["session"] = value
|
|
156
|
+
if value is not None and not isinstance(value, int):
|
|
157
|
+
# binde le thread COM propriétaire ; un re-bind volontaire (Connect
|
|
158
|
+
# To Session depuis un autre thread) reprend la propriété.
|
|
159
|
+
slot["com_thread"] = threading.get_ident()
|
|
160
|
+
slot.pop("threads_seen", None)
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def connection(self):
|
|
164
|
+
return self._active_slot().get("connection", -1)
|
|
165
|
+
|
|
166
|
+
@connection.setter
|
|
167
|
+
def connection(self, value):
|
|
168
|
+
self._active_slot()["connection"] = value
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def _saplogon_proc(self):
|
|
172
|
+
return self._context_state().get("saplogon_proc")
|
|
173
|
+
|
|
174
|
+
@_saplogon_proc.setter
|
|
175
|
+
def _saplogon_proc(self, value):
|
|
176
|
+
self._context_state()["saplogon_proc"] = value
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def _last_screen_signature(self):
|
|
180
|
+
return self._context_state().get("last_screen_signature")
|
|
181
|
+
|
|
182
|
+
@_last_screen_signature.setter
|
|
183
|
+
def _last_screen_signature(self, value):
|
|
184
|
+
self._context_state()["last_screen_signature"] = value
|
|
185
|
+
|
|
186
|
+
@property
|
|
187
|
+
def _object_tree_unsupported(self):
|
|
188
|
+
return self._context_state().get("object_tree_unsupported", False)
|
|
189
|
+
|
|
190
|
+
@_object_tree_unsupported.setter
|
|
191
|
+
def _object_tree_unsupported(self, value):
|
|
192
|
+
self._context_state()["object_tree_unsupported"] = value
|
|
193
|
+
|
|
194
|
+
def __init__(self, screenshots_on_error=True, screenshot_directory=None,
|
|
195
|
+
default_timeout="30s", poll_interval="0.1s"):
|
|
196
|
+
"""``default_timeout`` est la valeur de repli utilisée par chaque keyword
|
|
197
|
+
``Wait Until ...``. Accepte les chaînes de temps Robot (``30s``, ``500 ms``).
|
|
198
|
+
|
|
199
|
+
``poll_interval`` est le délai entre deux sondages de `Wait Until Busy
|
|
200
|
+
Done` (l'attente la plus fréquemment invoquée, après quasi chaque
|
|
201
|
+
action) : la valeur par défaut convient à une session locale, mais une
|
|
202
|
+
connexion à un serveur SAP distant/plus lent peut bénéficier d'un
|
|
203
|
+
intervalle plus large pour réduire le nombre d'accès COM inutiles."""
|
|
204
|
+
self._state_by_namespace = {}
|
|
205
|
+
SapGuiBase.__init__(self, screenshots_on_error, screenshot_directory)
|
|
206
|
+
self.default_timeout = timestr_to_secs(default_timeout)
|
|
207
|
+
self.poll_interval = timestr_to_secs(poll_interval)
|
|
208
|
+
self._saplogon_proc = None
|
|
209
|
+
|
|
210
|
+
@classmethod
|
|
211
|
+
def _has_nav_prefix(cls, transaction):
|
|
212
|
+
"""Vrai si la chaîne commence par un préfixe de navigation OK-code (/n, /o, /i).
|
|
213
|
+
|
|
214
|
+
``transaction[:2]`` ne suffit pas : un tcode de namespace commençant par
|
|
215
|
+
N, O ou I (``/IWFND/MAINT_SERVICE``, ``/IWBEP/...``) partage ses deux
|
|
216
|
+
premiers caractères avec un préfixe. La forme du reste tranche : après un
|
|
217
|
+
vrai préfixe ne peut venir qu'un tcode simple (sans ``/``) ou un tcode de
|
|
218
|
+
namespace (commençant par ``/``, ex. ``/n/BEV1/RCA01``). Un reste
|
|
219
|
+
contenant ``/`` sans commencer par ``/`` (``WFND/MAINT_SERVICE``) n'est
|
|
220
|
+
pas un tcode valide — le ``/X`` initial appartenait à un namespace.
|
|
221
|
+
"""
|
|
222
|
+
if transaction[:2].lower() not in cls._NAV_PREFIXES:
|
|
223
|
+
return False
|
|
224
|
+
rest = transaction[2:]
|
|
225
|
+
return not rest or rest.startswith("/") or "/" not in rest
|
|
226
|
+
|
|
227
|
+
def run_transaction(self, transaction, skip_if_error=False):
|
|
228
|
+
"""Exécute une transaction SAP et vérifie qu'elle s'est bien ouverte.
|
|
229
|
+
|
|
230
|
+
Remplace le keyword upstream, qui détectait une transaction inconnue en
|
|
231
|
+
comparant le *texte* de la barre de statut en néerlandais/anglais/allemand
|
|
232
|
+
uniquement — fragile dans toute autre langue SAP. Ici, on compare la
|
|
233
|
+
**transaction réellement active** (``session.Info.Transaction``) au code
|
|
234
|
+
demandé : c'est totalement indépendant de la locale et robuste (validé en
|
|
235
|
+
live, où une transaction inexistante renvoie un message de type ``S`` et non
|
|
236
|
+
``E`` — l'ancienne hypothèse « type ``E`` » était donc fausse).
|
|
237
|
+
|
|
238
|
+
Préfixe automatiquement ``/n`` pour démarrer la transaction même depuis un
|
|
239
|
+
autre écran — y compris pour un tcode de **namespace** (``/BEV1/RCA01``),
|
|
240
|
+
dont le ``/`` initial fait partie du code et n'est pas un préfixe de
|
|
241
|
+
navigation : seul un vrai préfixe déjà présent (``/n``, ``/o``, ``/i``)
|
|
242
|
+
dispense d'en rajouter un — même quand le namespace commence par la même
|
|
243
|
+
lettre qu'un préfixe (``/IWFND/MAINT_SERVICE``), voir `_has_nav_prefix`.
|
|
244
|
+
Définissez ``skip_if_error=True`` pour journaliser au lieu d'échouer
|
|
245
|
+
(étapes de navigation optionnelles).
|
|
246
|
+
"""
|
|
247
|
+
already_prefixed = self._has_nav_prefix(transaction)
|
|
248
|
+
okcode = transaction if already_prefixed else "/n" + transaction
|
|
249
|
+
self.session.findById("wnd[0]/tbar[0]/okcd").text = okcode
|
|
250
|
+
self.send_vkey(0)
|
|
251
|
+
self.wait_until_busy_done()
|
|
252
|
+
|
|
253
|
+
# Les OK-codes système (fermer session/fenêtre) ne démarrent pas de transaction.
|
|
254
|
+
if transaction.lower() in ("/nex", "/n", "/i", "/o", "/nend"):
|
|
255
|
+
return
|
|
256
|
+
|
|
257
|
+
expected = transaction.upper()
|
|
258
|
+
if already_prefixed and len(expected) > 2:
|
|
259
|
+
expected = expected[2:]
|
|
260
|
+
|
|
261
|
+
actual = self.session.Info.Transaction
|
|
262
|
+
# Un tcode de namespace peut être rendu par ``Info.Transaction`` avec ou
|
|
263
|
+
# sans son ``/`` initial : les deux côtés sont normalisés à l'identique
|
|
264
|
+
# (aucun tcode valide ne se distingue d'un autre par ses ``/`` de tête).
|
|
265
|
+
if actual.upper().lstrip("/") != expected.lstrip("/"):
|
|
266
|
+
status = self.session.findById("wnd[0]/sbar")
|
|
267
|
+
message = ("Transaction '%s' did not start (active='%s'). Status: %s"
|
|
268
|
+
% (transaction, actual, status.text))
|
|
269
|
+
if skip_if_error:
|
|
270
|
+
logger.warn(message)
|
|
271
|
+
return
|
|
272
|
+
self.take_screenshot()
|
|
273
|
+
raise ValueError(message)
|
|
274
|
+
|
|
275
|
+
def input_password(self, element_id, password: "str | Secret"):
|
|
276
|
+
"""Saisit un mot de passe dans le champ identifié, sans le journaliser.
|
|
277
|
+
|
|
278
|
+
Remplace le keyword upstream pour accepter, en plus d'une chaîne, le
|
|
279
|
+
type ``Secret`` de Robot Framework 7.4 — créez la variable typée dès la
|
|
280
|
+
ligne de commande (``-v "SAP_PASSWORD: Secret:<motdepasse>"``) : sa
|
|
281
|
+
valeur est alors masquée partout, y compris en niveau de log TRACE. Le
|
|
282
|
+
secret n'est déballé qu'ici, juste avant la frontière COM ; le
|
|
283
|
+
comportement pour une chaîne ordinaire est inchangé.
|
|
284
|
+
"""
|
|
285
|
+
return super().input_password(element_id, reveal_secret(password))
|
|
286
|
+
|
|
287
|
+
def get_current_transaction(self):
|
|
288
|
+
"""Retourne le **code de la transaction active** (``session.Info.Transaction``),
|
|
289
|
+
p.ex. ``SE16`` ou ``SESSION_MANAGER`` sur l'écran SAP Easy Access.
|
|
290
|
+
Indépendant de la langue — idéal pour les assertions de navigation."""
|
|
291
|
+
return self.session.Info.Transaction
|
|
292
|
+
|
|
293
|
+
def get_status_message(self):
|
|
294
|
+
"""Retourne ``(message_type, text)`` de la barre de statut courante.
|
|
295
|
+
|
|
296
|
+
``message_type`` vaut ``S`` (succès), ``W`` (avertissement), ``E`` (erreur),
|
|
297
|
+
``I`` (info), ``A`` (abandon), ou ``""`` quand la barre est vide — tous
|
|
298
|
+
indépendants de la locale."""
|
|
299
|
+
status = self.session.findById("wnd[0]/sbar")
|
|
300
|
+
return status.messageType, status.text
|
|
301
|
+
|
|
302
|
+
def status_message_should_be_success(self):
|
|
303
|
+
"""Échoue si la barre de statut n'affiche pas actuellement un message de succès (``S``)."""
|
|
304
|
+
msg_type, text = self.get_status_message()
|
|
305
|
+
if msg_type not in ("S", ""):
|
|
306
|
+
self.take_screenshot()
|
|
307
|
+
raise AssertionError(
|
|
308
|
+
"Expected a success status message but got type '%s': %s"
|
|
309
|
+
% (msg_type, text)
|
|
310
|
+
)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""SapEccLibrary — bibliothèque SAP GUI (backend ECC/S4) robuste pour Robot Framework.
|
|
2
|
+
|
|
3
|
+
L'import du nom du paquet comme bibliothèque Robot fonctionne car le nom de la classe
|
|
4
|
+
correspond au module : ``Library SapEccLibrary``.
|
|
5
|
+
"""
|
|
6
|
+
from .SapEccLibrary import SapEccLibrary
|
|
7
|
+
|
|
8
|
+
__version__ = "0.6.4"
|
|
9
|
+
__all__ = ["SapEccLibrary"]
|