SAP Test Automation Using Windows API in Python

Artur - AFINE cybersecurity team member profile photo
Michał Majchrowicz
February 13, 2026
13
min read

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:

  1. Select All Text – It issues a Ctrl+A combination to highlight all content in the currently focused field. This is done by pressing and releasing the Ctrl and A keys in sequence, ensuring the selection is complete.
  2. Delete the Content – After a short delay to allow the system to process the selection, the function sends a Backspace key 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

  1. 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 like find_window_by_title and find_child_window, which enumerate windows and match their captions.
  2. Focus Management
    Once the correct window is identified, the script uses set_focus to 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.
  3. Field Navigation and Clearing
    The script navigates between input fields using send_shift_tab (to move backward) and clears existing content with clear_current_field, which sends Ctrl+A followed by Backspace. This guarantees that old data does not interfere with the new login credentials.
  4. Keystroke Simulation
    Login details such as language, password, username, and client number are entered using send_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.
  5. Final Submission
    After filling in all required fields, the script sends an Enter keystroke using send_enter_key to 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: 0xd0816

1. 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.

FAQ

Questions enterprise security teams ask before partnering with AFINE for security assessments.

No items found.

Monthly Security Report

Subscribe to our Enterprise Security Report. Every month, we share what we're discovering in enterprise software, what vulnerabilities you should watch for, and the security trends we're seeing from our offensive security work.

By clicking Subscribe you're confirming that you agree with our Terms and Conditions.
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
Gradient glow background for call-to-action section