
SAP test automation is often associated with basic scripting techniques, but in many enterprise environments, these methods are either restricted or insufficient for complex workflows. This article explores advanced approaches to controlling SAP GUI using Python and Windows API, building upon concepts introduced in the AFINE article “Security Assessment of SAP GUI Controls Using Windows API in Python”. While the original piece focused on assessing SAP GUI controls for security using a basic API, it often doesn’t work for custom controls. Here, we take those principles further—demonstrating how low-level Windows API calls can be used to manage window focus, navigate dynamic fields, and simulate keystrokes for robust automation. These techniques provide greater flexibility and reliability, making them ideal for scenarios where standard SAP scripting falls short.
Clearing Input Fields Using Windows API for SAP Test Automation
One of the most practical examples of SAP GUI control is the ability to clear the contents of an active input field without relying on SAP’s built-in scripting engine. The clear_current_field() function demonstrates this by simulating keyboard actions at the operating system level through the Windows API. Instead of interacting with SAP GUI objects directly, this method sends low-level key events using user32.keybd_event, which emulate physical keystrokes.
The function works in two steps:
- Select All Text – It issues a
Ctrl+Acombination to highlight all content in the currently focused field. This is done by pressing and releasing theCtrlandAkeys in sequence, ensuring the selection is complete. - Delete the Content – After a short delay to allow the system to process the selection, the function sends a
Backspacekey event to remove the highlighted text.
Small time.sleep() intervals are included to prevent race conditions and ensure reliable execution. This approach is particularly useful when SAP GUI scripting is disabled or when dealing with dynamic screens where object IDs change frequently. However, developers should be aware of potential pitfalls such as focus issues (the wrong field being cleared) and timing problems on slower systems. To mitigate these risks, it’s recommended to implement error handling, dynamic delays, and checks to confirm the correct window or control is active before executing the function.
def clear_current_field():
# Press Ctrl+A
user32.keybd_event(VK_CONTROL, 0, 0, 0) # Ctrl down
user32.keybd_event(VK_A, 0, 0, 0) # A down
user32.keybd_event(VK_A, 0, KEYEVENTF_KEYUP, 0) # A up
user32.keybd_event(VK_CONTROL, 0, KEYEVENTF_KEYUP, 0) # Ctrl up
time.sleep(0.1) # Allow system to process Ctrl+A
# Press Backspace to delete
user32.keybd_event(VK_BACK, 0, 0, 0)
user32.keybd_event(VK_BACK, 0, KEYEVENTF_KEYUP, 0)
time.sleep(0.1)Managing Focus and Navigating Controls for SAP Test Automation
Controlling SAP GUI effectively often requires more than just interacting with visible elements – it demands precise management of focus and keyboard input at the operating system level. The following functions demonstrate advanced SAP test automation techniques for achieving this using the Windows API.
1. Sending the ENTER Key
The send_enter_key(hwnd) function is designed to trigger an action in a specific SAP GUI window by simulating an ENTER key press. It begins by calling SetForegroundWindow to bring the target window to the front, ensuring it is ready to receive input. After a short delay to allow the activation to settle, the function posts WM_KEYDOWN and WM_KEYUP messages for the VK_RETURN key directly to the window’s message queue. This approach is efficient because it bypasses higher-level input layers and communicates directly with the window, but it relies on the application correctly handling these messages as real keystrokes.
2. Setting Focus
The set_focus(hwnd) function complements the previous one by explicitly managing focus. It uses SetForegroundWindow to activate the window and SetFocus to ensure that subsequent input is directed to the correct control. This is particularly important in SAP environments where focus can shift unexpectedly during navigation or screen refreshes. Without proper focus management, keystrokes might be sent to the wrong field, causing automation errors.
3. Navigating with SHIFT+TAB
The send_shift_tab(hwnd) function simulates the Shift+Tab key combination, which moves the cursor to the previous input field according to the application’s tab order. It achieves this by issuing keybd_event calls for both Shift and Tab, pressing and releasing them in sequence. This method is useful for dynamic navigation when field positions change or when backward traversal is required in complex SAP screens.
def send_enter_key(hwnd):
"""Send an ENTER key to the specified window."""
# Bring the window to the foreground
user32.SetForegroundWindow(hwnd)
time.sleep(0.1) # Give time for the window to become active
# Send WM_KEYDOWN and WM_KEYUP messages for the ENTER key
user32.PostMessageW(hwnd, WM_KEYDOWN, VK_RETURN, 0)
user32.PostMessageW(hwnd, WM_KEYUP, VK_RETURN, 0)
def set_focus(hwnd):
"""Send an ENTER key to the specified window."""
# Bring the window to the foreground
user32.SetForegroundWindow(hwnd)
user32.SetFocus(hwnd)
time.sleep(0.1) # Give time for the window to become active
def send_shift_tab(hwnd):
user32.keybd_event(VK_SHIFT, 0, 0, 0)
user32.keybd_event(VK_TAB, 0, 0, 0)
user32.keybd_event(VK_TAB, 0, KEYEVENTF_KEYUP, 0)
user32.keybd_event(VK_SHIFT, 0, KEYEVENTF_KEYUP, 0)
time.sleep(0.1) 4. Best Practices for SAP Test Automation
While these functions provide powerful control, they operate at different layers—window message posting, synthesized keystrokes, and focus APIs. Combining them strategically can improve reliability:
- Step 1: Establish focus using
set_focus(). - Step 2: Navigate to the desired field with
send_shift_tab(). - Step 3: Trigger actions using
send_enter_key().
To avoid edge cases such as lost input or incorrect focus, developers should implement adaptive delays, verify the active window before sending keys, and consider using SendInput for more robust keystroke injection when PostMessage does not produce the expected behavior.
Sending Text as Keystrokes for SAP Test Automation
The two functions—get_vk_and_shift(char) and send_keys_via_keybd_event(text)—work together to convert human-readable text into low-level keystroke events that Windows can process, making them ideal for SAP GUI automation where direct text injection may not be supported.
1. Character Mapping with get_vk_and_shift
The get_vk_and_shift function determines the correct virtual-key (VK) code for a given character and whether the Shift key is required to produce it. It handles uppercase and lowercase letters, digits, common punctuation marks (such as _, -, /, and ?), and special symbols that require Shift combined with number keys (like !, @, #, $, %, and others). If an unsupported character is encountered, the function raises a ValueError, ensuring predictable behavior and easy debugging.
2. Simulating Keystrokes with send_keys_via_keybd_event
The send_keys_via_keybd_event function iterates through each character in the input string and uses the mapping provided by get_vk_and_shift to send the appropriate keystrokes. It employs keybd_event to simulate key press and release events, optionally pressing and releasing Shift when needed. A short delay (time.sleep(0.01)) between characters ensures that the target application processes each keystroke reliably, reducing the risk of missed inputs—especially in SAP GUI screens that validate fields dynamically.
3. Why This Approach Matters
This method offers granular control over input, bypassing limitations of higher-level automation APIs. It guarantees that the exact keystrokes expected by the UI are delivered, making it suitable for fields that intercept raw keyboard events or enforce strict input validation. Additionally, the design keeps error handling straightforward by logging unsupported characters without interrupting the entire input routine.
def get_vk_and_shift(char):
"""Returns (vk_code, use_shift) tuple for a given character."""
if 'A' <= char <= 'Z':
return ord(char), True
elif 'a' <= char <= 'z':
return ord(char.upper()), False
elif '0' <= char <= '9':
return ord(char), False
elif char == '_':
return 0xBD, True # Shift + '-' = '_'
elif char == '-':
return 0xBD, False # VK_OEM_MINUS
elif char == ' ':
return 0x20, False # Space
elif char == '/':
return 0xBF, False # VK_OEM_2
elif char == '?':
return 0xBF, True # Shift + '/' = '?'
# Special characters that require Shift with number keys
shift_symbols = {
'!': ('1', True),
'@': ('2', True),
'#': ('3', True),
'$': ('4', True),
'%': ('5', True),
'^': ('6', True),
'&': ('7', True),
'*': ('8', True),
'(': ('9', True),
')': ('0', True),
}
if char in shift_symbols:
digit_char, shift = shift_symbols[char]
return ord(digit_char), shift
raise ValueError(f"Unsupported character: {repr(char)}")
def send_keys_via_keybd_event(text):
for char in text:
try:
vk_code, use_shift = get_vk_and_shift(char)
if use_shift:
ctypes.windll.user32.keybd_event(VK_SHIFT, 0, 0, 0)
ctypes.windll.user32.keybd_event(vk_code, 0, 0, 0) # key down
ctypes.windll.user32.keybd_event(vk_code, 0, KEYEVENTF_KEYUP, 0) # key up
if use_shift:
ctypes.windll.user32.keybd_event(VK_SHIFT, 0, KEYEVENTF_KEYUP, 0)
time.sleep(0.01)
except ValueError as e:
print(e)Case Study: SAP Test Automation for SAP GUI Login
This case study demonstrates how a Python script can automate SAP GUI login without using SAP’s built-in scripting engine. The script leverages the Windows API via Python’s ctypes library to interact directly with SAP GUI windows and simulate keyboard input. This approach is ideal for environments where scripting is disabled or restricted due to security policies.
Core SAP Test Automation Functionality
- Window Detection
The script begins by searching for the main SAP GUI window using its title. If the main window is not found, it attempts to locate the SAP Logon window and interact with the “Log On” button to start a new session. This is done using functions likefind_window_by_titleandfind_child_window, which enumerate windows and match their captions. - Focus Management
Once the correct window is identified, the script usesset_focusto bring it to the foreground and ensure it is ready to receive keyboard input. This step is critical because simulated keystrokes will only work if the target window has focus. - Field Navigation and Clearing
The script navigates between input fields usingsend_shift_tab(to move backward) and clears existing content withclear_current_field, which sends Ctrl+A followed by Backspace. This guarantees that old data does not interfere with the new login credentials. - Keystroke Simulation
Login details such as language, password, username, and client number are entered usingsend_keys_via_keybd_event. This function converts each character into its corresponding virtual key code and simulates key presses, including handling special characters that require the Shift key. - Final Submission
After filling in all required fields, the script sends an Enter keystroke usingsend_enter_keyto submit the login form and complete the process.
import ctypes, sys
from ctypes import wintypes
import time, string
user32 = ctypes.windll.user32
WM_KEYDOWN = 0x0100
WM_KEYUP = 0x0101
VK_RETURN = 0x0D
VK_TAB = 0x09
VK_SHIFT = 0x10
VK_CONTROL = 0x11
VK_A = 0x41
VK_BACK = 0x08
KEYEVENTF_KEYUP = 0x0002
EnumWindowsProc = ctypes.WINFUNCTYPE(ctypes.c_bool, wintypes.HWND, wintypes.LPARAM)
def find_window_by_title(target_title):
found_hwnd = []
def callback(hwnd, lParam):
length = user32.GetWindowTextLengthW(hwnd)
if length > 0:
buffer = ctypes.create_unicode_buffer(length + 1)
user32.GetWindowTextW(hwnd, buffer, length + 1)
if target_title in buffer.value:
print(buffer.value)
found_hwnd.append(hwnd)
return False
return True
tmp=user32.EnumWindows(EnumWindowsProc(callback), 0)
return found_hwnd[0] if found_hwnd else None
WM_GETTEXT = 0x000D
WM_GETTEXTLENGTH = 0x000E
def get_window_caption(hwnd):
text_length = user32.SendMessageW(hwnd, WM_GETTEXTLENGTH, 0, 0)
if text_length > 0:
buffer = ctypes.create_unicode_buffer(text_length + 1)
user32.SendMessageW(hwnd, WM_GETTEXT, text_length + 1, ctypes.byref(buffer))
return buffer.value
return ""
def find_child_window(parent_hwnd, target_text):
found_child_hwnd = []
def callback(hwnd, lParam):
length = user32.GetWindowTextLengthW(hwnd)
window_caption=get_window_caption(hwnd)
if length > 0:
buffer = ctypes.create_unicode_buffer(length + 1)
user32.GetWindowTextW(hwnd, buffer, length + 1)
if target_text in buffer.value:
found_child_hwnd.append(hwnd)
return False
if window_caption and len(window_caption) > 0:
if target_text in window_caption:
found_child_hwnd.append(hwnd)
return False
return True
user32.EnumChildWindows(parent_hwnd, EnumWindowsProc(callback), 0)
return found_child_hwnd[0] if found_child_hwnd else None
def set_focus(hwnd):
"""Send an ENTER key to the specified window."""
user32.SetForegroundWindow(hwnd)
user32.SetFocus(hwnd)
time.sleep(0.1)
def send_shift_tab(hwnd):
user32.keybd_event(VK_SHIFT, 0, 0, 0)
user32.keybd_event(VK_TAB, 0, 0, 0)
user32.keybd_event(VK_TAB, 0, KEYEVENTF_KEYUP, 0)
user32.keybd_event(VK_SHIFT, 0, KEYEVENTF_KEYUP, 0)
time.sleep(0.1)
def send_shift_enter(hwnd):
user32.keybd_event(VK_SHIFT, 0, 0, 0)
user32.keybd_event(VK_RETURN, 0, 0, 0)
user32.keybd_event(VK_RETURN, 0, KEYEVENTF_KEYUP, 0)
user32.keybd_event(VK_SHIFT, 0, KEYEVENTF_KEYUP, 0)
time.sleep(0.1)
def get_vk_and_shift(char):
"""Returns (vk_code, use_shift) tuple for a given character."""
if 'A' <= char <= 'Z':
return ord(char), True
elif 'a' <= char <= 'z':
return ord(char.upper()), False
elif '0' <= char <= '9':
return ord(char), False
elif char == '_':
return 0xBD, True # Shift + '-' = '_'
elif char == '-':
return 0xBD, False # VK_OEM_MINUS
elif char == ' ':
return 0x20, False # Space
elif char == '/':
return 0xBF, False # VK_OEM_2
elif char == '?':
return 0xBF, True # Shift + '/' = '?'
# Special characters that require Shift with number keys
shift_symbols = {
'!': ('1', True),
'@': ('2', True),
'#': ('3', True),
'$': ('4', True),
'%': ('5', True),
'^': ('6', True),
'&': ('7', True),
'*': ('8', True),
'(': ('9', True),
')': ('0', True),
}
if char in shift_symbols:
digit_char, shift = shift_symbols[char]
return ord(digit_char), shift
raise ValueError(f"Unsupported character: {repr(char)}")
def send_keys_via_keybd_event(text):
for char in text:
try:
vk_code, use_shift = get_vk_and_shift(char)
if use_shift:
ctypes.windll.user32.keybd_event(VK_SHIFT, 0, 0, 0)
ctypes.windll.user32.keybd_event(vk_code, 0, 0, 0) # key down
ctypes.windll.user32.keybd_event(vk_code, 0, KEYEVENTF_KEYUP, 0) # key up
if use_shift:
ctypes.windll.user32.keybd_event(VK_SHIFT, 0, KEYEVENTF_KEYUP, 0)
time.sleep(0.01)
except ValueError as e:
print(e)
def clear_current_field():
# Press Ctrl+A
user32.keybd_event(VK_CONTROL, 0, 0, 0) # Ctrl down
user32.keybd_event(VK_A, 0, 0, 0) # A down
user32.keybd_event(VK_A, 0, KEYEVENTF_KEYUP, 0) # A up
user32.keybd_event(VK_CONTROL, 0, KEYEVENTF_KEYUP, 0) # Ctrl up
time.sleep(0.1) # Allow system to process Ctrl+A
# Press Backspace to delete
user32.keybd_event(VK_BACK, 0, 0, 0)
user32.keybd_event(VK_BACK, 0, KEYEVENTF_KEYUP, 0)
time.sleep(0.1)
def send_enter_key():
# Press Enter
ctypes.windll.user32.keybd_event(VK_RETURN, 0, 0, 0)
# Release Enter
ctypes.windll.user32.keybd_event(VK_RETURN, 0, KEYEVENTF_KEYUP, 0)
# Main logic
if __name__ == "__main__":
print("\nSAP GUI Login Tool v0.2.1 by Michał Majchrowicz AFINE Team\n")
if len(sys.argv) < 2:
print(f"{sys.argv[0]} <login_nubmer> [<PL>]\n");
exit(-1)
sap_login_number=1
sap_language="EN"
if len(sys.argv) > 1:
sap_login_number=int(sys.argv[1])
if len(sys.argv) > 2 and sys.argv[2].lower() == "pl":
sap_language="PL"
print(f"SAP login number: {sap_login_number}")
sap_login_str="USER"+str(sap_login_number)
main_window_title = "/000 SAP"
mainHwnd = find_window_by_title(main_window_title)
if not mainHwnd:
print(f"Main Window not found!!!")
sapLogonHwnd = find_window_by_title("SAP Logon")
print(f"SAP Logon found: {hex(sapLogonHwnd)}")
log_on_button_hwnd = find_child_window(sapLogonHwnd, "Log On")
if log_on_button_hwnd:
print(f"Log on button found: {hex(log_on_button_hwnd)}")
set_focus(log_on_button_hwnd)
send_shift_tab(sapLogonHwnd)
send_shift_enter(sapLogonHwnd)
time.sleep(3)
else:
print("Log on button not found!!!")
mainHwnd = find_window_by_title(main_window_title)
if not mainHwnd:
print(f"Main Window not found!!!")
exit(-1)
print(f"Main Window found: {hex(mainHwnd)}")
footer_window_hwnd = find_child_window(mainHwnd, "Footer")
if footer_window_hwnd:
print(f"Footer found: {hex(footer_window_hwnd)}")
set_focus(footer_window_hwnd)
send_shift_tab(mainHwnd)
clear_current_field()
send_keys_via_keybd_event(sap_language)
send_shift_tab(mainHwnd)
clear_current_field()
send_keys_via_keybd_event("Password")
if sap_login_number < 12 or sap_login_number > 20:
send_keys_via_keybd_event("?")
send_shift_tab(mainHwnd)
clear_current_field()
send_keys_via_keybd_event(sap_login_str)
send_shift_tab(mainHwnd)
clear_current_field()
if sap_login_number > 21:
send_keys_via_keybd_event("100")
else:
send_keys_via_keybd_event("110")
send_enter_key()
print("")Example Output
After executing the script, the following output can be seen:
PS C:\> python.exe .\sap_login.py 31
SAP GUI Login Tool v0.2.1 by Michał Majchrowicz AFINE Team
SAP login number: 31
Main Window not found!!!
SAP Logon 800
SAP Logon found: 0x103c4
Log on button found: 0x103ce
AFINE/000 SAP
Main Window found: 0x5e029c
Footer found: 0xd08161. Command Execution and Input Parsing
The script was executed using Python with the argument 31, which represents the SAP login number. The script successfully parsed the input and recognized that the user wants to log in with SAP user number 31. This value determines the username (USER31) and influences other logic, such as client selection
2. Main Window Check
Initially, the script could not locate the main SAP GUI window, as expected when the SAP session is not yet open. This triggers the fallback mechanism to start a new session through the SAP Logon interface.
3. Fallback to SAP Logon
The script detected the SAP Logon window and successfully found the “Log On” button. It also displayed hexadecimal values for the window handles of these elements, confirming that the script can interact with the SAP Logon interface to initiate a session.
4. Session Initialization
After interacting with the Log On button, the script successfully opened the SAP session. It found the main SAP window titled AFINE/000 SAP and its handle, as well as the footer control. The footer typically contains input fields for language, user, password, and client, indicating the script is ready to proceed with entering login details.
Conclusion
Automating SAP GUI login using Windows API and Python provides a powerful alternative to traditional SAP scripting, especially in environments where scripting is disabled or limited. This SAP test automation approach allows developers to interact with the SAP interface at the operating system level, ensuring precise control over window focus, navigation, and keystroke simulation. By leveraging functions such as find_window_by_title, set_focus, and send_keys_via_keybd_event, the script can reliably locate SAP windows, clear input fields, and enter credentials without relying on fragile UI element identifiers.
The analysis of the script’s output demonstrates its robustness: it successfully handles scenarios where the main SAP window is not initially available by falling back to the SAP Logon interface, initiating a session, and then proceeding with the login process. This resilience makes the solution practical for real-world use cases where SAP environments can vary or experience delays.
In summary, advanced control methods like these not only improve automation reliability but also open the door to integrating SAP GUI with broader automation workflows. When implemented with proper error handling and timing adjustments, this technique can significantly streamline repetitive tasks, reduce manual effort, and enhance operational efficiency in enterprise systems.




