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.
Files changed (50) hide show
  1. SapApiLibrary/SapApiLibrary.py +380 -0
  2. SapApiLibrary/__init__.py +4 -0
  3. SapApiLibrary/py.typed +0 -0
  4. SapEccLibrary/SapEccLibrary.py +310 -0
  5. SapEccLibrary/__init__.py +9 -0
  6. SapEccLibrary/_vendor/__init__.py +4 -0
  7. SapEccLibrary/_vendor/sapgui_base.py +791 -0
  8. SapEccLibrary/keywords/__init__.py +16 -0
  9. SapEccLibrary/keywords/_connection.py +214 -0
  10. SapEccLibrary/keywords/_diagnostics.py +347 -0
  11. SapEccLibrary/keywords/_embedded_browser.py +188 -0
  12. SapEccLibrary/keywords/_grid.py +204 -0
  13. SapEccLibrary/keywords/_healing.py +106 -0
  14. SapEccLibrary/keywords/_perception.py +940 -0
  15. SapEccLibrary/keywords/_pointer.py +117 -0
  16. SapEccLibrary/keywords/_semantic.py +206 -0
  17. SapEccLibrary/keywords/_sessions.py +321 -0
  18. SapEccLibrary/keywords/_waits.py +189 -0
  19. SapEccLibrary/py.typed +0 -0
  20. SapFioriLibrary/SapFioriLibrary.py +1308 -0
  21. SapFioriLibrary/__init__.py +5 -0
  22. SapFioriLibrary/_ui5_js.py +2340 -0
  23. SapFioriLibrary/_ui5_runtime.py +295 -0
  24. SapFioriLibrary/py.typed +0 -0
  25. SapFioriLibrary/regen_recorder.py +33 -0
  26. robotframework_sapfx-0.6.4.dist-info/METADATA +263 -0
  27. robotframework_sapfx-0.6.4.dist-info/RECORD +50 -0
  28. robotframework_sapfx-0.6.4.dist-info/WHEEL +5 -0
  29. robotframework_sapfx-0.6.4.dist-info/licenses/LICENSE +201 -0
  30. robotframework_sapfx-0.6.4.dist-info/licenses/NOTICE +92 -0
  31. robotframework_sapfx-0.6.4.dist-info/top_level.txt +4 -0
  32. sapfx_common/__init__.py +7 -0
  33. sapfx_common/abap_list.py +48 -0
  34. sapfx_common/auth_flows.py +77 -0
  35. sapfx_common/client_security.py +111 -0
  36. sapfx_common/com_safety.py +35 -0
  37. sapfx_common/fiori_diagnostics.py +215 -0
  38. sapfx_common/healing.py +119 -0
  39. sapfx_common/healing_telemetry.py +68 -0
  40. sapfx_common/object_tree.py +159 -0
  41. sapfx_common/perception_diff.py +144 -0
  42. sapfx_common/polling.py +77 -0
  43. sapfx_common/py.typed +0 -0
  44. sapfx_common/screen_watch.py +160 -0
  45. sapfx_common/secrets.py +10 -0
  46. sapfx_common/semantic.py +600 -0
  47. sapfx_common/session_context.py +22 -0
  48. sapfx_common/visual_baseline.py +124 -0
  49. sapfx_common/visual_hash.py +214 -0
  50. 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"]
@@ -0,0 +1,4 @@
1
+ # Vendored third-party code. See sapgui_base.py header and the project NOTICE file.
2
+ from .sapgui_base import SapGuiBase
3
+
4
+ __all__ = ["SapGuiBase"]