Advertisement
Open Source Projects by Phil Schwartz

Simulating Keyboard Input with Python for GUI Testing

GUI regression checks can spend as much time preparing windows as exercising features. A Python script that sends keyboard input turns that setup into a repeatable smoke test: focus the target field, type a value, press Enter, and capture the result. It suits applications whose behaviour depends on real focus, shortcuts, or input handlers.

On my open-source site, tools such as Kodos, DenyHosts, and Scratchy share a preference for compact, inspectable Python workflows. This simulator follows that principle: a small command that can drive a desktop without hiding what it sends. Australian software teams in Sydney, Melbourne, and Brisbane often work across macOS, Windows, and Linux. The script must remain portable across those systems without assuming one desktop permission model.

Why Synthetic Keyboard Input Matters

Synthetic input differs from calling a widget's setText method: it travels through the operating system, reaches whichever control has focus, and can trigger key handlers, validation, and application shortcuts. That makes it valuable for black-box checks of dialogs, search boxes, and terminal-like interfaces, where bypassing the keyboard would miss the behaviour under test.

It is still a model of human behaviour, not a complete replacement for one. Key repeat, dead keys, and input method editors can behave differently, while an overly fast script may outrun the application. Australian fixtures should exercise English spellings such as “organisation”, DD/MM/YYYY dates, and 24-hour clocks; most Australian keyboards use an English layout, but teams should still test any layout used by their users.

Choosing the Right Python Approach

PyAutoGUI is a practical starting point because its write, press, and hotkey calls work across the three major desktop platforms. The keyboard package can inject events more directly, although its privilege requirements vary. For an application you control, a native test API such as Qt Test or pytest-qt will usually produce cleaner assertions than a global input tool.

Choose the lowest level that still reproduces the requirement. Use a unit test to validate an internal method, a widget-level driver to validate one component, and synthetic keyboard input for end-to-end behaviour that crosses process or desktop boundaries. This keeps slow, sensitive input tests focused on genuinely user-facing paths.

Create an isolated environment and install the two small dependencies used by the example:

python -m venv .venv
source .venv/bin/activate
python -m pip install pyautogui pyperclip

On Windows, activate the environment with .venv\Scripts\activate instead.

Building a Small Keyboard Simulator

The following script takes plain text, waits for a configurable delay so the target window can receive focus, types the text, and optionally presses Enter. The fail-safe remains enabled, so moving the pointer into the upper-left corner aborts PyAutoGUI's input functions.

import argparse
import sys
import time

import pyautogui
import pyperclip

pyautogui.FAILSAFE = True
pyautogui.PAUSE = 0.05


def paste_shortcut():
    if sys.platform == "darwin":
        return ("command", "v")
    return ("ctrl", "v")


def send_text(value: str, interval: float) -> None:
    if value.isascii():
        pyautogui.write(value, interval=interval)
        return

    original = pyperclip.paste()
    pyperclip.copy(value)
    pyautogui.hotkey(*paste_shortcut())
    time.sleep(0.2)
    pyperclip.copy(original)


def main() -> None:
    parser = argparse.ArgumentParser(
        description="Send keyboard input to the focused window."
    )
    parser.add_argument(
        "--text", required=True, help="Text to type into the focused field."
    )
    parser.add_argument(
        "--delay", type=float, default=2.0,
        help="Seconds to wait before typing."
    )
    parser.add_argument(
        "--repeat", type=int, default=1,
        help="Number of sequences to send."
    )
    parser.add_argument(
        "--interval", type=float, default=0.03,
        help="Delay between key presses."
    )
    parser.add_argument(
        "--submit", action="store_true",
        help="Press Enter after each sequence."
    )
    args = parser.parse_args()

    if args.delay < 0 or args.repeat < 1 or args.interval < 0:
        parser.error("Delay, repeat, and interval must be valid.")

    time.sleep(args.delay)
    for sequence in range(args.repeat):
        send_text(args.text, args.interval)
        if args.submit:
            pyautogui.press("enter")
        if sequence + 1 < args.repeat:
            time.sleep(0.25)

    print(f"Sent {args.repeat} keyboard sequence(s).")


if __name__ == "__main__":
    main()

ASCII text is typed directly. For characters outside that set, the script copies the value to the clipboard, sends the platform's paste shortcut, and restores the previous clipboard content after a short pause. This is less faithful to physical typing, but it avoids silent omissions for accented names, symbols, and multilingual test data.

Run the script after focusing the intended field:

python keyboard_simulator.py --text "Quarterly update for Brisbane" --submit
python keyboard_simulator.py --text "Review at 09:30" --delay 4 --repeat 2

Running the Script Across Desktop Platforms

On macOS, grant the Terminal or IDE running Python access under System Settings, Privacy & Security, Accessibility. Without that permission, calls may appear to run while producing no keystrokes. The Command-V branch handles the paste shortcut, while normal text still uses individual key events.

Linux support is strongest under X11. Wayland's security model can prevent an ordinary desktop process from injecting input globally; an Xvfb virtual display is usually more reliable in CI. Check $XDG_SESSION_TYPE before diagnosing a framework problem as a script problem. The virtual environment should also have access to the active display.

Windows normally handles these calls through SendInput. The script and application should run at the same integrity level because Windows can block lower-privilege input directed at an elevated window. A locked remote desktop also has no active input target. Scheduling matters across Australia: Perth uses AWST, Brisbane uses AEST, and Sydney and Melbourne change offset during daylight saving, so test timestamps should be stored in UTC and rendered separately for each locale.

Making GUI Tests Repeatable and Safe

Repeatability comes from controlling the environment as tightly as the keyboard. Give each run a fresh application profile, use fixed test values, record the operating system and package versions, and wait for a visible condition before sending the next command. Fixed delays are still useful for the initial focus change, but they should not substitute for an application-ready check when a status indicator or dialog is available.

A typing script can affect whichever window has focus, so run it against a disposable test target rather than a live customer session. PyAutoGUI's upper-left-corner fail-safe provides a quick manual stop. For many Australian organisations, synthetic records should also use fictitious identities and avoid production customer details, particularly when tests run on shared machines or captured screenshots enter a CI system. The Privacy Act 1988, Australian Privacy Principles, and OAIC guidance make careful data handling a local compliance concern as well as sound engineering practice.

Synthetic keyboard input is strongest as a black-box layer in a mixed test strategy. Application-native drivers can cover internal logic and precise widget state, while this script covers focus, shortcuts, and full desktop workflows. Combining those layers produces stable coverage without treating an automated keystroke as a complete model of an Australian user.